您的位置:首页 > 运维架构 > Linux

Linux操作系统基础理论(4)-----Linux 存储管理的实现

2014-03-25 09:19 761 查看
Linux操作系统基础理论(4)-----Linux 存储管理的实现

目录

摘要:...
2

1.
引言... 2

1.1
存储管理简介... 2

1.2
虚拟存储管理... 2

2.
Linux 存储管理的实现... 2

2.1
80386内存管理机制和功能... 2

2.2
80386的分段机制在Linux中的应用... 3

2.3
80386的分页机制在Linux中的应用... 7

2.4
Linux虚拟存储管理... 9

2.4.1
Linux 虚拟存储管理的实现... 9

2.4.2
Linux 的内核空间和用户空间... 10

2.4.3
Linux 虚拟段的组织和管理... 11

2.4.4
Linux 虚拟页的组织和管理... 12

3.
总结... 15

参考文献...
15

Linux 存储管理的实现

摘要:内存是内核所管理的最重要的资源之一,也是操作系统中最重要的组成部分之一。本文将论述Linux中基于IntelCPU80386所支持的分段分页机制来实现虚拟存储管理器的方法,并介绍必备的数据结构。

关键字:Intel80386 分段 分页 虚拟存储管理器 数据结构

1. 引言

1.1 存储管理简介

从价格性能比的需要出发,存储器一般用 RAM 作为主存储器,用 Cache 做高速缓冲存储器,它比 RAM 速度快,容量小但价格高。常用磁盘作为辅助存储器,因为磁盘价格低于RAM,容量却可以比 RAM 大的多。通常把大量的程序数据等文件存储在辅助存储器上,需要执行或处理的部分可以调入 RAM 去执行,并且对于那些频繁处理,数量不算太大的信息利用 Cache 存储,可以节省时间,这就是所谓的高速缓冲、RAM 主存和大容量辅存三级存储器。这里所说的存储器管理主要是针对主存而言的。

存储管理子系统是操作系统中最重要的组成部分之一。在早期计算时代,由于人们所需要的内存数目远远大于物理内存,人们设计出了各种各样的策略来解决此问题,其中最成功的是虚拟内存技术。它使得系统中为有限物理内存竞争的进程所需内存空间得到满足。

到目前为止,内存管理已经有好多种手段和方法,即多种不同的内存管理方案,我们要

讲解的就是各种存储管理方案的实现原理和技术及其优缺点。大体可分为实存管理和虚拟存

储技术两大类。

(1)单一连续区存储管理法。内存除操作系统占用外,剩余区域作为用户区,但每次只能存放一道程序。当程序空间小于用户区时,有剩余区未能利用,但当程序空间大于用户区时,除非使用覆盖技术或交换技术,否则程序无法运行。

(2)固定分区法存储管理技术。这种方法是内存区除操作系统核心占用的内存区外,其余作为用户区,但要把用户区分成位置固定的几个区域,每个区域可以存放一道程序,另增加一个存储分配表登记分区的使用情况。这种方法可以支持多道程序技术,但内存利用仍不充分,有零头。

(3)可变分区法存储管理技术。这种方法随程序的需要划分内存的区域,但内存利用仍不够充分,而且这些分区法还需使用界限寄存器指出区域的范围,以实现信息的保护。

(4)为了使连续的程序不一定占用连续的存储区,以便充分利用内存,又引入了分页式存储管理技术。这种方法的主导思想是逻辑上连续的程序不一定占用连续的内存区,而是把程序分页,而且页的长度相等。内存分块,块的大小与页的大小相等。一个程序页占用一个内存块,然后再用一个叫页表的表格把哪个程序页放在哪个内存块登记起来,可以通过查页表方便地找到程序在内存中的位置。这种方法大大提高了内存的利用率。

(5)为了保证程序逻辑意义的完整,程序的分段按模块进行,即一个有独立意义的程序模块作为一段。当然,段的长度不是等长的。通过一个段表,登记每段程序的长度及其在内存中的开始位置。程序执行的过程中,也可经过查段表找到某段程序在内存中的位置。内存利用也比较充分。在页表或段表中有相应的保护信息,如最大页号、最大段长度等。经比较判定访问的程序是否在给定的区域内,否则拒绝读取内存信息,为越界错。

1.2 虚拟存储管理

实存管理技术主要在现有内存用户区内做文章,内存容量得不到扩充,为了使得小内存可以运行大程序,引入了虚拟存储管理技术。其主导思想是,平时将程序数据放在大容量的辅助存储器上,将要执行的部分先调入内存执行,其他部分逐步调入,而不是像实存管理那样将程序全部装入内存。当需要调入执行的程序时,如果内存已满,就选择暂时可能不执行的程序调出内存到外存,腾出内存区,将需要执行者调入。这样,就像电影院看电影一样,一场一场的来,通过牺牲时间的方法来换取空间。当然要增加一些设施及管理的开销,但实际上相当于扩充了内存的容量。

