当前位置: 首页 > news >正文

深入理解 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 位)

段名位数用途
页目录索引 PD10定位页目录中的第几个表项
页表索引 PT10定位页表中的第几个表项
页内偏移 Offset12页面内的具体字节偏移(最多 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)
    • 内核从磁盘中读取该页,重新加载进物理页框
    • 更新页表映射
http://www.dtcms.com/a/310506.html

相关文章:

  • 一体化智能截流井市场报告:深度解析行业现状与未来增长潜力
  • 【Dart 教程系列第 51 篇】Iterable 中 reduce 函数的用法
  • Vue2 项目实现 Gzip 压缩全攻略:从配置到部署避坑指南
  • 静电释放检测漏报率↓85%!陌讯多模态融合算法在电子厂ESD防护实战解析
  • 【数据可视化-77】中国历年GDP数据可视化分析:Python + Pyecharts 深度洞察(含完整数据、代码)
  • QT中的window()方法/获取到控件最顶部容器
  • Effective C++ 条款16: 成对使用new和delete时要采用相同形式
  • 1、【C语言】【进阶】数组,指针与退化
  • 【Node.js安装注意事项】-安装路径不能有空格
  • Go 语言中 ​10 个高频实用写法
  • C语言:20250801学习(构造类型)
  • C++___快速入门(下)(引用)
  • Linux基础 -- 内核快速向用户态共享内核变量方案之ctl_table
  • 大模型学习思路推荐!
  • 基于K近邻的缺失值填补:原理、步骤与实战解析
  • Winform 中实现控件与数据的绑定,一方改变另一方同步改变。
  • 【Onvif从零实践】02、Onvif 测试工具(ONVIF Device Test Tool)的 安装、使用 教程
  • C++入门自学Day4-- c++类与对象(友元)
  • JavaScript语法树简介:AST/CST/词法/语法分析/ESTree/生成工具
  • 水果忍者经典版:离线版,永久无限制!!
  • IPD数字化的困难与解法
  • 如何在 VMware Workstation 虚拟机中利用 Nvidia 显卡的硬件加速功能
  • 利用 AI 在 iPhone 上实现 App 文本情绪价值评估(下)
  • 浅谈低代码平台涉及的一些技术选型
  • 【BUUCTF系列】[ACTF2020 新生赛]Exec 1
  • 用 Ubuntu 22.04 (Jammy) 的 MongoDB 源
  • Skia-如何渲染文本(上)
  • Android中页面生命周期变化
  • 多人命题系统
  • Qt 开发自动化测试框架搭建