HyperNews Linux KHG 讨论页面

80386 内存管理

指令中指定的逻辑地址首先由分段硬件转换为线性地址。然后,此线性地址由分页单元转换为物理地址。

386 上的分页

在分页单元的地址转换中存在两个级别的间接寻址。一个页目录包含指向 1024 个页表的指针。每个页表包含指向 1024 个页面的指针。寄存器 CR3 包含页目录的物理基地址,并作为 TSS 的一部分存储在task_struct中,因此在每次任务切换时加载。

32 位线性地址的划分如下
31 ...... 2221 ...... 1211 ...... 0
DIR (目录)TABLE (表)OFFSET (偏移量)

然后(在硬件中)计算物理地址,如下所示
CR3 + DIR指向 table_base (表基址)。
table_base + TABLE指向 page_base (页基址)。
physical_address = (物理地址 =)page_base + OFFSET

页目录(页表)是页对齐的,因此较低的 12 位用于存储有关条目指向的页表(页)的有用信息。

页目录和页表条目的格式
31 ...... 1211 .. 9876543210
ADDRESS (地址)OS (操作系统)00D (脏位)A (已访问位)00U/S (用户/超级用户)R/W (读/写)P (存在位)

D (脏位)1 表示页面是脏页(对于页目录条目未定义)。
R/W (读/写)0 表示用户只读。
U/S (用户/超级用户)1 表示用户页面。
P (存在位)1 表示页面存在于内存中。
A (已访问位)1 表示页面已被访问(通过老化设置为 0)。
OS (操作系统)这些位可用于 LRU 等,并由操作系统定义。

Linux 的相应定义在.

当页面被交换出去时,页表条目的位 1-31 用于标记页面在交换空间中的存储位置(位 0 必须为 0)。

通过设置 CR0 中的最高位来启用分页。[在 head.S 中?] 在地址转换的每个阶段,都会验证访问权限,并且内存中不存在的页面和保护冲突会导致页错误。然后,故障处理程序(在 memory.c 中)会调入新页面或取消页面的写保护,或执行需要完成的任何操作。

页错误处理信息

转换后备缓冲区 (TLB) 是一个硬件缓存,用于缓存最近使用的虚拟地址的物理地址。当转换虚拟地址时,386 首先在 TLB 中查找以查看它需要的信息是否可用。如果不可用,它必须进行几次内存引用才能访问页目录,然后再访问页表,然后才能真正访问页面。对于每个逻辑内存引用,地址转换需要三次物理内存引用,这将严重影响系统性能,因此需要 TLB。

如果加载 CR3 或任务切换更改 CR0,则会刷新 TLB。在 Linux 中,通过调用invalidate()显式刷新 TLB,这只是重新加载 CR3。

80386 中的段

段寄存器用于地址转换,以从逻辑(虚拟)地址生成线性地址。
linear_address = segment_base + logical_address (线性地址 = 段基址 + 逻辑地址)
然后,线性地址通过分页硬件转换为物理地址。

系统中的每个段都由一个 8 字节的段描述符描述,其中包含所有相关信息(基址、限长、类型、特权)。

段是

常规段
系统段

系统段的特性

为了跟踪所有这些段,386 使用一个全局描述符表 (GDT),该表由系统在内存中设置(由 GDT 寄存器定位)。GDT 包含每个任务状态段、每个局部描述符表以及常规段的段描述符。Linux GDT 仅包含两个正常段条目

GDT 的其余部分填充了 TSS 和 LDT 系统描述符

LDT[n] != LDTn
LDT[n]= 当前任务的 LDT 中的第 n 个描述符。
LDTn= GDT 中第 n 个任务的 LDT 的描述符。

内核段具有基址 0xc0000000,这是内核在线性视图中所在的位置。在使用段之前,必须将该段的描述符内容加载到段寄存器中。386 具有一套复杂的段访问标准,因此您不能简单地将描述符加载到段寄存器中。此外,这些段寄存器具有程序员不可见的部分。可见部分通常称为段寄存器:cs、ds、es、fs、gs 和 ss。

