深入理解 Linux 进程地址空间
文章目录
- 一、引入例子
- 1. 代码分析
- 2. 从进程地址空间的角度解释
- 3. 正确的写法
- 二、如何看待页表
- 1. 页表在做什么?
- 三、进程地址空间上的结构
- 1. 页框和页帧
- 2. 虚拟地址到物理地址怎么转换的呢?
- 四、虚拟地址到物理地址的转换
- 五、伙伴系统
- 1. 基本工作原理:
- 2. 与 struct page 的关系
- 3. 为什么 Linux 需要伙伴系统?
- 4. 例子:分配 6 页内存
- 5. 总结
- 六、从 CPU 到磁盘的虚拟内存访问全过程
- 1. 【CPU】:发起指令访问虚拟地址
- 2. 【task_struct】:表示当前运行的进程
- 3. 【mm_struct】:进程的虚拟内存空间描述
- 4. 【页表】:虚拟地址三段拆分
- 5. 【物理内存】:页框 + struct page + 伙伴系统管理
- 6. 【磁盘】:页面换出 / 页帧(Swap)
一、引入例子
请你站在操作系统的进程地址空间的角度,解释一下这段代码为什么会报错?
char *str = "hello world";
*str = H;
从操作系统 进程地址空间 的角度出发,这段代码会报错的根本原因是:你尝试修改一段位于只读内存区域的字符串字面量,从而触发了段错误(Segmentation Fault)。
1. 代码分析
char *str = "hello world";
- 把字符串字面量
"hello world"
的地址赋给了指针str
。 - 注意:字符串字面量是存放在只读的代码段(文字常量区)中的。
*str = H;
- 等价于
str[0] = H;
,你试图修改只读区域的内容。 - 这是未定义行为,在大多数操作系统中,会导致段错误(Segmentation Fault)。
2. 从进程地址空间的角度解释
操作系统为每个运行的程序分配一个 虚拟地址空间,大致分为以下几部分:
+------------------------+ ← 高地址
| 栈区(stack) |
+------------------------+
| 空闲空间/guard |
+------------------------+
| 堆区(heap) |
+------------------------+
| 共享库(.so/.dll) |
+------------------------+
| 代码段 / 常量区(.text/.rodata) |
+------------------------+
| 数据段(.data) |
| BSS段(.bss) |
+------------------------+ ← 低地址
"hello world"
是字符串字面量,只存在于 只读数据段(.rodata
) 或合并在代码段中。- 指针
str
指向的是这段只读内存。 - 当你执行
*str = H;
,试图写入只读段,操作系统会检测到非法写访问,从而触发Segmentation Fault(段错误)。
3. 正确的写法
如果你想修改字符串内容,应该分配可写的内存,例如:
char str[] = "hello world"; // 分配在栈区,可写
str[0] = 'H'; // 合法
这里,str
是一个数组,字符串内容被复制到了栈上的可写内存中,修改是安全的。
二、如何看待页表
页表是虚拟地址到物理地址映射的核心数据结构,同时还记录着每一页的访问权限。
1. 页表在做什么?
如下图所示:
当你的程序运行时,看到的是一套虚拟地址空间。实际的内存访问,是由 CPU + 操作系统完成的地址转换:
- CPU 使用虚拟地址(virtual address)访问内存;
- 操作系统维护页表(Page Table),把虚拟地址映射到物理地址;
- 页表也记录每一页是否是:
- 可读(R)
- 可写(W)
- 可执行(X)
- 用户态/内核态(U/S)
所以,页表的作用不仅是地址映射,还有访问权限控制。
三、进程地址空间上的结构
如下图所示:
[虚拟地址空间]↓ 通过页表映射
[页表 Entry] ──→ [物理页框(Page Frame)]↓每个页框对应一个 struct page 结构↓页面调度时可与磁盘上的页帧 (swap/page file) 建立对应关系
1. 页框和页帧
页框(Page Frame)
- 是物理内存中的 一页大小的块(通常是 4KB),即 物理页。
- 是页表映射的目标。
- 在 Linux 中,页框有唯一编号 PFN(Page Frame Number),页表记录的就是 PFN。
struct page(Linux 内核结构体)
- 每个物理页框都由内核用一个
struct page
来表示和管理。 - 它包含的信息有:引用计数、标志位、页链表、所属进程/文件、是否换出等。
内核可以通过 PFN ↔
struct page \*
相互转换
磁盘上的页帧(Swap/Page File)
- 当物理内存不够时,页被换出(Swap Out)到磁盘的页文件区域。
- 每个页在磁盘上也以 4KB 为单位存储,称为磁盘页帧。
- 页表中的条目会标记页在磁盘中,而不是 RAM。
如图所示:
2. 虚拟地址到物理地址怎么转换的呢?
以 32 位系统、4KB 页大小为例:
-
虚拟地址为 32 位(如:
0000 0000 0000 0000 0000 0000 0000 0000
) -
页大小为:4KB = 2¹² → 页内偏移为 12 位
页表层级:二级页表结构
- 页目录(Page Directory):前 10 位
- 页表(Page Table):中间 10 位
- 页内偏移(Page Offset):后 12 位
虚拟地址分段(共 32 位)
段名 | 位数 | 用途 |
---|---|---|
页目录索引 PD | 10 | 定位页目录中的第几个表项 |
页表索引 PT | 10 | 定位页表中的第几个表项 |
页内偏移 Offset | 12 | 页面内的具体字节偏移(最多 4KB) |
例如:
虚拟地址: 0xCAFEBABE = 1100 1010 1111 1110 1011 1010 1011 1110
分段:[1100101011] [1111101011] [101010111110]↑ 10bit ↑10bit ↑12bit页目录索引 页表索引 页内偏移
转换过程如下所示:
- 假设虚拟地址:
0000 0000 0000 0000 0000 0000 0000 0000
- 页目录以虚拟地址中的前 10 个比特位为索引(2102^{10}210)
- 页表项以虚拟地址的中间 10 个比特位为索引(2102^{10}210)
- 此时页表项中的某一个条目指向物理内存的某一页,即指定页框的起始物理地址。
- 而虚拟地址还剩下最后 12 个比特位,刚好和我们对应的页框或页帧的大小是等价的(4kb),可以把这低 12 位作为页内便偏移量。
- 最后直接用【指定页框的起始物理地址】+【2122^{12}212 偏移量】就直接在一个页内找到了某一个物理地址。
如下图所示:
所以详细描述如下:
- 步骤 1️⃣:从 CR3 寄存器拿到页目录的基地址(Page Directory Base)
- 步骤 2️⃣:根据 页目录索引 查找页目录项
- 步骤 3️⃣:从该目录项中拿到页表的物理地址
- 步骤 4️⃣:根据 页表索引 查找页表项
- 步骤 5️⃣:从该页表项中拿到 页框的物理地址
- 步骤 6️⃣:页框起始物理地址 + 页内偏移(12位) → 得到最终物理地址
为什么我们页表的大小就叫做 4 KB?
- 因为一个页的大小 4 KB 它的偏移量最大就是 2122^{12}212。所以刚好就能配上我们对应的这里的虚拟地址,最后的12位。
总结虚拟地址到物理地址的转换过程:
- 通过虚拟地址的前10位找到页目录项,接着用中间10位找到页表项,页表项中存的是物理页框的起始地址,最后加上虚拟地址的低12位作为页内偏移,得到最终的物理地址。
- 公式:物理地址 = 页表项中的页框起始地址 + 虚拟地址低12位
整个过程靠的是 页目录 + 页表 + 页内偏移 的组合完成映射。
四、虚拟地址到物理地址的转换
虚拟地址转换成物理地址,不是简单地一刀切成几段硬凑起来,而是经过设计精巧、逻辑严密的三级结构化过程:
第一步:虚拟地址拆分为 10 - 10 - 12 三段
- 前 10 位:页目录索引(Page Directory Index)
- 中间 10 位:页表索引(Page Table Index)
- 后 12 位:页内偏移(Page Offset)
这个拆分不是随便定的,是由页表结构设计决定的。
第二步:逐级查找映射关系
- 先用前 10 位 去页目录中查页目录项,拿到 页表的物理地址
- 再用中间 10 位 去页表中查页表项,拿到 物理页框(4KB)的起始物理地址
- 最后用后 12 位,作为页内偏移,加在这个页框的地址上,得到 最终的物理地址
第三步:为什么偏移是 12 位?
- 因为页的大小是 4KB = 2¹² 字节
- 所以页内偏移天然就是 12 位,正好匹配虚拟地址的最后 12 位
- 这不是凑巧,而是 体系结构(比如x86)在设计页表时就规定好的粒度
如图所示:
总结一句话:
- 虚拟地址不是直接转成物理地址,而是通过拆成三级结构(页目录索引、页表索引、页内偏移),逐级查表、逐级映射,最终通过页框地址 + 偏移量精确定位到物理地址。
五、伙伴系统
Linux 内核中,物理内存是用 struct page {}
来描述的,而这些 struct page
所表示的物理页框,是由 伙伴系统(Buddy System) 来管理分配的。
定义:伙伴系统是一种 高效的物理页框分配算法,适用于 可变大小的内存块管理,特别是内核中 连续页框(物理内存) 的分配。
1. 基本工作原理:
1️⃣ 内存按 2 的幂次划分:
- 比如:1 页、2 页、4 页、8 页……一直到最大块(通常为 2¹⁰ 页 = 1024 页)
- 每个大小级别称为一个 “阶”(order),order = log₂(页数)
2️⃣ 每个阶都有一个空闲页块链表
- 比如
free_area[0]
代表 1 页大小的空闲块列表 free_area[3]
代表 8 页大小的空闲块列表
3️⃣ 分配时从最小足够阶中取块
- 如果要分配 4 页,优先从 order = 2 里取(2²=4)
- 如果没有,就在更高阶里拆分一个更大的块,分成两个 “伙伴”
4️⃣ 释放时尝试与“伙伴”合并
- 若两个伙伴都是空闲,就合并成一个更大块
- 合并过程递归进行,形成更高阶空闲块
2. 与 struct page 的关系
每个物理页框在 Linux 内核中都有一个 struct page
结构描述,在伙伴系统中,这些页框之间的组织方式如下:
信息字段 | 用途 |
---|---|
struct list_head | 链接到对应阶的 free_area 链表 |
unsigned long flags | 标志:是否空闲、是否是块头等 |
unsigned int _mapcount | 页映射计数 |
unsigned int order | 当前页块属于哪个阶(只有块头页设置) |
只有块头页(buddy block head)才存 order
,其他页不会记录这些信息。
3. 为什么 Linux 需要伙伴系统?
- 高性能:分配/释放操作复杂度为 O(log n)
- 支持合并:可以减少碎片
- 内核需求特殊:部分内核结构必须使用物理连续内存(如 DMA、页表本身、hugepage)
4. 例子:分配 6 页内存
1️⃣ 最小的能满足 6 页的是 order=3
(2³=8 页)
2️⃣ 如果没有空闲的 order=3 块:
- 去 order=4 拿一个 16 页块
- 拆成两个 8 页块,返回一个,另一个放回 order=3
3️⃣ 返回一个 8 页块给你,标记前 6 页用于分配,后 2 页可能浪费或切分再用
5. 总结
伙伴系统是 Linux 内核中用来管理 struct page{}
所表示的物理页框的核心算法,它通过将内存划分为 2 的幂次大小的块,并支持快速合并与拆分,实现高效、低碎片的页框分配。
六、从 CPU 到磁盘的虚拟内存访问全过程
1. 【CPU】:发起指令访问虚拟地址
CPU 执行某条指令,比如访问一个变量 x
,这个变量在程序中是一个 虚拟地址(Virtual Address)。
CPU 不会直接看到物理地址,它只能看到虚拟地址空间。
2. 【task_struct】:表示当前运行的进程
操作系统为每个进程维护一个 struct task_struct
,它是进程控制块 PCB。
其中包含一个指向 虚拟内存描述符的指针:
struct task_struct {...struct mm_struct *mm; // 当前进程的虚拟地址空间...
};
3. 【mm_struct】:进程的虚拟内存空间描述
mm_struct
结构体中,包含整个用户态的虚拟地址空间布局:
区域 | 说明 |
---|---|
代码段(text) | 程序指令 |
数据段(data) | 全局/静态变量 |
堆(heap) | malloc 等动态分配区域 |
栈(stack) | 函数调用栈 |
mmap 区域 | 文件映射 / 共享内存 |
关键字段:
struct mm_struct {pgd_t *pgd; // 页目录指针(顶层页表)...
};
4. 【页表】:虚拟地址三段拆分
虚拟地址被拆分为三部分:
名称 | 位数 | 含义 |
---|---|---|
页目录索引(PD) | 10 | 定位页目录项 |
页表索引(PT) | 10 | 定位页表项 |
页内偏移(Offset) | 12 | 页内具体字节位置(4KB页) |
转换步骤:
- 从 CR3 寄存器 拿到当前进程页目录的起始地址(pgd)。
- PD 索引 → 查页目录项 → 拿到页表地址。
- PT 索引 → 查页表项 → 拿到页框(Page Frame)物理地址。
- 页内偏移 → 加到页框地址 → 得到最终物理地址。
5. 【物理内存】:页框 + struct page + 伙伴系统管理
每一个页表项所指向的物理页框(Page Frame)大小为 4KB,它:
- 对应实际的 RAM 空间
- 被内核用一个
struct page
来抽象描述 - 页框编号称为 PFN(Page Frame Number)
管理机制:伙伴系统(Buddy System)
- 内存按 2ⁿ 页组织为阶(order)
- 分配时从最小满足阶取,支持合并和拆分
- 管理的就是这些页框的分配与回收
6. 【磁盘】:页面换出 / 页帧(Swap)
当物理内存不足,或者页面长时间未访问时:
- 内核会将某些物理页框的内容写入磁盘的交换空间(Swap)
- 被换出的页,在页表中会打上 “not present” 标记,并记录磁盘上的页帧位置
- 下次访问该虚拟地址时,会触发缺页中断(Page Fault)
- 内核从磁盘中读取该页,重新加载进物理页框
- 更新页表映射