Browsed by
月份:2017年4月

[Linux 0.11] Draft 6 IA32架构下多任务的硬件支持

[Linux 0.11] Draft 6 IA32架构下多任务的硬件支持

Overview

支持多任务的硬件结构为 Task Register (TR) Task State Segment (TSS), LDT 以及 Task Gate。而最核心的,存储任务上下文信息的就是 Task State Segment, 下面对其进行详细的说明

TSS基本数据结构

  • GDT, LDT
  • TSS(Task State Segment)  has its own descriptor called TSS Descriptor
  • Structure of 32 bit TSS, store the context and link to previous task, and 3 different privileged Stack

下面对一些比较关键的部分进行介绍

  • Previous Task Link: 存储的是上一个任务的选择符
  • LDT Segment Selector: 存储的是这个Task使用的LDT
  • I/O Map Base Address: I/O Map的基地址(要对 I/O Map 是什么进行进一步的解读: I/O Map 包含一个权限Map和一个Interrupt redirect Map)

TSS 描述符的一些说明

  • TSS Descriptor 用于描述 Task State Segment 的描述符,当选择符的TI Flag被置位的时候(即指向当前LDT)不可以访问TSS, 这种情况下,使用 CALL, JMP 会引发GP#, 使用IRET 会引发#TS(Invalid TSS Exception), TSS Descriptor只能被放在 GDT 中,不可以放在 LDT IDT 中
  • 下面是TSS描述符的结构

 Type 有两种 1001 1011, 前者表示该TSSD对应的Task处于 inactive 状态下,后者表示该 TSSD 对应的Task正处于Busy状态下

 

  • 每一个Task只能有一个TSS Descriptor对应
  • Task Gate Descriptor  做切换的时候不需要检查 CPL

 

任务切换过程

任务切换发生在下面几种情况下:

  • 当前运行的任务 JMP / CALL 了GDT中的一个TSS , 当 CALL / JMP 以 FAR 模式被调用的时候,如果 Segement Selector 指向的选择符是一个 TSSD, 那么就会发生 Task Switch, 忽略掉 CALL / JMP 的 offset。
  • 同样的,如果调用 JMP / CALL 而且 Selector 为 GDT / LDT 中的一个 Task Gate Descriptor(任务门描述符)也会发生 Task Switch
  • 发生中断/异常,且中断向量指向一个Task Gate Descriptor
  • IRET 指令被调用且 EFLAGS 寄存器的 NT Flag 被置位

 

任务切换过程比较繁琐,这里简单说明其过程

 

  1. 获取 TSS 的 Segment Selector, 如果是 JMP / CALL 则是直接给出的, 或者是通过中断指向的 Task Gate Descriptor 给出,如果是 IRET 指令,则查看当前 TSS 的 Previous Task Link (见上图 TSS结构图)获得 Selector
  2. 检查特权级别, 如果是 CALL / JMP 则需要检查特权级别,中断 和 IRET 不需要检查, INT n 指令引发的中断调用,会检查特权级别
  3. 检查 TSS 的合法性,以及其他必要检查
  4. 清除或者设置相应的标志位
  5. 将当前(old)任务状态保存到当前的TSS内,
  6. 如果任务切换不是因为 IRET 引起的,则将新任务的 TSSD 的 Busy 位置位
  7. 装载新的 Task Descriptor(Selector) 到 Task Register
  8. 将新的 Task State Segment(TSS) 中的上下文装入通用寄存器,段寄存器, EIP EFLAGS 等寄存器(见上图 TSS结构)
  9. 上下文(context)已经设置好了,开始执行新的任务(此时的EIP, CS等 寄存器已经是该TSS中给定的寄存器了,之前的任务完全转移走了)

下一篇文章会对Linux0.11中如何实现任务切换进行介绍

[Linux 0.11] Draft 5 虚拟内存管理(mm/memory.c)

[Linux 0.11] Draft 5 虚拟内存管理(mm/memory.c)

Overview

本文介绍 32 位保护模式下的内存分页, 以及 linux0.11 对物理内存的管理。

意义

既然要使用内存分页,那么就一定有他的优势所在。我们首先来探讨一下使用内存分页的意义, 分页在于为进程提供了虚拟地址空间,通过提供的这个虚拟地址空间,我们可以做到下面的这些:

  1. 精细的权限控制粒度: 内存页大小以 4KB 为一页单位(也可以是4MB, 这里不讨论这种情况), 可以针对每一个内存页设置该4K空间的访问权限。相比分段的粗粒度精细了很多,目前的分段功能只是一个兼容性考虑,分段都是将整个0 – 最大内存空间直接映射为一个段;
  2. 可以使得进程独享地址空间: 通过切换CR3寄存器的内容,可以实现多个进程占用同一个虚拟地址空间。
  3. 有效的解决了进程对大块连续内存的请求: 这里用一个例子来说明一下。

例子是这样的: 如果我们有16MB的物理内存,现在有进程A B C分别占用5MB, 2MB, 3MB内存, 且不共享内存。然后现在进程B退出了,我们还剩余8MB内存,可是如果不使用分页的话,内存连续分配, 我们现在没有办法分配出一个连续的8MB的空间了,只能分配最多最多6MB连续的内存空间, 如下图所示。

