** 博主名称**迷途之人不知返 个人专栏: 《C语言》、《数据结构》、《C》、《Linux》️ Gitee仓库: 《C语言》、《数据结构》、《C》、《Linux》/ 算法专栏: 《算法精选集》进程虚拟地址空间1、回顾空间分布1.1、三个细节1.2、两个问题2、进程虚拟地址空间2.1、虚拟地址的引出2.2、“地址相同内容不同”的解释2.2.1、进程还包括虚拟地址空间、页表2.2.2、写时拷贝2.3、真正理解虚拟地址空间2.3.1、什么是虚拟地址空间2.3.2、理解区域划分2.3.3、调整区域大小2.4、虚拟内存空间的存在意义2.4.1、理由一可以更好地对内存进行保护2.4.2、理由二空间排布无序变有序2.4.3、理由三进程管理与内存管理解耦2.5、补充说明首先说好我们当前研究的环境kernellinux-2.6.3232位平台下1、回顾空间分布我们以前学习C/C的时候涉及过一点点所谓空间分布的知识。事实真如上面的图一样吗#includestdio.h#includestdlib.h#includeunistd.hintg_val100;// 已初始化的全局变量intg_unval;// 未初始化的全局变量intmain(intargc,char*argv[],char*env[]){printf(code addr: %p\n,main);printf(init global addr: %p\n,g_val);printf(uninit global addr: %p\n,g_unval);constchar*strhelloworld;printf(const string addr: %p\n,str);printf(str addr: %p\n,str);staticinttest10;printf(test static addr: %p\n,test);char*heap_mem1(char*)malloc(10);char*heap_mem2(char*)malloc(10);char*heap_mem3(char*)malloc(10);char*heap_mem4(char*)malloc(10);printf(heap addr: %p\n,heap_mem1);printf(heap addr: %p\n,heap_mem2);printf(heap addr: %p\n,heap_mem3);printf(heap addr: %p\n,heap_mem4);printf(stack addr: %p\n,heap_mem1);printf(stack addr: %p\n,heap_mem2);printf(stack addr: %p\n,heap_mem3);printf(stack addr: %p\n,heap_mem4);inti0;for(;argv[i];i)printf(argv[%d]: %p\n,i,argv[i]);for(i0;env[i];i)printf(env[%d]: %p\n,i,env[i]);return0;}结果是仔细分类果然最开始的图片描述的那样。只要我们仔细观察打印的地址就能发现实际的空间分布和图片上描述的是相符的。这里有三个细节我们要了解一下。1.1、三个细节1、实际程序中栈空间的分布可能不太一样在某些函数栈帧内变量(栈)的分布可能不太一样。但是总体是呈图中所示即向低地址生长的。2、堆 vs 栈 vs 堆栈目前我们只需要知道堆就是堆栈就是栈。而堆栈属于栈。3、空间总大小是4GB首先我们来复习一下内存单位的换算1(Byte)8(bit)1(KB)1024(Byte)1(MB)1024(KB)1(GB)1024(MB)1(TB)1024(GB)32位机器意味着有32根地址总线。每一根线都可以用0或1表示那么每一根线都可以表示两种状态32根线就可以表示232种状态。访问内存的基本单位是字节那么空间的总大小就是232(Byte)。经过上面的内存大小换算空间的总大小就是4GB。1.2、两个问题当我们回答了上面三个细节后面对给出的空间分布图片和验证代码我们又提出了下面两个问题1、常量字符串不能被修改包裹的常量字符串不能被人为的修改。观察发现常量字符串的空间位置和main函数代码的空间位置非常接近即常量字符串处于正文代码区。所以常量字符串是只读的用户若想修改操作系统就会拦截并且报错。所以当我们尝试修改上述代码的str时比如这样修改*strH;就会发生报错有意思的是当我们不用const修饰内容为常量字符串的变量str时修改str代码编译能通过但是执行时出现段错误不加const编译能过而执行时出现严重问题加上const编译阶段就弹出报错。意味着const的作用是对编译器做进一步限制将运行报错转化为编译报错从而帮助我们更快发现错误。2、static修饰局部变量继续观察static修饰变量的所在空间与全局变量所在的空间也是非常接近的。// test3.c#includestdio.hintg_val100;intung_val;intmain(){staticintval10;printf(static int addr: %p\n,val);printf(init global addr: %p\n,g_val);printf(uninit global addr: %p\n,ung_val);return0;}实际上当一个全局变量被static修饰的时候这个变量的生命周期已经变成全局变量的生命周期了。只不过操作系统对这个被static修饰的变量做了限制使之只能在函数内访问即作用域不变。2、进程虚拟地址空间文章开头展示的图片真的指的就是内存空间吗不是的文章开头图片展示的是进程的虚拟地址空间。2.1、虚拟地址的引出// test4.c#includestdio.h#includeunistd.h#includesys/types.hintg_val10;intmain(){pid_tidfork();if(id0){perror(fork\n);return0;}elseif(id0)while(1){printf(child. pid: %d, g_val: %d, g_val: %p\n,getpid(),g_val,g_val);sleep(1);}elsewhile(1){printf(parent. pid: %d, g_val: %d, g_val: %p\n,getpid(),g_val,g_val);sleep(1);}return0;}创建子进程父进程与子进程共享代码和数据。当我们未对全局变量g_val做修改时父子两个进程拥有的g_val变量内容相同变量地址相同。然而当我们尝试在子进程将g_val不断但是在父进程对g_val不做任何修改。我们就会看到非常奇妙的现象父子两个进程中g_val变量内容不同但是变量地址却是相同的那么这个地址就绝对不是真实的物理地址。实际上被打印出来的我们能看到的地址都是虚拟地址。2.2、“地址相同内容不同”的解释要解释这个问题我们首先要知道创建好的进程还包含哪些东西。2.2.1、进程还包括虚拟地址空间、页表进程创建后操作系统会为它“开辟”一块虚拟地址空间。操作系统还会开辟一张“页表”完成虚拟地址到物理地址的映射页表将会填入虚拟地址空间的地址和物理内存空间的地址然后建立某种对应关系最终完成虚拟地址到物理地址的映射。简单流程图如下所以进程还需包含哪些东西呢我们可以得出一个小结论进程需要包含三大块PCB虚拟地址空间页表2.2.2、写时拷贝创建子进程的时候子进程需拷贝父进程的PCB、虚拟地址空间、页表。实际上子进程不仅拷贝了父进程的PCB、虚拟地址空间、页表虚拟地址空间和页表里的地址也拷贝下来了只拷贝地址这不就是我们之前学过的浅拷贝吗还记得吗浅拷贝的特点是两个指针同时指向一块资源也就是资源共享。意味着父进程与子进程共享同一块物理内存资源。未来子进程想对这块内存资源中的内容做修改由于父子共享父进程中的数据也会被改变。这不就与“进程之间相互独立”相违背了吗这时写时拷贝登场。子进程未对虚拟地址0x601058处做修改那么操作系统什么也不会做父子进程依旧共享内存数据。当操作系统检测到子进程想对虚拟地址0x601058处做修改操作系统就会在物理内存中重新开辟一个空间(假设为0x1111)然后干两件事将物理地址0x1234中的内容拷贝到0x1111中对于子进程的页表断开旧联系进而与新开辟的0x1111建立新联系最后按子进程的要求对新空间做修改。这就是写时拷贝即“要修改的时候再拷贝”。上面代码中的g_val正是通过了创建子进程时的写时拷贝才达到了我们看到的“地址相同内容不同”的结果。究其原因就是写时拷贝之后虚拟地址没有改变而最终访问到的物理地址不同访问到的内容也就不同。写时拷贝有两大优点父与子能够真正独立。写时拷贝技术的存在父进程与子进程能够真正分离保证了进程的独立性提高内存的利用率。写时拷贝是一种延时申请技术。不修改就不拷贝省下的空间可以给别的任务使用这就提高了内存的利用率进而提高了整机的运行效率。2.3、真正理解虚拟地址空间要真正理解虚拟地址空间我们就要从以下三个方面具体阐述到底什么是虚拟地址空间如何理解区域划分如何调整区域大小2.3.1、什么是虚拟地址空间来讲一个故事大家可能都知道国外一些富豪的私生活挺乱的()因此他们往往有很多私生子()。私生子之间往往不知道彼此的存在这就跟“进程的独立性”很像。现在有一个大富豪手握家产10个亿。大富豪一共有4个私生子。大富豪对这四个私生子的每一个人都许诺将来可以继承大富豪的这10亿。而今天四个私生子分别提出了自己的要求私生子1私生子1也是个生意人。私生子1要求大富豪给10000当作启动资金。大富豪认为这是一笔小钱很快就给了。私生子2私生子2是一个科研工作者。私生子2要求大富豪给5000买实验器材大富豪很快也给了。私生子3私生子3是一个女孩。私生子3出于爱美的心理想要大富豪给6000买一个名牌挎包大富豪依旧爽快地给了。私生子4私生子4还在上高中。私生子4向同学吹牛说他手里有老爹的10亿所以私生子4想要大富豪给10亿让同学们开眼大富豪想也没想就给了一巴掌()。在这个故事中私生子就是进程大富豪就是操作系统10个亿就是真实的物理内存大富豪给每个人承诺都会给10亿——画饼大富豪画的每一张饼——虚拟地址空间。那么问题来了画的饼本身需不需要管理假如公司老板今天给员工A承诺干完项目就涨工资给员工B承诺做完报告就升职员工A干完了项目老板说做的好再做点项目就给升职员工A一听就会发现老板的承诺牛头不对马嘴马上就会认为老板是在画饼同理员工B也是这样。所以画的饼需要管理那么虚拟地址空间也需要管理。如何管理——先描述再组织。所以虚拟地址空间实际上是一种内核数据结构对象。mm_struct就是Linux下进程的虚拟内存空间。小贴士1正如上述大富豪给私生子的画饼行为一样大富豪向私生子承诺会给10亿但是实际上只会给很小一部分那么进程的虚拟地址空间表面上计算下来有4个G实际进程真正占有的肯定是远远小于4个G。虚拟内存空间本质上就是一个结构体。那么结构体如何表示区域划分呢2.3.2、理解区域划分我们小学的时候或多或少会遇到好动的小朋友所以我们往往会在桌子上画一条“三八线”。“三八线”的本质就是区域划分。那么计算机是如何实现区域划分的呢例如小朋友A与小朋友B做同桌。A与B约定将共用的桌子分成100份A分到50份B分到50份。那么我们就可以这样标记区域划分structdesk{intsize100;intmate_a_start1;intmate_a_end50;intmate_b_start51;intmate_b_end100;}我们就通过规定区域的开始与结束划分出了一个个区域。观察源码所以源码的mm_struct中就会有很多个XXX_start和XXX_end这些成员是用来做区域划分的。小贴士2这里理解区域划分的时候可不要只盯着“1”“50”看。通过区域的开始与结束来划分区域意味着从开始到结束的每一个位置都是可以随意使用的。例如以1和50划分出某个区域那么[2], [3], [4], …, [49]都是可以使用的。2.3.3、调整区域大小如何调整区域大小调整数值即可。例如对于上面同学A与B的课桌空间划分今天A想分到60份空间那么B就只能分到40个空间。那么我们就可以这样调整structdesk{intsize100;intmate_a_start1;intmate_a_end60;// 调整数值intmate_b_start61;// 调整数值intmate_b_end100;}这里有两个细节。1、unsigned long对象表示一个地址地址本质上也是一个十六进制数值。32位机器的地址用32根地址总线表示地址的大小就是32bit而unsigned long在32位下占4字节合32bit所以unsigned long对象刚好就可以表示一个地址。有了首地址和尾地址我们自然就能够划分出一块行使特定功能的区域。2、代码区、数据区大小由什么决定答案是由磁盘上的程序决定。我们对于程序的定义是程序 代码 数据创建进程时程序的数据和代码需要加载到物理内存中然后与进程PCB建立联系。这就好比“看菜吃饭”的道理你程序有多大的代码和数据我进程就开辟多大的虚拟内存空间然后虚拟地址与物理地址建立联系。2.4、虚拟内存空间的存在意义2.4.1、理由一可以更好地对内存进行保护虚拟内存空间充当了进程与物理内存之间的“中间人”。进程若想访问物理地址空间必须通过虚拟地址在页表中的转化。“中间人”遇到会危害整个操作过程的问题时就会提醒操作系统拦截申请必要时强行“干掉”进程。虚拟内存空间作为“中间人”可能遇到的问题主要有两点1、进程访问的地址非法假设没有虚拟内存空间进程还是直接用的物理内存。进程PCB中若有指针对象释放资源变成野指针那么这个野指针很有可能会指向进程PCB中其它指针指向的资源当中接下来野指针一系列未定义的操作就会影响到其它指针指向的内容这就会导致安全隐患。这时虚拟地址空间的存在就能解决这个问题。当某一个指针变成野指针假设进程需要访问这个野指针的时候操作系统就会识别并拦截进程的请求转化失败。2、进程操作的权限不匹配实际上页表除了存虚拟地址与物理地址的对应关系还存在一项内容叫“权限标志位”。例如常量字符串在代码区页表就会给常量字符串标记上r只读权限。未来某个进程想对常量字符串做修改操作系统也会识别并拦截转化也会失败。2.4.2、理由二空间排布无序变有序实际上操作系统给进程分配物理空间的时候是随便申请的。目前我们可以理解为物理空间上哪里有空位置操作系统就占住送给进程。有了虚拟内存空间物理空间排布再怎么杂乱无章虚拟地址与物理地址经过页表建立映射关系后进程看到的地址空间也是连续、有序的。2.4.3、理由三进程管理与内存管理解耦有了虚拟内存空间进程想要申请空间只需要好好调整虚拟内存空间腾出一片位置用来存储就行而操作系统面对一个虚拟地址只需好好在物理内存上寻找一块空间足够的物理地址建立映射关系即可。意味着操作系统对进程管理和对内存管理的过程几乎是分开的耦合度是很低的这样就既保证了安全又保证了效率。2.5、补充说明整机运行的情况下堆区可能会存在“中间释放”的情况比如刚刚malloc出来的空间进程还没终止之前就free释放了导致堆区不连续。这种情况下进程的虚拟内存空间如何表示换句话说进程的虚拟内存空间还是如文章开头所示的图片一样吗实际上更详细的虚拟内存空间分布图应该是长这样的即用链表(队列)来维护。当需要释放中间资源的时候我们只需将其它无需释放的资源转移到新节点中然后释放旧节点连同需要被销毁的中间资源一起被释放即可。