目录

1. 段式内存管理机制

1.1 任务的全局部分和私有部分

1.2 任务的线性地址空间

1.3 段式内存管理

1.3.1 对物理内存的初步划分

1.3.2 段式内存管理策略

1.4 段式内存管理机制的缺点

1.5 解决段式内存管理缺点的思路

2. 页式内存管理机制

2.1 任务的虚拟内存空间

2.1.1 虚拟内存空间的初步划分

2.1.2 适用于分页模式的虚拟内存空间划分

2.2 虚拟内存如何映射到物理内存

2.2.1 物理内存的分页

2.2.2 段到页的拆分

2.3 虚拟内存映射到物理内存的硬件基础

2.3.1 必选的分段机制

2.3.2 段部件

2.3.3 页部件

2.4 页表详解

2.4.1 线性地址到物理地址映射示例

2.4.2 单级页表

2.4.3 多级页表

3. 使内核在分页机制下工作

3.1 为内核加载区创建恒等映射

3.1.1 什么是恒等映射

3.1.2 为什么要建立恒等映射

3.1.3 建立怎样的恒等映射

3.1.4 恒等映射建立流程

3.1.5 上机验证恒等映射效果

3.2 为内核加载区创建高端线性映射

3.2.1 什么是线性映射

3.2.2 为什么要建立线性映射

3.2.3 建立怎样的线性映射

3.2.4 线性映射建立流程

3.2.5 上机验证线性映射效果

3.3 为内核任务创建TCB

3.3.1 内核任务TCB线性地址

3.3.2 设置内核任务状态

3.3.3 设置内核任务下一个可分配线性地址

3.4 为内核任务创建TSS

3.4.1 分配TSS段[内存分配过程详解]

3.4.2 填充TSS段

3.4.3 确立内核任务

4 使用户任务在分页机制下工作

4.1 为用户任务创建TCB

4.2 创建用户任务阶段内存分配策略

4.2.1 创建用户任务阶段内存分配的目标与问题

4.2.2 创建用户任务阶段内存分配的解决方案

4.3 创建用户任务

4.3.1 清空内核任务页目录的前半部分

4.3.2 刷新TLB

4.4 为用户任务创建LDT

4.5 用户程序的加载和重定位

4.6 拷贝页目录


1. 段式内存管理机制

1.1 任务的全局部分和私有部分

在一个多任务系统中,每个任务包括2个部分,

1. 全局部分

所有任务共有,用来提供系统服务

2. 私有部分

每个任务各自的的数据和代码,与任务所要解决的具体问题有关,彼此不相同

由于任务是在内存中运行的,所以任务的全局部分和私有部分本质上是对内存空间的划分,即全局内存空间和私有内存空间

1.2 任务的线性地址空间

X86体系结构中对内存的访问是依靠段机制进行的,其中任务全局部分使用的内存段描述符定义在GDT中,任务私有部分使用的内存段描述符定义在每个任务的LDT中。我们可以计算一个任务线性地址空间的大小,

1. GDT有8192个表项,由于0号槽位不能使用,所以可用表项为8191个,每个段界限最长为4GB。因此,一个任务全局部分的线性地址空间最大为

(2^13 - 1) * 2^32 = 2^45 - 2^32 = 32TB - 4GB ≈ 32TB

2. LDT也有8192个表项,每个段界限最长为4GB,因此,一个任务私有部分的线性地址空间最大为32TB

因此,每个任务最大的线性空间约为64TB,但是32位的X86处理器最多只能访问4GB内存空间。解决该问题的方法, 就是使用段式内存管理策略

1.3 段式内存管理

1.3.1 对物理内存的初步划分

在一个多任务的系统中,对于每个任务,全局部分和私有部分各占32TB,所以可以将他们分别映射到4GB物理内存的高一半和低一半,即

1. 任务的全局部分占据物理内存的高2GB

2. 任务的私有部分占据物理内存的低2GB

说明1:这种对物理内存的划分不是强制的,更多地是本课程为了引入后续页式内存管理机制中对虚拟内存的划分而进行的铺垫

比如实际物理内存不足4GB的情况,就无法进行这样的内存划分,因此这里的划分更多是概念上的含义

说明2:需要注意的是,在段式内存管理中,内存段划分的就是物理内存空间。因为在保护模式中只使能分段内存管理的情况下,段部件发出的线性地址就是物理地址

说明3:在上述内存划分的条件下,由于全局部分和私有部分各自可用的物理内存只有2GB,因此定义的内存段长度最大也只能是2GB

1.3.2 段式内存管理策略

1.3.2.1 理论探讨

以任务的私有空间为例,假设任务的私有部分有8192个段,每个段均为2GB(这是一种极端情况,但是隐含了一种理想情况,就是每个段的长度相同),则每次只能将一个段加载到物理内存中,具体的策略如下,

1. 在创建这8192个段描述符时,将描述符的P位都清零,表示这些段都不在内存中

2. 将当前要访问或执行的段加载到物理内存的低2GB部分,然后将该段描述符的P位置为1

3. 如果需要访问或执行另一个段,可以将已加载到内存中的段从内存中移出,保存到磁盘中,然后将其对应的描述符P位清零。之后将需要访问或执行的段加载到物理内存,并将其对应的描述符P位置为1

通过上述基于段的换入换出,所有段轮流使用物理内存,从而实现以很小的物理内存运行线性地址空间很大的程序,这就是传统的段式虚拟内存管理策略

1.3.2.2 实际策略

上面是在理论上探讨如何使用有限的内存运行大程序多任务,但是在现实中,一个任务通常不会有那么多段;而且段的长度不同,通常都没有2GB

因为段的长度不同,而且段可能很小,所以在现实中物理内存可以同时容纳很多段(这些段可以来自不同任务)。尽管如此,如果任务很多,物理内存仍然会不足。如果物理内存不足,就需要进行段的换入换出,策略是将最少使用的段换出

说明1:利用段描述符A位实现将最少使用的段换出

① 在处理器访问一个段时,会自动将该段描述符A位置为1

② 段描述符A位的清零由操作系统负责,根据A位改变的频率,操作系统就可以知道哪个段是最少使用的,可以将他从物理内存换入磁盘,从而腾出空间给当前要访问或执行的段使用

说明2:进行段换入换出操作的时机

当处理器访问尚未加载到内存中的段时,由于其段描述符P位为0,处理器会触发段不存在异常(中断号为11),可以在该异常的处理函数中进行换入换出操作

当异常返回时,处理器会再次执行触发异常的那条指令(而不是触发异常的下一条指令),于是程序就可以继续运行了

1.4 段式内存管理机制的缺点

1. 换入换出耗时长

段式内存管理以段为单位进行换入换出,而读写磁盘相对处理器属于慢速操作,因此操作耗时长代价大

2. 内存碎片

随着段在内存中的换入换出,内存空间会变得支离破碎,会出现虽然总的可用内存足够但是并不连续的情况

例如要加载上图中的段5,虽然内存中确实存在可用空间,但是找不到一个长度足够的连续空间来存放该段

1.5 解决段式内存管理缺点的思路

我们先来理清造成段式内存管理缺点的原因,

1. 以段为单位进行操作,导致换入换出耗时长代价大

2. 段内要求物理地址连续,导致对连续内存要求高

3. 段的长度不同,导致内存碎片

为了解决这个问题,X86提供了分页技术,将物理内存分成大小相同的页,并将长度不同的段映射到长度相同的页

我们来看一下,引入分页技术后,能不能解决上面的问题,

1. 当物理内存不足时,以页为单位进行换入换出操作,代价较小(这里隐含的条件是页的长度一般小于段的长度)

2. 映射到物理内存中的页可以是离散的,降低了对连续内存的要求,只要页内连续即可

3. 页的大小是相同的,解决内存碎片问题

说明:分页模式下解决内存碎片问题,是指对内存的使用以大小相同的页为单位,不存在内存中有可用空间,但是不足以加载一个页的情况

而我们在Linux内存管理中常说的内存碎片,是以分配连续内存为度量的,即物理内存中存在空闲页,但是没有足够长度的物理连续内存。这和我们这里讨论的内存碎片,是两个不同的问题

其实在Linux内存管理中申请不定长度的连续物理内存,更类似段的特性,因此会在物理连续内存的管理中造成内存碎片,这也是Linux内核中引入slab等机制的原因

2. 页式内存管理机制

2.1 任务的虚拟内存空间

2.1.1 虚拟内存空间的初步划分

在X86处理器中可以假定每个任务都有独立的4GB虚拟内存空间,虚拟内存空间与32根地址线能访问的物理内存空间是一样大的

因为每个任务是由全局部分和私有部分组成,所以这个4GB的虚拟内存空间也被分为2部分,

1. 线性地址0x00000000 ~ 0x7FFFFFFF属于任务的私有内存空间

2. 线性地址0x80000000 ~ 0xFFFFFFFF属于任务的全局内存空间

说明:注意上图中的比例关系,任务的全局部分和私有部分很大,而4GB虚拟内存空间很小。这是因为如上文所述,任务全局部分和私有部分的线性地址空间最大为32TB