实现虚拟存储器管理技术需要有大容量的辅存、一定容量的主存和类似页表段表的地址

变换机构。所以,页式和段式存储管理方法都可以实现虚拟存储技术。虚拟存储技术是实现

虚拟存储器的技术,而虚拟存储器是什么呢?实际上,虚拟存储器是用户认为的容量很大的

存储器。比如说,用户有100KB 的程序,目前内存仅有50KB 的空闲区,但是通过内存管理技术完全可以完成用户 100KB 程序的内存需要。用户以为有 100KB 的内存空闲区供它使用。用户认为的这个他所需要的存储空间就是所谓的虚拟存储器。实际上它只是一个程序地址空间,而不是真正的物理内存空间。一旦采用虚拟存储管理技术,用户编程时不必太多地考虑内存容量不够用的问题。内存容量问题由操作系统的存储管理技术去解决,如请求页式、分段式存储管理技术。Linux 存储管理采用的是虚拟存储技术。

2. Linux 存储管理的实现

2.1 80386内存管理机制和功能

80386 的工作模式含实地址模式和虚拟地址模式(保护模式)。实地址模式与 80386 完全兼容,只能寻找 1MB 的地址空间,不分特权级,也不能用分页机制,分段功能也受到限制。在保护模式下,加强了分段机制,虚地址空间可以有 16k 个段,段长可变,段长最大可达4GB,从而构成 246字节容量的虚地址空间。80386 提供了片内页式管理机制,可通过二级页表实现从线性地址到物理地址的转换,为Linux 虚拟内存提供了直接的支持。

80386 的虚地址模式同时使用了段式和页式二级地址转换机制进行地址转换。第一级采

用分段机制,它首先把包含在段地址和段内偏移量的虚地址转变为一个线性地址。第二级采

用分页式机制把线性地址转变为物理地址。Pentium虚拟存储器的核心是两张表,LDT(Local

Descriptor Table,局部描述符表)和GDT(Global Descriptor Table,全局描述符表)。每个程序都

有自己的LDT,但是同一计算机上所有的程序共享一个GDT。LDT描述局部于每个程序的

段,包括代码、数据、堆栈等,GDT描述系统段,包括操作系统自己[1]。

为了访问一个段,Pentium程序必须把这个段的选择符(Selector)装入机器的六个段寄存

器中的某一个中。在运行过程中,CS寄存器保存代码段的选择符,DS寄存器保存数据段的选择符,其他的段寄存器不太重要。每个选择符是一个16位数 。

选择符中的一位指出这个段是局部的还是全局的(即,它是在LDT中还是在GDT中),其他的13位是LDT或GDT的入口号,因此这些表长度被限制在最多容纳8K个段描述符,还有两位和保护有关,我们将在后面讨论。描述符0是禁止使用的,它可以被安全地装入一个段寄存器中用来表示这个段寄存器目前不可用,如果使用会引起一次陷入。

具体如图1所示:

图 1 虚拟-物理地址转换

在地址转换过程中,分段机制总是要启用的,分页机制则根据需要被启用和被禁用。

若分页机制禁用,则由分段机制转换得到的线性地址,直接作为物理地址使用。分段机制和分页机制是两种不同的转换机制,是整个地址转换函数的不同转换层次。虽然两种机制都利用了存储在主存中的转换表(段表、页目录表和页表),但这些表具有彼此独立的结构。实际上,段表存储在线性地址空间,而页表存储在物理地址空间,因此,段表可由分页机制重新进行定位,而不需要分页机制的参与。分段机制把虚地址转换成线性地址,并在线性地址空间中访问段表,而不会察觉分页机制已经把线性地址转换为物理地址。同样,分页机制对于程序产生的地址所使用的虚地址空间一无所知。分页机制只是直接把线性地址转变成物理地址,并在物理地址中访问页表,并不知道虚地址空间的存在,甚至不知道段机制的存在。
如图2所示:

图2 保护模式下虚拟地址到物理地址的转换过程

2.2 80386的分段机制在Linux中的应用

分段式存储管理技术首先把程序地址空间按其逻辑含义划分为模块,称做段。每段都是从 0 开始编址,各段长度不一定相等。每一段必须由三方面的信息加以说明,如每段的开始地址(也叫基址),该段的长度(也叫限长),该段的属性。段的虚地址形式为

