内存寻址
概述
当通过逻辑地址访问物理地址时,内存管理单元通过分段单元将逻辑地址转换为线性地址,再通过分页单元将线性地址转换为物理地址,这一个过程就是保护模式下的地址转换。
硬件中的分段
段选择符(Selector
)与段寄存器
一个逻辑地址由两个部份组成:段和和段内偏移。其中,段可以用一个16 位的段选择符
来表示;而偏移,则可以用一个 32 位的变量来表示。具体逻辑地址的字段和位如下图:
15 ... 3 2 1 0 31 ... 1 0
+--------------+-+-+-+-----------------+
| |T| R | |
| INDEX |I| P | OFFSET |
| | | L | |
+--------------+-+-+-+-----------------+
|<-- 段选择符-->|<--- 段内偏移量 ------>|
段选择符字段描述:
INDEX
指示该地址在相应的全局段或局部段内索引号。TI
指示索引号属于全局段还是局部段。RPL
请求者权限,与cs
寄存器中的CPL
进行权限检查时使用。
处理器提供段寄存器来存放段选择符,以快速方便地找到段选择符:
-
cs
代码段寄存器,用于存放代码段,也就是程序的指令。该寄存器中包含了一个2-bit的区域:CPL
,也就是Current Privilege Level
, 代表了 CPU 的当前的特权等级 0为最高, 3为最低。 以便确认当前的 CPU 是否可以执行那段代码。此外,Linux中仅用了级别 0 和 3, 分别对应了内核态和用户态 -
ss
栈段寄存器 ,用于存放当前程序的栈。 -
ds
数据段寄存器 ,用于存放全局和静态的数据。 -
es
、fs
和gs
通用寄存器,这些寄存器用于不同的目的,一般先保存其中的值到内存,再写读,然后再用内存中的旧值恢复。
段描述符(Descriptor
)
每一个段
都由段选择器进行选择定位,并由大小为8个字节的段描述符来表示该段的各种属性。 段存放于GDT(Global Descritor Table)
或者LDT (Local Descritor Table)
中。 GDT 和 LDT 都有相应的寄存器存放,分别名为gdtr
和ldtr
。其结构如下图:
7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0
+---------------+-+-+---+-------+-+---+-+-------+---------------+
| | |D| A | | | D | | | |
| BASE(24-31) |G|+| V | LIMIT |P| P |S| TYPE | BASE(16-23) |
| | |B| l |(16-19)| | L | | | |
+---------------+-+-+---+-------+-+---+-+-------+---------------+
| BASE(0-15) | LIMIT(0-15) |
+-------------------------------+-------------------------------+
各个字段描述如下:
-
Base
该段的首字节的线性地址。 -
G(Granularity)
粒度。如果该位为 0 ,表明段的大小以字节
为单位, 如果为 1 ,表明大小以 4096 字节 为单位 . -
D/B
D或B标志,取决于是代码段还是数据段。如果段偏移量为32位则为 1;如果是16位则为 0 . -
AVL
Linux 中不使用该位。 -
Limit
中保存了该段的最后一个内存单元的偏移,它指明了该段的大小。 根据 G 的不同,该段的大小也不同:- G = 1: 段大小范围为 1 Byte ~ 1 MB (2^20 * 1 B)
- G = 0: 4K ~ 4GB (2^20 * 4096 B)
-
S(Sytem Flag)
,如果该 Flag 被设置,则表明该段中保存了关键的数据结构, 如 LDT 等等;否则,则说明该段为普通的数据段或者代码段。 -
TYPE
表明了该段的类型,以及访问权限。 -
DPL(Descriptor Privilege Level)
: 用于限制对该段的访问, 表明了想要对该段进行访问所需的最低权限。 例如, DPL=0的段只有CPL=0 (内核态)时候才能访问;而 DPL=3 的段,则任意 CPL 的 CPU 都可以访问。 -
P(Segment-Present flag)
: 该值为1则表明段存在于内存当中。因为Linux从来不把段交换到磁盘上去,所以总是1。
Linux将段描述符分为下列三类:
- 代码段描述符
- 数据段描述符
- 任务状态段描述符
逻辑地址到线性地址的转换
逻辑地址到线性地址的转换由分段单元(SEGMENTATION UNIT)
完成, 需要下面几个步骤:
-
根据
Selector->TI
选择GDT
或者LDT
。 -
Descriptor = GDT/LDT + Selector->Index * 8
由于每个段描述符都是 8 字节大小(步进),因此 Index * 8 才能得到段描述符。 -
LinearAddr = Descriptor->BaseAddr + Selector->Offset
得到线性地址。
Linux中的分段
分段与分页都是为进程分配不同线性地址空间,从而达到对进程的物理地址空间进行划分,以高效的管理访问内存。Linux着重在分页管理,分段管理非常的基础。
运行在用户态的所有进程都使用 用户代码段 和 用户数据段 进行指令和数据的寻址;内核态使用是 内核代码段 和 内核数据段 。这些代码或数据段的段描述符的基址都是0,所以逻辑地址的偏移量就是线性地址,且用户态和内核态可以使用相同的逻辑地址。
硬件中的分页
当需要访问物理内存时,分页单元将线性地址转换为物理地址(从页到页框的转换),在此过程中如果内存访问权限无效,会产生缺页异常。把线性地址映射到物理地址的数据结构就是页表
,存放在主存中。在系统启动初期,通过设置cr0寄存器的PG标志启用分页,并在后续启动中由内核对其初始化。
常规分页
线性地址的转换分为两步完成,每一步都基于一种转换表,第一种为页目录表(page directory),第二种为页表(page table)。每个活跃的进程都会分配一个页目录表,内核中称为内核全局也目录表,在系统启动时初始化。要完成这两步,首先将线性地址进行分段,一个32位的线性地址(这个值)被分成3个部分,如下图:
|7 6 5 4 3 2 1 0 7 6|5 4 3 2 1 0 7 6 5 4|3 2 1 0 7 6 5 4 3 2 1 0
+-------------------+-------------------+-----------------------+
| Directory Index | Table Index | Offset |
+-------------------+-------------------+-----------------------+
字段说明:
- Directory Index 页目录表中的索引
- Table Index 页表中的索引
- Offset 最终页表中页框对应物理地址的偏移
整个转换过程如下:
进程的 页目录表 的物理地址存放在控制寄存器 cr3 里面。在内核态与用户态切换时,回被内核自动装载相应的页目录表
- 根据
cr3
指向的页目录表地址
中找到页目录表
,再偏移线性地址中的Directory Index
个页目录表项
,从而得到相应的页表
。 - 从找到的
页表
再偏移线性地址中的Table Index
个页表项,得到页框表项
。 - 从找到的
页框表项
中的页框地址
(每一个以4096字节对其的页框
的地址的低 12 位均为0)偏移 线性地址中的Offset
个字节 得到最终的物理地址 。
每个页目录表项和页表项的结构都一样,仅字段内容有所不同,如下图:
|7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 7 6 5 4|3 2 1 0 7 6 5 4 3 2 1 0|
+---------------+---------------+-------+-----+-+-+-+-+-+-+-+-+-+
| HIGH 20 bit | | |P| | |P|P| | | |
| Page Directory Entry Addr |AVAIL|G|S|D|A|C|W|U|W|P|
| Page Table Entry Addr | | | | | |D|T| | | |
+---------------+---------------+-------+-----+-+-+-+-+-+-+-+-+-+
字段说明:
-
P(Present flag)
置1时候表示该项(目录项或者页表项)在主内存中;置0,则表示不在主内存中。 如果在对某个目录项或者页表项进行地址类型转换的时候发现该位为0, 分页单元 会将线性地址存到 cr2 上,然后触发 缺页异常 。 -
W(Writable)
表示该项是否可写,为1时候可写。 -
U(User/Supervisor Flag)
表明了访问该 page 所需的权限。 -
PWT(Write-through)
缓冲区相关。 -
PCT(Cache disable)
设置为1 的时候,表示关闭 缓冲存储器。 -
A(Accessed)
表明该项已经被访问过了。 -
D(Dirty)
仅用于页表项,表示该 Page Frame 被写过。 -
PS(Page Size Flag)
仅用于目录项,当该值为 0 时,表示目录项所指向的页面表(Page Table) 中的页面表项中的Frame 大小为 4K 。 而当该值为1时,页面表项的大小就变成了 4M 。 -
G(Global Flag)
仅用于页表项,为 1 时候,表明该页面为全局页面。 -
高20位为页框地址,由于页框有4KB对齐,所以物理地址低12位为0,故仅用20位表示物理地址。如果该项指向页目录表项,则页框就包含一个页表。如果指向页表项,则页框就包含最终要访问的物理地址基址。
每个项都是4字节,而一张表又是一个物理页框(大小4096字节),所以每级的表中的表项为1024个。而转换过程有2级表的操作,最终可以得出1024 * 1024 * 4096 = 4GB,这样一个可以映射的地址空间大小。
常规分页举例
这里有一个简单的例子阐明常规分页是如何工作的,假定内核已给一个正在运行的进程分配的线性地址空间范围是0x200000000到0x2003ffff,这个空间正好由64个页组成3。
我们从分配给进程的线性地址的最高10位,也就是Directory字段开始,这两个地址都以2开头后面跟着0,所以高10位有相同的值,即0x080或十进制的1284。因此,这两个地址的Directory字段都指向进程页目录的第129项。相应的目录项中必须包含分配给该进程的页表的物理地址。如果没有,则页目录的其余1023项都填0.
中间10位的值,也就是Table字段,其范围从0到0x03f,十进制的从0到63,因而只有页表的前64项是有意义的,其余的960个表项都填0.
假设进程需要读线性地址0x20021406中的字节,这个地址由分页单元按下面方法处理:
- Directory字段的0x80用于选择页目录的0x80目录项,此目录项指向和该进程的页相关的页表。
- Table字段0x21用于选择页表的第0x21表项,指向了包含所需页的页框。
- Offset字段0x406用于在目标页框中度偏移量位0x406中的字节。
如果0x21表项的Present标志为0,则说明此页不在主存当中,这种情况会产生一个缺页异常。当然,当进程试图访问任何超过0x200000000到0x2003ffff地址空间的范围之外的线性地址时,都会产生一个缺页异常。
地址转换过程总结图如下:
+-------------------------------+-------------------------------+
| Directory | Table | Offset |
+--------+----------------------+-------------------------------+
| | | Page Frame
| | | +---------+
| | | | |
| | v +---------+
| | Page +--->| ...... |
| | Table ^ +---------+
| | +---------+ | | |
| Page | | | | +---------+
| Directory v +---------+ | | |
| +----------+ +--->| ...... |---+--->+---------+
| | | ^ +---------+
v +----------+ | | |
+----> | ...... |----+--->+---------+
cr3 ^ +----------+
+----+ | | |
| |---------+----->+----------+
+----+
扩展分页
80x86还支持内存的扩展分页,允许页框大小4MB而不是4KB,这样可以把大段连续的线性地址转换成相应的物理地址。这样的转换少了中间页表转换的操作。
转换时,线性地址被分为表目录项索引和地址偏移两段,过程如下图:
+----------------------+-----------------------+
| Directory(10bit) | Table(22bit) |
+--------+-------------+--------+--------------+
| |
| |
| |
| |
| | Page
| | Frame
| | +---------+
| Page | | |
| Directory v +---------+
| +----------+ +--->| ...... |
| | | ^ +---------+
v +----------+ | | |
+----> | ...... |----+--->+---------+
cr3 ^ +----------+
+----+ | | |
| |---------+----->+----------+
+----+
Linux中的分页
为了保持 Linux 的可移植性,内核中使用的虚拟地址(Virtual Address, Linear Address)被分成了五段,相应的用于操作的页表也被分为四层, 分别为:
- 全局页目录表
PGD(Page Global Directory)
。 - 上层页目录表
PUD(Page Upper Directory)
。 - 中间页目录表
PMD(Page Middle Directory)
。 - 页表
PTE(Page Table)
。
这几个层次操作后得到页框地址再加上最后的一个页内偏移,共同构成了 Linux 的线性地址。
转换过程图(略):
- 请参考常规分页
- 根据体系的不同每个段位数不同。列如:80x86-64为9,9,9,9,12的段位数分布
Linux内核中的线性地址与各个段位相关宏如下:
|<------------------------------ BITS_PER_LONG --------------------------------->|
+---------------+---------------+---------------+---------------+----------------+
| PGD | PUD | PMD | PTE | Offset |
+---------------+---------------+---------------+---------------+----------------+
| | | |<- PAGE_SHIFT ->|
| | |<----------- PMD_SHIFT -------->|
| |<---------------- PUD_SHIFT ------------------->|
|<-------------------------- PGDIR_SHIFT ----------------------->|
Linux内核中页表结构如下图。
struct mm_struct PGD PUD PMD PTE PAGE FRAME
+----------+ +----------+ +----------+ +----------+ +----------+ +----------+
|pgd_t *pdg|---+ | pgd_t | | pud_t | | pmd_t | | pte_t | +-->| ...... |
+----------+ | +----------+ +----------+ +----------+ +----------+ | +----------+
| ...... | | | ...... | | ...... | | ...... | | ...... | | | ...... |
+----------+ | +----------+ +----------+ +----------+ +----------+ | +----------+
| | ...... | +-->| pud_t |---+ | ...... | | ...... | |
| +----------+ | +----------+ | +----------+ +----------+ |
| | ...... | | | ...... | | | ...... | +-->| pte_t |---+
| +----------+ | +----------+ | +----------+ | +----------+
+-->| pgd_t |---+ | ...... | | | ...... | | | ...... |
+----------+ +----------+ | +----------+ | +----------+
| ...... | | ...... | | | ...... | | | ...... |
+----------+ +----------+ | +----------+ | +----------+
| ...... | | ...... | +-->| pmd_t |---+ | ...... |
+----------+ +----------+ +----------+ +----------+