在一个多任务系统中,多个任务共享一个全局部分,也就是共享一个2GB的虚拟内存空间。同时每个任务又都有自己的私有部分,也就是都有自己独立的2GB虚拟内存空间

2.1.2 适用于分页模式的虚拟内存空间划分

上节虚拟内存空间划分的缺点就是任务的全局部分和私有部分依然可以有很多段,且段的长度也不一致,仍然需要每个段轮流使用虚拟内存,也仍然会在虚拟内存中存在碎片。那么为每个任务引入虚拟内存就失去了意义,在此基础上引入分页机制也就失去了意义

因此,为了适配分页模式,我们按如下方式对虚拟内存进行划分

1. 每个任务的总长度是4GB,其中全局部分2GB,私有部分2GB

2. 任务的全局部分和私有部分仍然可以分成多个段,只是这些段的总长度不超过2GB

此时,就可以将任务的全局部分和私有部分中的每个段按照他们原来的顺序和大小整体映射到虚拟地址空间

说明1:上述虚拟内存划分方案中,要求任务的全局部分和私有部分段的总长度不超过2GB,这是对不重叠访问范围的要求

如之前示例代码中4GB数据段和内核数据段的重叠访问,是不冲突的

说明2:使用上述虚拟内存划分方案后,就相当于完全不使用段式虚拟内存管理机制了

这其实是引入分页的目的,我们限制了任务全局部分和私有部分的总长度,就是为了不再使用段式虚拟内存管理机制

说明3:本课程中是假设每个任务都有独立的4GB虚拟内存空间,在实际操作系统中,可以为每个任务创建独立的4GB虚拟内存空间(e.g. 从Linux 2.2开始),也可以创建一个为所有任务共用的4GB虚拟内存空间(e.g. Linux 0.11)

2.2 虚拟内存如何映射到物理内存

为每个任务引入独立的虚拟内存空间,并将任务整体映射到虚拟内存,只是第一步,下一步就是将虚拟内存映射到物理内存

在这一部分,我们先说明对物理内存的分页,之后说明如何将虚拟内存中的段映射到物理内存中的页

2.2.1 物理内存的分页

1. 32根地址线可访问的最大物理内存为4GB,可以从逻辑上划分为若干等份,这就是分页

2. 根据不同的体系结构与设置,物理分页的大小是不同的,以最常见的4KB分页为例,4GB物理内存可以划分为(2^32 / 2^12 = 2^20)个页

说明1:对物理内存的分页是逻辑上的,而不是物理上的

说明2:对页起始地址的要求

页并不是起始于任何物理地址都可以,对于4KB的分页,每个页的起始地址必须4KB(2^12)对齐,也就是低12位均为0

对页起始地址的要求其实和页表项的设计也是相辅相成的,从理论上说,分页只是逻辑操作,对起始地址应该没有这么高的对齐要求。但是在设计页表项时,如果使用4KB分页,页表项中只记录地址的高20位,这就导致页的起始地址只能4KB对齐

关于页表项的具体格式,详见下文

说明3:计算页的起始物理地址

按上图进行分页,则第一个页的起始物理地址为0,那么要计算第N个页的物理地址,就是将(N - 1)左移12位

例如计算第0x100个页的物理地址,就是(0x100 - 1)左移12位(也就是十六进制后面加3个0),即0xFF000

2.2.2 段到页的拆分

一旦将物理内存分页,就可以将虚拟内存中的段按4KB进行拆分,并加载到物理内存的页中

2.2.2.1 从文件到虚拟内存中的段

1. 每个任务都有自己的可执行文件和其他文件,内核也有自己的可执行文件和其他文件

可执行文件中包含了代码段、数据段、栈段的定义以及段的实际内容。其他文件则可以是文档、图像、音视频等,他们可以被加载到数据段中进行处理

2. 当任务执行时,需要先将可执行文件中的段映射到虚拟内存。所谓映射,就是计算每个段在虚拟内存中的起始地址和长度,然后创建和安装他们的段描述符

2.2.2.2 从虚拟内存中的段到物理页

以上图中蓝色的段为例,他的长度为12606B,需要映射到4个页,其中3个页用满,1个页只使用318B(即使只需要1B,也要分配一个完整的页)

这里需要注意,虚拟内存中的段是连续的,但是分配的物理页不需要是连续的,处理器会确保跨页访问时的正确性

说明:页不连续的原因

在系统运行过程中,有些页被占用,有些页被释放,所以空闲的页和被占用的页是随机交错的

2.3 虚拟内存映射到物理内存的硬件基础

2.3.1 必选的分段机制

如上文所述,如果开启了分页机制,在执行任务前,要先将可执行文件中的段映射到虚拟内存,然后才能将段中的内容加载到物理内存中的页

为什么要引入虚拟内存并且要先将程序映射到虚拟内存呢 ?

因为Intel处理器是基于分段机制工作的,段管理机制对于Intel处理器而言是最基本的,在保护模式下无法关闭。所以我们必须按照处理器的要求分段,只不过在分页模式下,段是映射到虚拟内存的

说明:将程序映射到虚拟内存的主要工作,就是规划所有段在虚拟内存中的布局和位置,并根据这些信息来创建段描述符

需要注意的是,这只是一个规划,并不会将段中的数据或代码加载到这个位置,因为这只是虚拟内存,而不是真实的物理内存

2.3.2 段部件

在未开启分页功能时,不使用虚拟内存,而是直接在物理内存中分段。此时段部件输出的线性地址,也就是物理地址,用于直接访问物理内存

2.3.3 页部件

如果开启了分页功能,将使用虚拟地址。此时段部件输出的线性地址是虚拟内存中的地址,需要页部件将其转换为物理地址,才能访问物理内存

说明:开启分页功能后,我们就是在虚拟内存中分段,然后由页部件将虚拟内存映射到物理内存

2.4 页表详解

2.4.1 线性地址到物理地址映射示例

当一个程序加载时,操作系统既要在左边的虚拟内存中分配段空间,又要在右边的物理内存中分配相应的页面,并且要建立二者之间的对应关系

以上图为例,虚拟内存到物理内存的映射关系如下,该映射关系是以页为单位的,即虚拟页与物理页的对应关系

虚拟内存

物理内存

0x00200 000

0x00002 000

0x00201 000

0x00004 000

0x00202 000

0x00007 000

我们在这里引入

我们在这里引入2个概念,便于后续讨论

1. 虚拟页号

将虚拟内存中的段按页拆分时,会产生虚拟内存中的页地址,这个页地址也必须是4KB对齐的(如示例中,段的起始地址为0x002000C0,但是虚拟页的起始地址为0x00200000),也就是十六进制的后3位为0

虚拟页地址的高20位,是虚拟页号

2. 物理页号

与虚拟页对应的物理页地址的高20位,是物理页号

2.4.2 单级页表

2.4.2.1 单级页表引入

在开启分页功能后,段部件发出的线性地址被送到页部件,页部件会将其转换为物理地址,该转换过程如下,

1. 页部件将线性地址拆分为虚拟页号(线性地址高20位)和页内偏移(线性地址低12位)

2. 根据虚拟页和物理页的对应关系,将虚拟页号替换为物理页号(如示例中的0x00201 --> 0x00004)

3. 将物理页号和页内偏移组成物理地址,访问物理内存

因此,对于页部件而言,就需要在物理内存中建立一张映射关系表,记录虚拟页号和物理页号的对应关系,如下图所示

这是一个一维表格,所以称作单级页表,在表格建立后,使用虚拟页号作为索引来访问这个表格,就可以得到对应的物理页号,进而计算出物理地址

2.4.2.2 单级页表的建立

在加载程序分配内存时,就需要记录虚拟页号和物理页号的对应关系,这样在通过线性地址访问内存时,才能反向找到对应的物理内存

1. 加载程序时,首先在虚拟内存中规划和安排段,并创建段描述符

2. 对虚拟内存中的段进行拆分,如上图中的分段,需要3个虚拟页,虚拟页号分别为0x00200、0x00201和0x00202

3. 搜索物理内存中的空闲页,假设分配到的物理页号为0x00002、0x00004和0x00007

4. 以虚拟页号为索引(索引 * 4为表内偏移)访问页表,将物理页号写入对应的页表项

说明:根据虚拟地址就可以知道要填写哪个页表项,因为虚拟地址就包含访问页表的索引

2.4.2.3 从线性地址到物理地址的转换过程

1. 页部件将线性地址拆分为虚拟页号(线性地址高20位)和页内偏移(线性地址低12位)

2. 以虚拟页号为索引(索引 * 4为表内偏移)访问页表,查找对应的物理页号

3. 将物理页号和页内偏移组成物理地址,访问物理内存

说明:分页功能一旦开启,处理器就始终使用线性地址而不是物理地址工作了。也就是说,此时CPU发出的都是虚拟地址,都需要经过页表的映射

2.4.2.4 段页式内存管理机制

在页式内存管理中,页面的管理与分配是独立的,和虚拟内存空间中的分段没有关系。操作系统所要做的就是寻找空闲页面,把他分配给需要的段,并将物理页号填写到页表中

基于以上特点,同时为了充分挖掘分页内存管理的潜力,一般来说,每个任务都可以拥有4GB的虚拟内存空间,而实现的方式就是每个任务都有自己独立的页表。但是在整个系统中,物理页是统一调配的