[段号段内偏移地址],虚拟地址到线性地址的映射关系,如图3所示:

图3 虚拟地址到线性地址的映射

各段的情况放在一个叫段描述符表的存储区中,实际相当于段表。在实模式下,段的属性是代码段、数据段、堆栈段等。段的始址、长度等在保护模式下,用 8 个字节的数表示,称段描述符,其一般格式如图 4 所示。

图4 段描述符的一般格式

从图4可以看出,一个段描述符指出了段的 32 位基地址和 20 位段界限。

第 6 个字节的 G 位是粒度位,当 G=0 时,段长以字节为单位,即一个段最长可达 1MB。当 G=1 时,段长以 4KB 为单位,即一个段最长可达 1M×4K=4GB。D 位表示默认操作数的大小,如果 D=0,操作数为 16 位;如果 D=1,操作数为 32 位。第 6 个字节的其余两位为 0,这是为了与将来的处理器兼容而必须设置为 0 的位。

第 5 个字节是存取权字节,它的一般格式如图 5所示。第 7 位 P 位(Present)是存在位,表示段描述符描述的这个段是否在内存中。如果在内其中P=0,表示该段不在内存中 ,反之则在。DPL(Descriptor Privilege Level),它占两位,其值为 0~3,用来确定这个段的存取权限,即保护等级。S 位(System)表示这个段是系统段还是用户段。如果 S=0,则为系统段;如果 S=1,则为用户程序的代码段、数据段或堆栈段。系统段与用户段有很大的不同。类型表示是段的类型和保护位。类型占
3 位,第 3 位为 E 位,表示段是否可执行。当 E=0 时,为数据段描述符,这时的第 2 位 ED 表示扩展方向。当 ED=0 时,向地址增大的方向扩展。当 ED=1 时,表示向地址减少的方向扩展。第 1 位(W)是可写位。当 W=0 时,数据段不能写;W=1 时,数据段可写入。在 80386 中,堆栈段也被看成数据段,因为它本质上就是特殊的数据段。当描述堆栈段时,ED=0,W=1。

图5 存取权字节的一般格式

1. 系统段描述符

其格式如图 6所示:

图6 系统段描述符
可以看出,系统段描述符的第 5 个字节的第 4 位为 0,说明它是系统段描述符,
类型占4 位,没有 A 位。第 6 个字节的第 6 位为 0,说明系统段的长度是字节粒度,所以,一个系统段的最大长度为 1M 字节。系统段的类型为16种,如表1所示:

表1 系统段类型

在这 16 种类型中,保留类型和有关 286 的类型不予考虑。门是用来控制访问在

