Linux学习总结—内存分段和分页管理

Linux技术 专栏收录该内容
7 篇文章 0 订阅
1x86 内存架构Linux的分段管理
x86 内存架构
x86 架构中,内存被划分成 3 种类型的地址:
·         逻辑地址 (logical address) 是存储位置的地址,它可能直接对应于一个物理位置,也可能不直接对应于一个物理位置。逻辑地址通常在请求控制器中的信息时使用。
·         线性地址 (linear address) (或称为 平面地址空间)是从 0 开始进行寻址的内存。之后的每个字节都可顺序使用下一数字来引用( 0 1 2 3 等),直到内存末尾为止。这就是大部分非 Intel CPU 的寻址方式。 Intel® 架构使用了分段的地址空间,其中内存被划分成 64KB 的段,有一个段寄存器总是指向当前正在寻址的段的基址。这种架构中的 32 位模式被视为平面地址空间,不过它也使用了段。
·         物理地址 (physical address) 是使用物理地址总线中的位表示的地址。物理地址可能与逻辑地址不同,内存管理单元可以将逻辑地址转换成物理地址。
CPU 使用两种单元将逻辑地址转换成物理地址。第一种称为分段单元 (segmented unit) ,另外一种称为分页单元 (paging unit)
 
2. 转换地址空间使用的两种单元
段由两个元素构成:
·         基址 (base address) 包含某个物理内存位置的地址
·         长度值 (length value) 指定该段的长度
每个段都是一个 16 位的字段,称为段标识符 (segment identifier) 或段选择器 (segment selector) x86 硬件包括几个可编程的寄存器,称为 段寄存器 (segment register) ,段选择器保存于其中。这些寄存器为 cs (代码段)、 ds (数据段)和 ss (堆栈段)。每个段标识符都代表一个使用 64 位( 8 个字节)的段描述符 (segment descriptor) 表示的段。这些段描述符可以存储在一个 GDT (全局描述符表, global descriptor table )中,也可以存储在一个 LDT (本地描述符表, local descriptor table )中。每次将段选择器加载到段寄存器中时,对应的段描述符都会从内存加载到相匹配的不可编程 CPU 寄存器中。每个段描述符长 8 个字节,表示内存中的一个段。这些都存储到 LDT GDT 中。段描述符条目中包含一个指针和一个 20 位的值( Limit 字段),前者指向由 Base 字段表示的相关段中的第一个字节,后者表示内存中段的大小。
段选择器包含以下内容:
·         一个 13 位的索引,用来标识 GDT LDT 中包含的对应段描述符条目
·         TI (Table Indicator) 标志指定段描述符是在 GDT 中还是在 LDT 中,如果该值是 0 ,段描述符就在 GDT 中;如果该值是 1 ,段描述符就在 LDT 中。
·         RPL (request privilege level) 定义了在将对应的段选择器加载到段寄存器中时 CPU 的当前特权级别。
由于一个段描述符的大小是 8 个字节,因此它在 GDT LDT 中的相对地址可以这样计算:段选择器的高 13 位乘以 8 。例如,如果 GDT 存储在地址 0x00020000 处,而段选择器的 Index 域是 2 ,那么对应的段描述符的地址就等于 (2*8) + 0x00020000 GDT 中可以存储的段描述符的总数等于 (2^13 - 1) ,即 8191
3. 从逻辑地址获得线性地址

