内存管理
每个进程独占的内存空间(虚拟地址空间)都是独立的、相互隔离的。
虚拟地址空间分为内核空间和用户空间,站在进程的视角虚拟地址空间如下图:
用户空间从最低位开始排起:
TextSegment 是存放二进制可执行代码的位置
Data Segment 存放静态常量
BSSSegment 存放未初始化的静态变量
(这里就是把二进制执行文件的三个部分加载到内存里面)
堆(Heap)堆是往高地址增长的,是用来动态分配内存的区域
Memory Mapping Segment 这块地址可以用来把文件映射进内存
栈(Stack)主线程的函数调用的函数栈就是用这里的
进程的视角虽然可以看见内核空间,但是不能访问,如果需要进行更高权限的工作,就需要通过系统调用进入内核。
一旦进入了内核,就换了一副视角。刚才是普通进程的视角,觉着整个空间是它独占的,没有其他进程存在。当然其他进程也这样认为,因为它们互相看不到对方。但是到了内核里面,无论是从哪个进程进来的,看到的都是同一个内核空间。
内核的代码访问内核的数据结构,大部分的情况下都是使用虚拟地址的,虽然内核代码权限很大,但是能够使用的虚拟地址范围也只能在内核空间,只能用 30 号到 39 号这些编号,不能用 0 到 29 号,因为这些是被进程空间占用的。而且,进程有很多个。你现在在内核,但是你不知道当前指的 0 号是哪个进程的 0 号。
上面介绍了虚拟地址空间的布局,但是它是如何映射成物理空间的呢?
答案是利用分段机制,如图:
分段机制下的虚拟地址由两部分组成:
段选择子:保存在段寄存器中。记录短号和特权等标识位,其中段号用作段表的索引。段表里面保存的是这个段的基地址、段的界限和特权等级等
段内偏移量:位于 0 和段界限之间
如果段内偏移量是合法的,就将段基地址加上段内偏移量得到物理内存地址,如下图:
分段机制虽解决了虚拟空间映射成为物理地址的问题,但由图可以看出分段机制的内存使用效率很低。所以Linux 更倾向于另外一种从虚拟地址到物理地址的转换方式,就是分页机制。
对于物理内存,操作系统把它分成一块一块大小相同的页,这样更方便管理,例如有的内存页面长时间不用了,可以暂时写到硬盘上,称为换出。一旦需要的时候,再加载进来,叫作换入。这样可以扩大可用物理内存的大小,提高物理内存的利用率。
换入和换出都是以页为单位的。页面的大小一般为 4KB。为了能够定位和访问每个页,需要有个页表,保存每个页的起始地址和页内偏移量,组成线性地址,就能对于内存中的每个位置进行访问了。如图:虚拟地址分为两部分,页号和页内偏移。页号作为页表的索引,页表包含物理页每页所在物理内存的基地址。这个基地址与页内偏移的组合就形成了物理内存地址。
最后我们来看下进程运行状态下的内存结构(64位):