【Note】《深入理解Linux内核》Chapter 9 :深入理解 Linux 内核中的进程地址空间管理机制
《深入理解Linux内核》Chapter 9 :深入理解 Linux 内核中的进程地址空间管理机制
摘要:虚拟内存、内存映射、VMA、mm_struct、进程隔离、brk、mmap、栈、堆、页表、多线程共享、地址空间布局
一、地址空间的重要性
地址空间(Address Space)是操作系统为每个进程构建的内存抽象。Linux 使用虚拟内存技术,使得:
- 每个进程拥有独立的虚拟地址空间;
- 用户空间与内核空间相互隔离;
- 支持按需分页、写时复制、共享内存等机制;
- 实现系统安全性与进程稳定性。
二、虚拟地址空间结构概览
在 Linux 中,每个进程拥有完整的虚拟地址空间,通常结构如下(以 32 位系统为例):
0x00000000|| 用户空间|| text段(代码段)| data段(已初始化静态数据)| bss段(未初始化全局数据)| heap(堆)—— 从低地址向上增长(brk)| mmap区域—— 映射文件/共享内存/动态库| 栈(stack)—— 从高地址向下增长|
0xC0000000|| 内核空间(共享)|
0xFFFFFFFF
64 位架构使用更大的地址空间,并对地址段进行了更灵活的划分。
三、核心数据结构:mm_struct 与 VMA
3.1 mm_struct
每个进程地址空间由一个 mm_struct
表示,结构定义如下:
struct mm_struct {pgd_t *pgd; // 页目录基地址struct vm_area_struct *mmap; // VMA 链表struct rb_root mm_rb; // VMA 红黑树索引unsigned long start_code, end_code;unsigned long start_brk, brk;unsigned long start_stack;...
};
其中 pgd
指向页目录表,mmap
/mm_rb
负责管理所有虚拟内存区域。
3.2 vm_area_struct(VMA)
每个内存区域由一个 vm_area_struct
表示:
struct vm_area_struct {unsigned long vm_start;unsigned long vm_end;unsigned long vm_flags;struct file *vm_file;struct vm_area_struct *vm_next;struct mm_struct *vm_mm;...
};
vm_start
/vm_end
:区域范围;vm_flags
:属性(只读/可写/可执行等);vm_file
:若该区域为文件映射,则指向文件结构;- 所有 VMA 通过链表与红黑树组织,便于快速查找。
四、地址空间的组成与分配方式
4.1 代码段(.text)
- 存储可执行指令;
- 映射自 ELF 可执行文件;
- 通常为只读 + 可执行(VM_READ | VM_EXEC);
- VMA 来源:
load_elf_binary()
中的do_mmap()
;
4.2 数据段(.data / .bss)
- 包含初始化与未初始化的全局变量;
- 映射于执行文件的特定段,或通过
brk()
分配; - BSS 初始化为 0,占用匿名内存。
4.3 堆区(heap)
- 通过
brk()
或sbrk()
系统调用增长; - 默认由 glibc 封装为
malloc()
等接口; - 从低地址向高地址扩展;
- 受
start_brk
与brk
限定。
unsigned long brk = current->mm->brk;
4.4 栈区(stack)
- 每个线程分配一个私有用户栈;
- 从高地址向低地址增长;
- 由
start_stack
标示栈底地址; - 受限于
ulimit -s
设置; - 页表保护栈底防止溢出。
4.5 mmap 区域
- 映射文件、动态链接库、匿名内存等;
- 使用
mmap()
系统调用映射; - 起始地址通常从某个随机高地址向下分配;
- 是现代内存分配(如
malloc()
)的主力方式。
五、地址空间创建流程
5.1 execve():创建新地址空间
当调用 execve()
执行新程序时:
- 内核释放当前 mm_struct;
- 调用
load_elf_binary()
; - 分配新
mm_struct
; - 使用
do_mmap()
映射 text/data/bss 段; - 初始化堆、栈区域;
- 设置页表,切换进程上下文。
5.2 fork():共享地址空间
fork()
创建子进程时,会复制父进程地址空间;- 页表不复制,而是使用写时复制(COW);
- 子进程拥有新
mm_struct
与 VMA 链表; - 页框共享,设置只读位标志;
- 当子进程尝试写时,触发缺页异常并复制页。
六、多线程共享与 mm_struct
6.1 多线程如何共享地址空间?
- 所有线程共享同一个
mm_struct
; CLONE_VM
标志确保地址空间共享;- 栈空间由每个线程单独分配。
6.2 线程栈分配
- 用户空间栈由 glibc 分配;
- 内核通过
do_fork()
设置新线程栈地址; - 默认栈大小可通过 pthread_attr 设置。
七、内存映射(mmap)机制详解
7.1 mmap 系统调用
void *mmap(void *addr, size_t length, int prot, int flags,int fd, off_t offset);
- 映射文件或匿名页;
- 创建新的 VMA;
mmap_region()
完成实际映射,更新页表;
7.2 使用场景
- 动态库加载;
- 映射大型文件;
- 创建共享内存;
malloc()
大块分配。
7.3 匿名映射(anonymous mmap)
mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_ANONYMOUS | MAP_PRIVATE, -1, 0);
- 分配未关联文件的内存;
- 相当于手动实现
malloc
; - 常用于线程栈、堆、大数据结构。
八、地址空间保护与随机化
8.1 访问控制标志(vm_flags)
标志 | 含义 |
---|---|
VM_READ | 可读 |
VM_WRITE | 可写 |
VM_EXEC | 可执行 |
VM_MAYSHARE | 可共享映射 |
VM_GROWSDOWN | 栈可向下增长 |
8.2 ASLR(地址空间布局随机化)
- 提高安全性,防止缓冲区溢出攻击;
- text、heap、stack、mmap 等区域随机偏移;
- 内核通过
/proc/sys/kernel/randomize_va_space
控制;
九、页表与地址空间的连接
9.1 每个进程的页表基地址
current->mm->pgd
- 指向该进程页目录起始地址;
- 与
vm_area_struct
共同组织地址空间; - 每次上下文切换时更新 CR3(x86)寄存器;
9.2 页表更新与映射
mmap()
映射区域仅创建 VMA;- 首次访问触发缺页异常,由
handle_mm_fault()
创建页表项; do_page_fault()
处理实际映射并分配页框。
十、地址空间的释放与销毁
10.1 进程退出时
- 通过
exit_mm()
释放mm_struct
; - 清除页表项,释放页框;
- 销毁所有 VMA 结构;
- 若有共享页,减少引用计数。
10.2 映射销毁
munmap(addr, length);
- 调用
do_munmap()
删除对应 VMA; - 卸载页表项;
- 若映射文件,写回脏页。
十一、调试工具与接口
接口/工具 | 用途 |
---|---|
/proc/<pid>/maps | 显示进程地址空间各段信息 |
/proc/<pid>/smaps | 内存占用细节(RSS、PSS) |
/proc/<pid>/pagemap | 虚拟地址与物理地址映射 |
gdb + info proc mappings | 调试进程内存映射 |
strace ./a.out | 捕捉 mmap/brk/munmap 系统调用 |
十二、源码文件与核心函数
源码路径 | 说明 |
---|---|
mm/mmap.c | 实现 mmap、munmap、brk 逻辑 |
mm/memory.c | 缺页异常与页面映射 |
fs/exec.c | 加载 ELF,创建初始地址空间 |
kernel/fork.c | fork、clone、线程创建 |
include/linux/mm_types.h | mm_struct 、vm_area_struct 定义 |
十三、实验与实践建议
- 写程序使用
mmap()
分配内存,并查看/proc/<pid>/maps
; - 写一个
fork()
后修改堆内容的程序,观察 COW 效果; - 使用
strace
观察程序使用brk()
和mmap()
; - 利用
gdb
调试栈地址,验证其向下增长; - 多线程程序中手动设定每个线程栈地址,研究其映射变化;
- 阅读 ELF 执行文件头部结构,理解各段加载关系。
十四、总结对比表
区域 | 分配方式 | 分配函数 | 成长方向 | 描述 |
---|---|---|---|---|
text段 | ELF 映射 | load_elf_binary | 固定 | 可执行指令,只读可执行 |
data段 | ELF 映射 | load_elf_binary | 固定 | 已初始化全局变量 |
bss段 | brk/mmap | brk | 固定 | 未初始化全局变量 |
heap | brk/mmap | malloc/brk | 向上 | 动态分配内存区 |
stack | 线程创建时设定 | clone/pthread_create | 向下 | 每线程独立栈 |
mmap区域 | 映射文件/匿名内存 | mmap | 动态 | 灵活共享/文件映射 |