
文章目录一、引入问题二、虚拟地址空间与页表2.1、初谈虚拟地址空间与页表2.2、再谈页表三、虚拟地址的管理3.1、描述虚拟地址空间3.2、理解区域划分3.3、如何管理各区域四、虚拟地址空间的意义一、引入问题学习C/C的过程中我们一定见识过这样一种空间布局图。可是我们对它的理解并不深刻。难免会有疑问问题物理内存的空间布局是这样的吗直接输出结论该结构不是物理内存而叫做进程地址空间或者虚拟地址空间。至于如何验证下一小节将给出答案。本文将以该图为出发点粗滤谈论虚拟地址空间相关知识。二、虚拟地址空间与页表2.1、初谈虚拟地址空间与页表我们首先来看下面一段代码#includestdio.h#includeunistd.hintmain(){intval0;printf(我是一个进程pid:%d, ppid:%d, val:%d, val:%p\n,getpid(),getppid(),val,val);pid_t idfork();if(id0){//fatherwhile(1){printf(我是一个父进程pid:%d, ppid:%d, val:%d, val:%p\n,getpid(),getppid(),val,val);sleep(1);}}elseif(id0){//childwhile(1){val;printf(我是一个子进程pid:%d, ppid:%d, val:%d, val:%p\n,getpid(),getppid(),val,val);sleep(1);}}return0;}观察现象我们不免会有疑惑问题怎么同一个地址的值即可以大于0又可以等于0呢这一现象进一步验证了我们先前的说法图中所显现的不是物理地址语言上学习的地址都称为虚拟地址它实际上就是一个编号。每一个进程都各自会有一套虚拟地址空间和页表。虚拟地址空间我们相对来说比较熟悉而页表实际上就是用来实现物理内存的地址和虚拟地址之间的相互映射的表。当我们fork()出一个子进程的时候子进程会以父进程为模板创建出自己的tesk_struct结构体从而创建出自己的虚拟地址空间和页表其当前内容与父进程的完全一致类似拷贝这也说明了为什么fork后父子进程会共享代码与数据。我们已经知道进程之间具有独立性。那么既然父子进程共享代码与数据我们如何保证父子进程之间的独立性呢 代码与数据、内核数据结构都不相同。并且操作系统规定父子进程中任何一个进程尝试对共享的变量进行修改操作都无法直接修改而是会发生“写时拷贝”修改一个进程对应的页表中对应变量所映射的物理内存的地址开辟一个新的物理地址。这也很好地保证了父子进程之间的独立性。有了以上对虚拟地址空间的认知我们也能够很好地解释为什么一个变量能够同时接收不同的返回值的问题了。这也是我们在fork()部分遗留的一个问题2.2、再谈页表经过上面的初步认识我们已经基本了解页表的功能。此时我们就已经可以解答我们在学习C/C时经常感到疑问的点问题1️⃣常量字符串和代码为什么是只可读的呢页表中实际上存在着许多标志位其中就写明了每一块对应空间的权限属性。r、w、x代码与常量字符串的地址被操作系统在页表中仅仅标识为可读权限本质上是不让进程能够再写入因此常量字符串与代码只可读。标志位除了可以标识对应地址的权限还可以标识该虚拟地址所指向的数据是否存在于物理内存。看到这里肯定会有人感到奇怪明明页表上都写明了虚拟地址与物理地址的映射关系为什么还需要这样一个标识符来记录数据是否存在于物理内存中呢难道我们的数据还可能不保存在物理内存中吗还真的有这种可能当我们的物理内存容量不足的情况下操作系统会将内存中一些暂时无用的进程与数据放入磁盘的swap分区中以此减缓内存的压力。此时我们的对应的进程的页表中便会将一些数据标识为0状态即不存在于物理在内存中,相反若存在于物理内存中则被标识为1。当我们重新访问这些数据的时候操作系统会将对应数据从swap分区中拉回内存。补充查表的操作由MMU内存管理单元进行MMU是存放于CPU内部的一种硬件。三、虚拟地址的管理3.1、描述虚拟地址空间由上文我们已经大致了解了虚拟地址空间与页表的作用作为操作系统的重要组成部分我们一定是要将它好好的管理起来那么我们如何进行管理呢我们曾经说过管理的本质就是“先描述再组织”因此我们首先应该描述虚拟地址空间。如何描述当然是使用一个内核数据结构在Linux中描述虚拟地址空间的所有的信息的结构体是mm_struct。每个进程只有一个mm_struct结构体在每个进程的task_struct 结构中有一个指向该进程的mm_struct结构体指针。可以说mm_struct结构体是对整个用户空间的描述。每一个进程都会有自己独立的mm_struct这样每一个进程都会有自己独立的地址空间才能互不干扰。3.2、理解区域划分问题1️⃣如何对虚拟地址空间进行空间划分呢??我们既然知道虚拟地址空间本质上就是一个结构体那么我们只需要在mm_struct结构体内记录某段区域开始的位置以及结束的位置即可。下图则为mm_struct结构体中对应的区域划分相关内容。语言中的地址本质上就是一个编号归根结底是一个整数上图也能够验证这段话内核中用unsigned long类型来表示。我们知道堆区与栈区的范围是可变的有了以上的知识储备我们应该能够想到调整区域的大小本质上是修改内核中对应的变量。3.3、如何管理各区域我们已经了解区域划分相关内容但是仍然不够虚拟地址空间极其庞大我们应该更加细粒度地管理各个区域例如以下问题问题1️⃣区域划分后大部分区域在使用时都是连续的但是我们使用堆区地空间时是不连续的此时我们应该如何管理这些离散的空间呢??mm_struct结构体中有一个mmap指针指向由vm_area_struct结构体所组成的链表结构。vm_area_struct结构体更加细粒度地划分了每一个区域其中包含两个变量vm_end和vm_start用来表明用户所使用的每一块内存的上下限。与之相比之前所说的mm_struct中的变量则是更加宏观的划分用于指向各个区域的上下限。四、虚拟地址空间的意义如果要更加深刻地理解虚拟地址空间我认为我们必须先明白为什么要有虚拟地址空间。假设没有虚拟地址空间我们用户就得直接与物理内存打交道无疑这种行为绝对很危险就像我们不能够直接接触操作系统内核一样。因此虚拟地址空间的存在是极其必要的它能够控制进程的行为拦截非法的操作例如野指针问题修改常量字符串等保护物理内存。倘若就非得让我们用户直接与物理内存打交道物理内存如此复杂我们是否必须对其进行管理?无疑肯定是要的。比起管理物理内存管理虚拟地址内存相对来说还是要更加轻松并且有了虚拟地址空间与页表进行映射原则上进程的代码与数据可以在内存中随意地加载无序变为有序。并且有了虚拟地址空间进程则只需考虑虚拟地址空间与页表部分而无需在意物理内存等硬件之间地操作相反物理内存等硬件也无需考虑进程相关地问题。这就体现了进程管理与内存管理解耦合。问题1️⃣LinuxOS中创建一个进程是先创建task_struct还是先加载代码与数据??当然是先创建一个task_struct,再加载对应的代码与数据。可是当我如果不着急执行这个进程可以需要执行代码或者访问数据的时候再进行加载吗当然可以这种行为被称为懒加载。懒加载也很好地体现了虚拟地址空间对进程管理与内存管理解耦合的作用。用效率换取空间的合理使用C/C中的new和malloc本质上就是在虚拟地址空间的堆区上申请空间在真正使用这些空间的时候操作系统才会在物理内存中申请空间。这也被称为“缺页中断引起的内存二次申请”综上所述虚拟地址空间的意义总结如下控制进程的行为拦截非法的操作野指针问题修改常量字符串等保护物理内存。有了虚拟地址空间与页表进行映射原则上进程的代码与数据可以在内存中随意地加载无序变为有序进程管理与内存管理解耦合。完