Linux 中的段控制单元
Linux 中,所有的段寄存器都指向相同的段地址范围 —— 换言之,每个段寄存器都使用相同的线性地址。这使 Linux 所用的段描述符数量受限,从而可将所有描述符都保存在 GDT 之中。这种模型有两个优点:
·         当所有的进程都使用相同的段寄存器值时(当它们共享相同的线性地址空间时),内存管理更为简单。
·         在大部分架构上都可以实现可移植性。某些 RISC 处理器也可通过这种受限的方式支持分段。
Linux 使用以下段描述符:
·         内核代码段
·         内核数据段
·         用户代码段
·         用户数据段
·         TSS
·         默认 LDT
GDT 中的内核代码段 (kernel code segment) 描述符中的值如下:
·         Base = 0x00000000
·         Limit = 0xffffffff (2^32 -1) = 4GB
·         G (粒度标志) = 1 ,表示段的大小是以页为单位表示的
·         S = 1 ,表示普通代码或数据段
·         Type = 0xa ,表示可以读取或执行的代码段
·         DPL = 0 ,表示内核模式
与这个段相关的线性地址是 4 GB S = 1 type = 0xa 表示代码段。选择器在 cs 寄存器中。 Linux 中用来访问这个段选择器的宏是 _KERNEL_CS
内核数据段 (kernel data segment) 描述符的值与内核代码段的值类似,惟一不同的就是 Type 字段值为 2 。这表示此段为数据段,选择器存储在 ds 寄存器中。 Linux 中用来访问这个段选择器的宏是 _KERNEL_DS
用户代码段 (user code segment) 由处于用户模式中的所有进程共享。存储在 GDT 中的对应段描述符的值如下:
·         Base = 0x00000000
·         Limit = 0xffffffff
·         G = 1
·         S = 1
·         Type = 0xa ,表示可以读取和执行的代码段
·         DPL = 3 ,表示用户模式
Linux 中,我们可以通过 _USER_CS 宏来访问此段选择器。
用户数据段 (user data segment) 描述符中,惟一不同的字段就是 Type ,它被设置为 2 ,表示将此数据段定义为可读取和写入。 Linux 中用来访问此段选择器的宏是 _USER_DS
除了这些段描述符之外, GDT 还包含了另外两个用于每个创建的进程的段描述符 —— TSS LDT 段。
每个 TSS (TSS segment) 描述符都代表一个不同的进程。 TSS 中保存了每个 CPU 的硬件上下文信息,它有助于有效地切换上下文。例如,在 U->K 模式的切换中, x86 CPU 就是从 TSS 中获取内核模式堆栈的地址。
每个进程都有自己在 GDT 中存储的对应进程的 TSS 描述符。这些描述符的值如下:
·         Base = &tss (对应进程描述符的 TSS 字段的地址;例如 &tss_struct )这是在 Linux 内核的 schedule.h 文件中定义的
·         Limit = 0xeb TSS 段的大小是 236 字节)
·         Type = 9 11
·         DPL = 0 。用户模式不能访问 TSS G 标志被清除
所有进程共享默认 LDT 。默认情况下,其中会包含一个空的段描述符。这个默认 LDT 段描述符存储在 GDT 中。 Linux 所生成的 LDT 的大小是 24 个字节。默认有 3 个条目:
    UP 系统中只有一个 GDT 表,而在 SMP 系统中每个 CPU 有一个 GDT 表。所有 GDT 存放在 cpu_gdt_table[] 数组中,段的大小和指针存放在cpu_gdt_descr[]数组中。Linux的GDT布局如下图所示。它包含18个段描述符和14个Null、保留、未使用的段描述符。包括任务状态段TSS、用户和内核代码数据段、所有进程共享的局部描述段、高级电源管理使用的数据段APMBIOS data、即插即用设备代码数据段PNPBIOS、三个线程局部存储段TLS、第一个为null的段用于处理段描述符异常。
4 Linux Global Descriptor Table
 
Linux 启动时 GDT 段表的初始化
全局描述表 GDT 表的初始化分两个阶段:
  • 第一个阶段在setup中完成,此处是为系统进入保护模式做准备,把内核代码段和数据段的两个段描述符初始化放在GDT表中,这只是一个并不完整的临时GDT表。
  • 第二个阶段在arch/i386/kernel/head.S 文件中的startup_32()函数里,在这里加载head.s 文件中已经初始化的cpu_gdt_table描述表,该表有32项。
 
2Linux的三级分页管理
X86中的分页管理
x86 架构中指定分页的字段,这些字段有助于在 Linux 中实现分页功能。分页单元进入作为分段单元输出结果的线性字段,然后进一步将其划分成以下 3 个字段:
  • Directory 10 MSB 表示(Most Significant Bit,也就是二进制数字中值最大的位的位置 —— MSB 有时称为最左位)。
  • Table以中间的 10 位表示。
  • Offset 12 LSB 表示。(Least Significant Bit,也就是二进制整数中给定单元值的位的位置,即确定这个数字是奇数还是偶数。LSB 有时称为最右位。这与数字权重最轻的数字类似,它是最右边位置处的数字。)