而现在存在分页,虚拟地址,我们完全可以分配连续的8MB的虚拟地址空间,其中2MB映射到物理内存的中间那个2MB空隙,其余6MB映射到最后剩余的6MB。

由此可见,分页是十分必要的,那么下面就来介绍一下如何实现分页&虚拟内存管理

地址转换过程

我们先来看一下虚拟地址是如何转换为物理地址的 完整的转换过程为

虚拟地址 --> (GDT) --> 线性地址 --> (Page Table) --> 物理地址

关于VirtualAddr -> LinearAddr 的过程我在之前的文章中有过介绍,这里就不做说明,想要跳过那个文章的读者可以简单的理解为目前的虚拟地址和线性地址值是相同的,因而我们直接从线性地址出发。首先来看下图

 

(图片来源:Intel® 64 and IA-32 Architectures Software Developer’s Manual)

线性地址为32位的地址, 其中高十位为 Page Directory,中间十位为 Page Table,最后十二位是 Offset。过程如下:

  1. 根据 CR3 寄存器(忘记了寄存器的功能的话戳这里)找到了页目录表的地址。
  2. 根据 Linear Address 的高十位与页表目录地址相加,找到了对应的页目录项(PDE)。
  3. 页目录项内存有该页目录的起始地址,将此地址与 Linear Address 21 – 12 位(即中间十位)相加,找到对应的页表项(PTE)
  4. 根据页表项中记载的物理页地址,找到对应的物理页起始地址,再与 Offset 相加,对应到实际的物理地址。

通过上面的过程就实现了线性地址到物理地址的转换,可以看出,页表和页目录在这个过程中扮演了十分重要的角色,下面就来介绍一下他们的结构。

结构

一个页目录项(PTE)和页表项(PDE)均占用 4Bytes 空间

(图片来源:Intel® 64 and IA-32 Architectures Software Developer’s Manual)

如上图所示,这是一个 Page Table Entry (页面大小为4K)

首先我们看 Bit 0 ,这是 Present bit,只有当这个 Bit 为 1 的时候此项才表示一个 PTE(或者PDE),否则就是一个无效的表项。

Bit 1 表示这个页面的读写权限,当这个位被置 1 的时候 表示可读可写, 为 0 则表示只读 ,不过这里要注意一点,对于 Ring0 且 CR0 的 WP 位为 0 情况来说,Ring0 可以对只读页面进行写入,如果想要让 Ring 0 也不可以写入只读页面,需要将 CR0 的 WP 位置1

Bit 2 表示这个页面是系统页面还是用户页面,如果是系统页面则用户态无法使用此页面。

后续的标志位暂时不介绍。我们来看高20位, 这里表示了物理页的起始地址,如果这是一个页目录项而不是页表项,则这里表示的是页目录的起始物理地址

转换缓存

通过上述的过程我们也可以看出, Page Transform 的过程需要多次读取内存,会映像执行效率,为了解决这个问题,CPU对转换信息进行了缓存,如  TLB, PCID  等技术,这些不在本文进行介绍,这里要说明的是,因为缓存的存在,导致如果更新了已被缓存的页表项的信息(如修改FLAG, 或者物理地址),我们需要使缓存失效,具体的做法就是重新装载一次 CR3 寄存器。

示例

说了这么多理论知识,我们来进行几个实际的操作看看。我们基于 linux0.11 的模型来进行下面四个实验(毕设结束后会放出git repo,目前暂无)

  • 使得某个特定的线性地址无效 (下文有介绍)
  • 将线性地址映射到给定的物理地址 (提示,查看 mm.c put_page 函数)
  • 修改该页的权限为只读并验证  (修改 FLAG 的另一个实验)
  • 给出当前线性地址所在页的详细信息 (打印出必要的FLAG信息,以及该线性地址对应的物理页地址)

disable_linear 实现

我们设计了这样的函数原型:

void disable_linear(unsigned long addr)

这个函数内我们需要对线性地址进行禁用, 根据上文中所介绍的,使得线性地址无效,实际上就是使得那个对应的页表项无效,因而我们需要通过线性地址对应到其页表项,并对该页表项的 Bit 0 清零。如何 Disable 与 Reload TLB 留给大家自行实现

linear_to_pte这个函数的实现过程实际上就是地址转换过程的前三步, 实现的时候要注意移位操作不要出问题,其他的没有什么难点

这里给出一个实现

上面的代码对页目录项是否存在,以及是否是页目录项进行了判断,失败将返回 0 。

下面我们需要对函数的正确性进行验证,由于此代码版本还没有补充缺页异常的处理代码,因而当访问失效的虚拟内存地址时,会引发Page Fault -> Double Fault -> Triple Fault最后导致我们的OS不断重启,我们来验证一下,验证用代码如下:

运行之后, 看到 qemu / bochs 的确在不停的 reboot , 并且可以通过 bochs 的 GUI 界面看到我们的页表由原有的16MB一致映射变为了中间缺少 0xdad000 页。

下面以实验结果截图结束本文