『 Linux 』进程地址空间概念

2023-12-12 15:20

本文主要是介绍『 Linux 』进程地址空间概念,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

文章目录

    • 🫙 前言
    • 🫙 进程地址空间是什么
    • 🫙 写时拷贝
    • 🫙 可执行程序中的虚拟地址
    • 🫙 物理地址分布方式


🫙 前言

请添加图片描述

在c/C++中存在一种内存的概念;

一般来说一个内存的空间分布包括栈区,堆区,代码段等等;

且内存是自底向上(由0x000000000xFFFFFFFF);
以该图为例:

在这里插入图片描述

该图即为常见的内存分布图;

  • 正文代码段

    正文代码段所存放的数据一般为函数体的二进制代码;

  • 已初始化数据区

    已初始化数据区所存放的数据是在程序中声明的,并且具有初始值的变量,这些变量需要占用存储器的空间;

  • 未初始化数据区

    未初始化数据区所存放的数据是没有进行初始化或者初始值为0的数据,这些数据在存储时不需要额外占用存储器的空间;

  • 堆空间一般为动态空间,即需要成需要手动分配释放;若是分配了堆区空间但使用过后未对堆空间进行手动释放则将会出现内存泄漏的问题;

  • 一般情况下栈所存放的数据基本上都为局部变量;

  • 命令行参数/环境变量

    命令行参数/环境变量,顾名思义该段空间用来存放OS给程序所传递的命令行参数与环境变量;

  • 内核空间

    在Linux操作系统当中,内存的分布一般为其中3G为用户空间,1G为内核空间;

以下操作均在CentOS7_x64环境下进行

存在一个程序 ( mytest ) :

  int init = 10; int uninit; int main(int argc,char *argv[],char *env[])
{char*ch1= new char[10]; char*ch2= new char[10];char*ch3= new char[10];char*ch4= new char[10];char*ch5= new char[10];printf("init : %p\n",&init);//已初始化数据printf("uninit : %p\n",&uninit);//未初始化数据printf("text : %p\n",main);//正文代码段cout<<"--------------"<<endl;//堆区printf("heap1 : %p\n",ch1);printf("heap2 : %p\n",ch2);printf("heap3 : %p\n",ch3);printf("heap4 : %p\n",ch4);printf("heap5 : %p\n",ch5);cout<<"--------------"<<endl;//栈区printf("stack1 : %p\n",&ch1);printf("stack2 : %p\n",&ch2);printf("stack3 : %p\n",&ch3); printf("stack4 : %p\n",&ch4);printf("stack5 : %p\n",&ch5);cout<<"--------------"<<endl;//命令行参数for(int i = 0;i<argc;++i){printf("argv[%d] : %p\n",i,argv[i]);}cout<<"--------------"<<endl;//环境变量for(int i = 0;env[i];++i){printf("env[%d] : %p\n",i,env[i]);}return 0;
}

从这段代码中可以打印出内存中不同数据的内存分布情况;

但实际上在OS层面中,这些所谓的内存并非物理内存;


🫙 进程地址空间是什么

请添加图片描述
在上文中说到,进程所访问的地址并不是物理地址;

存在一个程序(证明):

using namespace std;int tmp = 100;int main()
{pid_t id = fork();if(id == 0){int s = 5;while(1){cout<<"pid : "<<getpid()<<" ppid : "<<getppid()<<" tmp : "<<tmp<<" &tmp : "<<&tmp << endl;sleep(1);s--;if(!s) tmp = 200;}}else{while(1){cout<<"pid : "<<getpid()<<" ppid : "<<getppid()<<" tmp : "<<tmp<<" &tmp : "<<&tmp << endl;sleep(1);}}return 0;
}

在该程序中定义了一个全局变量,并使用fork()函数对该进程创建了一个子进程,同时分别在父子进程中打印该全局变量的值与地址;

pid : 28930 ppid : 28929 tmp : 100 &tmp : 0x60108c
pid : 28929 ppid : 28812 tmp : 100 &tmp : 0x60108c
pid : 28930 ppid : 28929 tmp : 200 &tmp : 0x60108c
pid : 28929 ppid : 28812 tmp : 100 &tmp : 0x60108c