说明1:基于页的虚拟内存管理

虚拟页面的分配最终要落实到物理页才能真正访问,所以随着对物理页的使用,会出现物理内存不足的情况。此时操作系统可以将暂时不使用的页换出到磁盘,并换入马上要使用的页,实现基于页的虚拟内存管理

说明2:每个任务可以使用相同的线性地址空间

由于每个任务有各自的4GB虚拟内存空间,所以每个任务都是在自己的虚拟内存空间中运行,从而实现了任务之间的隔离

以32位的Linux为例,每个进程都拥有相同的线性地址空间布局,如下图所示

还有一点值得说明,就是在没有分段机制的CPU中,这种通过每个任务的页表实现线性地址空间隔离的方法也被保留了下来

这也为操作系统内存管理机制兼容不同体系结构的CPU提供了保障,通过页表都是实现线性地址到物理地址的映射,只是对于不同的CPU,线性地址的生成方式不同

对于X86体系结构,线性地址是分段机制产生的;对于没有分段机制的CPU(e.g. ARM),CPU产生的就是线性地址

2.4.3 多级页表

2.4.3.1 单级页表的缺点

单级页表虽然理解和使用简单,但是非常消耗内存。以上文中的单级页表为例,共有2^20个页表项,每个表项4B,因此每个页表需要消耗4MB内存

说明1:每个页表消耗4MB多吗 ?

我们可以分析如下2组数据,

① 第一个支持分页模式的Intel处理器是80386,20世纪80年代,一台80386电脑的主流配置是拥有2MB内存。连一个页表都放不下,何况每个任务都有一个页表

② 对于现代电脑,虽然内存容量已经以GB为单位,但是由于每个任务的页表都需要4MB内存,如果系统中有1000个任务,4GB内存就没有了,这还仅仅只存储了任务的页表信息

说明2:每个单级页表都需要占满4MB内存吗 ?

一种思路是先只使用少部分内存,建立部分映射,然后根据需要再动态扩展。但是如上文所述,每个任务分为全局部分和私有部分,并且是分别占有虚拟内存的一半,他们将页表分开,并各自使用一半。这就导致在建立页表时,需要完全定义(虽然页表项可以无效,但是他所占据的内存已经消耗)

再参考上节中给出的Linux线性地址空间布局,从总体上是"两头实,中间空"的,这也导致在建立页表时需要完全定义

说明3:如果就是要实现单级页表的动态扩展,可以做到吗 ?

① 之所以原先的单级需要完全定义,是因为在单级页表中其实没有记录虚拟页号,而是直接使用线性地址中的虚拟页号部分来索引单级页表

② 因此,通过扩展单级页表,同时记录虚拟页号和物理页号,则可以实现单级页表的动态扩展,即单级页表中只包含需要映射的条目,以下表为例

虚拟页号

物理页号

0x00000

0x00010

0x00001

0x00015

0x00003

0x00020

可见未使用的虚

可见未使用的虚拟页号0x00002就没有建立页表项

③ 但是这样做也是有代价的

  • 页表项增大

由于要同时记录虚拟页号和物理页号,所以每条页表项长度要加倍

  • 查询耗时

由于不能使用线性地址中的虚拟页号部分直接索引页表,所以每次查表时都需要进行线性查找,时间复杂度为O(n)。即使使用二分查找改进,时间复杂度也为O(long(n)),而且还要根据虚拟页号维护表项的有序性

因此是得不偿失的

2.4.3.2 页目录与页表

从上面的分析可以看出,要解决单级页表内存消耗大的问题,就是要做到不使用的线性地址空间可以不建立映射,不为其建立页表

因此引入了多级页表机制,在32位X86体系机构中,将页表组织为页目录(Page Directory Table,PDT)和页表两级

1. 每个任务都有一个页目录表,页目录表有1024个表项(每个表项4B),指向1024个页表

2. 每个页表也有1024个表项(每个表项4B),指向1024个物理页

3. 由于页表的数量有1024(2^10)个,而每个页表可以管理1024(2^10)个物理页,所以可以管理2^20个物理页,这和使用单级页表是相同的

说明1:多级页表如何减少内存使用 ?

① 考虑一种极端情况,就是任务需要使用全部4GB虚拟内存空间,此时需要1个页目录和1024个页表,所需内存为(1 * 1024 * 4 + 1024 * 1024 * 4 = 4MB + 4KB),其实比单级页表还多消耗了4KB

② 但是在一个真实的系统中,程序或者任务并不会使用这么多虚拟内存空间。我们再考虑另一种极端情况,就是任务只使用4GB虚拟内存空间的最低和最高4KB。如果使用单级页表,需要4MB内存

如果使用两级页表,只需要1个页目录和2个页表(分别对应第1个和第1024个页目录项),所需内存为(1 * 1024 * 4 + 2 * 1024 * 4 = 12KB),这将大大减少内存消耗

也就是说,使用多级页表时,在最差情况下,消耗的内存与单级页表相当;在大多数情况下,消耗的内存远小于单级页表

说明2:多级页表的缺点

一个东西不可能只有优点没有缺点的~ 如果使用多级页表,在页部件进行地址翻译时,需要增加一次内存访问。也就是先索引页目录,再索引页表

为了解决该问题,CPU中引入了TLB,用于加快地址翻译的过程,详见下文

说明3:多级页表为什么要这么划分 ?

将页目录设计为1024个表项,页表也设计为1024个表项,不是没有原因的,这里核心的目标就是让页目录和页表都可以正好存储在一个物理页中

而页目录和页表所需内存 = 表项数 * 每个表项的大小 = 页大小

在32位X86体系结构中,每个页为4KB,每个表项4B,所以表项数就是1024个。也就是说,对于不同的体系结构,如果页大小和每个表项的大小有变化,则每一级的表项数也会不同

这里被限制的是每级表项的最大个数,假设页大小为4KB,但是每个表项的大小为8B,那么每级页表的表项数量最大个数就是(4KB / 8B = 512)

说明4:那么问题又来了,为什么要求将页目录和页表存储在一个物理页内呢 ?

因为每级页表的物理地址都会记录在上一级页表的表项中,如果本级页表分散在2个物理页中,无论连续还是非连续,将无法记录

这里要注意,物理内存中的每个页都要记录在一个表项中才能访问,即使是页表本身所在的页

说明5:页目录的地址由谁记录 ?

如上图所示,物理页的物理地址记录在页表中,页表的物理地址记录在页目录中,那么页目录的物理地址记录在哪里呢 ?

处理器中的CR3寄存器存储着当前任务页目录的物理地址,CR3寄存器的格式如下,

可见由于页4KB对齐,CR3只记录页目录物理地址的高20位。CR3中的PCD & PWT位为全局页表的Cache属性

如上图所示,任务的TSS段中也包含了CR3字段,也就是说每个任务都有自己的页目录地址。当任务切换时,CR3寄存器的内容也用TSS中的CR3字段更新,从而指向新任务的页目录。而页目录再指向一个个页表,这样就使得每个任务都只在自己的地址空间内运行

从上图也可以看出,页目录和页表也是普通的页,他们和普通页的差别只是在于功能不同。当任务终止时,页目录和页表所占用的页和任务占用的普通页一样,将被回收并分配给其他任务使用

2.4.3.3 多级页表下的地址转换

多级页表划分带来的是对线性地址的划分,而对线性地址划分的依据就是每级页表的表项数

如果采用上文所述的两级分页模式,页部件会将段部件发出的线性地址划分为三段,

1. 高10位:作为页目录索引,与CR3寄存器结合,索引到页表的物理地址

2. 中间10位:作为页表索引,和页表物理地址结合,索引到页的物理地址

3. 低12位:作为页内偏移,和页的物理地址结合,索引到物理页中的指定位置

说明:页部件在使用页目录和页表索引时,会自动将索引值 * 4,也就是每个表项的长度

2.4.3.4 页目录项和页表项格式

X86中页目录项和页表项格式类似,每个表项均为4B,具体字段如下,

1. 页表 / 页物理地址

由于页4KB对齐,所以只记录高20位,剩余的12位可以用于存储页表 / 页的属性

2. P(Present)存在位

1表示页表或页位于内存中;0表示页表或页不在内存中,需要先予以创建或者从磁盘换入

3. RW(Read / Write)读/写位

1表示页表或页可读可写,0表示只能读取

4. US(User / Supervisor)用户/管理位

1表示页表或页可以被任何特权级的程序访问,0表示只允许特权级为0 / 1 / 2的程序访问

5. PWT(Page-level Write-Through)页级通写位

1表示写数据到Cache时也要将数据写入内存(Write Through方式),0表示写数据到Cache时只更新Cache标记,并不同步更新内存(Write Back方式)

6. PCD(Page-level Cache Disable)页级高速缓存禁止位

1表示该页内容不可以被Cache,0表示可以(此时还要看CR0寄存器中的CD位这个总开关是否也是0)

7. A(Accessed)已访问位

该位由处理器固件设置,用来表示该页已经被访问过。对A位的清零由操作系统定期进行,通过统计A位被设置的次数,就可以知道该页被使用的频率

