存储管理是操作系统的重要组成部分。Linux操作系统采用了请求分页虚拟存储管理的方法。系统为每个进程提供了4G的虚拟内存空间。各个进程的虚拟内存彼此独立。
Linux运行在X86架构时,进程的虚拟内存为4GB。进程虚存空间的划分在系统初始化时由GDT确定。
Linux的存储管理主要是管理进程虚拟内存的用户区。进程虚拟内存的用户区分为代码段、数据段、堆栈以及进程运行的环境变量、参数传递区域等。每个进程都有一个mm_struct结构体来定义它的虚存用户区。mm_struct结构体首地址在任务结构体task_struct成员项mm中。
虚存区域是虚存空间中一个连续的区域,在这个区域中的信息具有相同的操作和访问特性。每一个虚拟区域用一个vm_area_struct结构体进行描述。
虚存空间的映射和虚存区域的建立
虚拟内存(虚存)使计算机可以操纵更大的地址空间,还可以使系统中的每一个进程都有自己的虚拟地址空间。这些虚拟的地址空间是相互完全分离的,所以运行一个应用程序的进程不会影响另外的进程。另外硬件的虚拟内存机制允许对内存区写保护。这可以防止代码和数据被错误的程序覆盖,内存映射可以将CPU的虚拟地址空间映射到系统的物理内存。
核心的共享虚拟内存机制,虽然允许进程拥有分离(虚拟)的地址空间,但有时也需要进程之间共享内存。例如,系统中可能有多个进程运行命令解释程序bash。尽管可以在每个进程的虚拟地址空间都拥有一份bash的备份文件,但更好的方法是在物理内存中只拥有一份备份文件,所有运行bash的进程共享代码。动态链接库是多个进程共享执行代码的另一个常见例子。另外共享内存也经常用于进程间通信机制,两个或多个进程可以通过共同拥有的内存交换信息。Linux系统支持系统V的共享内存IPC机制。
Linux虚存采用动态地址映射方式,即进程的地址空间和存储空间的对应关系是在程序的执行过程中实现的。进程使用的是虚拟地址,因此它对每个地址的访问都需要通过MMU把虚拟地址转化为内存的物理地址。
动态地址映射使Linux可以实现进程在主存中的动态重定位、虚存段的动态扩展和移动,也为虚存的实现提供了基础。当Linux中的进程映像执行时,需要调入可执行映像的内容。但并不用把这些数据直接调入物理内存,而只需要把这些数据放入该进程的虚拟内存区。只有当执行需要这些数据时才真正的调入内存。这种进程的映像和虚拟进程空间的连接称为内存映像。当需要将进程映像调入进程的虚拟内存空间时,需要申请一段合适的虚拟内存空间,这时需要用到mmap系统调用来获得所需的内存空间。
内存空间/地址类型
CPU地址空间(Address Space)是一段表示内存位置的地址范围。在X86架构下,地址空间有三种:物理地址空间、线性地址空间和逻辑地址空间(虚拟地址空间)。而在ARM体系结构下只有物理地址空间和逻辑地址空间(虚拟地址空间)。
物理地址是一个系统中可用的真实的硬件地址。例如,一个系统有128MB 内存,它的合法地址范围是0~0x8000000(以十六进制表示)每个地址都对应于所安装的SIMMs中的一组晶体管,而且对应于处理器地址总线上的一组特定信号。
逻辑地址则是CPU所能处理的地址空间的总和,对于32位CPU而言,它的逻辑地址空间是4GB。采用逻辑地址空间的好处是每个用户进程都有自己独立的运行空间,而不用管自己在物理内存的实际位置。在Linux系统中,4GB的地址空间由Linux内核与Linux应用程序共同分享。
用户空间使用0x00000000~0xBFFFFFFF共3G的地址空间,用户态进程可以直接访问此空间。内核空间则使用0xC0000000~0xFFFFFFFF剩下的1GB地址空间,存放内核访问的代码和数据,用户态进程不能直接访问。用户进程只有通过中断或系统调用进入核心态时才有权利访问。
在逻辑地址和物理地址之间相互转换的工作是CPU的内存管理单元MMU完成的。Linux内核负责告诉MMU如何把逻辑页面映射到物理页面。通常,内核需要维护每个进程的逻辑地址和物理地址对照表,在切换进程时,更新MMU的对照表信息。而MMU在进程提出内存请求时会自动完成实际的地址转换工作。
在X86体系结构上,把线性地址映射到物理地址分为两个步骤。提供给进程的线性地址被分为3个部分:一个页目录索引、一个页表索引和一个偏移量。页目录(Page Directory)是一个指向页表的指针数组,页表(Page Table)是一个指向页面的指针数组,因此地址映射就是一个跟踪指针链的过程。一个页目录能够确定一个页表,继而得到一个页面,然后页面中的偏移量(Offset)能够与指出该页面中的一个地址。
为了进行更详细而准确的描述,给定页目录索引中的页目录项保存着存储在物理内存中的一个页表地址,给定页表索引中的页表项保存着物理内存上相应物理页面的基地址,然后线性地址的偏移量加到这个物理地址上形成最终物理页面的目的地址。
分页机制与MMU
Linux的内存管理采用分页管理,使用多级页表,动态地址转换机构与主存、辅存共同实现虚拟内存。每个用户进程拥有4GB的虚拟地址空间,进程在运行过程中可以动态浮动和扩展,为用户提供了透明的、灵活有效的内存使用方式。这正是进程被分配一个逻辑地址空间的原因之一。即使每个进程有相同的逻辑地址空间,通过分页机制,相应的物理地址也是不同的,因此他们在物理上不会彼此重叠。
从内核角度来看,逻辑地址和物理地址都被划分成为固定大小的页面。每个合法的逻辑页面敲好处于一个物理页面中,方便MMU的地址转换。当地址转换无法完成时(例如由于给定的逻辑地址不合法或由于逻辑页面没有对应的物理页面),MMU将产生中断,向核心发出信号。Linux核心可以处理这种页面错误(Page Fault)问题。
MMU也负责增强内存保护,当一个应用程序试图在它的内存中队一个已标明是只读的页面进行写操作时,MMU也会产生中断错误,通知内核。在没有MMU的情况下,内核不能防止一个进程非法存取其他进程的内存空间。
每个进程都有一套自己的页目录与页表,其中页目录的基地址是关键,通过它才能查到逻辑所对应的物理地址。页目录的基地址是每个进程的私有资源,保存在该进程的task_struct对象的mm_struct结构变量mm中。
在进程切换时,CPU会把新进程的页目录基地址填入CPU的页目录寄存器,供MMU使用。当新进程有地址访问时,MMU会根据被访问地址的最高10位从页目录中找到对应的页表基地址,然后根据次高10位再从页表中找到对应的物理地址的页首,最后根据剩下的12位偏移量与页首地址找到逻辑地址对应的真正物理地址。
高速缓存
Linux使用了许多与高速缓存相关的内存管理策略。
1缓冲区高速缓存
缓冲区高速缓存中包含被块设备驱动使用的数据缓冲,这些缓冲单元的大小一般都固定,包含从块设备读出或写入的信息块。块设备是仅能够以固定大小块进行读/写操作的设备,所有的硬盘都是块设备。利用设备标示符和所需块号作索引可以在缓冲区高速缓存中迅速找到数据块。块设备只能够通过缓冲区高速缓存来存取。如果数据在缓冲区缓存中可以找到,则无须从物理块设备(如硬盘)中读取,这样可以加速设备的访问速度。
2页高速缓存
页高速缓存用来加速块设备上可执行映像文件与数据文件的存取。它每次缓冲一个页面的文件内容。页面从块设备上读入内存后放入页高速缓存中。
3交换高速缓存
只有修改过的页面才存储在交换文件中。如果这些页面在写入到交换文件后没有被修改,则下次被交换出内存时就不必再进行更新写操作。这些页面都可以丢弃在交换频繁发生的系统中。交换高速缓存可以减少很多不必要且耗时的块设备操作。
4硬件高速缓存
常见的硬件高速缓存是处理器中的指令和数据Cache,它缓存CPU最近访问过的指令和数据,使CPU不需要到内存中获得数据。Cache是CPU与内存之间的桥梁。目前常见的CPU中Cache的实现按照读/写方式分类:
1)贯穿读出式
该方式将Cache隔在CPU与主存之间,CPU将主存的所有数据请求都首先发送到Cache,由Cache自行在自身查找。如果命中,则切断CPU对主存的请求,并将数据送出;
如果没有命中,则将数据请求传给主存。该方法的优点是降低了CPU对主存的请求次数,缺点是延迟了CPU对主存的访问时间。
2)旁路读出式
在这种方式中,CPU发出数据请求时,并不是单通道地穿过Cache,而是向Cache和贮存同时发出请求。由于Cache速度更快,如果命中,则Cache在将数据会送给CPU的同时,还来得及中断CPU对主存的请求;如果没有命中,则Cache不做任何动作,由CPU直接访问内存。它的优点是没有时间延迟,缺点是每次CPU对主存的访问都存在,这样就占用了一部分总线时间。
3)写穿式
任一从CPU发出的写信号送到Cache的同时,也写入主存,以保证主存的数据能同步更新。它的优点是操作简单,但由于主存的速度慢从而降低了系统的写速度并占用了总线的时间。
4)回写式
为了克服贯穿读出式中每次数据写入时都要访问主存,从而导致系统写速度降低并占用总线时间的弊病,尽量减少对主存的访问次数,又有了回写式。它的工作原理是:数据一般只写到Cache,这样有可能出现Cache中的数据得到更新而主存中的数据不变(数据陈旧)的情况。但此时可在Cache中设定一个标识地址及数据陈旧的信息,只有当Cache中的数据被再次更改时买菜将原更新的数据写入主存相应的单元中,然后再接受再次更新的数据。这样保证了Cache和主存中的数据不产生冲突。