目标码段的入口点,有调用门、任务门、中断门和陷阱门四种。调用门主要用来将程序控制转移到一个更高的特权级。任务门用于切换任务,它只能涉及任务状态段。所谓任务状态段 TSS(Task StateSegment),就是一个特殊的固定格式的段,它包含了任务和与之相链接的允许嵌套的任务的所有状态信息。中断门和陷阱门用于中断处理,其中的地址是指向中断或陷阱处理子程序起点的指针。
2.描述符和寻址方式
各种各样的用户描述符和系统描述符,都放在对应的全局描述符表、局部描述符表和中断描述符表中。
(1)描述符表。描述符表(即段表)定义了 80386 系统的所有段的情况。所有的描述符表本身都占据 1 个字节数为 8 的倍数的存储器空间,空间大小在 8 个字节(至少含 1 个描述符)到 64KB(至多含 8K 个描述符)之间。80386 的段描述符表分为全局描述符表(GDT)和局部描述符表(LDT),它们是包含段描述符表的两个特殊的段。虚地址空间(最多 16K个段)的一半由 GDT 映射,另一半由 LDT 映射。
① 全局描述符表(GDT)。
全局描述符表 GDT(GlobalDescriptor Table),除了任务门、中断门和陷阱门描述符外,
包含着系统中所有任务都可用的那些描述符。它的第一个 8 字节位置没有使用。
② 中断描述符表(IDT)。
中断描述符表 IDT(InterruptDescriptor Table),可以包含 256 个描述符,每个描述为
8 个字节。IDT 中只能包含任务门、中断门和陷阱门描述符,虽然它最长也可以为 64KB,但只能存取 2KB 以内的描述符,即 256个。因为 Intel 公司保留了 32 个中断描述符供自己使用,所以它最少为 256 个描述符。规定这些数字都是为了和早期的机器兼容。
系统中每个中断在 IDT中都有一项。中断描述符表中的描述符不在全局描述符表中。
③ 局部描述符表(LDT)。
局部描述符表 LDT(LocalDescriptor Table),包含了与一个给定任务有关的描述符,每一个任务都有一个各自的 LDT。有了 LDT,就可以使给定任务的代码、数据与别的任务相隔离。不同的任务可以有相同的描述符,这样就可以共享全局数据和代码。
每一个任务的局部描述符表 LDT也用一个描述符来表示,称为 LDT 描述符,它包含了有关局部描述符表的信息,在全局描述符表 GDT 中。一个局部描述符表的描述符,它是被放在全局描述符表GDT 中。由这个描述符的第 5 个字节第 4 位 S=0,得知它表
示一个系统段。低 4 位类型域=2,所以它是一个 LDT描述符。最高位 P=1,所以该描述符在存储器中是有效的。由于是局部描述符表描述符,所以它的 DPL 域是无用的。此描述符表的段基地址为 00100000H,界限为 000FFH,即 256。粒度为字节粒度,所以此表最多可放入 32 个局部描述符。
Linux 定义的中断描述符表和全局描述符表最多可以有 256 个描述符,即段数最大可以达到 256 段。
(2)选择器与描述符表寄存器。在实模式下,段寄存器存储的是真实的段地址:在保护模式下,16 位的段寄存器无法放下32 位的段地址,因此,它们被称为选择器,即段寄存器的作用是用来选择描述符。这样就把描述符中的 32 位段地址作为实际的段地址。
选择器有三个域:第 15~3 位这 13 位是索引域,表示的数据为 0~8129,用于指向全局描述符表中相应的描述符。第 2 位为选择域,如果TI=1,就是从局部描述符表中选择相应的描述符;如果 TI=0,就从全局描述符表中选择描述符。第 1,0 位是特权级,表示
选择器的特权级,0~3 级,0 级最高被称为请求者特权级RPL(Requestor Privilege Level)。
只有请求者特权级 RPL高于(数字低于)或等于相应的描述符特权级 DPL,描述符才能被存取,这就可以实现一定程度的保护。
每一个描述符表都有一个与之相联系的寄存器,分别是全局描述符表寄存器 GDTR、中断描述符表寄存器IDTR 和局部描述符表寄存器 LDTR。

因此,在没有分页操作时,寻址一个存储器操作数的步骤如下:
① 在段选择器中装入 16 位数,同时给出 32 位地址偏移量(比如在 ESI,EDI 中等等)。
② 根据段选择器中的索引值、TI 及 RPL 值,再根据相应描述符表寄存器中的段地址和段界限,进行一系列合法性检查(如特权级检查、界限检查)。若无异常,就取出相应的描述符放入段描述符高速缓冲寄存器中。
③ 将描述符中的 32 位段基地址和放在ESI,EDI 等中的 32 位有效地址相加,就形成了32位物理地址。
注意:在保护模式下,32 位段基地址不必向左移 4位,而是直接和偏移量相加形成 32
位物理地址(只要不溢出)。这样做的好处是,段不必再定位在被 16 整除的地址上,也不必左移 4 位再相加。

2.3 80386的分页机制在Linux中的应用

分页机制是存储管理机制的第二部分。分页机制在段机制之后进行,以完成虚拟-物理地址的转换过程。段机制把虚拟地址转换成线性地址,分页机制进一步把该线性地址再转换为物理地址。