当内存紧张时,可以将较少使用的页换出到磁盘,同时将其页表项的P位清零,然后将释放的页分配给马上要使用内存的程序

8. D(Ditry)脏位

该位由处理器固件设置,用来表示该页是否已经被写入过数据

9. PAT(Page-Attribute Table)页属性表支持位

该位涉及更复杂的分页系统,和页高速缓存有关

10. G(Global)全局位

该位用于指示该页是否具有全局性质,如果页是全局的,那么他将在TLB中一直保持,这就意味着该页的地址转换速度会更快

因为TLB容量有限,只能缓存那些频繁使用的页表项。而且当改变CR3寄存器中的内容时(e.g. 任务切换时),整个TLB的内容会被刷新

11. AVL(Available)程序可以使用的位

这些位被处理器忽略,软件可以使用

2.4.3.5 页表填充示例

在分页模式下,某程序运行时,假设段部件发出一个线性地址0x0C005032访问内存数据,如果该线性地址对应的物理页是0x0000A000,页表的物理地址是0x00003000,那么操作系统在此程序开始运行前,需要如何设置与该线性地址相关的页目录项和页表项 ?

我们首先对线性地址进行拆分,

0x0C005032 -->0b0000 1100 0000 0000 0101 0000 0011 0010 -->0b00 0011 0000 | 00 0000 0101 | 0000 0011 0010页目录索引:0x030 页表索引:0x005 页内偏移:0x032

1. 填充页目录项

将页目录索引0x030 * 4,访问页目录,在该页目录项中填写页表的物理页号0x00003

2. 填充页表项

将页表索引0x005 * 4,访问页表,在该页表项中填写页的物理页号0x0000A

说明:这是加载程序时根据线性地址和分配到的页物理地址创建页表的过程,也是缺页异常时的处理流程主体,而缺页异常是Linux虚拟内存管理的核心

在发生缺页异常时,CR2寄存器中会保存导致异常的线性地址,操作系统在缺页异常处理函数中,分配物理页,之后根据CR2寄存器中的线性地址和分配到的页物理地址,即可填充相关表项

3. 使内核在分页机制下工作

3.1 为内核加载区创建恒等映射

3.1.1 什么是恒等映射

1. 恒等映射是转换前后虚拟地址(VA)和物理地址(PA)相等的映射

2. 建立恒等映射是小范围的,占用的空间通常是内核镜像的大小,一般以MB为单位

3.1.2 为什么要建立恒等映射

1. 目前我们在内核中开启分页功能,但是此时内核已经被加载到内存中,也就是说内核是在开启分页功能之前加载的

2. 此时页目录和页表还没有建立,要想让当前处理流程在开启分页后还能够正常执行,就必须让页部件发出的物理地址和段部件发出的线性地址相同,也就是建立恒等映射

说明:因为我们选择的开启分页功能的时间点是在内核中,才导致内核在开启分页之前就已经被加载。当然,这也是一般操作系统的处理方式,也就是uboot在关闭MMU的情况下将Linux内核加载到内存,之后跳转到内存执行,然后在内核中创建页表开启分页

但是如果在MBR中已经开启分页,则不存在上面的问题

3.1.3 建立怎样的恒等映射

我们首先来看一下内核被加载到物理内存后的布局,因为此时尚未开启分页功能,所以这既是线性地址空间布局,也是物理内存布局

1. 由于我们的内核很小,低端1MB内存足够使用,因此我们对低端1MB建立恒等映射

2. 一个页表项可以映射4MB(2^10 * 2 ^ 12)内存,因此建立低端1MB的恒等映射需要一个页目录和一个页表

说明:内核页目录表和页表的位置

页目录和页表各需要占用一个物理页,我们将他们部署在内核加载后的空闲内存区域中,其中,

① 内核页目录物理地址:0x00020000

② 内核页表物理地址:0x00021000

3.1.4 恒等映射建立流程

3.1.4.1 所有页目录项清零

将所有页目录项清零的目的是将表项的P位清零,表明页表均不在内存中,如果在此时进行地址转换,将会引发处理器异常

3.1.4.2 设置页目录项

此处共设置了2个页表项,我们分别予以说明。需要注意的是,此时尚未开启分页功能,所以线性地址就是物理地址

说明1:0号页目录项

① 0号页表项的索引为0,在页目录表内的偏移量也为0,将其设置为内核页表的物理地址0x00021000

② 写入页表项的值还包含页表权限,对应到页目录项格式,

  • P = 1,表示对应的页表位于内存中
  • RW = 1,表示对应的页表可读可写
  • US = 0,表示对应的页表只能由特权级为0 / 1 / 2的程序访问

说明2:0x3FF号页目录项

① 0x3FF号页表项的索引为0x3FF,在页目录表内的偏移量为(0x3FF * 4 = 0xFFC = 4092)

② 写入物理地址为0x00020000,是页目录的地址,也就是说此处将页目录也作为页表使用

③ 之所以在页目录表中登记页目录自己的物理地址,是为了便于在开启分页功能后使用线性地址访问页目录自身,详见后文分析

④ 写入该页表项的权限与之前相同

⑤ 使用最后一个页目录项指向页目录自身,会占用一个页目录项,进而使得虚拟地址最高端的4MB内存无法映射到普通物理页

3.1.4.3 设置页表项

1. 在低端1MB的恒等映射中,内核占据了虚拟内存和物理内存的低端1MB,线性地址范围和物理地址范围为0x00000000 ~ 0x000FFFFF

2. 上述地址范围可以划分出256个4KB页,因此需要填写256个内核页表表项,填充效果如下图所示

3. 在填充完有效地址范围后,将内核页表的其他表项清零

说明:此处建立的恒等映射不仅在虚拟地址上连续,在物理地址上也连续。这是因为内核已经被加载到物理连续的物理内存中,我们为之建立的页表映射肯定也是连续的

3.1.4.4 启动分页功能

此时,内核页目录和页表均已创建,我们将页目录表的物理基地址传送到CR3寄存器,并操作CR0寄存器使能分页功能

说明1:由于CR3记录了当前任务的页目录表基地址,因此也称作页目录表基地址寄存器PDBR(Page Directory Base address Register)

说明2:控制寄存器操作指令

mov CRx, r32 ;从32位通用寄存器传送到控制寄存器mov r32, CRx ;从控制寄存器传送到32位通用寄存器

可以使用mov指令直接操作控制寄存器(但是我们需要知道,操作控制寄存器的mov指令和普通的mov指令的编码是不同的,因为控制寄存器是随保护模式引入的)

说明3:由于已经使能了保护模式,此处将CR0的bit 31置为1,即可使能分页功能

只有在保护模式下才能开启分页功能,如果PE为0时设置PG位,将产生异常中断

3.1.5 上机验证恒等映射效果

3.1.5.1 启动分页之前

1. 使用info tab命令查看页表状态

可见当前未使能分页功能

2. 使用creg命令查看控制寄存器状态

可见CR0寄存器的PG位未设置

3.1.5.2 启动分页之后

1. 使用info tab命令查看页表状态

说明1:按照当前的内核页目录和页表设置,共建立了3个线性地址到物理地址的映射,下面逐一说明

① 线性地址0x00000000 ~ 0x000FFFFF --> 物理地址0x00000000 ~ 0x000FFFFF

这就是低端1MB内存的恒等映射

② 线性地址0xFFC00000 ~ 0xFFC00FFF --> 物理地址0x00021000 ~ 0x00021FFF

这里映射了一个页(4KB),映射到的物理页起始地址为0x00021000,也就是内核页表的地址。通过拆分该线性地址,就可以知道为什么该线性地址映射的是内核页表

  • 高10位:0x3FF,索引到页目录表的0x3FF号表项,此时页目录作为页表使用
  • 中间10位:0x0,索引到页表的0号表项,该表项填写的是内核页表的物理地址

因此通过该线性地址最终访问的就是内核页表

③ 线性地址0xFFFFF000 ~ 0xFFFFFFFF --> 物理地址0x00020000 ~ 0x00020FFF

这里也是映射了一个页(4KB),映射到的物理页起始地址为0x00020000,也就是内核页目录的地址。通过拆分该线性地址,就可以知道为什么该线性地址映射的是内核页目录

  • 高10位:0x3FF,索引到页目录表的0x3FF号表项,此时页目录作为页表使用
  • 中间10位:0x3FF,索引到页表的0x3FF号表项,此时页表作为页使用

因此通过该线性地址最终访问的就是内核页目录

说明2:一旦使能分页功能,段部件发出的地址就都是虚拟地址了,需要经过页部件的转换。此时即使知道内核页目录和页表的物理地址,也只能通过线性地址访问

通过上面2个映射,就可以用线性地址访问内核页目录与页表自身。当然,通过低端1MB内存的恒等映射也可以访问内核页目录和页表

2. 使用creg命令查看控制寄存器状态

① CR0寄存器的PG位已设置

② CR3寄存器中记录的页目录物理地址为0x00020000

3. 使用page命令查看指定线性地址的映射关系

page命令共有3行输出

① PDE:线性地址对应的页目录项,也就是页表的物理地址及权限属性

② PTE:线性地址对应的页表项,也就是物理页的物理地址及权限属性