程序员使用一个 16 位值(称为选择器)加载这些寄存器之一。选择器唯一地标识其中一个表中的段描述符。硬件验证访问并加载相应的描述符。

目前,Linux 在很大程度上忽略了 386 提供的(过度?)复杂的段级保护。它偏向于分页硬件和相关的页级保护。应用于用户进程的段级规则是

  1. 进程无法直接访问内核数据或代码段
  2. 始终存在限长检查,但鉴于每个用户段都从 0x00 到 0xc0000000,因此不太可能应用。[这已更改,需要更新,请。]

80386 中的选择器

段选择器加载到段寄存器(cs、ds 等)中,以选择系统中的常规段之一作为通过该段寄存器寻址的段。

段选择器格式
15 ...... 32 10
index (索引)TI (表指示器)RPL (请求特权级)

TI 表指示器
0 表示选择器索引到 GDT 中
1 表示选择器索引到 LDT 中
RPL 请求特权级。Linux 仅使用两个特权级别。
0 表示内核
3 表示用户

示例

内核代码段
TI=0, index=1, RPL=0, 因此选择器 = 0x08 (GDT[1])
用户数据段
TI=1, index=2, RPL=3, 因此选择器 = 0x17 (LDT[2])

Linux 中使用的选择器
TI (表指示器)index (索引)RPL (请求特权级)selector (选择器)segment (段)
0100x08内核代码GDT[1]
0200x10内核数据/堆栈GDT[2]
030??????GDT[3]
1130x0F用户代码LDT[1]
1230x17用户数据/堆栈LDT[2]
系统段的选择器不能直接加载到段寄存器中。相反,必须加载 TR 或 LDTR。

进入系统调用时

段描述符

有一个段描述符用于描述系统中的每个段。有常规描述符和系统描述符。这是一个完整的描述符。奇怪的格式主要是为了保持与 286 的兼容性。请注意,它占用 8 个字节。
63-545554535251-4847464544-4039-1615-0
Base (基址)
31-24
G (粒度)D (脏位)R (保留位)U (保留位)Limit (限长)
19-16
P (存在位)DPL (描述符特权级)S (段描述符类型)TYPE (类型)Segment Base (段基址)
23-0
Segment Limit (段限长)
15-0

Explanation (解释)
R (保留位)reserved (保留) (0)
DPL (描述符特权级)0 表示内核,3 表示用户
G (粒度)1 表示 4K 粒度(在 Linux 中始终设置)
D (脏位)1 表示默认操作数大小为 32 位
U (保留位)programmer definable (程序员可定义)
P (存在位)1 表示存在于物理内存中
S (段描述符类型)0 表示系统段,1 表示正常代码或数据段。
Type (类型)有很多可能性。对于系统和正常描述符的解释不同。

Linux 系统描述符
TSS: P=1, DPL=0, S=0, type=9, limit = 231,可容纳 1 个tss_struct.
LDT: P=1, DPL=0, S=0, type=2, limit = 23,可容纳 3 个段描述符。
基址在fork()期间设置。每个任务都有一个 TSS 和 LDT。

Linux 常规内核描述符: (head.S)
code: P=1, DPL=0, S=1, G=1, D=1, type=a, base=0xc0000000, limit=0x3ffff (代码段:P=1, DPL=0, S=1, G=1, D=1, 类型=a, 基址=0xc0000000, 限长=0x3ffff)
data: P=1, DPL=0, S=1, G=1, D=1, type=2, base=0xc0000000, limit=0x3ffff (数据段:P=1, DPL=0, S=1, G=1, D=1, 类型=2, 基址=0xc0000000, 限长=0x3ffff)

task[0] 的 LDT 包含: (sched.h)
code: P=1, DPL=3, S=1, G=1, D=1, type=a, base=0xc0000000, limit=0x9f (代码段:P=1, DPL=3, S=1, G=1, D=1, 类型=a, 基址=0xc0000000, 限长=0x9f)
data: P=1, DPL=3, S=1, G=1, D=1, type=2, base=0xc0000000, limit=0x9f (数据段:P=1, DPL=3, S=1, G=1, D=1, 类型=2, 基址=0xc0000000, 限长=0x9f)

