Linux 进程地址空间
一.解决历史遗留问题
本节我们将通过解决全局变量gval引发的写时拷贝问题,来深入讲解Linux的进程地址空间。之后可能会把虚拟地址空间和进程地址空间混用,这是同一个概念。
1.进程地址空间引入
1.一个进程,一个进程地址空间
进程地址空间:宽度1字节,32位机器2^32个, 64位2^64个=4GB
0-3GB:用户空间 3-4GB:内核空间
作为用户,我们可以拿着变量的名字货地址直接访问变量
当我们创建一个变量时:有一个物理地址,被加载到内存;有一个虚拟地址,OS为每一个进程构建了一个叫做页表的东西(让进程的地址空间找到虚拟地址——物理地址的映射)
作为一个int变量,全局变量gval有四个字节,但是我们这里只拿到一个字节的地址 :地址数值最小的那个。 正因为有类型,所以根据起始地址+偏移量就可以访问到一整个变量。
那么对于子进程,地址空间,PCB,页表等内核数据结构都会从父进程拷贝(其实是浅拷贝):那么也就会有gval的虚拟地址。这样就能理解上面的例子,相当于让父子进程的虚拟地址都指向同一个物理地址。
2.写时拷贝引入
那么后来,子进程就要对变量进行修改。我们知道,进程具有独立性:那么是如何修改的?这时OS介入,把老变量的内容拷贝到物理内存得到一个新物理地址,让子进程的虚拟地址对应指向这个新物理地址,这就是发生了写时拷贝,保证了进程的独立性。这时父子进程的对于全局变量gval的虚拟地址还是相同的。
tips:OS把物理地址隐藏,我们是无法直接拿到物理地址的。
那么我们就能回答之前关于fork函数的回答:为什么会同时执行else-if和else?因为拿到的是同一个虚拟地址,但是映射到不同的物理地址,展现给用户的就是这种效果。
3.深入理解进程地址空间
我们先来讲一个例子。
假如现在老板有10亿用来发公司年终奖,它分别对四个员工说:只要你们好好干,公司的10亿年终奖就是你的了。我们一般称这种行为叫——画大饼。要让每个员工都拿到10亿年终奖是不可能的,因为预算只有10亿;但是每个员工用少量的钱去买个中午饭,还是可以全部满足的。
画大饼让每一个进程都认为自己都有4GB物理内存(独占内存)。
如果人(进程)很多,人需要管理;那么画的饼(虚拟地址空间)也需要管理。
那么怎么管理?先描述,再组织。
OS为了要把每个进程的虚拟地址空间管理,定义了一个数据结构+算法管理。
也就是说,虚拟地址空间本质就是一个结构体对象。
在此深入之前,我们先讲讲什么叫区域划分。
1.区域划分:
我们小时候可能都有过这样的经历,和同桌约定好桌子上的“三八线”,谁越过了谁就要受惩罚。
我们用计算机量化就是:约定好总尺寸,并约定好单位长度(编址),然后划分范围。
对于进程地址空间中,每一个“小朋友”(数据段)都有自己的起止位置,那么mm_struct应该包括:
各式各样的数据段的起止位置。
如此这般就把地址空间成功划分。调整区域,就是修改某些区域的起止位置。
我们来内核源码中看一看这个所谓的进程地址空间:可以看到mm_struct在每一个进程的task_struct中。
再来详细看看mm_struct
2.mm_struct中的地址划分
你磁盘中的代码和数据加载到物理内存时,占多少物理空间,就会预先在mm_struct的正文部分预留多少空间,然后由页表映射物理地址和虚拟地址,申请物理空间。我们接下来要聊两个问题:
问题1:怎么在虚拟地址空间中申请指定大小的空间?
调整划分区域大小即可
问题2:那么,mm_struct的初始化如何完成?
在程序加载到物理内存时进行的。
3.为什么要设置地址空间?
1.将无序变有序,数据和代码加载到内存时是不确定在哪里的,用户拿着虚拟地址去访问某个物理内存中的代码是软硬件协作的结果,用户无需关注。
2.页表中除了虚拟地址和物理地址,还有权限区。如果当前代码只读,用户做了非法操作,操作系统的读页表操作就无法正常转到物理内存,甚至直接杀掉进程。从而保护物理内存。
3.什么是野指针:如果我们访问一个在虚拟地址空间中已经被释放的变量或对象,在页表中不存在这对映射关系,查页表会失败,进程会被杀掉。对于下面的代码:
char *str="helloworld";
*str='H';
为什么字符常量区要写入,程序会崩溃?因为在查找页表时,发生了权限拦截。
4.假如现在要加载2GB内存,现在虚拟地址空间的正文部分划分好,并且把页表的虚拟地址部分填好,而物理地址部分只填写四分之一。继续跑后面代码时,发现虚拟地址存在,但是物理地址没填写,就会出发缺页中断,此时再把磁盘中一部分内容加载到内存中,就实现了动态加载的方式。
合理的区分出进程管理和内存管理,实现了一定程度解耦。
5.实际上,可以只创建进程,PCB,地址空间,页表及初始化,而可以一点数据代码都不加载
创建进程,现有PCB,mm_struct等数据结构,再有代码和数据的加载。
6.那么如何理解进程挂起?发现一个进程的状态为阻塞,而内存空间严重不足 ,OS把页表清空,把当前进程的代码和数据换到磁盘的swap分区。下次缺页中断时直接从swap分区加载即可
4.关于mm_struct的地址划分
再聊这话题前,我们先考虑一件事:不同于栈的空间,堆的空间是由代码通过new/malloc操作创建的一系列离散的空间,mm_struct如何管理这些离散的空间?
我们需要重点关注mm_struct中的VMA(vm_area_struct)结构体。
事实上,mm_struct会维护一张叫VMA的链表,这样就能把各个离散的结构组织起来。
这样看来,mm_struct是对进程地址空间的整体描述,具体各个字段的管理由vmarea进行。
这里的VMA太多,会转化为红黑树进行管理。
5.进程独立性
进程拥有独立性,就是内核数据结构独立+加载到内存的代码和数据互相独立。