③ 线性地址与物理地址的对应关系

说明:启动分页之后,也就使能了虚拟地址(线性地址),相关的调试命令也需要对应修改

① 使用线性地址设置断点

我们之前经常使用的b命令是以物理地址设置断点,而lb命令是以线性地址设置断点

② 使用线性地址查看内存

xp命令使用物理地址查看内存,x命令使用线性地址查看内存

3.2 为内核加载区创建高端线性映射

3.2.1 什么是线性映射

① 线性映射是转换前后虚拟地址(VA)和物理地址(PA)相差固定偏移量的映射

② 在线性映射中,可以通过简单计算,实现虚拟地址和物理地址"数值上的"相互转换

3.2.2 为什么要建立线性映射

如上文所述,每个任务都有自己的4GB虚拟内存空间,其中

1. 全局部分占用高2GB空间,线性地址范围为0x80000000 ~ 0xFFFFFFFF

2. 私有部分占用低2GB空间,线性地址范围为0x00000000 ~ 0x7FFFFFFF

因此我们需要将内核映射到虚拟内存的高2GB空间

3.2.3 建立怎样的线性映射

此时内核被加载到物理内存的低1MB空间(并且我们为其建立了1MB的恒等映射),因此我们需要将虚拟地址0x80000000 ~ 0x800FFFFF映射到物理内存0x00000000 ~ 0x000FFFFF,效果如下图所示

由于之前已经建立了恒等映射,因此在建立了线性映射后,将有2段虚拟内存空间映射到同一片物理内存

说明:在32位Linux中也会建立线性映射,将加载到物理内存低端的内核映射到虚拟地址空间高端(从3GB开始)

3.2.4 线性映射建立流程

3.2.4.1 修改内核页目录表

目的:将0x80000000开始的线性地址映射到物理地址低端1MB处

说明1:内核页目录表修改思路

① 因为已经有内核页表映射了低端1MB物理地址,所以只要修改页目录表中0x80000000线性地址对应的页目录项,使其指向已有的内核页表即可

如此一来,在页目录中有2个页目录项指向同一个页表,那么在访问上述2个线性地址范围时,最终访问的是同一段物理内存

② 那么0x80000000对应的页目录项是哪个呢?

如上文所述,线性地址的高10位是页目录表索引。线性地址0x80000000的高10位为0x200,因此需要修改第0x200号页目录项

③ 由于每个页目录项为4B,0x200号页目录项在页目录中的偏移量为0x800

因此,我们的任务就是在页目录偏移量为0x800的位置写入内核页表的物理地址,那么接下来的问题就是如何通过线性地址访问页目录

说明2:在分页机制下访问页目录自身

我们知道页目录的物理地址为0x00020000,但是由于已经开启了分页功能,程序只能使用线性地址。也就是说,访问页目录也需要经过页目录和页表地址转换

由于我们在建立恒等映射时,将页目录的0x3FF号表项设置为页目录自身的物理地址,所以页目录的线性地址为0xFFFFF000,这点在恒等映射章节已有分析

这里要特别注意0~4GB数据段的作用,示例代码中给出的页目录线性地址0xFFFFF000并不是段部件发出的线性地址,而只是段内偏移。因为有了0~4GB数据段,这里的段内偏移才和段部件发出的线性地址在数值上相同

说明3:示例代码分析

① 要修改的页目录项线性地址

  • 页目录的线性地址为0xFFFFF000
  • 要修改的页目录项偏移为0x800

所以要修改的页目录项线性地址为0xFFFFF800

② 如何得到该页目录项线性地址

示例代码中页目录的线性地址直接给出,然后通过移位运算得到页目录项偏移

  • 先右移22位,相当于仅保留线性地址0x80000000的高10位,也就是页目录索引
  • 再左移2位,也就是乘以4,将页目录索引转换为页目录偏移量

3.2.4.2 修改每个段描述符

目的:将每个段的线性地址迁移到0x80000000处

1. 在页目录中添加对应高端线性地址的页目录项后,只是意味着页部件可以对0x80000000 ~ 0x800FFFFF的线性地址进行转换,将其映射到物理地址的低端1MB

2. 页部件处理的线性地址是由段部件发出的,而段部件依据GDT或LDT中段描述符的线性基地址来生成线性地址。因此,如果段部件发出的线性地址不在0x80000000 ~ 0x800FFFFF范围,依然无法为内核建立高端线性映射

3. 因此,需要修改内核相关段描述符中的段基地址部分,将原段基地址 + 0x80000000;同时还要修改全局描述符表GDT自己的线性地址

4. 结合段描述符格式,只要将高双字的bit 31置为1即可,因此示例代码中使用了或运算

说明1:0 ~ 4GB数据段的描述符无需修改,因为该段的基地址为0,段界限为4GB,就是用于访问整个线性地址空间的

说明2:与GDT的修改类似,IDT也要进行相应修改,将其线性基地址增加0x80000000

说明3:显式刷新段寄存器内容,使段描述符修改生效

① 在修改完段描述符之后,虽然通过lgdt指令更新了GDTR寄存器中的内容,但是段描述符的修改并不会立即生效,也就是说此时段部件发出的线性地址还不在0x80000000 ~ 0x800FFFFF范围内

② 这是因为段寄存器分为可见的段选择器和不可见的段描述符高速缓存,而段描述符高速缓存的内容仅在段选择器内容被修改时更新,因此我们显式刷新段寄存器的内容,之后段部件发出的线性地址就在0x80000000 ~ 0x800FFFFF范围内了

③ 因为程序不能直接访问CS段寄存器,对他的刷新通过转移指令实现

说明4:示例代码中只是修改了IDT的线性基地址,并没有修改其中的门描述符,但是在建立高端线性映射之后,中断处理函数也应该被映射到高端线性地址

这里之所以无需修改门描述符,是因为门描述符中填写的是中断处理函数所在段的段选择子和偏移量,而该目标代码段选择子所指向的段描述符已经在GDT中进行了修改

说明5:将内核映射到高端线性地址之后的内存布局,如下图所示

3.2.5 上机验证线性映射效果

1. 首先,在页目录中添加高端线性映射表项之后,当前段部件产生的线性地址依然在低端,也就是之前建立的恒等映射区域

2. 使用info tab命令查看页表状态

可见线性地址0x80000000 ~ 0x800FFFFF已经映射到物理地址0x00000000 ~ 0x000FFFFF

3. 在通过转移指令刷新CS段寄存器之前,反汇编代码的线性地址在低端1MB

4. 刷新CS段寄存器之后,反汇编代码的线性地址在高端

说明1:至于同时新增的线性地址0xFFE00000 ~ 0xFFE00FFF到物理地址0x00021000 ~ 0x00021FFF(内核页表物理地址)的映射,则是由于将页目录作为页表使用造成的,具体原因上文已有说明

说明2:通过恒等映射访问页目录

示例代码中通过在页目录中记录页目录自身的物理地址来访问页目录(这个简直就是绕口令啊~),我们也可以通过恒等映射来访问页目录,建立的页表效果与之前一致

说明3:那么课程中提供的绕口令方法好在哪里呢?

如果使用课程中提供的方法,物理内核被映射到线性地址的哪个范围,内核页目录的线性地址都是0xFFFFF000。而通过恒等映射访问,需要知道内核页目录的物理地址;通过高端线性映射访问,需要知道内核页表在高端线性映射中的线性地址

3.3 为内核任务创建TCB

3.3.1 内核任务TCB线性地址

1. 在示例代码中,内核任务TCB所使用的内存是指定的,而不是动态分配的。内核任务的TCB位于物理内存的低端1MB,物理地址为0x0001F800,具体布局如下图所示

2. 由于已经将内核整体映射到高端线性地址,所以内核任务TCB的线性地址为0x8001F800,示例代码中将其定义为常量

3.3.2 设置内核任务状态

TCB中偏移为0x4处为任务状态,由于当前系统中只有内核任务且正在运行,因此将其设置为0xFFFF,即任务为忙状态

3.3.3 设置内核任务下一个可分配线性地址

1. TCB中新增了一个成员,用于存储该任务下一个可分配线性地址

2. 对于内核任务,将其初始值设置为0x80100000

说明1:为何需要记录任务的下一个可用于分配的线性地址

如上文所述,现在每个任务(包括内核任务)都有自己独立的4GB虚拟内存空间,当任务创建时,需要分配内存,以加载任务自己的代码和数据。在任务执行期间,也可能根据需要分配内存空间。而任务在分配内存时,都只在自己的虚拟内存空间中进行

对这部分的理解,就涉及使能分页功能后任务分配内存的流程

说明2:任务分配内存流程概述

① 分配虚拟页

任务首先在自己的虚拟内存中分配一段线性地址空间,分配线性地址时以页为单位,也就是分配虚拟页

② 分配物理页

操作系统搜索空闲的物理页,并分配足够的物理页给任务

③ 修改页表

在页目录和页表中登记线性地址和物理页的对应关系,建立映射

说明3:内核任务的下一个可用于分配的线性地址初值为何是0x80100000

① 内核任务使用的是线性地址中的全局部分,也就是高2GB(线性地址范围为0x80000000 ~ 0xFFFFFFFF)