图7 寻址过程
如图7所示,分页机制由 CR0中的 PG 位启用。若 PG=1,启用分页机制,便是把线性地址转换成物理地址。若 PG=0,禁用分页机制,直接把段机制产生的线性地址当做物理地址使用。80386 使用 4KB 大小的页。每一页都有 4KB 长,并在 4KB 的边界上对齐,即每一页的起始地址都能被 4K 整除。因此,80386 把 4GB 的线性地址空间划分为 1G 个页面,分页机制通过把线性地址空间中的页重新定位到物理地址空间来进行管理。因为每个页面的整个4KB 作为一个单位进行映射,并且每个页面都对齐
4KB 的边界,因此,线性地址的低 12 位经过分页机制直接作为物理地址的低 12 位使用。重定位函数也因此可看成是把线性地址的高20 位转换为对应物理地址高20 位的转换函数。
线性-物理地址转换函数,可将其意义扩展为允许将一个线性地址标记为无效,而不是实际地产生一个物理地址。有两种情况可能使页被标记为无效:其一是线性地址是操作系统不支持的地址;其二是在虚拟存储器系统中,线性地址对应的页存储在磁盘上,而不是存储在物理存储器中。在前一种情况下,程序因产生了无效地址而必须被终止。对于后一种情况,
该无效的地址实际上是请求操作系统的虚拟存储管理系统把存放在磁盘上的页传送到物理存储器中,使该页能被程序所访问。由于无效页通常是与虚拟存储系统相联系的,这样的无效页通常称为未驻留页,并且用页表属性位中叫做存在位的属性位进行标识。未驻留页是程序可访问的页,但它不在主存储器中。对这样的页进行访问,形式上是发生异常,实际上是通过异常进行缺页处理。
1.分页结构,分页是将程序分成若干相同大小的页,每页 4KB。实际上,各页与程序的逻辑结构没有关系,与描述符中的粒度也没有关系。如果不允许分页(CR0 的最高位置 0),那么经过段选择器、全局或局部描述符表转化而来的 32 位线性地址就是物理地址。但如果允许分页(CR0 的最高位置 1),就要将 32 位线性地址通过一个两级表格结构转化成物理地址。
(1)两级页表结构。为什么采用两级页表结构呢?在 80386 中页表共含 1M 个表项,每个表项占 4 个字节。如果把所有的页表项存储在一个表中,则该表将占 4MB 连续的物理存储空间。为避免使页表占用如此巨额的物理存储器资源,故对页表采用了两级表的结构,而且对线性地址的高 20 位的线性-物理地址转化也分为两步完成,每一步各使用其中的 10 位。两级表结构的第一级称为页目录,存储在一个 4KB 的页中。页目录表共有 1K 个表项,每个表项为 4 个字节,并指向第二级表。线性地址的最高
10 位(即位 31~位 22)用来产生第一级的索引,由索引得到的表项中,指定并选择了 1K 个二级表中的一个表。
两级表结构的第二级称为页表,也刚好存储在一个 4KB 的页中,包含 1KB 的表项,每
个表项包含一个页的物理基地址。第二级页表由线性地址的中间 10 位(即位 21~位 12)进行索引,以获得包含页的物理地址的页表项。这个物理地址的高 20 位与线性地址的低 12 位形成了最后的物理地址。如图8 所示为两级页表结构。

图8 两级页表结构
(2)页目录项。页目录表最多可包含 1024 个页目录项,每个页目录项为 4 字节。第 31~12 位是 20 位页表地址,由于页表地址的低 12 位总为 0,所以用高 20 位指出 32
位页表地址就可以了。因此,一个页目录最多包含 1024 个页表地址。第 0 位是存在位,如果P=1,表示页表地址指向的页在内存中;如果 P=0,表示不在内存中。第 1 位是读/写位,第 2 位是用户/管理员位,这两位为页目录项提供保护属性。当特权级为 3 的任务要想访问页面时,需要通过页保护检查,而特权级为 0,1,2 的任务就可以绕过页保护。第 5 位是访问位,当对页目录项进行访问时,A 位=1。第 9~11 位由操作系统专用,不
必涉及。
(3)页面项。80386 的每个页目录指向一个页表,页表最多含有 1024 个页面项,每项 4个字节,包含页面的起始地址和有关该页面的信息。页面的起始地址也是 4K 的整数倍,所以页面的低 12 位也留作他用,如图 9 所示。
图9 页表中的页面项
第 31~12 位是 20 位页表地址,第 0,1,2,5 位及 9~11 位的用途和页目录项一样,第 6 位是页面项独有的,当对涉及的页面进行写操作时,D 位被置 1。虚拟存储器只有一个页目录,它有 1024 个页目录项,每个页目录项又含有 1024 个页面项,因此,存储器一共可以分成 1024×1024=1M 个页面。由于每个页面为 4KB,所以,存储器的大小正好(最多)为 4GB。
(4)线性地址到物理地址的转换。当访问一个操作单元时,如何由分段结构确定的 32
位线性地址通过分页操作转化成 32 位物理地址呢?
第 1 步,CR3 包含着页目录的起始地址,用 32 位线性地址的最高 10 位 A31~A22 作为页目录的页目录项的索引,将它乘以 4,与 CR3 中的页目录的起始地址相加,形成相应页表的地址。
第 2 步,从指定的地址中取出32 位页目录项,它的低 12 位为 0,这 32 位是页表的起始地址。用 32 位线性地址中的 A21~A12 位作为页表中的页面的索引,将它乘以 4,与页表的起始地址相加,形成 32 位页面地址。
第 3 步,将 A11~A0 作为相对于页面地址的偏移量,与 32 位页面地址相加,形成32
位物理地址。
2. 页面高速缓冲寄存器
在分页情况下,每次存储器访问都要存取两级页表,这就大大降低了访问速度。所以,
为了提高速度,在80386 中设置一个最近存放页面的高速缓冲寄存器,即所谓的“快表”,它自动保持 32 项处理器最近使用的页面地址,因此,可以覆盖 128KB 的存储器地址。当进行存储器访问时,先检查要访问的页面是否在高速缓冲寄存器中,如果在,就不必经过两级访问了,如果不在,再进行两级访问。平均来说,页面高速缓冲寄存器大约有 98%的命中率,也就是说每次访问存储器时,只有 2%的情况必须访问两级分页机构,这就大大加快了速度。

