Linux Kernel: Memory Addressing
1-内存地址
我们偶尔会引用内存地址(Memory Address)访问内存单元内容;在 x86 处理器中,需要区分以下三种不同的地址:
- 逻辑地址(logical address)
- 在机器语言指令中用来指定一个操作数或者指令的地址。在 x86 中需要将程序分为若干段,每个逻辑地址都由一个段(segment)和偏移量(offset)组成
- 线性地址(linear address)or 虚拟地址(virtual address)
- 用一个 32 位无符号整数表示 4GB 的虚拟内存地址;通常用 16 进制数字表示,0x00000000 ~ 0xffffffff
- 物理地址(physical address)
- 用于内存芯片级内存单元寻址;物理地址由 32 位或者 36 位无符号整数表示
内存控制单元(MMU)通过分段单元(segmentation unit)的硬件电路把逻辑地址转换为线性地址,之后通过分页单元(paging unit)的硬件电路把线性地址转换为物理地址。
在多处理器系统中,所有 CPU 共享同一内存,因此多个 CPU 可以并发地访问 RAM 芯片。由于 RAM 芯片上的读写操作必须串行执行,所以一种所谓的内存仲裁器来控制并发访问。
2-Linux 分段
Linux 以非常有限的方式使用分段,实际上分段与分页在某种程度上有点多余,因为它们都用来划分进程的物理空间:分段可以给每个进程分配不同的线性地址空间;分页可以把同一个线性地址空间映射到不同的物理空间。与分段相比,Linux 更常用分页的方式:
- 当所有进程使用相同的段寄存器值时,内存管理比较简单,即所有进程能够共享同样的一组线性地址
- 使用分页的方式能够将 Linux 移植到大多数处理器平台上
在 Linux 中,逻辑地址与线性地址时一致的,逻辑地址的偏移量字段值与相应的线性地址值总是一致的。
3-硬件中的分页
分页单元(paging unit)把线性地址转换成物理地址。
线性地址被分成以固定长度为单位的组,称为页(page)。页内部连续的线性地址被映射到连续的物理地址中。这样,内核可以指定一个页的物理地址及页的存取权限,而不用指定页所包含的全部线性地址的的存取权限,提高了效率。
按照约定,页既指一组线性地址,也指包含在这组地址中的数据
分页单元把所有的 RAM 分成固定长度的页框(page frame,物理页)。页框与页的长度一致,因此每个页框包含一个页。页框是一个存储区域,是主存的一部分。
页只是一个数据块,可以存放在任何页框或者磁盘中
把线性地址映射到物理地址的数据结构称为页表(page table)。页表存放在主存中,并在启用分页单元之间必须由内核对页表进行适当的初始化。
3-1 常规分页
Intel 处理器的分页单元处理 4KB 的页。32 位的线性地址被分成 3 个域:
- Directory(目录:高 10 位)
- Table(页表:中间 10 位)
- Offset(偏移量:低 12 位)
线性地址的转换非为两步,每一步都基于一种转换表,第一种转换表称为页目录表(page directory),第二种转换表称为页表(page table)。
这种二级模式主要用于减少每个进程页表所需的 RAM 数量。每个活动进程必须有一个分配给它的页目录;不过,*没必要马上为进程的所有页表都分配 RAM*。只有在进程实际需要一个页表时才会给该页表分配 RAM。
不同的进程共享同一个线性地址空间集合,每个进程只使用 32 位地址空间的子集
进程正在使用的页目录的物理地址存放在控制寄存器 cr3 中。线性地址的 Directory 字段决定页目录中的目录项,而目录项指定适当的页表;线性地址中的 Table 字段又决定页表中的表项,而表项包含页所在页框的物理地址;线性地址中的 Offset 字段决定页框内的相对地址。
线性地址通过分页单元转换为物理地址
页目录项与页表项具有相同的结构,每项都包含下面的字段:
- Present 标志:1 表示所指的的页(页表)在主存中;0 表示该页不在主存中,触发缺页异常
- Field:包含页框物理地址的最高 20 位的字段;*如果这个字段指向一个页目录,相应的页框就会包含一个页表;如果该字段指向一个页表,则相应的页框就包含有一页的数据*。由于一个页框有 4KB 的容量,因此该字段指定的物理地址必须是 4096 的倍数(物理地址的最低 12 位总是 0)
- Accessed 标志:当分页单元对相应页框进行寻址时设置该标志
- Dirty 标志:只应用于页表项中;当对一个页框进行写操作时,设置该标志
- Read & Write 标志:页或者页表的存取权限
- User & Supervisor 标志:访问页表或者页所需要的特权级(硬件保护方案)
3-2 硬件保护方案
与页和页表相关的特权级有两种:如果 User & Supervisor 标志为 0,只有处理器处于内核态时才能对页进行寻址,如果 User & Supervisor 标志为 1,则总能对页进行寻址。
存取权限有两种:如果页目录项或者页表项的 Read & Write 标志被设为 0,则说明相应的页表或者页是只读的,否则是可读写的。
3-3 分页寻址流程
假设内核给一个进程分配的线性地址空间范围是 0x20000000 ~ 0x2003ffff(我们不需要关心包含这些页的页框的物理地址)。
假设进程需要读取线性地址 0x20021406 中的字节:
- Directory 字段的 0x80 用于选择页目录的第 0x80 目录项,该目录项指向那个与该进程相关的页表
- Table 字段的 0x21 用于选择页表的第 0x21 表项,该表项指向包含所需页的页框
- Offset 字段 0x406 用于在目标页框中读取偏移量为 0x406 中的字节
如果页表第 ox21 表项的 Present 标志为 0,则该页就不在主存中,此时会产生缺页异常,需要给其分配一个空闲页框之后再读取。另外,当进程试图访问 0x20000000 ~ 0x2003ffff 之外的线性地址时,会产生另一种缺页异常。
当新的进程被调度时,必须为新进程重置 MMU,刷新 TLB,清除以前进程的痕迹
由于只给该进程分配了第 0x80 个目录项,因此页目录的其余 1023 项都置为 0;同时,页表中只有前 64 个表项有意义,则其余的 960 个表项也都置为 0。
3-4 分页相关的工作
操作系统需要在进程创建时,进程执行时,缺页中断时和进程终止时四个时间段做一些与分页相关的工作。
3-4-1 进程创建
新进程创建时,操作系统需要确定程序和数据在初始化时有多大,并为其创建一个页表:在内存中为页表分配空间并对其进行初始化。当进程被换出时,页表不需要驻留在内存中,但是当进程运行时,必须在内存中。
3-4-2 进程执行
当调度一个进程执行时,必须为新进程重置 MMU,刷新 TLB;新进程的页表必须成为当前页表。
3-4-3 缺页中断
发生缺页中断时,操作系统必须能够知道是哪个虚拟地址造成的缺页中断;并计算出需要哪个页面,在磁盘上对该页面进行定位。操作系统需要找到合适的页框来存放新的页面,必要时置换出老页面,然后把页面读入到页框。最后重新执行引起缺页中断的指令。
3-4-4 进程终止
进程退出时,操作系统需要释放进程的页表,页面和页面在磁盘上所占的空间。如果某些页面时与其他线程共享的,当最后一个进程终止时才需要释放。
3-5 转换后援缓冲器(TLB)
x86 处理器中包含了一个被称为转换后援缓冲器(Translation Lookaside Buffer)的高速缓存用于加快线性地址的转换。当一个线性地址被第一次使用时,通过慢速访问 RAM 中的页表计算出相应的物理地址。同时,物理地址被存放在一个 TLB 表项(TLB Entry)中,以便以后对同一个线性地址的引用可以快速得到转换。
在多处理器系统中,每个 CPU 都有自己的 TLB,被称为该 CPU 的本地 TLB。当 CPU 的 cr3 控制寄存器被修改时,硬件自动使本地 TLB 中的所有项都失效。
当新的一组页表被启用时,TLB 指向的使旧数据
4-Linux 中的分页
Linux 采用了中同时适用 32 位与 64 位系统的普通分页模型。对 32 位系统来说,两级页表已经够了,但是 64 位系统需要更多数量的分页级别;Linux 采用了 4 级分页模型:
- 页全局目录
- 页上级目录
- 页中间目录
- 页表
线性地址被分成 5 部分;页全局目录包含若干页上级目录的地址,页上级目录包含若干页中间目录的地址,页中间目录包含若干页表的地址,每个页表项又指向一个页框。
Linux 进程的处理很大程度上依赖于分页;从线性地址到物理地址的转换使得:
- 给每个进程分配一块不同的物理地址空间,有效防止寻址错误
- 区别页(即一组数据)与页框(即主存中的物理地址)之间的不同。从而允许存放在某个页框中的一个页,然后保存到磁盘上,以后重新装入该页时又可以被装在不同的页框中。
每个进程都有自己的页全局目录与页表集。*当发生进程切换时,Linux 把 cr3 控制器的内容保存在前一个执行进程的描述符中,然后把下一个要执行进程的描述符的值装载到 cr3 寄存器中*。因此当新进程重新开始在 CPU 上执行时,分页单元指向一组正确的页表。
4-1 物理内存布局
在初始化阶段,内核需要指定哪些物理地址范围对内核可用,而哪些范围不可用。内核将下列页框标记为保留:
- 在不可用的物理地址范围内的页框
- 含有内核代码和已初始化的数据结构的页框
保留页框中的页绝对不会被动态分配或者交换到磁盘上
前 768 个页框(3 MB)
4-2 进程页表
进程的线性地址空间分为两部分:
- 0x00000000 ~ 0xbffffff 的范围,无论进程运行在用户态还是内核态都可以寻址
- 0xc00000000 ~ 0xffffffff 的范围,只有内核态才能寻址
当进程运行在用户态时,其产生的线性地址小于 0xc0000000;当进程运行在内核态时,执行内核代码,所产生的地址大于或等于 0xc0000000。但是,有时内核为了检索或者存放数据必须访问用户态线性地址空间。
页全局目录的第一部分表项映射的线性地址小于 0xc0000000,具体值依赖于特定进程
4-3 内核页表
内核维持着一组自己使用的页表,驻留在所谓的主内核页全局目录(master kernel page global directory)中。
内核初始化自己的页表分为两个阶段:
- 内核创建一个有限的地址空间,该地址空间仅能够将内核装入 RAM 和对其初始化的核心数据结构
- 内核充分利用剩余的 RAM 并适当建立分页表
4-4 处理 TLB
一般来说,任何进程切换都意味着更换活动页表集。相对于过期页表,本地 TLB 表项必须被刷新;这个过程在内核把新的页全局目录的地址写入 cr3 控制器时会自动完成。不过在有些情况下将避免 TLB 被刷新:
- 两个使用相同页表集的普通进程之间执行进程切换时
- 一个普通进程与一个内核线程之间执行进程切换时。内核线程并不拥有自己的页表集,他们使用刚在 CPU 傻姑娘执行过的普通进程的页表集