当五秒过后,子进程修改了全局变量的值;

可在父进程当中的这个全局变量并未被更改,且父子进程中所显示的这个全局变量tmp地址相同;

然而实际上,一个程序在运行的过程中所使用的内存地址为虚拟地址(线性地址);

在过去的计算机中,进程对于内存的访问是以直接访问的形式,即运行程序时程序载入至内存当中称为进程,CPU根据进程中的代码数据对内存的各个地址(物理地址)进行操作;

在这里插入图片描述

但是由于访问的是物理内存地址,所以若是程序在内存当中误操作则会导致某些进程的崩溃;

这种操作是十分不安全的操作;

所以为了保证安全性同时也保证进程间的独立性,现在的OS当中,出现了进程地址空间的概念;

在这里插入图片描述

每个进程都存在一个称为进程地址空间的数据结构(mm_struct结构体);

在这个结构体当中以一种类似于区间的方式模拟出地址(在Linux2.6的版本中使用unsigned long类型实现);

/*释放线性区的调用方法*/void (*unmap_area) (struct mm_struct *mm, unsigned long addr);
#endifunsigned long mmap_base;		/* base of mmap area ,内存映射区的基地址*/unsigned long task_size;		/* size of task vm space */unsigned long cached_hole_size; 	/* if non-zero, the largest hole below free_area_cache */unsigned long free_area_cache;		/* first hole of size cached_hole_size or larger */pgd_t * pgd;                            /* 页表目录指针*/atomic_t mm_users;			/* How many users with user space?,共享进程的个数 */atomic_t mm_count;			/* How many references to "struct mm_struct" (users count as 1),主使用计数器,采用引用计数,描述有多少指针指向当前的mm_struct */int map_count;				/* number of VMAs ,线性区个数*/struct rw_semaphore mmap_sem;spinlock_t page_table_lock;		/* Protects page tables and some counters,保护页表和引用计数的锁 (使用的自旋锁)*/struct list_head mmlist;		/* List of maybe swapped mm's.	These are globally strung* together off init_mm.mmlist, and are protected* by mmlist_lock*/unsigned long hiwater_rss;	/* High-watermark of RSS usage,进程拥有的最大页表数目 */unsigned long hiwater_vm;	/* High-water virtual memory usage ,进程线性区的最大页表数目*/unsigned long total_vm, locked_vm, shared_vm, exec_vm;unsigned long stack_vm, reserved_vm, def_flags, nr_ptes;unsigned long start_code, end_code, start_data, end_data;     /*维护代码区和数据区的字段*/unsigned long start_brk, brk, start_stack;       /*维护堆区和栈区的字段*/unsigned long arg_start, arg_end, env_start, env_end;  /*命令行参数的起始地址和尾地址,环境变量的起始地址和尾地址*/unsigned long saved_auxv[AT_VECTOR_SIZE]; /* for /proc/PID/auxv */

除此之外在进程地址空间这个结构体中有一个指针,这个指针所指向的位置即为页表;

所谓的页表就是一种映射关系,这种映射关系以一种key/value的模型将对应的物理地址与虚拟地址进行一种存储,在查找或访问时将访问至虚拟地址,通过该虚拟地址通过页表的key/value模型找到其对应的物理内存再进行访问;

在CPU中存在一个内存管理单元(MMU),这个内存管理单元是CPU中的一个模块,这个模块具体的作用为负责虚拟地址到物理地址的转换;

在这里插入图片描述

以该图为例,其中task_struct表示PCB结构体,即进程控制块;

mm_struct即为该进程的进程地址空间,mm_struct中的pgd即为页表;


🫙 写时拷贝

请添加图片描述

当多个进程或线程共享同一块内存时,内核会使用写时拷贝来优化内存的复制行为;

当有一个进程尝试修改共享内存页面时,Linux内核会触发写时拷贝机制;

它会为修改的进程创建一个新的私有副本,并将修改的内容写入新的副本中,而不是立即修改原始的共享页面;