2.4 Linux虚拟存储管理

2.4.1 Linux 虚拟存储管理的实现

Linux 虚拟内存即程序地址空间的组成模块如下,其实现的源代码大部分放在/mm 目录下。

(1)内存映射模块(mmap)。负责把磁盘文件的逻辑地址映射到虚拟地址以及把虚拟地址映射到物理地址。该模块实现的源程序分别是:

mmap.c 文件中主要函数 do_mmap 的功能是把文件中的逻辑地址映射成虚存的线性

地址,即把文件结构中得到的逻辑地址转换成 vm_area_struct 结构所需的地址。

mremap.c 文件中的主要函数 sys_mremap 的功能是扩张或缩小现存的虚拟内存空间。

filemap.c 文件中的主要函数功能是处理内存映射和处理页高速缓存器,即把线性地

址映射到内存且修改页高速缓存。这部分含有从磁盘读写的 I/O 操作。

(2)交换模块(swap)。负责控制内存的内容换入和换出。它通过替换机制,使得在物理内存的页框(RAM页)中保留有效的逻辑页,淘汰主存中最近没被访问的逻辑页,保存近来访问过的逻辑页。该模块实现的源程序分别是:page_io.c 主要函数的功能是读写交换文件。swap_state.c 主要函数的功能是修改变换高速缓存(swap cache)。swapfile.c 主要函数的功能是完成换入换出系统调用(sys_swapin,sys_swapon)。swap.c 主 要 函 数 的
功 能 是 定 义 交 换 使 用 的 数 据 结 构 和 常 量 , 如free_page_low,free_page_high,swap_control_t等。kswapd 是一个周期性地处理交换内存和外存的守护进程(内核线程)。
(3)核心内存管理模块(core)。负责核心内存中页的管理,这些功能将被别的内核子系统(如文件系统)所调用。该模块实现的源程序分别是:page_alloc.c 主要函数功能是处理页的释放、回收和分配。memory.c 请页的相关函数以及有关用于请页处理的函数代码。

(4)结构特定模块。负责给各种硬件平台提供通用接口,这个模块通过执行命令来改变硬件 MMU 的虚拟地址映射,并在发生页错误时,提供公用的方法来通知别的内核子系统。这个模块是实现虚拟内存的物质基础。该模块实现的源程序分别是:Arch/I386/mm/fault.c 处理页异常。Arch/I386/mm/init.c 内存初始化的有关函数。

2.4.2 Linux 的内核空间和用户空间

在 80386 中,线性地址从 0GB 到 4GB,它不是一个物理地址,而是一个虚拟地址。内核模式和用户模式(1,2,3 级)的区别反映在线性地址空间中就是内核空间和用户空间所处的位置不同,如图11 所示,内核空间处在 3GB~4GB 的空间,而用户空间处在 0GB~3GB的空间。

图 11 线性地址空间
在 Linux 中,内核空间是靠内核段来描述的。内核段包括数据段和代码段,它被定位在全局描述符表(GDT)中,用来映射 3GB~4GB 的线性空间,这部分虚拟空间可以由内核页目录表 swapper_pg_dir 来寻址。类似地,在用户模式下每个进程都有一个局部描述符表(LDT),它包括一个代码段、一个数据段和一个堆栈段,这些用户段的线性地址可以从 0 到 3GB。这里,我们进一步说明逻辑地址与线性地址的含义。一个逻辑地址包含一个段选择和一个偏移量,段选择指向一个段,偏移量告诉在段中的位置。逻辑地址通过段机制变换成了从0~4GB
的线性地址,也就是本章多次用到的虚拟地址。内核在内核空间的寻址是不同于用户程序在用户空间的寻址的。内核在启动时已装入内存,因此,可以把内核空间映射到线性地址空间的 3GB 以上的空间(实际上,mm/vmaloc.c中的函数 vremap()可以完成把任意物理空间映射到内核虚拟地址空间)。所以从用户程序的角度看来,也就是从虚拟内存的角度来看,用户程序如果要在内核空间中寻址,就需通过内核页目录 swapper_pg_dir 中的指针得到页表,再通过页表中的指针来指向相应的物理内存。这里要注意内核目录和页表是保留的,且不变,它们是在启动时建立的。所有的物理内存可由内核页目录和内核页表来寻址,并且这种一一对应关系是固定的。由于这种固定关系,内核对物理内存可快速地寻址,这样一来,也方便了对内存的分配和回收。在用户空间中寻址则是通过用户页目录中的指针得到用户页表,再通过用户页表中的指针来指向相应的物理内存,但这里的用户页目录和页表是动态建立的,是可变的。所以物理内存与页目录和页表没有固定的对应关系,有时甚至是多对一的关系,即几个页表项同指一物理内存页。