② 内核的主体部分使用了从0x80000000 ~ 0x800FFFFF的1MB线性地址空间,因此可分配的线性地址从0x80100000开始

3.4 为内核任务创建TSS

创建TSS是处理器的要求,对于任何一个任务,TSS段都是不可或缺的,具体流程如下

3.4.1 分配TSS[内存分配过程详解]

内核任务TSS段使用的内存是动态分配的,而且必须是在内核任务的线性地址空间中分配,下面我们逐个例程地分析内存分配过程

3.4.1.1 allocate_memory例程

allocate_memory例程是在当前任务的线性地址空间中分配内存,因此先搜索TCB链表,查找当前状态为忙的任务,之后调用task_alloc_memory分配内存

3.4.1.2 task_alloc_memory例程

1. 首先根据TCB中记录的下一个可分配线性地址以及要分配的字节数计算出本次分配的起始线性地址和终止线性地址

2. 构造循环,以页为单位分配内存并建立映射关系

说明:以页为单位分配内存

在task_alloc_memory例程中,会对要分配内存的线性地址范围进行4KB对齐,而且是一种向下对齐,这种对齐方式的效果如下

① 对起始线性地址向下4KB对齐

如果分配的起始线性地址不是4KB对齐,会将对应该线性地址的虚拟页全部划归此次分配,也就是页的起始部分有空闲不使用

② 对终止线性地址向下4KB对齐

结合jle指令的判断条件,会将对应该线性地址的虚拟页全部划归此次分配,也就是页的尾部有空闲不使用

这就是分页机制造成的内部碎片

3.4.1.3 alloc_inst_a_page例程

注:inst是install的简写

函数调用推进到alloc_inst_a_page例程时,已经得到了要分配的虚拟页线性地址,据此就可以分析出要填写的页目录项和页表项。此时就可以分配物理页,并修改页表建立映射

从alloc_inst_a_page例程的流程可见,需要访问页目录项和页表项,因此要通过线性地址访问页目录和页表,相关方法在上文中已有介绍,关键是在页目录中填写了页目录自身的物理地址,此处通过图示再次进行说明

说明1:访问线性地址对应的页目录项

① 首先是要得到页目录的线性地址

高10位(页目录索引):0b11 1111 1111 = 0x3FF,将页目录作为页表使用

中间10位(页表索引):0b11 1111 1111 = 0x3FF,将页表作为页使用

低12位(页内偏移):0x000,也就是线性地址为页目录的起始地址

因此0xFFFFF000就是页目录的线性地址

② 其次要得到页目录项在页目录中的偏移

线性地址的高10位为页目录索引,将其乘以4,就是要访问的页目录项在页目录中的偏移。在示例代码中,就是将线性地址高10位右移20位得到

需要注意的是,这里之所以可以直接右移20位,而不是右移22位再左移2位,是因为已经通过AND指令将无关位清零

说明2:访问线性地址对应的页表项

① 首先是要得到页表的线性地址

高10位(页目录索引):0b11 1111 1111 = 0x3FF,将页目录作为页表使用

中间10位(页表索引):将线性地址的高10位作为页表索引使用

低12位(页内偏移):0x000,也就是线性地址是页表的起始地址

② 其次是要得到页表项在页表中的偏移

线性地址的中间10位为页表索引,将其乘以4,就是要访问的页表项在页表中的偏移

在示例代码中,通过整体移动线性地址的页目录索引和页表索引,一次性达到了目的,效果如下图所示

说明3:清空新建页表

在清空新建页表时,也要访问页表,但是是从页表的起始地址开始。和上例相比,就是减少了计算页表项在页表中的偏移

效果如下图所示

说明4:页目录已分配的场景

一个页目录项可以映射4MB内存,只要该范围内的一页建立了映射,该页目录项即会分配

说明5:页表已分配的场景

同样的道理,一个页表可以映射4KB内存,只要该范围内建立过映射,该页表项即会分配。比如2次分配在同一个虚拟页范围内,则只有第一次会分配物理页并建立映射

说明6:关于分配出的物理页权限

示例代码中将分配的页表与页的权限均设置为0x00000007,也就是,

① P(Present)= 1,页表或页在内存中

② RW(Read / Write)= 1,页表或页可读可写

③ US(User / Supervisor)= 1,页表或页可以被任何特权级的程序访问

其实对于页表(页表应该只有内核能操作)以及内核使用的页,特权级3的用户程序应该不能访问。示例代码只是做了简化处理,内核任务和用户任务均使用allocate_memory例程分配内存

3.4.1.4 allocate_a_4k_page例程

说明1:物理页管理概述

① 物理内存是有限的,而且由所有任务共享。为了分配页,需要跟踪哪些页已经分配,哪些页为空闲。如果物理页已经耗尽,还需要执行磁盘的换入换出,将较少使用到的页的内容写入磁盘,然后将该物理页重新映射给另一个即将要使用的线性地址

② 操作系统必须在刚获得系统控制权时就检测实际的物理内存数量,并建立数据结构登记每个物理页的信息,该数据结构有2个要点

  • 标识物理页的起始地址(要填入页目录项或页表项)
  • 标识物理页的分配状态(用于查询是否有空闲页)

③ 当有程序申请内存时,就在该数据结构中寻址空闲页,如果找到,则将该页分配给该任务,并将其状态标记为已分配

说明2:使用页映射位串管理物理页

示例代码中使用页映射位串来管理物理页,结构如下图所示

我们来看页映射位串能否解决上文提到的物理页管理的2个问题,

① 标识物理页的起始地址

  • 比特位在页映射位串中的位置,标识了他所对应的物理页地址
  • 将比特位在位串中的序号(从0开始)乘以0x1000(即左移12位),就得到他所对应的物理页的地址

② 标识物理页的分配状态

  • 每个比特的值决定了物理页的分配情况
  • 当比特值为0,表示他所对应的页未分配,是可以用于分配的空闲页;否则就表示该页已被占用

说明3:页映射位串的长度

① 页映射位串的长度取决于实际拥有的物理内存数量

② 对于32位处理器,可访问的物理内存为4GB,因此页映射位串有2^20个比特位,由于每字节包含8个比特,所以折合128KB(2^20 / 8 = 2 ^ 20 / 2 ^3 = 2 ^17)

③ 在示例代码中,没有实际检测物理内存的容量,而是假设拥有2MB物理内存。对于2MB物理内存,可划分为(2^21 / 2^12 = 2^9 = 512)个页,因此页映射位串有512个比特,也就是64B

需要注意的是管理物理页的数据结构也要存储在内存中,所以该数据结构本身不能占用太多内存

说明4:页映射位串的定义

① 程序中没有声明位串的方法,只能声明字节 / 字 / 双字等,因此只能用连续的节 / 字 / 双字数据形成位串(具体可使用的数据类型,与体系结构有关)

② 在示例代码中,通过声明连续的64个字节来表示2MB物理内存的页映射位串

③ 在示例代码定义的页映射位串中,低端1MB的比特值几乎均为1,其中的几个值为0是人为设置的。用于展示任务申请的虚拟内存是连续的,但是映射的物理内存可以是离散的。这些设置为0的物理页位于内核的空闲区域,因此不会导致问题

说明5:页映射位串的检索

① 检索页映射位串的目的是查找系统中的空闲页

② 检索的方法是查找首个比特值为0的比特,然后通过该比特在位串中的位置,计算其对应的物理页地址,这就是空闲页的物理地址

③ 页映射位串的检索通过bts指令实现

说明6:bts(Bit Test and Set)指令

① bts指令格式如下,

bts r/m, r

功能:测试位串中的指定比特位,用该比特位的值设置EFLAGS寄存器中的CF标志位,然后将该比特位置为1

目的操作数:可以是16 / 32 / 64位通用寄存器,或者是一个指向位串起始位置的起始地址

源操作数:可以是16 / 32 /64位通用寄存器,用于指定待测试的比特在位串中的位置(索引)

如果目的操作数和源操作数都是寄存器,那么寄存器的长度必须是一致的

② 如果目的操作数是通用寄存器,那么指定的位串就是该寄存器的内容,所以串的长度是16 / 32 / 64位

但是源操作数可以指定很大的索引值,远超过目的操作数寄存器的长度。所以在执行bts指令时,如果目的操作数是寄存器,处理器会根据目的操作数的长度先求得源操作数寄存器除以16 / 32 /64的余数,并将其作为待测试的比特位索引

③ 如果目的操作数是一个内存地址,那么他给出的就是位串在内存中的起始地址。同样地,源操作数用于给出待测试比特在位串中的位置

因为位串在内存中,所以位串的长度可以最大限度地延伸,具体长度取决于源操作数的尺寸(即可索引的最大长度)。如果位串长度超过最大索引值,则无法索引(e.g. 位串长度超过64KB,就无法使用16位寄存器进行索引)

④ 与bts同类型的指令还有btr / btc / bt

说明7:在示例程序中,当物理内存页耗尽时,执行hlt停机指令。在实际操作系统中,会执行物理页的换入换出操作

说明8:处理器如何检测实际物理内存数量

内存空间来自于插在主板上的内存条,按照新的工业标准,每个内存条上焊有一个很小的只读存储器,用于标明该内存条的容量和工作参数。作为一个PCI(E)设备,软件可以读取他,以获取计算机上的物理内存容量

