Linux 存储
存储管理概述
- 存储空间的分配;
- 存储地址的变换;
- 存储空间的保护;
- 存储空间的扩充。
内存的分配与回收
内存分配是为进入系统准备运行的进程分配内存空间,内存回收是当进程运行结束后回收其所占用的内存空间。
存储分配方案主要包括以下要素:
-
存储空间的描述结构。
-
存储分配的策略。
把程序中的逻辑地址转换为程序所在的实际内存地址,这一转换过程称为存储空间的地址变换,或称为地址映射。
存储地址变换是由内存管理模块与硬件的地址变换机构共同完成的。
地址的概念
-
符号地址
在用高级语言编写的源程序中,编程者使用符号名(变量名、函数名、语句标号等)来表示操作对象或控制转移的地址。
-
逻辑地址
编译程序将源代码中的语句逐条翻译为机器指令,为每个变量分配存储单元,并用存储单元的地址替换变量名。这些指令和数据顺序存放在一起,从0开始编排地址,形成目标代码。目标代码所占有的地址范围称为逻辑地址空间。
-
物理地址
物理内存由一系列的内存单元组成,这些存储单元从 0 开始按字节编址,称为内存地址。
地址变换
-
静态地址变换。
程序在装入内存前一次性完成地址转换。程序装入内存后即可直接执行。DOS系统的程序就是采用这种方式加载的。采用静态地址变换的程序在内存中始终处于最初加载的位置,不可移动。这种方式不利于内存管理,目前已被淘汰。
-
动态地址变换。
程序在装入内存时不进行地址变换,而是保持指令中的逻辑地址不变。在程序执行过程中,每执行一条指令时,如果指令中用到了逻辑地址,地址变换机构就会自动进行地址变换,将其变换为实际地址。动态地址变换是现代操作系统普遍采用的方式,其特点是程序在内存中可移动、可共享。
内存保护
确保每个进程都在自己的地址空间中运行,互不干扰,尤其是不允许用户进程访问操作系统的存储区域。
对于允许多个进程共享的内存区域,每个进程也只能按自己的权限(只读、读写或执行)进行访问,不允许超越权限进行访问。
常用的存储保护措施如下:
-
界限保护。
在CPU中设置界限寄存器,限制进程的活动空间。
-
保护键。
为共享内存区设置一个读写保护键,在CPU中设置保护键开关,表示进程的读写权限。只有进程的开关代码和内存区的保护键匹配时方可进行访问。
-
保护模式。
将CPU的工作模式分为用户态与核心态。核心态下的进程可以访问整个内存地址空间,而用户态下的进程只能访问在界限寄存器所规定范围内的空间。
存储管理方案
段式存储管理
段( segment)是逻辑上完整的信息单位,划分段的依据是信息的逻辑完整性以及共享和保护等需要。分段后,程序的逻辑地址空间是一个二维空间,其逻辑地址由段号和段内位移两部分组成。
段式分配策略是以段为单位分配内存,每个段分配一个连续的分区。段与段间可以不相邻接,用段表描述进程的各段在内存中的存储位置。段表中包括段长和段起始地址等信息。
段的分配与释放
段式分配的方法是:系统用表格记录已分配分区和空闲分区的分布和使用情况。当进程建立时,系统为进程的各段分配一个连续的存储区,并为它建立段表。进程结束后,系统回收段所占用的分区,并撤销段表。进程在运行过程中也可以动态地请求分配或释放某个段。
段式地址变换
段式系统通过段表进行动态地址变换。每个进程有一个段表,另外在CPU中设有一个段表寄存器。
以逻辑地址中的段号为索引去检索段表,得到该段在内存的起始地址,与逻辑地址中的段内位移相加就可得到实际的内存地址。
段式存储的共享、保护与扩充
段式存储允许以段为单位的存储共享。段的共享就是内存中只保留该段的一个副本,供多个进程使用。
段式存储的保护方式主要是界限保护。当CPU访问某逻辑地址时,硬件将段号与段表长度进行比较,同时还要将段内地址与段表中该段长度进行比较,如果访问地址合法则进行地址变换,否则产生地址越界中断信号。对共享段还要检验进程的访问权限,权限匹配则可进行访问,否则产生读写保护中断。
段式存储管理的特点与问题
便于程序模块化处理,可以充分实现分段共享和保护。但由于段需要连续存储,可能出现“碎片”问题,降低了存储空间的利用率。
页式存储管理
分页(paging)的概念是:将进程的逻辑地址空间分成若干大小相等的片段,称为页面(page),用 0, 1, 2, …
序号表示;同时,把内存空间也按同样大小分为若干区域,称为页帧(page frame),也用 0, 1, 2, …
序号表示。
经过分页后,进程使用的逻辑地址可看成由两部分组成,即页号+页内位移。
以页为单位为进程分配内存,每个页帧装一页。一个进程的逻辑地址空间的各个页面可分散存放在不相邻的页帧中,用页表记录页号与页帧号之间的映射关系。
页表是进程的一个重要资源,它记录了进程的页面与页帧的对应关系。
页面的分配与释放
系统设有一个内存分配表,记录系统内所有页帧的分配和使用状况。内存分配表可采用位图的方式或空闲链表方式表示。
当进程建立时,系统根据进程地址空间的大小查找内存分配表,若有足够的空闲页帧则分配给进程,为其建立页表并将页表信息填入进程的PCB中。若没有足够的空闲页帧则拒绝进程装入。进程结束时,系统将进程占用的页帧回收,并撤销进程的页表。
页式地址变换
每个进程有一个页表,通常存放在内存中,页表的长度和内存地址等信息则存放在进程的PCB中。另外,在 CPU中设有一个页表寄存器,用来存放正在执行的进程的页表长度和内存地址。
将逻辑地址按位分成页号和页内位移两部分,再以页号为索引去检索页表,得到该页号对应的页帧号。将页内位移与页帧号拼接即得到实际内存地址。
页式存储的保护与扩充
页式存储的地址保护是通过对访问地址的页号进行控制来实现的。在地址变换前,硬件将页号与页表长度进行比较,如果没有超出页表长度,则进行转换,否则产生地址越界中断信号。
对共享页面的操作是通过访问权限来限制的,方法是在页表中增加一个读写权限字段,只有当对该页的访问操作与此权限的设置相匹配时方可访问,否则产生读写保护中断。
页式存储管理的特点
页式存储管理是目前大部分系统所采用的内存管理方案。页式管理的优点是解决了内存碎片问题,有效地利用了内存,使存储空间的利用率大大地提高。
虚拟存储管理
内存扩充
基本思想是借用外存空间来扩展内存空间,方法是让程序的部分代码进入内存,其余驻留在外存,在需要时再调入内存。
-
覆盖(overlay)
技术的原理是将一个程序划分为几个模块。程序的必要模块(主控或常用功能)常驻内存,其余模块共享一个或几个存储空间。
覆盖是用户有意识地进行的,用户所看到的地址空间还是实际大小的空间。
-
交换技术(swapping)
在多个程序并发执行时,往往有一些程序因等待某事件而暂时不能运行。如果将暂时不能运行的程序换到外存中,就可以获得空闲内存空间来运行别的程序。
交换是以进程为单位进行的。交换技术的优点是增加了可并发运行的程序数目,且对程序结构没有要求。其缺点是对整个进程进行换入、换出的操作往往需要花费大量的CPU时间。
-
虚拟存储器
虚拟存储(virtual memory)的原理是只将程序的部分代码调入内存,其余驻留在外存空间中,在需要时调入内存。程序代码的换入和换出完全由系统动态地完成,用户察觉不到。因此,用户看到的是一个比实际内存大得多的虚拟内存。
进程看到的是一个比实际内存大得多的虚拟内存。
虚拟存储中,进程的逻辑地址空间可以超越实际内存容量的限制。
程序的局部性原理使虚拟存储成为可能。
虚拟存储器(virtual memory)的原理是用外存模拟内存,实现内存空间的扩充。
做法是在外存开辟一个存储空间,称为交换区。进程启动时,只有部分程序代码进入内存,其余驻留在外存交换区中,在需要时调入内存。
页式虚拟存储器原理
虚拟存储器的实现技术主要有页式虚存和段式虚存两种。目前页式虚存是最常用的,也是Linux系统所采用的虚存技术。
页式虚拟存储器的思想就是在页式存储管理基础上加入以页为单位的内外存空间的交换来实现存储空间扩充功能。这种存储管理方案称为请求页式存储(demand paged virtual memory)。
请求页式的页表中除了页帧号外还增加了一些信息字段,设置这些信息是为了实施页面的管理和调度,如地址变换、缺页处理、页面淘汰以及页面保护等。
对缺页故障的检测
CPU响应此中断后,暂停当前进程的运行,转去执行中断处理程序。缺页中断的处理程序负责将缺页调入内存,并相应地修改进程的页表。待原进程再次运行时即可使用该页了。
抖动
如果淘汰算法不当,系统可能会产生“抖动”(thrashing)现象,即刚调出的页很快又被访问到,马上又被调入。抖动的系统处于频繁的页交换状态,CPU的大量时间都花在处理缺页中断上,故系统效率大幅度降低。
产生抖动的原因一个是页面调度不当,另一个就是实际内存过小。
Linux的内存访问机制
目前的x86/x64架构的内存模式仍是段式,但在段式的基础上可以选择启用页式机制。运行Linux系统需要启动分页机制。
x86/x64的地址分为3种,即虚拟地址、线性地址和物理地址。
-
虚拟地址
程序中使用的逻辑地址。由于是段式存储模式,所以虚拟地址是二维的,用段基址和段内位移表示。
-
线性地址
虚拟地址经过段式变换得到的一维地址。
-
物理地址
线性地址经过页式变换得到的实际内存地址。这个地址被送到地址总线上,定位实际要访问的内存单元。
段式地址变换
段式地址变换的过程是:
- 根据指令类型确定其对应的段(如跳转类指令用cs 段,读写类指令用ds 段等);
- 再通过对应的段寄存器在段描述符表中选出段描述符;
- 用指令给出的虚拟地址作为段内位移,对照段描述符进行界限和权限检查;
- 检查通过后,将段内位移值与段描述符中的段基址相加,形成线性地址。
页式地址变换
Linux利用了共享0基址段的方式,使段式映射实际上不起作用。对于Linux 来说,虚拟地址与线性地址是一样的。
Linux系统仍然利用了分段机制的保护作用。Linux的进程映像被划分为多个段,包括用户态与核心态的各个代码段、数据段和栈段等。每个段除了基址外还有“存取权限”和“特权级别”设置,这些设置可以起到段保护的作用。
页式地址变换的作用就是将线性地址中的页号变为物理地址中的页帧号,这是通过页表映射完成的。
进程映像的每个页面都对应一个页表项(Page Table Entry,PTE)。
多级分页
将页表项组织成多个页表,再在页表上增加一个页目录表,每个页目录表项记录一个页表的页帧地址。这些页表和页目录表就形成了一个二级页表。
进程地址空间的管理
进程的地址空间被分为两个部分:供内核使用的空间称为内核空间,供用户进程使用空间称为用户空间。内核空间由系统内的所有进程共享,而用户空间则是进程的私有空间。
地址空间的结构
-
映像文件
进程的原始映像以映像文件的形式驻留在硬盘存储空间,映像文件就是二进制的可执行文件。
-
虚存区
进程准备运行时,内核将为其建立地址空间,并将映像链入地址空间中。进程映像的每个片段占用地址空间中的一个连续区间,大小为页的整数倍。称为“虚存区”(Virtual Memory Area,VMA)。
-
进程的可用地址空间
图中未覆盖的空白区是没有被占用的空地址,是进程不可用的。不过进程在运行时可以根据需要动态地添加或删除虚存区,从而改变自己的可用地址空间。
映射
在页式虚存中,正在运行的映像会进入物理内存,其余部分则以文件的形式驻留在磁盘的后备存储空间中。两个物理存储空间之间的联系纽带就是进程的地址空间,而联系的方式就是地址空间的映射。
-
虚存地址空间到文件空间的映射,称为文件映射;
-
虚存地址空间到内存空间的映射,称为页表映射;
-
内存空间到交换空间的映射,称为交换映射。
-
文件映射
-
文件映射
文件被打开后,文件映射就是将虚存区的地址映射到文件空间的一段地址上,这样就可以通过虚存区的线性地址获得该段文件的内容了。text 和 data 虚存区都是以这种方式映射到映像文件的代码段和数据段,从中获取代码和数据的映像。
-
匿名映射
匿名映射的虚存区没有对应任何实际的文件对象,内核隐含地将其映射到一个抽象的“零页”文件。stack、bss 和 heap 虚存区都是采用匿名方式映射的,因此它们获得的初值为全0。
-
-
页表映射
文件映射只是将文件中的映像映射进了虚存空间,而进入了物理内存的映像则是通过页表来映射的。页表映射是在虚存区的线性地址到物理内存的页帧地址间建立的映射关系。建立了页表映射的地址空间部分是进程实际占有的、可直接访问的内存空间。
进程开始执行时,只有很少一部分映像被装入内存,其余部分则是在被访问到时才调入内存。因此,页表映射会随着进程的执行而改变。
-
交换映射
用户进程的映像进入内存后也并非始终驻留于内存中。
页式虚存的页面交换操作可能会在内存紧张时将其换出到硬盘的交换空间中,当被访问时再交换回内存。
交换映射是在内存地址与交换空间地址之间的映射,映射关系由内核确定,与用户进程无关。
主要数据结构
地址空间的建立与释放
fork()
系统调用创建子进程时,也将自己的地址空间完整地复制给了子进程。因此,新建的子进程拥有与父进程相同内容的mm_struct结构、vma对象和页全局目录,它们所映射的自然就是父进程的物理内存空间。
当一个进程试图修改共享段的内容时会引发页故障,在处理页故障时,内核会将该段复制一份给进程使用,使两个进程各自拥有一个该段的副本,这就是“写时复制”技术。
执行 exec()
来更换执行的映像,也就避免了写时复制。更换映像的主要工作就是更换进程的虚存区,方法是:
- 在磁盘上找到指定的映像文件并打开它;
- 根据文件中的映像结构建立起相应的虚存区;
- 然后将新映像的入口执行地址装入eip寄存器;
- 之后进程就开始执行全新的映像了。
内存空间的管理
内存分配算法
Linux采用伙伴(Buddy)算法来分配和回收内存,分配和回收的空间都是2的幂大小的页块。当要分配内存时,首先要根据需要的空间大小确定要分配的页块大小。如需要m页,\(2^{i-1}<m\le2^{i}\),则应分配一个2i大小的页块
分配算法是:在free_area[i]的链表中找一个空闲页块,将其从链表中删除,然后返回首帧的地址。如果没有2i大小的空闲页块,就在 free_area[i+1] 的链表中取出一个,一分为二,分配一个,将另一个链入free_area[i]的链表中。如果没有2i+1大小的空闲页块,就进一步地分裂更大的空闲页块。如此继续,直到分配成功。
回收内存的过程与分配相反,就是根据回收页块的大小将其链入适当的空闲链表中。如果该页块的伙伴也在链表中,则将其与伙伴合并取出,加入下一个级别的链表中。如果还能合并,就进一步合并下去。
内存的分配机制
-
kmalloc()
用于获得以字节为单位的一个连续的小块内存区(通常小于128MB),分配成功则返回该内存区的首地址。与kmalloc()
函数对应的释放函数是kfree()
。 -
vmalloc()
函数用于分配一个线性地址连续但物理地址不保证连续的内存区。由于不要求物理地址连续,用vmalloc()
可以分配较大的内存空间。与vmalloc()
对应的释放函数是vfree()
。 -
malloc()
函数是用户进程使用的,分配的是用户空间中的虚存区(即 heap区)。malloc()
只是在分配的区间上建立匿名映射,通常并不直接分配物理内存。当进程访问到这个虚存空间时会产生页故障,在页故障处理时才会为其分配页帧。
为适应小块内存的分配与释放,Linux提供了slab缓存机制。slab 缓存区是预先从内存分配器获取的一个连续的内存区,由slab分配器管理。slab分配器管理着多个slab,每个用于一种结构类型的内存分配。根据要分配的对象类型,slab 的存储空间被构造成同类型的一个个内存对象。
页面的交换
-
Linux的页面调入策略是按需调入,即当进程访问到一个不在内存的页面时引发缺页中断,在中断处理程序中调入页面;
-
Linux 的页面换出策略是预先换出,即当内核发现内存空间紧张时就进行页面换出操作,回收页帧。
Linux系统提供了两种形式的交换空间。交换空间按照优先级排序使用,通常是以交换区为主,以交换文件为辅。
-
一种是利用一个特殊格式的(swap)磁盘分区,称为交换区。
-
另一种是利用文件系统中具有固定长度的特殊文件,称为交换文件。
交换空间的管理方式类似于页式管理。整个交换空间被划分为与内存页同样大小的块,称为页插槽(page slot),每个插槽可“插入”一个物理页面。
回收内存页帧
主要的是以下两种:
-
回收高速缓存中的页帧。
高速缓存(如目录项缓存、页面缓存)是为了提高文件访问速度或内存分配性能而设置的。它们不属于进程的空间,因此释放后不须修改页表。另外,由于高速缓存的 flush机制会定期地将“脏页”(即修改过的页)写回到磁盘,因此交换进程看到的页大多是干净的,可以直接回收。
所以回收高速缓存页面是最简便的办法。
-
回收进程占用的页帧。
若上述措施没有得到足够的空闲页帧,交换进程就要通过淘汰算法寻找适合的进程页面,将其换出,回收其占用的页帧。页面回收方式取决于页面类型和使用模式,情况比较复杂。
粗略地说,映射到后备文件的页如果是干净的可直接舍弃,是脏页则要将其内容写回文件;没有对应后备文件的匿名页需要先为其建立后备存储,也就是在交换区中分配一个页槽,然后将其写入;写时复制的页同匿名页一样要回收到交换区。页帧回收后,其对应的页表项也要做相应的修改。通常是将Р位清零,其余位保存交换地址或清零。
可以看出,与前一种途径相比,换出进程页面的操作较复杂,效率也较低。