2.4.3 Linux 虚拟段的组织和管理

我们已经知道用户共有 4GB 的虚存空间,但并不是所有的 4GB 空间都可以让用户态进程读写或申请使用。用户态进程实际可申请的虚存空间为 0 至 3GB。在用户进程创建时,已由系统调用 fork()的执行函数 do_fork()将内核的代码段和数据映射到 3GB 以后的虚存空间,供内核进程访问。所有进程的3GB~4GB 的虚存空间的映像都是相同的,以此方式共享内核代码段和数据段。为了能以“自然”的方式管理进程虚存空间,Linux 定义了虚存段(vma,即 vitual memory
area)。一个 vma 段是某个进程的一个连续的虚存空间,在这段虚存里的所有单元拥有相同的特征。例如,属于同一进程,相同的访问权限,同时被锁定(locked),同时受保护(protected)等等。
vma 段由数据结构 vm_area_struct(见include/linux/mm.h)描述。vm_area_atruc 结构及几个重要的属性解释如下:
structvm_area_struct{
structmm_struct*vm_mm; /*VM area parameters */
unsignedlong vm_start;
unsignedlong vm_eng;
pgprot_tvm_page_prot;
unsignedshort vm_flags;
/* AVLtree of VM areas per task, sorted by address */
shortvm_avl_height;
structvm_area_struct *vm_avl_left;
structvm_area_struct *vm_avl_right;
/* linkedlist of VM areas per task, sorted by address */
structvm_area_struct *vm_next;
/* forareas with inode, the circular list inode->i_mmap */
/* for shmareas, the circular list of attaches */
/*otherwise unused */
structvm_area_struct *vm_next_share;
structvm_area_struct *vm_prev_share;
/* more */
structvm_operations_struct * vm_ops;
unsignedlong vm_offset;
structinode *vm_inode;
unsignedlong vm_pte;
/*用于共享内存,含 SHM_SWP_TYPE 和共享内存段 ID 号 */
};
(1)unsignedlong vm_start,vm_end;
vma 描述的虚拟内存段始于 vm_start,终于 vm_end。
(2)structinode *vm_inode;
如果 vma 段的内容是关于磁盘文件或设备文件的,vm_inode 指向该文件的 inode 结构,否则,vm_inode为 NULL。
(3)unsignedlong vm_offset;
如果 vma 段的内容是关于文件的,则vm_offset 就是该段内容相对于文件起始位置的偏移量。如果 vma 段的内容是关于共享内存的,则 vm_offset 就是 vma 段的起始地址vm_start相对于共享段始址的偏移量。
(4)structvm_operations_struct *vm_ops;
如果将 vma 段理解成“对象”的话,vm_ops规定了对该对象的操作“方法”。这些操作包括 open,close,swapout 等,见 include/linux/mm.h。进程通常占用几个vma 段,分别用于代码段、数据段、堆栈段等。属于同一进程的 vma段通过 vm_next 指针连接,组成链表。struct mm_struct 结构的成员 structvm_area_struct *mmap 表示进程的 vma 链表的表头。
为了提高对 vma 段查询、插入、删除操作的速度,Linux 同时维护了一个 AVL(Adelson
Vslskii and Landis)树。在树中,所有的vm_area_struct 虚存段均有左指针 vm_avl_left 指向相邻的低地址虚存段,右指针 vm_avl_right 指向相邻的高地址虚存段, struct mm_struct 结构的成员 struct vm_area_struct *mmap_avl 表示进程的 AVL 树的根,vm_avl_height
表示 AVL 树的高度。对 vma 段可以进行加锁、加保护、共享和动态扩展等操作。

2.4.4 Linux虚拟页的组织和管理