Intel 的分页机制
5 x86 分页机制
 
Linux 的三级分页模型
虽然 Linux 中的分页与普通的分页类似,但是 x86 架构引入了一种 32 位和 64 位通用的三级页表机制,包括:
  • 页全局目录 (Page Global Directory),即 pgd,是多级页表的抽象最高层。每一级的页表都处理不同大小的内存 —— 这个全局目录可以处理 4 MB 的区域。每项都指向一个更小目录的低级表,因此 pgd 就是一个页表目录。当代码遍历这个结构时(有些驱动程序就要这样做),就称为是在遍历页表。
  • 页中间目录 (Page Middle Directory), pmd,是页表的中间层。在 x86 架构上,pmd 在硬件中并不存在,但是在内核代码中它是与 pgd 合并在一起的。
  • 页表条目 (Page Table Entry),即 pte,是页表的最低层,它直接处理页(参看PAGE_SIZE),该值包含某页的物理地址,还包含了说明该条目是否有效及相关页是否在物理内存中的位。
6 Linux 三级页表机制
为了支持大内存区域, Linux 采用了这种三级分页机制。在不需要为大内存区域时,即可将 pmd 定义成 “1” ,返回两级分页机制。
分页级别是在编译时进行优化的,我们可以通过启用或禁用中间目录来启用两级和三级分页(使用相同的代码)。 Intel 32 位处理器使用的是 pmd 分页,而 64 位处理器使用的是 pgd 分页。
每个进程都有自己的页目录和页表。为了引用一个包含实际用户数据的页框,操作系统(在 x86 架构上)首先将 pgd 加载到 cr3 寄存器中。 Linux cr3 寄存器的内容存储到 TSS 段中。此后只要在 CPU 上执行新进程,就从 TSS 段中将另外一个值加载到 cr3 寄存器中。从而使分页单元引用一组正确的页表。
pgd 表中的每一条目都指向一个页框,其中中包含了一组 pmd 条目; pdm 表中的每个条目又指向一个页框,其中包含一组 pte 条目; pde 表中的每个条目再指向一个页框,其中包含的是用户数据。如果正在查找的页已转出,那么就会在 pte 表中存储一个交换条目,(在缺页的情况下)以定位将哪个页框重新加载到内存中。
Linux 为内核代码和数据结构预留了几个页框。这些页永远不会 被转出到磁盘上。从 0x0 0xc0000000 PAGE_OFFSET 的线性地址可由用户代码和内核代码进行引用。从 PAGE_OFFSET 0xffffffff 的线性地址只能由内核代码进行访问。
这意味着在 4 GB 的内存空间中,只有 3 GB 可以用于用户应用程序。
 
Linux 分页的启动
Linux 进程使用的分页机制包括两个阶段:
  • 在启动时,系统为 8 MB 的物理内存设置页表。
  • 然后,第二个阶段完成对其余所有物理地址的映射。
在启动阶段, startup_32() 调用负责对分页机制进行初始化。这是在 arch/i386/kernel/head.S 文件中实现的。这 8 MB 的映射发生在 PAGE_OFFSET 之上的地址中。这种初始化是通过一个静态定义的编译时数组 ( swapper_pg_dir ) 开始的。在编译时它被放到一个特定的地址( 0x00101000 )。
这种操作为在代码中静态定义的两个页 —— pg0 pg1 —— 建立页表。这些页框的大小默认为 4 KB ,除非我们设置了页大小扩展位(有关 PSE 的更多内容,请参阅 扩展分页 一节)。这个全局数组所指向的数据地址存储在 cr3 寄存器中,我认为这是为 Linux 进程设置分页单元的第一阶段。其余的页项是在第二阶段中完成的。
第二阶段由方法调用 paging_init() 来完成。
32 位的 x86 架构上, RAM 映射到 PAGE_OFFSET 和由 4GB 上限 (0xFFFFFFFF) 表示的地址之间。这意味着大约有 1 GB RAM 可以在 Linux 启动时进行映射,这种操作是默认进行的。然而,如果有人设置了 HIGHMEM_CONFIG ,那么就可以将超过 1 GB 的内存映射到内核上 —— 切记这是一种临时的安排。可以通过调用 kmap() 实现。 
  • 0
    点赞
  • 2
    评论
  • 0
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值