Linux内存寻址
目录
1 内存地址类型
程序员通过内存地址 (memory address) 来访问内存单元中存储的内容。在 X86 架构上,需要区分下面三种地址类型:
- 逻辑地址 (Logical address)
逻辑地址是机器语言中指令操作符的操作数的地址。逻辑地址由段和偏移两个部分构成。
- 线性地址 (Linear address)
线性地址也被成为虚拟地址,一个 32 位的无符号数最多可以寻址 4 GB 的空间。线性地址通常用 16 进制来表示, 例如 32 位无符号数的寻址空间: 0x00000000 ~ 0xffffffff 。
- 物理地址 (Physical address)
物理地址用来在内存芯片中寻址,其地址和处理器向内存总线发出的电信号的引脚向对应,一般用 32 或者 36 位的无符号数来表示。
换句话说,物理地址是 CPU 和内存之间使用的地址。
三种地址之间的转换关系如下图:
+--------------+ +--------+
Logical Addr | SEGMENTATION | Linear Addr | PAGING | Physical addr
--------------> | UNIT |------------------>| UNIT |--------------->
+--------------+ +--------+
其中的 SEGMENTATION UNIT 负责将逻辑地址转换成线性地址, PAGING UNIT 负责将线性地址转换成为物理地址。这两个 UNIT 会在后面的小节中介绍。
多 CPU 会共用内存,而内存的读写必须是串行的,为此一个名为 memory arbitor (仲裁器) 的硬件。其实在单 CPU 的机器上,也使用这个仲裁器,因为单 CPU 上在使用 DMA 的时候也要处理内存的并发访问问题。但从编程的角度上来看,我们不需要关心这个仲裁器。
2 硬件上的“内存段式管理”
2.1 段选择符和段寄存器
前面提到,逻辑地址由两个部份组成:段和和段内偏移。其中,段可以用一个 16 位的“ 段选择符”来表示,而偏移,则可以用一个 32 位的变量来表示。
X86 处理器提供了段寄存器以便快速获取段描述符,这些寄存器包括:
- cs
Code Segment Register , 用于存放代码段,也就是程序的指令。
- ss
Stack Segment Register ,用于存放当前程序的栈。
- ds
Data Segment Register ,用于存放全局和静态的数据。
- es, fs, gs
通用寄存器。
其中, cs 寄存器还有一个重要功能: cs 寄存器中包含了一个 2-bit 的区域: CPL,也就是 Current Privilege Level, 代表了 CPU 的当前的特权等级 0为最高, 3为最低。以便确认当前的 CPU 是否可以执行那段代码。此外,Linux中仅用了级别 0 和 3, 分别对应了内核态和用户态。
2.2 段描述符 (Segment Descritor)
每一个”段“ 都由段选择器进行选择,并由段描述符来表示该段的各种属性,包括段的起始位置,段的长度等等。段描述符为 8 Byte 大小,存放于 GDT (Global Descritor Table) 或者 LDT (Local Descritor Table) 中。 GDT 和 LDT 都有相应的寄存器存放,分别名为 gdtr 和 ldtr 。至于该 Descritor 究竟存放于哪个寄存中,取决于段选择符的 TI 这个 bit.
下面这个表格说明了段描述符的组成:
Segment Descritor
Byte/ 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|7 6 5 4 3 2 1 0|
+-------------------------------+-------------------------------+
7 | BASE(24-31) |G|D|.|.|LIMIT()|P|DPL|S| TYPE | BASE(16-23) | 4
+-------------------------------+-------------------------------+
3 | BASE(0-15) | LIMIT(0-15) | 0
+-------------------------------+-------------------------------+
其中, 第6个Byte的第4个bit 是 AVL
其中:
- Base
该段的第一个 Byte 的线性地址。
- G
Granularity,粒度。如果该位为 0 ,表明段的大小以 byte 为单位,如果为 1 ,表明大小以 4096 bytes (一个内存页的大小)为单位 .
- D
- 1: address 为 32 位
- 0: address 16 bit.
- AVL
Linux 中不使用该位。
- Limit
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 的段,则任意 PL 的 CPU 都可以访问。
- P
Segment-Present flag: 该值为1则表明段存在于内存当中,否则该段则存在于 SWAP 中。
2.3 段描述符的快速访问
前面提到, 段选择符长度为 16 bit, 其组成如下:
Segment Selector
Byte/ 1 | 0 |
/ | |
|7 6 5 4 3 2 1 0|7 6 5 4 3 2 1 0|
+-------------------------------+
| Index |.| . |
+-------------------------------+
^ ^
| |
TI ---------------------+ | (0 -- GDTR, 1 -- LDTR)
RPL ------------------------+ (Required Privilege Level)
其中:
- index
Index 指出了段选择符所指向的段描述符在 GDT 或者 LDT 中的位置。
- TI
Table Indicator, 指出了段选择符是在 GDT 还是 LDT。
- TI = 0: GDT 中。
- TI = 1: LDT 中。
- RPL
Requestor Privilege Level, 表明了加载该段选择符时候的 CPU 的 CPL。
通过段选择符中的 TI 和 Index ,可以找到相应的段描述符。此外, 80X86 处理器为每一个段选择符寄存器都提供了一个不可编程的寄存器,用于存放该选择符对应的段描述符。每次段选择符寄存器中装载了段选择符,相应的段描述符就会自动装载到对应的那个不可编程的寄存器中,以便于访问。
GDT/LDT Segment
+-------------+ +---------------+
| NULL | +----------->|.#.%#%---.+ +. |<----------+
+-------------+ | |+.++*#m#-m*-+m | |
+----> | 段描述符 1 |-------+ |**#+#%--m#%++-.| |
| +-------------+ |-..#*+%#m%m-%+ | |
| | 2 | |..+-- .+..-++ | |
| +-------------+ +---------------+ |
| | 3 | |
| +-------------+ |
| | . | |
| +-------------+ |
| | . | |
| +-------------+ |
| |
| |
| --------------- |
| 段寄存器 ----/ 不可编程寄存器 \---- |
| +-------------------+ / +------------------+ \ |
+---| 段选择符 | ( | 段描述符 | )----+
+-------------------+ \ +------------------+ /
----\ /----
---------------
2.4 逻辑地址到线性地址的转换
逻辑地址到线性地址的转换由 SEGMENTATION UNIT 完成, 需要下面几个步骤:
- 根据 Selector->TI 选择 GDT 或者 LDT ( 下面标记选择好的table 为 DT)。
- Descriptor = DT + Selector->Index * 8
由于每个段描述符都是 8 Byte大小,因此 Index * 8 才能得到段描述符。
- LinearAddr = Descriptor->BaseAddr + Selector->offset
用一段伪代码来表示:
addr_t logical_2_linear(seg_selector selctor)
{
descriptor_table dt = NULL;
seg_desc desc = NULL;
/* Step 1 */
switch (selctor->TI) {
case 0: {
dt = GDT;
break;
}
case 1: {
dt = LDT;
break;
}
default:
dt = GDT;
break;
}
/* Step 2 */
desc = dt + selector->index * 8;
/* Step 3 */
return desc->base + selector->offset;
}
80X86 上, 由于引入了不可编程的寄存器用于存储段选择符相应的段描述符,这个转换过程的前两步可以省略掉。
3 Linux 里内存段式管理
相对于内存的段式管理, Linux 更喜欢内存的页式管理。80x86 上, 尽在需要的时候才会使用一下段式管理。对段式管理我没有仔细看,只知道:
- Linux 的内存段式管理,针对内核空间和用户空间,分别将内存分为了代码段和数据段,共四个段。
- 每个段的 base 都为 0 ,这样,selector->offset 实际上就是内存的线性地址。
其他的东西,遇到的时候再看吧。
4 硬件上的页式管理
PAGING UNIT 负责将线性地址转换成为物理地址。线性地址按照固定长度细分成页。
这里有若干基本概念,如下:
- PAGE Framge
指将 RAM 按照 Page Size 划分成的空间。
- PAGE
指 page frame 或者磁盘中的数据。
- Page tables
页面表,存放于主内存中,由内核负责初始化。页面表用于将线性地址映射成物理地址。
4.1 常规分页
前面提到, 线性地址的长度为 32 bit ,它可以分成三个部分,如下图所示:
Byte/ 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|7 6 5 4 3 2 1 0|
+-------------------------------+-------------------------------+
| Directory | Table | Offset |
+-------------------------------+-------------------------------+
线性地址向物理地址的转换需要两个步骤,其中需要两个表: Page Directory 和 Page Table 。每一个活跃的进程都必有分配一个 Page Directory ,但 Page Table 中对应的 Page Frame 却没有必要立即分配, Pages Frame 在有需要的时候再进行分配即可。
进程的 Page Directory 的物理地址存放在控制寄存器 cr3 里面,这样,线性地址到物理地址的转化可分为如下几个步骤:
- 根据线性地址中的 Directory ,可以从 cr3 指向的 Page Directory 中找到 Page Table
- 根据线性地址中的 Table , 可以从前面找到的 Page Table 中找到相应的 Page Frame
- 根据线性地址中的 Offset , 从前面找到的 Page Frame 中找到该 PageFrame 中的相对位置。
整个过程如下图所示:
Linear Address
-------------+---------------+---------------+---------------+
Byte/ 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|7 6 5 4 3 2 1 0|
+-------------------------------+-------------------------------+
| Directory | Table | Offset |
+--------+----------------------+----------------+--------------+
| | | Page Frame
| | | +---------+
| | | | |
| | | +---------+
| | Page +----->| ###### |
| | Table +---------+
| | +---------+ | |
| Page | | | +---------+
| Directory | +---------+ | |
| +------------+ +--->| ###### |------> +---------+
| | | +---------+
| +------------+ | |
+----> | ########## |-----> +---------+
cr3 +------------+
+--------+ | |
| |--------------> +------------+
+--------+
当 Page 以 4K 为单位对齐的时候,内存中 Page Frame 大小为 4K ,此时,每一个 PageFrame 的地址的低 12 位均为0, Page Directory 和 Page Table 中的目录项和页表项都将低 12 位作为控制字,形如:
Byte/ 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|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| | | |
+---------------------------------------+-----+++++++++++++++++++
| | | | | | | | |
| | | | | | | | +---> Present
| | | | | | | +-----> Writable
| | | | | | +-------> User/Supervisor Flag
| | | | | +---------> Write-through
| | | | +-----------> Cache disable
| | | +-------------> Accessed
| | +---------------> Dirty
| +-----------------> Page Size Flag
+-------------------> Global Flag
其中:
- Present flag
置位时候表示该项(目录项或者页表项)在主内存中;清零,则表示不在主内存中。如果在对某个目录项或者页表项进行地址类型转换的时候发现该位为0, PAGING UNIT 会将线性地址存到 cr2 上,然后触发 Page Fault 。
- Writable
表示该项是否可写,为1时候可写。
- User/Supervisor Flag
表明了访问该 page 所需的权限。
- PWT
缓冲区相关,表明是否 Write-through,后面会涉及到。
- PCT
设置为1 的时候,表示关闭 缓冲存储器。
- Accessed
表明该项已经被访问过了。
- Dirty
仅用于页面表项,表示该 Page Frame 被写过。
- Page Size Flag
仅用于目录项,当该值为 0 时,表示目录项所指向的页面表(Page Table) 中的页面表项中的Frame 大小为 4K 。而当该值为1时,页面表项的大小就变成了 4M 。
- Global Flag
仅用于页面表项(Page Table Entry) ,为 1 时候,表明该页面为全局页面。
4.2 拓展分页
从 Pentium 开始, Intel 引入了拓展分页 (Extended Pageing) , 拓展分页将页大小改成了 4 M ,这种情况下,线性地址的前 10 位仍为目录项,但是中间的 Page Table 被去掉了, OFFSET 从原来的 12 位变成了 22 位。如下图所示:
Linear Address
-------------+---------------+---------------+---------------+
Byte/ 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|7 6 5 4 3 2 1 0|
+-------------------------------+-------------------------------+
| Directory | OFFSET |
+--------+-------------------------+----------------------------+
| |
| | 4MB Page
| | +----------------+
| | | |
| | +----------------+
| | | |
| | +----------------+
| Page +------->| ############# |
| Directory +----------------+
| +------------+ | |
| | | +----------------+
| +------------+ | |
+----> | ########## |------------->+----------------+
cr3 +------------+
+--------+ | |
| |--------------> +------------+
+--------+
拓展分页和前面的普通分页的区别在于:
- 拓展分页中,目录项中的 PS 为 1 ,而普通分页中,为 0 。
- 拓展分页没有页面表,后面22位均为 offset 。
5 Linux 中的内存页式管理
为了保持 Linux 的可移植性, Linux 里面的页式管理,被封了一层、一层,一层,一层,又一层。
Linux 内核中使用的虚拟地址(Virtual Address, Linear Address),出于可移植性的考虑,分成了四层,分别为:Page Global Directory, Page Upper Directory, Page Middle Directory, 和 Page Table 。这几个层次再加上最后的一个页内偏移,共同构成了 Linux 的线性地址, 如下图所示:
|<------------------------------ BITS_PER_LONG --------------------------------->| +---------------+---------------+---------------+---------------+----------------+ | PGD | PUD | PMD | PTE | Offset | +---------------+---------------+---------------+---------------+----------------+ | | | |<- PAGE_SHIFT ->| | | |<----------- PMD_SHIFT -------->| | |<---------------- PUD_SHIFT ------------------->| |<-------------------------- PGDIR_SHIFT ----------------------->|
The linear address space of a process is divided into two parts:
- 0x00000000 ~ 0xbfffffff:
内核空间和用户空间的进程都可访问。
- 0xc0000000 ~ 0xffffffff:
仅限于内核态进程访问。
Author:yangyingchao, 2010-09-10
引用:http://blog.163.com/vic_kk/blog/static/4947052420108104224228/