在 Linux 中,每一个用户进程都可以访问 4GB 的线性虚拟空间。其中从0 到 3GB 的虚拟内存地址是用户空间,用户进程可以直接对其进行访问。从 3GB 到 4GB 的虚拟内存地址为内核态空间,存放仅供内核态访问的代码和数据,用户态进程不可直接访问。当用户进程通过中断或系统调用访问内核态空间时,就会触发处理器的特权级转换(从处理器的特权级3 切换到特权极 0),即从用户态切换到内核态。所有进程从 3GB 到 4GB 的虚拟空间都是一样的,有同样的页目录项,同样的页表,对应到同样的物理内存段。Linux
以此方式让内核态进程共享代码段和数据段。内核态虚拟空间从3GB到3GB+4MB的一段(也就是进程页目录第768项所管辖的范围)被映射到物理空间 0 到 4MB 段。因此,进程处于内核态时,只要通过访问虚拟空间 3GB 到3GB+4MB 段,即访问了物理空间 0 到 4MB 段。
上述两种空间对用户进程来说都是透明的,用户进程所访问的内存地址都是连续的 4GB线性虚拟地址。因此,我们首先关心的是 Linux 是如何划分虚拟空间的。Linux 采用请求页式(DemandPaging)技术管理虚拟内存。标准 Linux 的虚存页表应为三级页表,依次为页目录(PGD,Page Directory)、中间页目录(PMD,Page Middle Directory)和页表(PTE,Page Table),如图12所示:

图12 Linux 的三级页表结构
在 Intel 微机上,Linux 的页表结构实际为两级。80386 体系结构的页管理机制中的页目录就是 PGD,页表就是 PTE,而 PMD 和 PGD 实际是合二为一的,所有有关 PMD 的操作实际上是对PGD的操作。所以源代码中形如*_pgd_*()_*pmd_*()的函数所实现的功能是一样的。其实现方法是提供一组转换宏(见 include/asm/pgtable.h),使得转换页表时不需要知道页表的入口格式。宏定义形如:
#define_PAGE_4M0x080 /* 4MB 页 */
/*PMD_SHIFTdetermine the size of the area a second-level page table can map */
#definePMD_SHIFT 22
#definePMD_SIZE (1UL<<PMD_SHIFT) /* PMD 页表大小为4G=1024*4K
#definePMD_MASK ((PMD_SIZE-1))
/*PGDIR_SHIFT determines what a third-level page table entry can map */
#definePGDIR_SHIFT 22
#definePGDIR_SIZE (1UL<<PGDIR_SHIFT)
#definePGDIR_MASK(~(PGDIR_SIZE-1))
#definePTRS_PER_PTE 1024
#definePTRS_PER_PMD 1
#definePTRS_PER_PGD 1024
externinline pmd_t * pmd_alloc_kernel(pgd_t * pgd,unsigned long address)
{
return(pmd_t *)pgd;
}
externinline pmd_t* pmd_alloc(pgd_t * pgd,unsigned long address)
{
return(pmd_t *)pgd;
}
每 当 启 动 一 个 新 进 程 , Linux 都 为 其 分 配 一 个 task_struct 结 构 , 内 含saved_kernel_stack,kernel_stack_page,ldt,tss,mm等内存管理信息。其中,task_struct 结构内嵌mm_struct结构,此结构包含了用户进程中与存储有关的信息。
structmm_struct
{
ugd_t *pgd;
unsignedlong context;
unsignedlong start_code,end_code,start_data,end_data;
unsignedlong start_brk,brk,start_stack,start_mmap;
unsignedlong arg_start,arg_end,env_start,env_end;
unsignedlong rss,total_vm,locked_vm;
unsignedlong def_flags;
structvm_area_struct * mmap;
structvm_area_struct * mmap_avl;
structsemaphore mmap_sem;
};
其中参数意义如下所示:
pgd:进程页目录的起始地址。
start_code,end_code:进程代码段的起始地址和结束地址。
start_data,end_data:进程数据段的起始地址和结束地址。
start_brk,brk:进程未初始化的数据段的起始地址和结束地址。
arg_start,arg_end:调用参数区的起始地址和结束地址。
env_start,env_end:进程执行时环境区的起始地址和结束地址。
rss:进程内容驻留在物理内存的页面总数。
mmap:指向 vma 段双向链表的指针。
mmap_avl:指向 vma 段 AVL 树的指针。
mmap_sem:对 mmap 操作的互斥信号量,由 down()和 up()更改。
进程的虚存管理数据结构及其关系见图13.

图13 进程的虚拟内存管理数据结构及其关系

3. 总结

通过对Linux的虚拟内存管理子系统的分析,可以得知,其采用了比MINIX更为复杂的结构。其使用了Intel CPU 80386所支持的分段分页机制,使得共享更为方便,也使得保护有了更小的粒度,运用三级页表使得Linux所占用的内存更加少了。通过,此论述,得知为什么Linux在嵌入式领域发展迅速了。

参考文献

[1] [美] Andrew S.Tanenbaum,Albert S. Woodhull 著,向勇等译,《操作系统设计与实现》,第三版,电子工业出版社2011.6.
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: