目录程序地址空间虚拟地址进程地址空间虚拟内存管理为什么要有虚拟地址空间程序地址空间程序地址空间回顾我们在讲C语⾔的时候⽼师给⼤家画过这样的空间布局图.可是我们对他并不理解可以先对其进⾏各区域分布验证:来理解一下语言层的一些概念之前可能只是记住了这些语言的用法但是不知道是为什么会这么用现在来浅浅的理解一下。之前我们说字符串常量不能修改为什么呢我们可以看到字符串常量的地址和正文代码区的地址很接近因为它被硬编码到正文代码区了正文代码区的内容是只读的不能做修改因此字符串常量不能修改。const char *str helloworld;之前我们说static修饰的变量生命周期是全局的做不过作用域在函数内部而已这又是为什么呢我们可以看到static常量的地址和正文代码区的地址很接近其实static修饰的变量在初始化数据区内。static int test 10;虚拟地址来段代码感受⼀下。本来父子进程应该看到的是同一个变量但是子进程修改了变量的值所以会发生写时拷贝但是为什么父子进程中的g_val的是地址相同的所以说这还是物理内存吗怎么可能一个地址会有两个不同的值没错这个不是物理内存中的地址是虚拟内存地址。我们发现⽗⼦进程输出地址是⼀致的但是变量内容不⼀样能得出如下结论:变量内容不⼀样,所以⽗⼦进程输出的变量绝对不是同⼀个变量但地址值是⼀样的说明该地址绝对不是物理地址在Linux地址下这种地址叫做虚拟地址我们在⽤C/C语⾔所看到的地址全部都是虚拟地址物理地址用户⼀概看不到由OS统⼀管理。OS必须负责将 虚拟地址 转化成 物理地址 。进程地址空间所以之前说‘程序的地址空间’是不准确的准确的应该说成 进程地址空间 那该如何理解呢看图分⻚虚拟地址空间虚拟地址空间和页表物理地址三者之间的动态过程数据和代码加载到物理内存根据代码和数据的大小来初始化虚拟地址空间本质上就是调整划分区域空间此时页表对虚拟地址和物理地址进行映射物理地址转化为虚拟地址提供给上层使用。创建进程的同时struct mm_struct也会被创建开辟空间怎么初始化呢加载代码和数据到内存的时候进行初始化根据他们的大小。父进程创建时会同时创建PCBstruct mm_struct,页表并加载父进程对应的代码和数据将物理内存中的地址转化为虚拟地址再构建页表映射子进程拷贝父进程的PCBstruct mm_struct,页表当子进程修改数据时OS为了进程的独立性发生写时拷贝再修改子进程虚拟地址映射的物理地址所以虚拟地址是相同的物理地址不同。上⾯的图就⾜矣说明问题同⼀个变量地址相同其实是虚拟地址相同内容不同其实是被映射到了不同的物理地址结论一个进程一个虚拟地址空间。一个进程一个页表页表是用来虚拟地址胡物理地址映射的。一个整形变量是4个字节可以通过该变量的虚拟起始地址加上偏移量就可以找到访问全部地址了。打印的地址是虚拟地址这就是为什么子进程的g_val改变了同时也发生了写时拷贝但是父子进程的g_val的地址是相同的。那么每一个进程的虚拟地址空间需要管理吗答案是需要。虚拟内存管理描述linux下进程的地址空间的所有的信息的结构体是 mm_struct 内存描述符。每个进程只有⼀个mm_struct结构在每个进程的task_struct结构中有⼀个指向该进程的结构。structtask_struct{structmm_struct*mm;//对于普通的⽤⼾进程来说该字段指向他的虚拟地址空间的⽤⼾空间部分对于内核线程来说这部分为NULL。structmm_struct*active_mm;//表示当前任务运行时CPU 实际激活、正在使用的地址空间。}可以说mm_struct结构是对整个⽤⼾空间的描述。每⼀个进程都会有⾃⼰独⽴的mm_struct这样每⼀个进程都会有⾃⼰独⽴的地址空间才能互不⼲扰。先来看看由task_struct到mm_struct进程的地址空间的分布情况定位mm_struct⽂件所在位置和task_struct所在路径是⼀样的不过他们所在⽂件是不⼀样的mm_struct所在的⽂件是mm_types.h。1structmm_struct2{3/*...*/4structvm_area_struct*mmap;/* 指向虚拟区间(VMA)链表 */5structrb_rootmm_rb;/* red_black树 方便查找mmap中的节点也就是堆区*/6unsignedlongtask_size;/*具有该结构体的进程的虚拟地址空间的⼤⼩*/7/*...*/8// 代码段、数据段、堆栈段、参数段及环境段的起始和结束地址。9unsignedlongstart_code,end_code,start_data,end_data;10unsignedlongstart_brk,brk,start_stack;11unsignedlongarg_start,arg_end,env_start,env_end;12/*...*///简化版本。可以根据这个来理解上面那个是源码里面的内容structmm_struct{pgd_t*pgd;// 页表根地址unsignedlongstart_code;// 代码段起始地址unsignedlongend_code;unsignedlongstart_data;// 数据段起始地址unsignedlongend_data;unsignedlongstart_brk;// 堆起始地址unsignedlongbrk;// 当前堆顶unsignedlongstart_stack;// 栈起始地址// 虚拟内存区域 VMA 等};每一个进程都会有自己独立的 mm_struct 来统筹整个内存空间。但由于一个进程的虚拟空间被划分成了众多不同属性的虚拟内存区域VMA操作系统需要将该进程内所有的 vm_area_struct 高效地组织起来在 mm_struct 内部虚拟空间的组织方式同时采用了以下两种数据结构1.为了方便从头到尾遍历所有的内存区域内核使用双向链表将它们串起来由 mm_struct 里的 mmap 指针指向链表头2.为了在程序发生缺页中断时能极快地查找到某个内存地址所在的区域内核同时使用红黑树较新内核为 Maple Tree进行管理由 mm_struct 里的 mm_rb 指针指向这棵树的根节点。”linux内核使⽤ vm_area_struct 结构来表⽰⼀个独⽴的虚拟内存区域(VMA)由于每个不同质的虚拟内存区域功能和内部机制都不同因此⼀个进程使⽤多个vm_area_struct结构来分别表⽰不同类型的虚拟内存区域。上⾯提到的两种组织⽅式使⽤的就是vm_area_struct结构来连接各个VMA⽅便进程快速访问。structvm_area_struct{unsignedlongvm_start;//虚存区起始unsignedlongvm_end;//虚存区结束structvm_area_struct*vm_next,*vm_prev;//前后指针structrb_nodevm_rb;//红⿊树中的位置unsignedlongrb_subtree_gap;structmm_struct*vm_mm;//所属的 mm_structpgprot_tvm_page_prot;unsignedlongvm_flags;//标志位struct{structrb_noderb;unsignedlongrb_subtree_last;}shared;structlist_headanon_vma_chain;structanon_vma*anon_vma;conststructvm_operations_struct*vm_ops;//vma对应的实际操作unsignedlongvm_pgoff;//⽂件映射偏移量structfile*vm_file;//映射的⽂件void*vm_private_data;//私有数据atomic_long_tswap_readahead_info;#ifndefCONFIG_MMUstructvm_region*vm_region;/* NOMMU mapping region */#endif#ifdefCONFIG_NUMAstructmempolicy*vm_policy;/* NUMA policy for the VMA */#endifstructvm_userfaultfd_ctxvm_userfaultfd_ctx;}__randomize_layout;所以我们可以对上图在进⾏更细致的描述如下图所示注意虚拟地址是一块整体的空间然后vm_area_struct是里面不同的区域块。为什么要有虚拟地址空间这个问题其实可以转化为如果程序直接可以操作物理内存会造成什么问题在早期的计算机中要运⾏⼀个程序会把这些程序全都装⼊内存程序都是直接运⾏在内存上的也就是说程序中访问的内存地址都是实际的物理内存地址。当计算机同时运⾏多个程序时必须保证这些程序⽤到的内存总量要⼩于计算机实际物理内存的⼤⼩。那当程序同时运⾏多个程序时操作系统是如何为这些程序分配内存的呢例如某台计算机总的内存⼤⼩是128M现在同时运⾏两个程序A和BA需占⽤内存10MB需占⽤内存110。计算机在给程序分配内存时会采取这样的⽅法先将内存中的前10M分配给程序A接着再从内存中剩余的118M中划分出110M分配给程序B。这种分配⽅法可以保证程序A和程序B都能运⾏但是这种简单的内存分配策略问题很多。安全⻛险◦ 每个进程都可以访问任意的内存空间这也就意味着任意⼀个进程都能够去读写系统相关内存区域如果是⼀个⽊⻢病毒那么他就能随意的修改内存空间让设备直接瘫痪。地址不确定◦ 众所周知编译完成后的程序是存放在硬盘上的当运⾏的时候需要将程序搬到内存当中去运⾏如果直接使⽤物理地址的话我们⽆法确定内存现在使⽤到哪⾥了也就是说拷⻉的实际内存地址每⼀次运⾏都是不确定的⽐如第⼀次执⾏a.out时候内存当中⼀个进程都没有运⾏所以搬移到内存地址是0x00000000但是第⼆次的时候内存已经有10个进程在运⾏了那执⾏a.out的时候内存地址就不⼀定了效率低下◦ 在没有引入分页机制的早期内存管理中一个进程是被作为一个完整的独立内存块进行操作的。当物理内存不够用时为了给新程序腾出空间操作系统只能将某个暂时不活跃的进程整个拷贝到磁盘的交换分区中。由于进程占用的空间往往很大这种把整个进程在内存和磁盘之间来回搬运的方式极其耗时导致系统运行效率极低。而现代的虚拟内存技术通过引入‘分页机制’解决了这个问题它允许操作系统以极小的‘页’通常为 4KB为单位将不常用的内存数据换出到磁盘再也不需要笨重地搬运整个进程了(页表的功能)。存在这么多问题有了虚拟地址空间和分⻚机制就能解决了吗当然地址空间和⻚表是OS创建并维护的是不是也就意味着凡是想使⽤地址空间和⻚表进⾏映射也⼀定要在OS的监管之下来进⾏访问也顺便 包括各个进程以及内核的相关有效数据!保护了物理内存中的所有的合法数据。因为有地址空间的存在和⻚表的映射的存在我们的物理内存中可以对未来的数据进⾏任意位置的加载物理内存的分配 和 进程的管理就可以做到没有关系进程管理模块和内存管理模块就完成了解耦合。因为有地址空间的存在所以我们在C、C语⾔上new, malloc空间的时候其实是在地址空间上申请的物理内存可以甚⾄⼀个字节都不给你。⽽当你真正进⾏对物理地址空间访问的时候才执⾏内存的相关管理算法帮你申请内存构建⻚表映射关系延迟分配这是由操作系统⾃动完成⽤⼾包括进程完全0感知因为⻚表的映射的存在程序在物理内存中理论上就可以任意位置加载。它可以将地址空间上的虚拟地址和物理地址进⾏映射在进程视⻆所有的内存分布都可以是有序的。总结1.将内存地址从无序变有序。虚拟地址是有序的物理地址是无序的。2.虚拟地址转化为物理地址OS查找页表的过程中也可以对你的地址和操作进行非法判定页表中有读写权限从而实现对物理内存的保护。例如野指针访问地址空间但是页表无法转换页表构建映射时该地址没有被使用可以直接杀掉你的进程。char* str hello,*str world;这个代码编译会通过但是运行崩溃因为常量字符串是在字符串常量区也就是正文代码区而正文代码区是只读的如果修改的话程序会运行崩溃查找页表时权限会被拦截。3.进程管理模块和内存管理模块就完成了解耦合。例如加载代码和数据量大时只加载一部分到物理内存但此时虚拟地址已经初始化好了页表也已经构建好了。把加载到内存中代码运行完之后虚拟地址映射物理地址时发现页表上只有虚拟地址没有物理地址OS会再加载剩下的代码到内存同时构建好页表这个也叫做动态内存这个一整个过程叫做缺页中断。这也做到了进程管理模块和内存管理模块的解耦合。澄清一些细节问题如何理解挂起把对应的物理内存中的数据唤出到磁盘中swap交换分区。我们可以不加载数据和代码到物理内存只有PCBstruct mm-struct,页表。创建进程时先有PCBstruct mm-struct,页表再加载代码和数据。