尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

揭秘进程地址空间:虚拟地址背后的奥秘

揭秘进程地址空间:虚拟地址背后的奥秘 进程地址空间我们之前在学习CPP的时候了解过C的程序地址空间我们现在通过代码将不同区域的数据地址打印出来看看有没有上面规律我们查看的地址有代码地址全局定义区地址全局未定义地址堆区栈区地址字符常量地址环境变量和命令行参数地址我们通过地址的大小比较我们可以知道我们的此处的地址空间是下图结构的从正文代码到命令行参数环境变量的地址是依次增大的我们以前叫这个叫程序地址空间上图也可以知道全局变量为什么又全局性因为全局变量存储在整个地址空间的初始化和未初始化区内所以具有全局性。然后栈区内栈是向地址减小方向增长的我们调用函数要形成栈帧不断的入栈最后形成临时变量当我们的函数调用完了就需要弹栈要释放临时变量所以栈结构是向地址减小方向增长 堆区就是向上增长并且堆栈之间的地址差别特别大所以堆栈之间有一大块的镂空空间叫共享区还有一个就是helloworld这个字符串是不可以被修改的所以是被放在字符常量区我们看看这个字符串的地址发现这个字符串常量的地址在正文代码区附近所以我们平时写的字符串常量其实是被硬编码到代码的因为代码是只读的所以字符串也是只读的了只不过在代码区会有个区域叫字符常量区我们上面的一个变量static int test 10;这个变量若不是静态就是一个函数内的局部变量但是加上static就是一个静态局部变量作用域依旧不变只是生命周期是全局的了我们来看看这个变量的地址发现静态局部变量的地址就在全局区甚至这个变量的地址就在全局已初始化变量和全局未初始化变量之间所以静态局部变量就是全局变量只是要求这个全局变量只作用于当前所在函数中但是这个变量为什么生命周期又是全局的呢因为这个变量已经被编到了已初始化数据中了所以现在问一个问题程序地址空间是内存吗不是如果一个进程的数据是按这样排布的其他的进程怎么办计算机运行进程会有很多个进程都按这样规律的排布其他进程要如何放有进程创建就有退出所以这么多的进程内存空间不能满足每个进程都这样规律的排布还有就是要更正一下概念我们学习的这个地址排布不叫程序地址空间应该叫进程地址空间虚拟地址空间, 这应该是系统层的概念不是语言层的概念这也是为什么在学习语言或者语言的书籍上看不到这样的地址排布图片只是单词的介绍有什么区域堆区栈区......以往提要这个图也是为了让这部分的学习有立脚点不那么抽象那么我们先证明一下这个进程地址空间不是内存吧这个实验代码做的是让子进程循环输出gval全局变量输出全局变量地址其pid和ppid并且每次循环都让gval父进程就是只做输出但是不做gval我们来看看gval的地址怎么样令我们感到震惊的是父子进程的gval全局变量的地址居然相同按理说子进程对父进程全局变量gval那就要写时拷贝一份gval来呀如果是内存地址那此时就会出bug的我们现在做到例子就摆在面前实践出真知如果这里是内存地址那绝对不可能是这样的情况数据会覆盖才对的但是现在是父子进程访问同一个地址但是数据不一样所以我们断定这地址一定不是内存地址那这个地址到底是什么呢我们现在只能告诉你这个地址根本不是物理内存的地址这个地址叫做虚拟地址是OS为了进程所虚拟出来的全新的地址所以我们之前学习c/c指针用到的地址全部都是虚拟地址新概念——虚拟地址空间我们上面得出的结论虚拟地址空间不是内存一个进程一个虚拟地址空间每一个进程需要用一个task_struct来组织而每一个task_struct最终都会指向虚拟地址空间虚拟地址空间的单位即宽度的单位为1字节在32位机器下虚拟地址空间的范围为 2^32^个在64位机器下虚拟地址空间的范围为 2^64^个就32位机器来讲虚拟地址空间从低到高会有2^32^种组合即虚拟地址空间的范围为2^32^个而宽度为1字节所有虚拟地址空间的容量为 2^32^ X 2^8^ 2^40^ 4GB1K 1024B 2^10^B1MB 1024K 2^20^B1GB 1024MB 2^30^B所以容量为 4GB在这里因为64位的容量太大了不方便讲解所有一般都是用32位来讲解所以这里会有2^32^个地址一个地址表示1字节而虚拟地址有4GB那么从0~3GB被称为用户空间3 ~4GB被称为内核空间我们目前不能理解内核空间我们现在重点学习用户空间我们可以发现用户空间的3G空间用户是可以直接通过地址来直接访问我们编写的程序编译好后有些变量名不在了被编译器转换成内存地址或寻址方式所以用户空间就是可以通过地址直接去访问的空间这里不是重点接下来才是一个进程一套页表页表是用来做虚拟地址和物理地址映射不管这么说程序的数据肯定是要存储在物理存储空间的那么物理存储空间和虚拟地址空间是如何联系的就以全局变量g_val来讲解全局变量g_val数据位100在内存中的物理存储地址为0x112233而在虚拟地址空间中也会有4字节的变量g_val其起始的虚拟地址为0x111111那这两者之间是如何联系的呢关键在于OS在创建进程的时候要为进程创建一个页表这个页表就是链接虚拟地址和物理地址的桥梁我们现在还没有能力展开页表了解内部原理我们只能理解表层进程地址空间可以找到其对应的页表页表左侧填写进程中某个变量的虚拟地址右侧填写变量所对应的物理地址所以进程访问虚拟地址时OS会自动将虚拟地址通过查表的方式找到虚拟地址并转化成物理地址进而找到访问到指定变量即页表是用来做虚拟地址和物理地址映射所以我们程序中所以的代码、变量、未初始化变量对象、堆、栈....这些都会有地址每个元素都会有对应的虚拟地址都会填入到页表虚拟地址都会加载到物理内存通过页表映射找到物理内存我们来说是虚拟地址我们上面例子g_val是int型的全局变量按理说应该有四个地址的但是虚拟地址和物理地址只提供了一个地址这个地址是这个变量最低位的那一个地址只提供一个地址只能访问一个字节那要如何访问这整个变量呢所以我们会有类型这就一般会通过了解变量类型来获取偏移量这时访问变量我们知道低位的起始地址起始地址加上偏移量这样就可以读取这个变量所以的地址了回顾例子Figure 335我们回到上面开头的例子父子进程输出全局变量有了上面的学习认识我们可以知道一个进程一个虚拟地址空间一个进程一套页表父进程有虚拟地址空间有页表那么子进程也要有而我们之前讲父子进程的时候子进程是拷贝父进程的PCB所以子进程也是拷贝父进程的虚拟地址空间和页表内的数据因为子进程拷贝父进程虚拟地址所以父进程和子进程全局变量g_val输出的地址相同!而子进程页表也是拷贝父进程的所以子进程对应的变量访问的实际物理地址是父进程对应变量的实际物理地址上面的操作感觉非常像C的浅拷贝所以全局变量默认下是父子进程共享的他们的映射关系是一样的从虚拟到物理对子进程来说就是指针发生浅拷贝代码也同理所以默认情况下代码和数据对应父子进程来说是共享的那子进程修改全局变量那就需要深拷贝了我们都清楚进程具有独立性子进程对g_val指向这个代码子进程在页表通过虚拟地址映射到物理地址访问物理地址做操作这里问题就来了此时g_val对应父子进程来说是共享的子进程对g_val当前物理地址操作父进程的数据也会修改但是父进程认为自己的g_val没用改动这样就没用保证独立性了这里其实有别的做法得到g_val代码子进程访问g_val时候先不做操作OS介入对被操作空间的数据拷贝到新的物理地址空间获得了新的物理地址0x223344OS此时修改子进程页表的映射关系访问新变量0x111111虚拟地址映射到0x223344构建全新的映射关系这时候子进程修改变量是修改新变量对父进程变量没用影响这样的操作是写实拷贝这里就解释了g_val数据不一样但是地址一样就是因为虚拟地址一样但是物理地址不一样写时拷贝OS会自动做虚拟地址与进程地址空间 是什么✨通过一个例子来了解知识点一个大富翁有10亿 这个大富翁有很多私生子这个大富翁对每个私生子都承诺让他们继承这10亿这里大富翁给每个私生子画大饼让每个私生子都认为自己有10亿但我们断定你不敢一次性把10亿都要走要也要上零零散散的要用完还要还大富翁 —— OS10亿 —— 物理内存私生子 —— 进程大饼 —— 虚拟地址空间虚拟地址空间本质上是大饼让每个进程都认为自己有4GB的物理内存或者每个进程都认为自己在独占物理内存现在再来看这个图我们更能明显感受到OS告诉进程你有4GB的内存可以用画大饼这样的事很常见公司老板给员工画饼如果公司人数很多老板连饼是谁的都不知道这要怎么办我们知道员工要管理那大饼也要管理一个进程一个虚拟地址空间而我们OS上有很多进程那就是有很多个虚拟地址空间所以虚拟地址空间需要被管理起来不然会认错如何管理先描述再组织描述虚拟地址空间属性构建虚拟地址空间类在类内设置指针定义一个虚拟地址空间指针作为入口创建一个虚拟地址空间对象就用指针链接把所有的对象用链表管理起来老板对大饼的管理转化成立老板对链表的管理所以虚拟地址空间本质上是一个数据结构struct mm_struct创建一个进程就要有一个虚拟地址空间所以这个虚拟地址空间就是一个结构体对象task_struct通过指针链接综上虚拟地址空间是在内核中为进程创建的结构体对象怎么做虚拟地址空间是如何实现的即讨论类的组成有些什么属性什么叫做区域划分例子:幼儿园里面有小张和小美他两个小张是同桌小张有点邋遢小妹有点嫌弃而小张又一直缠着小美小美生气了打了小张并且在桌子上画了一条‘3 8 线’小张越过这个线小美又要打他小美做的线本质就是区域划分小女孩区域划分用计算机量化一下struct Destop { int size; int xz_start; int xz_end; int xm_start; int xm_end; } struct Destop area {100, 0 , 49 , 50 , 99};区域划分只需要确认区域的开始和结束即可小张的区域范围为0~49所以在这张100cm的桌子小张在他的区域范围内任何位置可以使用100cm的桌子用cm为单位有100个刻度张三就非常精确的使用这个刻度铅笔盒放到第二个刻度铅笔放到第三个刻度橡皮擦在第四个刻度所以这里的刻度就相当于地址只要小张在范围内里面的地址可以随便用 我们也只需要记录起始位置期间是线性连续的这里我们知道桌子长度我们对桌子进行0~99的刻度设置这样的操作叫做对桌子进行统一编地所以现在我们要访问桌面上的位置就直接使用地址来访问地址可以直接使用int来保存桌子 —— 地址空间100cm —— 2^32^刻度 —— 地址空间上的地址孩子个数 —— 地址空间的区域所以现在我们可以知道虚拟地址空间结构体内部有些什么属性了struct mm_struct //内存描述符 { long code_start; long code_end; long init_start; long init_end; long uninit_start; long uninit_end; ...//结构体内存储的是每个区域开始和结束虚拟地址 }所以将虚拟地址空间结构体内输入对应的起始与终止地址就可以划分出对应的区域通过以上例子的讲解OS要对进程虚拟地址空间要做管理虚拟地址空间是内核的一种结构体这个结构体中大部分属性都是各个区域的起始与终止地址再讲讲调整区域小美忍无可忍小张屡次越过38线侵入到小美的活动范围小美直接下掉现有的55开的38线将线左移到37开的38线这就是在调整区域计算机如何实现呢area.xm_start-20; area.xz_end -20;所以区域调整只需要对区域内整数进行加减调整即可看源码这个就是进程的地址空间mm_struct---内存描述符total_vm 虚拟地址空间的总大小start_code end_code 代码区起始与终止start_data end_date 数据区起始与终止start_brk 堆区的开始 brk 堆区的顶部 start_stack栈区的开始arg_start arg_end 命令行参数区的开始与结束env_start env_end 环境变量区的开始与结束再来谈谈地址空间创建进程会创建虚拟地址空间与页表进程PCB内会有struct mm_struct * mmmm指针会指向当前进程所属的虚拟地址空间虚拟地址空间内堆、栈、共享区这些是程序运行后才开辟的动态内存我们先不谈但是程序编译好了虚拟内存空间内正文代码、初始化和未初始化数据肯定是会有数据的这些区域内的大小与程序的体量有直接影响我们写的helloworld输出语句这个程序代码就100字节使用这个程序正文代码也是100字节 如果这个程序是王者这样体量的正文代码可能就1G了所以虚拟地址空间内部正文代码也要有1G的空间当进程被调用代码和数据都要被加载到物理内存中在物理内存中被加载的物理内存字节要同批次的在虚拟地址空间内部创建数量同等的地址物理内存100字节虚拟内存也会开辟100字节的地址此时填写页表代码和虚拟地址就可以一一对应联系起来1、虚拟地址空间中申请指定大小的空间9如何申请调整区域划分2、加载程序申请物理空间1 — 2 — 页表进行映射 —— 物理地址转化成虚拟地址虚拟地址提供给上层用户使用上层用户使用虚拟地址查虚拟地址通过页表可以找到实际的物理地址mm_struct1、开辟空间2、初始化的值从哪里来—— 数据从磁盘到内存加载的时候进行初始化最终我们再回到父子进程的例子讲讲父进程创建时候会创建PCB创建虚拟地址空间创建页表然后将代码和数据加载到内存在加载的过程中OS对加载所消耗的物理内存空间相应的转换到虚拟地址空间上的线性地址从而可以给上层提供很多的虚拟地址而页表可以将虚拟地址和物理地址的映射关系都构建好子进程就会把父进程使用的数据结构都拷贝一份PCB、虚拟地址空间、页表所以子进程会有和父进程相同的g_val虚拟地址到页表映射虚拟地址默认下会映射到相同的物理空间但是一旦子进程修改OS就会维护进程独立性继续写时拷贝为什么✨虚拟地址空间也是先描述再组织描述我们知道了是描述为结构体那怎么组织呢所以是用一个mm_struct链表来组织管理的接下来我们来讲讲为什么要有虚拟地址空间将地址从无序变有序有序化我们之前讨论了那么多现在有个问题从磁盘加载到内存的代码和数据是要有序的加载还是无序的加载很显然现在是可以无序的加载因为有虚拟地址空间通过虚拟地址空间划分有序的空间通过页表将无序的代码和数据地址找到从此上层用户看到虚拟地址空间内的代码和数据都是连续且有序的地址转换的过程中也可以对你的地址和操作进行合法性判定进而保护物理内存用户调用虚拟地址去访问实际数据虚拟地址需要转化成物理地址怎么转化的 OS查找页表实际上和硬件有关但是不谈为什么我们访问地址不直接访问物理地址要是要通过虚拟地址转化再来呢例子小明过年拿到了压岁钱得到了200块之后小明就去小店随意购买东西辣条、玩具什么的这个时候小明妈就发现小明妈那些玩家就算了他居然买辣条这些垃圾食品这个时候小明妈就和小明说“来小明把压岁钱给妈来管理你要用啥跟我讲我给你钱”为什么要有虚拟地址空间就像上面为什么小明和小店之间要加入一个妈妈的关系小明妈只和小明讲表明的真正的意图是控制小明花钱的对象比如小明找妈要钱去买辣条小明妈说辣条吃了不健康不给这里小明妈就是驳回了小明错误的请求同理虚拟地址空间就起到了一个对虚拟地址以及用户操作的合法性判断这里的判定操作都在页表实现我们来拓展一下页表的知识页表除了有记录虚拟地址和物理地址还会记录权限rwx此时要对一个代码区地址继续写入但是发现这个用户的操作权限只要读权限OS查表就会发现你没用写的权限OS就不给你向这个地址写甚至把你这个进程杀掉这就是虚拟地址对物理内存的保护以及用户访问错误的虚拟地址错误的访问错误的虚拟地址查页表的时候就发现找不到对应的虚拟地址页表就查找失败了这就是野指针问题什么是野指针使用未被定义的地址空间或是一个指向区域已被释放的内存区域已被释放则地址空间需要被释放物理空间也需要被释放映射关系也要去掉我们现在对一个已释放的内存区域进行访问到页表中根本发现不了虚拟内存和物理内存之间的映射关系查页表失败所以OS会把进行杀掉所以访问野指针可能会让程序崩溃char *str helloworld; *str H我们对字符串常量进行修改这样的操作虽然编译能过但是启动就会崩溃字符常量区就在正文代码区和初始化代码区之间中被硬编码到其中的所以字符串常量是只读的这里要修改字符串常量页表转化失败所以OS不让你转这就解释了为什么字符常量区写入就会崩溃查找页表的时候权限拦截了让进程管理 和 内存管理 进行一定程度的解耦合现在有一个程序这个程序代码部分就有2G的内存此时虚拟地址空间的正文代码区域就给程序开辟2G的地址空间但到实际的物理地址我们只加载代码内存的1/4所以在页表查询中正文代码虚拟地址都可以找到全部但只有1/4的物理地址才可以找到对应的物理内存让程序开始运行当前1/4的内存要用完时暂停进程在把之后1/4的内存加载到内存把物理地址与虚拟地址的映射构建好在将进程切换回来运行上面这样的操作就是缺页中断上面磁盘加载数据到内存以及OS申请内存并向页表进行映射关系构建属于OS的内存管理模块PCB、虚拟地址模块就属于进程管理模块这样就可以让这二者之间解耦合如果没用虚拟地址空间和页表进程地址直接指向物理内存这样二者就强相关了申请了物理内存还要去进程PCB内修改指针而有进程地址空间和页表加载内存就修改页表就行进程的管理和物理内存没用任何关系扩展我们可以不加载代码和数据只有PCB、mm_struct、页表创建进程的时候我们可不可以只创建PCB虚拟地址空间页表然后物理内存内不加载进程是数据这样做可以吗答案是可以进程通过可执行程序内的代码和数据在虚拟地址空间开辟空间并在页表虚拟地址部分写入main函数的物理地址还是要获得的当进程运行时CPU拿到PCB就直接访问虚拟地址OS发现虚拟地址到物理地址的映射转换不过来所以页表内还有一个标记位记录虚拟地址对应的物理地址是否在内存里面若不在OS就会自动开展缺页中断自动加载数据到内存并且重新填充页表做缺页中断时进程是暂停的当缺页中断做完进程继续运行创造进程先有PCB、mm_struct等还是先加载代码和数据很明显上一个问题解决了这个问题要现有PCB这类的数据结构之后才会陆陆续续的加载数据如何理解进程挂起进程阻塞并且OS内存资源严重不足此时OS就要把阻塞进程的代码和数据唤出到磁盘上需要的时候再唤入那要如何理解进程挂起呢OS访问进程在PCB内查看进程状态发现进程阻塞状态此时OS就对进程做挂起操作清空页表并把对应的物理内存都唤出到磁盘的swap交换分区只保留进程管理部分内存就腾出来了进程挂起的时候OS中进程还存在这里就解耦的非常好进程可以分批加载内存也可以分批唤出内存 只对物理操作不会影响到进程管理堆区内不只有一个起始虚拟地址吧堆区细节性话题堆区虚拟地址有一个起始和终止的虚拟地址但是我们平时申请的堆区空间都可以获得对应的起始虚拟地址呀但是堆区只有一个起始地址堆区是离散的呀这要怎么解释栈有栈顶和栈底只需要移动栈顶需要移动代码区和数据区都是固定的命令行环境变量区是表也只需要在表内动但是堆区是离散的一个起始地址这么够在mm_struct中有一个vm_area_struct * mmap这样的链表vm_area_struct这个结构体中有vm_start、vm_end所以现在不用担心堆区不是连续的问题了因为虚拟地址空间内有mmap这样的一个结构体链表维护了vm_area_struct这个结构体这个链表会记录下来每一个子区域的起始和终止地址所以堆区空间不连续也没关系可以通过vm_area_struct记录下不同堆区的地址位置以分段不连续的方式吧地址空间划分好所以堆区可以有很多份每一份就有个vm_area_struct维护说到底mm_struct中只有代码段的vm_area_struct结点其他区域都会有对应的vm_area_struct节点维护记录区域的开始和结束而mm_struct是对整个虚拟地址空间的整体描述mm_struct对整体描述vm_area_struct是对具体区域进行描写这两者是一块的他们的地址数据可能有重复的但是mm_struct是整体的排布堆区有多个分散的那么mm_struct就记录整体堆区的开始和结束的地址而对应的子区域都有对应的vm_area_struct所以进程查地址的时候都可以不用关心mm_struct的起始与终止地址只需要在mmap的vm_area_struct中找到对应结构体中区域的起始和终止地址如果虚拟地址空间数据过多在链表中查看的效率会不太高所以会更改成红黑树来存储总结我们在Linux上使用地址都是虚拟地址在虚拟地址空间中提供的内存虚拟地址会通过页表映射到物理地址, 每一行代码和数据加载到内存都会有自己的地址所以到虚拟地址空间内也要开辟同等规模的地址然后通过页表构建映射关系进而完成虚拟到物理的转化进程 内核数据结构PCBmm_struct、页表 代码和数据每一个进程都会有自己独立的PCB虚拟地址空间和页表而页表通过映射可以找到不同位置的物理空间从而进程就具有独立性根据这节课也解决了之前很多的问题fork....
返回列表