【Linux手册】进程地址空间:从虚拟到物理的内存寻址之旅
目录
前言
是什么
地址总线是什么
进程地址空间的概念
操作系统如何实现它的
虚拟地址
页表
两个标志位
为什么要有地址空间
补充
前言
进程地址空间是大家学习操作系统时不可绕过的一个话题,大家对于进程地址空间的影响可能都是很难,抽象,不理解。是的,也确实如此。本文将从是什么,为什么两个方面来具体剖析进程地址空间,希望能帮你更好的梳理进程地址空间的脉络和内容。
是什么
进程地址空间是什么???在学习C语言或C++时,老师一定讲过代码数据都是放在内存中的指定位置:代码段,栈或堆......那这些是不是进程地址空间呢?? 下方放入示意图
是的,这就是进程地址空间的经典布局示意图。
对于上面代码可以通过一个程序简单验证一下。
栈区是向下增长的,堆区向上增长,两者相向增长。static修饰的局部变量,编译时已经被放在了全局区,所以在函数调用结束之后不会被销毁。
在介绍进程地址空间概念之前,先说说地址总线是什么,地址总线会直接影响到CPU能拿取内存中那些区域的数据。
地址总线是什么
在32位计算机中,用32个比特位来表示地址,CPU与内存之间有32根地址总线;通过这32根数据总线来先内存发送信号,用低电平代表0,用高电平代表1,将这32根总线的数据信息进行整合,就能得到一个32个比特位的地址,来告诉内存要访问什么地址。
32个比特位能够表示的组合方式一共有:2^32种所以内存的范围就是[0,2^32),每一个字节都有一个地址,所以2^32个地址对应2^32个字节,就是4GB的空间,所以在32位机器上,CPU能够读取到4GB大小的空间,这些空间也真是进程能够使用的空间大小。
进程地址空间的概念
所谓的进程地址空间就是描述进程可视范围的大小,是对内存区域一个个的划分。用来描述内存中各个区域存放什么数据的,比如栈区存放临时变量,堆区存放动态开辟的空间.....
操作系统如何实现它的
进程地址空间也要被操作系统进行管理,而操作系统管理的逻辑就是:先描述,再组织。那操作系统中用什么来描述进程地址空间呢??在Linux中有一个结构体mm_struct(内存描述符)。看看源码中有什么:
上面大部分源码我们看不懂,但是可以看到mm_struct结构体中确实存放的有各个区域范围划分,其通过两个变量来记录一个区域起始和结束的位置。
通过对区域进行划分,进程在获取资源的时候就可以到指定的区域进行查找了。
操作系统要同时管理多个进程,那么操作系统是如何对这些进程地址空间进行组织的呢??
通过两种方式:
- 当进程地址空间较少的时候,使用双链表进行组织,由mmap指针指向双链表的头;
- 当进程地址空间较多的时候,使用红黑树进行管理,由mm_rb指向红黑树。
进程地址空间是由多个区域组成的,在Linux中使用vm_area_struct来描述每一个区域,其组织的方式与上面两种方式类似也是双链表或红黑树。
虚拟地址
我们看到的地址是"真"的吗???
什么意思呢???我们在程序中打印出的地址难不成会是"假的"???是的,我们看到的地址空间是"假的",可以通过以下代码进行简单验证。
x的地址是一样的,但是值为什么不一样。相同的地址打印出了不同的值,这说明我们看到的地址一定不是数据在内存上的地址,一定不是物理地址。我们所看到的地址是虚拟地址或线性地址。
我们平时在写C/C++等代码的时候使用指针,指针内存放的是虚拟地址,而不是物理地址。那这也同样说明进程地址空间是对虚拟地址的管理而不是物理地址,是对虚拟地址的划分。
如果上面的地址空间都是假的,是虚拟地址空间,那CPU计算处理的时候一定要拿到数据呀,它肯定要到物理地址上去访问呀,那CPU又是如何拿到物理地址的呢?
物理地址和虚拟地址是依靠页表连接起来的。
页表
页表是一张"表",其上面记录了每一个虚拟地址对应的物理地址。
通过页表可以将虚拟地址映射到物理地址上。
那么怎么解释在相同的虚拟地址打印出了不同内容???
fork在创建子进程的时候:
- 先根据父进程的PCB结构体对象为子进程创建一个PCB数据结构体对象;
- 因为资源和数据是共享的,所以子进程的进程地址空间与父进程一样,拷贝父进程的;
- 页表映射关系也与父进程的一样,拷贝父进程的;
- 当子进程要对数据进行修改时,因为进程之间的具有独立性,会进行写实拷贝:操作系统先将原数据拷贝一份放到内存的其他位置,让子进程进行修改。在这个过程中子进程页表的物理地址进行了修改,而虚拟地址是0感知的,没有进行修改。
所以才出现了"相同地址"存储不同内容的现象。
操作系统要同时管理多个进程,那他又是如何对页表进行管理的呢???
对于页表的管理比较简单,在CPU中有一个cr3的寄存器其中专门存放着正在CPU上运行的进程页表起始地址,通过这个起始地址就能拿到页表。寄存器上的内容本质上也是进程的上下文,当进程从CPU上下来的时候,会保存到进程的PCB结构体对象中,这样进程的页表信息就不会丢失。
两个标志位
页表上不仅有虚拟地址到物理地址的映射,其上面还有两个标记位:权限位和存在位/有效位。
权限位
权限位用来标记虚拟地址对应物理地址上的可读,可写权限。
通过权限位使得页表可以进行很好的权限管理,阻止用户的一些非法访问。如:当进程要对一个位置进行修改的时候, 操作系统会拿着虚拟地址到页表上进行比对,如果发现进程要进行非法修改,就会直接将进程杀死。
所以代码区和字符常量区为什么不能修改???
因为代码区和字符常量区在页表中的权限是只读权限,操纵系统禁止进程对这些区域进行修改。
有效位
在计算机上同时有多个进程在跑,CPU通过并发的方式使得各个进程在一段时间内都能运行,每个进程只能在CPU上跑一会,这就意味着进程只能运行部分代码,处理部分数据,那么内存是如何加载进程的代码和信息的,如果直接将代码和数据全部直接加载到内存中必定会造成浪费,如果不是又是如何解决的呢???
为了防止内存空间的浪费,在页表上增加了一个有效位,用来记录进程要访问的数据是否已经加载到内存中了,如果没有加载到内存中,就会发生缺页中断让数据再加载到内存中来。
为什么要有地址空间
先说说如果没有进程地址空间,让进程直接访问物理内存会怎么样:
- 不安全。进程直接管理物理内存就意味着进程能够任意的管理内存空间,如果是一个病毒就有可能随意篡改内部数据;
- 地址不确定。程序如果多次运行其在物理内存上的地址都是可能不一样的,因为如果程序第一次运行时,前面有进程和前面没有进程存放的位置可能都是不一样的;
- 效率不高。如果当前进程已经加载到内存一部分后发现内存的空间不够,此时如果空出了一部分的内存供其使用,那么进程原来的部分就需要进行重新分配,重新进行拷贝。
使用进程地址空间就能有效的解决这些问题:
- 让所有进行以统一的视角看待内存,每个位置存放的数据类型都是固定的;
- 通过页表中的权限为可以让操作系统阻止一些非法访问,对我们的寻址请求进行审视,如果发现我们要对只读区进行修改就直接将进程杀死,使得请求不会到物理层,保护了物理内存;
- 页表可以很好的将内存管理和进程管理进行解耦,当进程要访问数据的时候只需要拿着虚拟地址去访问即可,不需要关心数据和代码是如何存放的。
补充
fork在创建子进程是有一个细节:
- 子进程在拷贝父进程的页表时会将页表的所有权限都设置为只读;
- 当子进程要对数据进行修改的时候就会发生缺页中断,让操作系统为其开辟额外空间。
当当然对于代码区和字符常量区操作系统还是能分辨的,子进程要对这些内容进行修改时,操作系统就直接杀死进程了,而不是缺页中断。