剩余任务的默认 LDT (exec())
code: P=1, DPL=3, S=1, G=1, D=1, type=a, base=0, limit= 0xbffff (代码段:P=1, DPL=3, S=1, G=1, D=1, 类型=a, 基址=0, 限长= 0xbffff)
data: P=1, DPL=3, S=1, G=1, D=1, type=2, base=0, limit= 0xbffff (数据段:P=1, DPL=3, S=1, G=1, D=1, 类型=2, 基址=0, 限长= 0xbffff)

内核段的大小为 0x40000 页(4KB 页,因为 G=1 = 1 吉字节)。类型意味着代码段的权限是读-执行,数据段的权限是读-写。

与分段关联的寄存器。

段寄存器的格式:(只有选择器对程序员可见)
16-bit (16 位)32-bit (32 位)32-bit (32 位)
selector (选择器)physical base addr (物理基地址)segment limit (段限长)attributes (属性)
段寄存器的不可见部分以程序员设置的描述符表条目中使用的格式更方便地查看。描述符表具有与之关联的寄存器,用于在内存中定位它们。GDTR(和 IDTR)在表定义后在启动时初始化。LDTR 在每次任务切换时加载。

GDTR(和 IDTR)的格式
32-bits (32 位)16-bits (16 位)
Linear base addr (线性基地址)table limit (表限长)

TR 和 LDTR 从 GDT 加载,因此具有其他段寄存器的格式。任务寄存器 (TR) 包含当前执行任务的 TSS 的描述符。跳转到 TSS 选择器的执行会导致状态保存在旧 TSS 中,TR 加载新描述符,寄存器从新 TSS 恢复。这是 schedule 用于切换到各种用户任务的过程。请注意,字段tss_struct.ldt包含该任务的 LDT 的选择器。它用于加载 LDTR。(sched.h)

用于设置描述符的宏

一些汇编器宏在 sched.h 和 system.h 中定义,以简化描述符的访问和设置。每个 TSS 条目和 LDT 条目占用 8 个字节。

操作 GDT 系统描述符

_TSS(n), _LDT(n)
这些为第 n 个任务提供 GDT 中的索引。
_LDT(n)存储在tss_structfork 的 ldt 字段中。
_set_tssldt_desc(n, addr, limit, type)
ulong *n指向要设置的 GDT 条目(参见 fork.c)。
段基址(TSS 或 LDT)设置为 0xc0000000 +addr.
上述的特定实例是,其中 ltype 指的是包含 P、DPL、S 和类型的字节
set_ldt_desc(n, addr)ltype = 0x82
P=1, DPL=0, S=0, type=2 表示 LDT 条目。
limit = 23 => 可容纳 3 个段描述符。
set_tss_desc(n, addr)ltype = 0x89
P=1, DPL=0, S=0, type = 9, 表示可用的 80386 TSS limit = 231,可容纳 1 个 tss_struct。
load_TR(n),
load_ldt(n)将任务号 n 的描述符加载到任务寄存器和 ldt 寄存器中。
ulong get_base (struct desc_struct ldt)
从描述符获取基址。
ulong get_limit (ulong segment)
从段选择器获取限长(大小)。
返回段的大小(以字节为单位)。
set_base(struct desc_struct ldt, ulong base),
set_limit(struct desc_struct ldt, ulong limit)
将设置描述符的基址和限长(4K 粒度段)。
此处的限长实际上是段的大小(以字节为单位)。
_set_seg_desc(gate_addr, type, dpl, base, limit)
默认值 0x00408000 => D=1, P=1, G=0
存在,操作数大小为 32 位,最大大小为 1M。
gate_addr必须是(ulong *)

版权所有 (C) 1992, 1993, 1996 Michael K. Johnson, johnsonm@redhat.com。
版权所有 (C) 1992, 1993 Krishna Balasubramanian 和 Douglas Johnson


消息

1. 反馈: 分页初始化,文档更新 作者:droux@cs.unm.edu
2. 反馈: 用户代码和数据段不再在 LDT 中。 作者:Lennart Benschop