Linux 内存管理

在Linux中,对于内存地址的处理,采用的是虚拟内存地址技术。之所以要使用虚拟内存,是因为对于每一个进程来说,都有自己的地址空间,而计算机的物理内存只有一份,每一个程序在编写的时候,都不会考虑到其他程序所需要的内存地址,也无法考虑,由于不同的进程的地址空间都是独立的(除了共享的之外),这样的话,程序编写就不知道怎样获得自己的所需内存,而采用虚拟内存的话,就解决了很多的问题,每一个进程的虚拟内存地址都是一样的,而实际运行时所对应的真实物理地址就直接的交给系统来处理,程序不需要考虑,同时也解决了内存空间独立的问题。
对于32为的机器,其所能访问的物理内存大小为4GB,所以每一个进程所能得到的虚拟内存大小为4GB,由于系统运行需要使用内存,也就是所谓的内核空间,一般大小为1GB,所以一般的进程所能使用的用户空间为3GB,地址从0开始。对于64位的机器,内存地址可用空间为0x0000000000000000 ~ 0xFFFFFFFFFFFFFFFF,这是个相当庞大的空间,Linux实际上只用了其中一小部分(256T)。Linux64位操作系统仅使用低47位,高17位做扩展(只能是全0或全1)。所以,实际用到的地址为空间为0x0000000000000000 ~ 0x00007FFFFFFFFFFF和0xFFFF800000000000 ~ 0xFFFFFFFFFFFFFFFF,其中前面为用户空间(User Space),后者为内核空间(Kernel Space),如下图所示:
64位Linux虚拟内存空间

对于内存的管理,一般有两种方式:分段机制和分页机制,下面就只简单的介绍一下分页机制。

以32位的系统来说,对于4GB的虚拟内存,系统要怎样来管理该内存呢?一般就是采用所谓的分页机制,就是把这么大的内存按照每一页的大小分成很多页,内存的管理也就以页作为单位,而不是以字节作为单位。对也4GB的地址,如果按照每一页4K大小计算的话,那么总共需要的页数为2^20,这个时候就需要一个页目录来存储这些页的信息,以方便查找,每一个页表项存储的就是对应页的内存起始地址,每一项的大小为4Byte,这样的话,页目录所需要的内存大小为2^20*4Byte,也就是4MB的大小。

而对于进程来说,一般不会使用这么大的内存空间,加上程序对内存的访问具有局部性,这样的话,就会出现很多的页表项不会被用到,也就是程序所需要的页数很少。如果一直将所有的页目录存在内存的话,或造成很到的内存浪费,此时就出现了多级页表了。

以二级页表来说,将总的页目录按照页的大小(4KB)划分,所得到的二级页数为:4MB/4KB=1K,此时引入一级页表,用来存储二级页表的信息,那么每一个一级页表项的大小为4B,所需要的一级页表大小为4KB,恰好也是一个页的大小,这样,进程在运行的时候,只需要先读取一级页表,接着在根据需要对二级页表以及内存页进行配置,这样就可以大大减少页的索引信息了(因为大部分都是不会被索引的到的,只需要记录目前需要索引的页信息)。

上面说道二级页表,那么对于一个线性地址(虚拟地址),内存怎样把他映射为对应的物理地址呢?我们知道在二级页表下,一级页的大小为4KB,也就是对应着1K的二级页表,所以要索引二级页表,需要将虚拟地址的高10位用来作为一级页表的表内便偏移索引,在找到二级页表后,二级页表也有1K的页数,所以需要虚拟地址的中10位作为二级页表的表内偏移索引,在得到对应的物理页地址的时候,由于每一页有4K大小,想要找到具体的字节地址,那么需要12位的索引,也就是32位地址所剩下的底12位。这样就完成了一个虚拟地址到实际的物理地址的映射。

对于一级页表,其起始地址要怎样存储呢?一般的话,由于起始地址是一个4B的指针,可以存储在寄存器上,所以每次进程运行的时候,每一个进程都有自己的一级页表起始地址,当进程被加载运行的时候,操作系统为其分配的一级页表地址就直接的存在CR3寄存器中,这样开始了进程的虚拟地址访问。

完成虚拟地址到物理地址的转换一般是MMU(Memory Management Unit)硬件来实现的。为了实现跟快的转换,就有了TLB(TranslationLook-aside Buffer),用来根据程序访问内存的局部性机制来缓存已经转换过的虚拟页与实际页的对应关系!TLB 中包含了最近使用过的页面的内存映射信息,处理器提供了专门的电路来并发地读取并比较TLB中的页面映射项。因此,对于频繁使用的虚拟地址,它们很可能在TLB中有对应的映射项,因而处理器可以绝对快速地将虚拟地址转译成物理地址;反之,如果一个虚拟地址没有出现在TLB中,那么处理器必须采用以上介绍的两次查表过程(意味着要两次访问内存)才能完成地址转译。在这种情况下,这一次内存访问会慢一些,但是,经过这次访问以后,此虚拟页面与对应物理页面之间的映射关系将被记录到TLB中,所以,下次再访问此虚拟页面时,处理器就可以从TLB 中实现快速转译,除非此映射项已经被 TLB 移除了。研究表明,由于计算机程序的内存访问有一定的局部性,因此,即使处理器只维护一个相对较小的TLB,程序的运行也能获得较显著的性能提升。

进程的建立和执行

执行程序时,操作系统会创建一个执行该程序的进程,然后装载程序或程序片段等,然后开始顺序执行代码段。在这个过程中,操作系统总的来说做三件事情:

(1) 为进程创建一个独立的虚拟地址空间(范围)

例如在32位系统常规分页状态下,操作系统发现待执行程序的指令和数据总和为32KB,那么操作系统会为进程分配8个页的虚拟内存空间,并分配页目录和页表,把页目录装入CR3,把进程用到的页表加载到内存。但并不把指令和数据加载到内存。

(2) 读取程序可执行文件文件头,并且建立虚拟空间与可执行文件中的代码段、数据段的逻辑地址的映射关系

这一步将程序指令和数据映射到虚拟内存空间中。

(3) 将 CPU 的指令寄存器设置成可执行文件的入口地址,启动运行

执行程序过程时,如果当前指令或数据之在虚拟地址空间中,而实际上并不在物理内存中(前两步都没有将指令或数据加载到物理内存),将发生页错误,这时操作系统再从物理内存分配一个空闲的物理页帧,并将虚拟地址页对应的数据从磁盘拷贝加载到物理页帧中,并建立页表项和页帧的映射关系。随着进程的执行,页错误也会不断产生,操作系统也会响应每个页错误并为进程分配物理内存页帧。但物理内存是有限的,为一个进程可分配的物理内存也有限。全部可用物理内存都分配给进程后,如果进程继续抛出页错误请求更多物理内存,这时候操作系统根据自身的页置换操作算法,在保证进程正常运行的前提下,将先前为进程分配的物理内存页帧收回,重新分给该进程。