以该例子为例:

using namespace std;int tmp = 100;int main()
{pid_t id = fork();if(id == 0){int s = 5;while(1){cout<<"pid : "<<getpid()<<" ppid : "<<getppid()<<" tmp : "<<tmp<<" &tmp : "<<&tmp << endl;sleep(1);s--;if(!s) tmp = 200;}}else{while(1){cout<<"pid : "<<getpid()<<" ppid : "<<getppid()<<" tmp : "<<tmp<<" &tmp : "<<&tmp << endl;sleep(1);}}return 0;
}

在该例子中程序运行的结果为:

pid : 28930 ppid : 28929 tmp : 100 &tmp : 0x60108c
pid : 28929 ppid : 28812 tmp : 100 &tmp : 0x60108c
pid : 28930 ppid : 28929 tmp : 200 &tmp : 0x60108c
pid : 28929 ppid : 28812 tmp : 100 &tmp : 0x60108c

两个进程中的变量的地址相同但其值不同的原因就是在于其所在的虚拟地址相同但页表中虚拟地址所映射的物理地址不同;

在这个程序当中,使用fork()函数创建了子进程,由于子进程是由父进程创建的,所以对应的子进程的PCB结构体继承于父进程,即当父进程创建出一个子进程时,该子进程将会对父进程的PCB结构体进行一次浅拷贝,所以父子进程所对应的代码资源是共享的;

在这里插入图片描述

在只读的情况下两个进程的页表所映射至的物理地址也许相同的,而当一个进程要修改该物理内存中的内容时,OS将会重新在物理内存中申请一块空间,同时修改该进程所对应的页表映射关系;

在这里插入图片描述


🫙 可执行程序中的虚拟地址

请添加图片描述
实际在可执行程序当中也存在着所谓的虚拟地址,在一般的教材当中也被称为"逻辑地址";

存在一个程序:

#include<iostream>
using namespace std;int g_val = 100;int main()
{cout<<&g_val<<endl;return 0;
}

这个程序运行之后可以打印出该程序中全局变量g_val的地址;

在Linux中存在一个命令可以打印出一个可执行程序中的逻辑地址(虚拟地址),即objdump;

语法:

objdump -x <executable_file>

在此处配合| grep打印出该可执行程序中的虚拟地址,即:

objdump -x mytest | grep g_val

使用该命令后运行该程序:

$ objdump -x mytest | grep g_val
00000000004007f7 l     F .text	0000000000000015              _GLOBAL__sub_I_g_val
000000000060105c g     O .data	0000000000000004              g_val
$ ./mytest 
0x60105c

在上面的程序当中,程序运行的结果(打印全局变量地址)与使用objdump所显示出磁盘中的全局变量g_val地址相同,由此可见其进程中的虚拟地址与本在磁盘中的虚拟地址相同;

实际上在计算机当中,本质上无论是磁盘中的虚拟地址(逻辑地址)还是在进程当中的虚拟地址都是相同的;

只不过是在进程与磁盘中的表现形式不同;

当程序编译链接完成时生成的可执行程序当中将会存在代码数据等,在这些代码数据当中存在着静态的虚拟地址,这些地址被称作逻辑地址;

当这个程序被执行后即被加载至内存当中成为进程时,进程将会去初始化自身的PCB结构体;相对应的PCB结构体内的各种数据结构也将要被进行维护与初始化;

磁盘中的虚拟地址(逻辑地址)将会初始化PCB结构体中对应的进程地址空间,使得进程地址空间中的虚拟地址与原本磁盘内的虚拟地址(逻辑地址)保持一致;

在这里插入图片描述


🫙 物理地址分布方式

请添加图片描述
在上面的图中可以发现:

在对进程地址空间进行初始化时,真正将虚拟地址与物理地址进行关联的时候,其物理地址并没有按照原本的虚拟地址原模原样的进行对应的初始化;

在对对应物理地址进行初始化时更像是以一种随机的方式;

为了物理内存的安全性,Linux中采用了一种地址空间随机化(ASLR)的一种内存攻击缓存技术;