说明9:Linux 0.11的物理页管理方式

Linux 0.11中使用mem_map数组管理物理页,mem_map数组定义如下

可见mem_map数组为unsigned char类型,成员个数为物理页个数,也就是说每个物理页使用1B管理

在系统启动过程中,首先将mem_map数组所有成员均初始化为USED状态,之后再根据实际物理内存容量将可分配物理页对应的成员设置为0

我们同样来分析一下,这样的物理页管理方式是否能解决物理页管理的2个问题,

① 标识物理页的起始地址

  • 数组下标标识了他所对应的物理页地址
  • 将数组下标乘以0x1000(即左移12位),就得到他所对应的物理页的地址

② 标识物理页的分配状态

  • 数组成员的值决定了物理页的分配情况
  • 当数组成员值为0,表示他所对应的页未分配,是可以用于分配的空闲页;当数组成员值为USED(即100),表示该页已被占用

3.4.1.5 内存分配全过程回顾

3.4.2 填充TSS

在通过内存分配得到内核任务TSS段所需内存后,需要填充如下内容

说明:TSS段中的其他成员没有填写,是因为示例代码中第一次任务切换一定是从内核任务切换到其他任务,此时处理器会将内核任务的当前执行状态保存到内核任务的TSS中。这些没有填写的信息,将由处理器在第一次任务切换时自动填写

由此也可以发现,示例代码中也可以不填写CR3寄存器的值,该值也会在第一次任务切换时被记录

3.4.3 确立内核任务

创建并填充内核任务的TSS段之后,就是创建TSS段描述符并将其安装在GDT中。最后使用ltr指令加载内核任务TSS段选择子,此时,内核任务就确立起来了

4 使用户任务在分页机制下工作

4.1 为用户任务创建TCB

1. 在内核线性地址空间创建用户任务TCB

① 在创建用户任务之前,先分配任务的TCB。此处调用allocate_memory例程分配TCB所需内存,如上文分析,该例程在当前任务的线性地址空间中分配内存。而当前任务正是内核任务,也就是在内核的线性地址空间中创建用户任务的TCB

② 之所以要在内核线性地址空间中创建,是因为内核需要访问用户任务TCB,以便进行任务管理(e.g. 任务调度时在TCB链表中选择目标任务并进行任务切换)

③ 如果不是在内核线性地址空间中分配TCB所需内存,就意味着在内核的页目录和页表中没有指向TCB所在物理页的表项,内核就不可能访问到他

2. 用户任务可用于分配的初始线性地址

每个用户任务都有自己的4GB线性地址空间,且其中的低2GB为任务的私有部分。因此在用户任务内部分配内存时,可以从线性地址0开始分配

4.2 创建用户任务阶段内存分配策略

4.2.1 创建用户任务阶段内存分配的目标与问题

1. 每个任务都有独立的4GB线性地址内存空间,实现的方式就是每个任务有自己独立的页目录和页表来实现线性地址到物理地址的映射

2. 创建用户任务的第一步就是分配内存,并将用户任务对应的程序加载进来。具体流程如下,

① 创建用户任务自己的页目录和页表,即分配页目录和页表所需物理页,并将页表的物理地址登记在页目录中

② 分配加载用户程序所需的内存,包括在线性地址空间分配虚拟页,在物理地址空间分配物理页,并将二者的映射关系登记在页表中

③ 将用户程序从硬盘中读出,并写入分配的虚拟页中,分页机制将通过页表的设置最终访问物理页

3. 内核可以为用户任务创建页目录和页表,也能够根据用户程序的大小分配物理页,并修改页目录和页表。但是他修改的应该是用户任务的页目录和页表,而不是内核任务自己的

现在的问题是,当前任务是内核任务,不是用户任务,所以使用的是内核任务的页目录和页表

4.2.2 创建用户任务阶段内存分配的解决方案

我们讨论如下3种方案

1. 在内核创建用户任务的页目录后,修改CR3寄存器,使其指向新分配的用户任务页目录

这种方案是不可行的,因为目前还在用户任务创建阶段,所调用的内核函数需要依靠内核页目录和页表才能工作。一旦切换为新分配的用户任务页目录,由于该页目录还是空的,当内核访问自己的地址空间时就会出错

2. 在内核创建用户任务的页目录后,将内核页目录的内容复制过去,然后切换到用户页目录工作

这种方案是可行的,他所依赖的理论基础,就是所有任务高2GB的线性地址空间都对应着相同的全局部分,也就是内核

3. 在创建用户任务时,先在内核任务的低2GB线性地址空间中分配内存,这会在内核页目录的低一半创建对应的页目录项,同时还会创建相应的页表,并分配物理页

之后从硬盘中读取用户程序,并写入分配的物理页中。等这些工作完成后,将内核任务的页目录复制一份,作为新创建用户任务的页目录

之所以可以使用内核任务低2GB线性地址空间,是因为内核任务没有私有部分,这部分页目录本身就是空闲的

说明:用户任务同时需要内核页表和用户页表

① 用户任务访问私有空间时,通过用户页表访问自己的私有物理页,不同的任务是互不干扰的

② 当用户任务需要使用内核服务时,访问全局空间,通过内核页表访问内核所占用的物理页

③ 因此用户任务的页目录中,低一半指向用户页表,高一半指向内核页表

而从特权级3的用户态进入特权级0的内核态,在示例代码中,则是依靠调用门和中断机制实现

这里埋伏一个问题,就是内核页表的修改如何同步到每个用户任务中。例如某次在内核态运行的函数在内核态分配了内存,新增了内核页目录项,那么这个页目录项的修改如何同步到其他任务

4.3 创建用户任务

下面说明load_relocate_program例程中的核心步骤

4.3.1 清空内核任务页目录的前半部分

4.3.1.1 为何要清空

由于创建每个用户任务时,都是借用内核页目录的低一半,因此在创建一个用户任务时,其中还保留着前一个任务的相关页目录项。如果不清空,内存分配例程会检查到以前已经分配过,并不予分配(参考上文alloc_inst_a_page例程的分析)。那么使用的就是上一个用户任务的页目录项,这是错误的

4.3.1.2 示例代码

示例代码中清空了内核页目录的前512项,对应着内核任务0 ~ 2GB线性地址空间。清空后重新载入CR3寄存器的值,则是刷新TLB的操作

4.3.2 刷新TLB

4.3.2.1 TLB概述

1. TLB是Translation Lookaside Buffer的简称,中文翻译则是五花八门,比如转换速查表缓冲器、转译后备缓冲区、快表等。我们统一使用TLB的简称

2. TLB的结构如下图所示

① TLB分为多个条目(entry)

② TLB的每个entry分为2个部分,

  • 标记:内容为线性地址的高20位,也就是虚拟页号
  • 页表数据:内容为访问权限和与虚拟页对应的物理页地址的高20位,也就是物理页号

说明1:TLB entry中的属性位来自于页表项,比如D(Dirty)位

说明2:TLB entry中的访问权来自页目录项和页表项,比如RW位和US位

由于页目录项和页表项均有访问权限位,而且可能不同。此时按最严格的访问权限执行,也就是说TLB中的访问权位是页目录项和页表项权限位的逻辑与操作

3. 引入TLB的原因

① 从TLB的结构就可以猜到他的作用是加快从线性地址到物理地址的翻译过程

② 在开启分页功能后,处理器需要使用页目录和页表将线性地址转换成物理地址,而访问页目录和页表是非常费时的(他们均在内存中)

因此,将页表项预先存放在处理器中可以加快地址翻译的速度。为此,在处理器内部专门构造了TLB这个特殊的高速缓存装置

4. TLB工作原理

在分页模式下,当段部件发出一个线性地址时,处理器用线性地址的高20位查找TLB中的entry

① 如果找到匹配项,即TLB命中,则使用entry中数据部分的物理页号进行地址翻译

② 如果没有找到匹配项,即TLB miss,则处理器访问内存中的页目录和页表,找到对应的页表项并将其填写到TLB中,之后进行地址翻译

说明1:TLB容量很小,如果装满了,需要及时淘汰掉不经常使用的条目

说明2:引入TLB之后的对性能的影响取决于TLB命中率,而TLB命中率由程序的时间局部性和空间局部性原理保证

说明3:处理器仅仅缓存P位为1的页表项,而且TLB的工作与CR3寄存器的PCD和PWT位无关

4.3.2.2 为何要刷新TLB

1. 对于页表项的修改不会同时反应到TLB中,如果内存中的页表项已经被修改,但是TLB中的entry还没有被更新,那么转换后的物理地址必定是错误的

2. 内核任务页目录的低一半用于创建用户任务,所以是频繁更新的。在创建用户任务时,需要先清空这部分页目录项,并刷新TLB。否则处理器将使用缓存的表项访问内存,这将产生错误

4.3.2.3 刷新TLB的方法

1. TLB的entry软件是不能直接访问的,所以不能直接更改或刷新其内容,但是重新加载CR3寄存器可以使得TLB中的所有entry失效,这就是示例代码中的做法

