日期:2014-05-16  浏览次数:20741 次

Linux内存分段和分页管理

1.x86 内存架构和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项。
?
2.Linux的三级分页管理
X86中的分页管理
x86 架构中指定分页的字段,这些字段有助