当对应的进程地址空间的虚拟地址在初始化时通过页表映射至物理内存时将会采用这种方式;

使得对应进程的物理内存地址无法被预测,也保证了进程在运行时的安全性;

这篇关于『 Linux 』进程地址空间概念的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/485072

相关文章

Linux脚本(shell)的使用方式

《Linux脚本(shell)的使用方式》:本文主要介绍Linux脚本(shell)的使用方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录概述语法详解数学运算表达式Shell变量变量分类环境变量Shell内部变量自定义变量:定义、赋值自定义变量:引用、修改、删

Windows的CMD窗口如何查看并杀死nginx进程

《Windows的CMD窗口如何查看并杀死nginx进程》:本文主要介绍Windows的CMD窗口如何查看并杀死nginx进程问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地... 目录Windows的CMD窗口查看并杀死nginx进程开启nginx查看nginx进程停止nginx服务

Linux链表操作方式

《Linux链表操作方式》:本文主要介绍Linux链表操作方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录一、链表基础概念与内核链表优势二、内核链表结构与宏解析三、内核链表的优点四、用户态链表示例五、双向循环链表在内核中的实现优势六、典型应用场景七、调试技巧与

详解Linux中常见环境变量的特点与设置

《详解Linux中常见环境变量的特点与设置》环境变量是操作系统和用户设置的一些动态键值对,为运行的程序提供配置信息,理解环境变量对于系统管理、软件开发都很重要,下面小编就为大家详细介绍一下吧... 目录前言一、环境变量的概念二、常见的环境变量三、环境变量特点及其相关指令3.1 环境变量的全局性3.2、环境变

Linux系统中的firewall-offline-cmd详解(收藏版)

《Linux系统中的firewall-offline-cmd详解(收藏版)》firewall-offline-cmd是firewalld的一个命令行工具,专门设计用于在没有运行firewalld服务的... 目录主要用途基本语法选项1. 状态管理2. 区域管理3. 服务管理4. 端口管理5. ICMP 阻断

Linux实现线程同步的多种方式汇总

《Linux实现线程同步的多种方式汇总》本文详细介绍了Linux下线程同步的多种方法,包括互斥锁、自旋锁、信号量以及它们的使用示例,通过这些同步机制,可以解决线程安全问题,防止资源竞争导致的错误,示例... 目录什么是线程同步?一、互斥锁(单人洗手间规则)适用场景:特点:二、条件变量(咖啡厅取餐系统)工作流

Linux中修改Apache HTTP Server(httpd)默认端口的完整指南

《Linux中修改ApacheHTTPServer(httpd)默认端口的完整指南》ApacheHTTPServer(简称httpd)是Linux系统中最常用的Web服务器之一,本文将详细介绍如何... 目录一、修改 httpd 默认端口的步骤1. 查找 httpd 配置文件路径2. 编辑配置文件3. 保存

Linux使用scp进行远程目录文件复制的详细步骤和示例

《Linux使用scp进行远程目录文件复制的详细步骤和示例》在Linux系统中,scp(安全复制协议)是一个使用SSH(安全外壳协议)进行文件和目录安全传输的命令,它允许在远程主机之间复制文件和目录,... 目录1. 什么是scp?2. 语法3. 示例示例 1: 复制本地目录到远程主机示例 2: 复制远程主

Java进程CPU使用率过高排查步骤详细讲解

《Java进程CPU使用率过高排查步骤详细讲解》:本文主要介绍Java进程CPU使用率过高排查的相关资料,针对Java进程CPU使用率高的问题,我们可以遵循以下步骤进行排查和优化,文中通过代码介绍... 目录前言一、初步定位问题1.1 确认进程状态1.2 确定Java进程ID1.3 快速生成线程堆栈二、分析

Linux基础命令@grep、wc、管道符的使用详解

《Linux基础命令@grep、wc、管道符的使用详解》:本文主要介绍Linux基础命令@grep、wc、管道符的使用,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐... 目录grep概念语法作用演示一演示二演示三,带选项 -nwc概念语法作用wc,不带选项-c,统计字节数-