2. 在任务切换时,由于会使用目标任务TSS段中的CR3值设置CR3寄存器,也会隐式地导致TLB中的所有条目失效

说明:上述方法对于全局表项(即页表项中G位为1)是无效,被设置为全局的页表项会一直缓存在TLB中

4.4 为用户任务创建LDT

1. 在用户任务线性地址空间分配LDT

① 每个任务都有自己的TCB、TSS段和LDT段,其中TCB和TSS段应该创建在内核的线性地址空间中,这样做是为了保证内核可以访问到他们,并对任务进行管理。同时内核会映射到用户任务的全局部分,所以每个任务自己也可以访问到他们(当然,需要相应的特权级,这点可以通过陷入内核态实现)

② 用户任务的LDT不需要被内核访问,只有用户自己使用,因此可以在用户任务自己的私有空间中分配内存

说明:在哪个线性地址空间分配内存,影响到的是在哪个页目录和页表中有指向分配到的物理页的表项。因此,在用户任务的线性地址空间中分配LDT所需内存,在内核页目录和页表中就没有相应的表项指向LDT

2. 通过task_alloc_memory例程在指定任务的线性地址空间分配内存

我们来回顾一下task_alloc_memory例程的参数

其中EBX指向的是要分配内存的任务TCB线性地址,此时是内核任务代替用户任务分配内存。虽然是在用户任务的线性地址空间中分配内存,但是却是登记在内核任务的页目录和页表中,后续会将内核页目录拷贝给用户任务

4.5 用户程序的加载和重定位

用户程序的加载和重定位过程与开启分页功能之前类似,此处不再赘述。这里说明一下SALT表中新增的malloc函数

通过SALT表和调用门,用户任务可以调用malloc函数。而内核中实现malloc函数功能的则是allocate_memory例程,该例程在当前任务的线性地址空间中分配内存,同时分配相应的物理页并修改页表

4.6 拷贝页目录

在load_relocate_program例程的最后,调用create_copy_cur_pdir例程将内核任务的页目录拷贝给用户任务

下面我们就来分析create_copy_cur_pdir例程

1. 将新分配的页目录物理地址登记在内核页目录中

将为用户任务新分配的页目录物理地址登记在内核页目录的倒数第2个页目录项中,是为了使用线性地址访问该页目录,这种方法在上文中已有说明

这里就涉及2个问题,

① 要登记的页目录项的线性地址

高10位:0x3FF,索引到页目录的0x3FF号表项,此时页目录作为页表使用

中间10位:0x3FF,索引到页表的0x3FF号表项,此时页表作为页使用

低12位:登记在低0x3FE号表项,每个表项4B,因此页内偏移为0xFF8

因此要登记的页目录项的线性地址为0xFFFFFFF8,效果如下图所示

② 登记后新页目录的线性地址

高10位:0x3FF,索引到页目录的0x3FF号表项,此时页目录作为页表使用

中间10位:要索引到0x3FE号表项,所以值为0x3FE,此时新分配的页目录作为页使用

低12位:访问新分配页目录的起始地址,因此页内偏移为0

因此登基后新页目录的线性地址为0xFFFFE000,效果如下图所示

2. 刷新TLB中的指定entry

① 线性地址0xFFFFFFF8对应的页目录项位于内核页目录中,每当我们创建一个新的用户任务时,都用他来指向新分配的用户任务页目录

② 我们在修改这个页目录项时,只修改了内存中的内容,并不能同步到TLB中。此时TLB中对应entry缓存的通常是上一个被创建任务的页目录,所以需要强制刷新这个页目录的缓存,以便他与内存中新修改的内容保持一致

说明:INVLPG(Invalidate TLB Entry)指令

invlpg指令用于刷新TLB中的单个条目,指令格式如下

invlpg m

该指令需要指定一个线性地址,指令的操作数是一个内存地址,而不是一个立即数,因此指令的正确用法如下

invlpg [0xfffffff8] ;正确invlpg 0xfffffff8   ;错误

注意:invlpg是特权指令,只能在特权级0下执行;且该指令不影响任何标志位

X86汇编语言从实模式到保护模式19:分页和动态页面分配相关推荐

  1. x86汇编语言从实模式百度云_Intel x86 CPU 32位保护模式杂谈之任务切换 上

    目录: 什么是任务 任务由什么组成 任务门描述符是什么东东?有了TSS描述符为什么要有任务门描述符? 参考文献 什么是任务 任务(task)是处理器可以分配.执行.挂起的工作单位,笔者认为和我们操作系 ...

  2. 硬盘和显卡的访问与控制(一)——《x86汇编语言:从实模式到保护模式》读书笔记01

    本文是<x86汇编语言:从实模式到保护模式>(电子工业出版社)的读书实验笔记. 这篇文章我们先不分析代码,而是说一下在Bochs环境下如何看到实验结果. 需要的源码文件 第一个文件是加载程 ...

  3. 16位模式/32位模式下PUSH指令探究——《x86汇编语言:从实模式到保护模式》读书笔记16...

    一.Intel 32 位处理器的工作模式 如上图所示,Intel 32 位处理器有3种工作模式. (1)实模式:工作方式相当于一个8086 (2)保护模式:提供支持多任务环境的工作方式,建立保护机制 ...

  4. 《x86汇编语言:从实模式到保护模式》视频来了

    <x86汇编语言:从实模式到保护模式>视频来了 很多朋友留言,说我的专栏<x86汇编语言:从实模式到保护模式>写得很详细,还有的朋友希望我能写得更细,最好是覆盖全书的所有章节. ...

  5. 《x86汇编语言:从实模式到保护模式》读书笔记之后记

    本来打算把整本书的读书笔记写完,可是由于有其他的计划(就叫做"B计划"吧)且优先级更高,所以我的读书笔记搁浅了.为了全力以赴执行B计划,我的博客要荒芜一段时间(我希望不要永远荒芜下 ...

  6. 处理器在实施任务切换时的操作——《x86汇编语言:从实模式到保护模式》读书笔记39

    处理器在实施任务切换时的操作--<x86汇编语言:从实模式到保护模式>读书笔记39 处理器可以通过以下四种方法实施任务切换: 1. call指令或者jmp指令的操作数是GDT内的某个TSS ...

  7. 任务切换——《x86汇编语言:从实模式到保护模式》读书笔记38

    任务切换--<x86汇编语言:从实模式到保护模式>读书笔记38 本文及后面的几篇博文是原书第15章的学习笔记. 本章依然使用第13章的主引导程序. 1. 协同式多任务与抢占式多任务 有两种 ...

  8. 任务切换的方法——《x86汇编语言:从实模式到保护模式》读书笔记37

    任务切换的方法--<x86汇编语言:从实模式到保护模式>读书笔记37 1. 中断门和陷阱门 在实模式下,内存最低端的1M是中断向量表,保存着256个中断处理过程的段地址和偏移.当中断发生时 ...

  9. 任务和特权级保护(五)——《x86汇编语言:从实模式到保护模式》读书笔记36

    任务和特权级保护(五)--<x86汇编语言:从实模式到保护模式>读书笔记36 修改后的代码,有需要的朋友可以去下载(c14_new文件夹).下载地址是: GitHub: https://g ...

最新文章

  1. 根号均摊 ---- E. Xenia and Tree(树形dp + 暴力根号均摊)
  2. chrome浏览器的跨域设置 Google Chrome浏览器下开启禁用缓存和js跨域限制--disable-web-security...
  3. MPLS基本结构是怎样的?—Vecloud微云
  4. 手机屏幕适配遇到虚拟键的问题
  5. python中协程实现的本质以及两个封装协程模块greenle、gevent
  6. 注意System.currentTimeMillis()潜在的性能问题
  7. Highcharts 配置语法;Highcharts 配置选项详细说明
  8. 2016年个人技术总结(前端)
  9. Apache Kylin 与 ClickHouse 的对比
  10. Alamofire拦截请求AOP,URLProtocol
  11. DIY基于Arduino的CNC绘图机
  12. 第四章 子载波均衡和导频矫正
  13. Rancher搭建Longhorn分布式存储
  14. 【从0开始学web】89-150 php特性
  15. 乐吾乐2D可视化绘图引擎
  16. 使用muscle进行多序列比对
  17. OTA线下攻防战 | 一点财经
  18. 一体化伺服电机编码器值清零或设置原点如何操作?
  19. Win11 与 macOS 12 界面对比
  20. C++高阶 RAII机制(以对象管理资源)

热门文章

  1. 树状数组求区间和 和 单点更新
  2. Python的pyproject.toml文件中的tool.poetry.dev-dependencies选项
  3. Nacos 启动报错 Unable to start embedded Tomcat
  4. java 与sas交互_SAS与MACRO的交互使用
  5. pb预览状态下的pagecount_QuickLook高效文件预览神器,方便到令你意想不到
  6. 核心编程第五版 配套代码_攻略Python的免费书单:走进编程,从这五本书开始...
  7. php-fpm通道,Go语言通道(chan)——goroutine之间通信的管道
  8. android通知背景色,android – 更改通知RemoteViews背景颜色
  9. ppt给图片增加高斯模糊_【毕业答辩】PPT美化:如何设计毕业答辩的封面
  10. springboot 事务_第六章:springboot开启声明式事务