进程虚拟内存管理
1️⃣ 进程的虚拟地址空间

2️⃣ 内核如何管理进程?
内核通过 task_strcut 来对进程进行管理
因为在进程描述符 task_struct 结构中,有一个专门描述 进程虚拟地址空间 的内存描述符 mm_struct。由于每个进程都有唯一的 mm_struct 结构体,因此每个进程的虚拟地址空间都是 独立的。
1 | struct task_struct { |
子进程是如何被创建的?

- 调用
fork()函数创建进程的时候, 子进程地址空间 的mm_struct结构会随着子进程描述符task_struct的创建而创建。 copy_process函数中创建task_struct结构,并拷贝父进程的相关资源到子进程的task_struct结构里,其中就包括拷贝父进程的虚拟内存空间mm_struct结构。copy_mm函数首先会将父进程的虚拟内存空间current->mm赋值给指针oldmm。- 通过
fork()创建的子进程, 通过dup_mm()函数将 父进程的虚拟内存空间 以及 相关页表 拷贝到子进程的mm_struct结构中。最后将拷贝出来的mm_struct赋值给子进程的task_struct结构。 - 通过
clone()创建的子进程,通过mmget()增加父进程的引用计数并直接将 父进程的虚拟内存空间以及相关页表直接赋值给子进程。这样一来父进程和子进程的虚拟内存空间就变成共享的了。
- 通过
内核线程与用户线程
内核线程和用户态线程的区别: 内核线程没有相关的内存描述符 mm_struct ,内核线程对应的 task_struct 结构中的 mm 域指向 Null,所以 内核线程之间调度是不涉及地址空间切换。
当一个内核线程被调度时,它会发现自己的虚拟地址空间为
Null。它不会访问用户态的内存,但是它会访问内核内存,内核会将调度之前的上一个用户态进程的虚拟内存空间mm_struct直接赋值给内核线程,因为内核线程不会访问用户空间的内存,它仅仅只会访问内核空间的内存,所以直接复用上一个用户态进程的虚拟地址空间就可以避免为内核线程分配mm_struct和相关页表的开销,以及避免内核线程之间调度时地址空间的切换开销。
3️⃣ 进程虚拟地址布局 mm_struct
内核 是通过 mm_struct 来表示 进程的虚拟内存布局。接下来,将详细讲解 mm_struct 是进行表示的。
mm_struct 结构体简要表示
1 | struct mm_struct { |
task_size:定义了 用户态地址空间 与 内核态地址空间 之间的分界线。start_code/end_code:代码段 的 起始位置 和 结束位置。程序编译后的二进制文件中的机器码被加载进内存之后就存放在这里。
start_data/end_data:定义 数据段 的 起始位置 和 结束位置。程序编译后的二进制文件中存放的全局变量和静态变量被加载进内存中就存放在这里。
bss:存放 未被初始化 的 全局变量 和 静态变量。这些变量在加载进内存时会生成一段
0填充的内存区域 (BSS段),BSS段的大小是固定的。start_brk/brk:start_brk定义 堆的起始位置,brk定义 *堆当前的结束位置**。mmp_base: 定义 内存映射区的起始地址。进程运行时所依赖的动态链接库中的代码段,数据段,
BSS段以及调用mmap映射出来的一段虚拟内存空间就保存在这个区域。start_stack:栈的起始位置,在RBP寄存器中存储。栈的结束位置也就是栈顶指针
stack pointer在RSP寄存器中存储。arg_start/arg_end:参数列表 的位置。env_start/env_end:环境变量的位置。arg/env都位于栈中的最高地址处。total_vm/locked_vm/pinned_vm等等:虚拟内存与物理内存映射内容相关的统计变量。

vm_area_struct
在 mm_struct 的简要介绍中,每一个 段 不可能只用 unsing long 进行表示。实际上是通过 vm_area_struct 结构体来描述这些虚拟内存区域 vma。
1 | struct vm_area_struct { |
vm_start/vm_end:指向这块虚拟内存区域的 起始地址 和 终止地址。vm_page_prot/vm_flags:表示这块虚拟内存区域的 访问权限 和 行为规范。虚拟内存区域
VMA由许多的虚拟页 (page) 组成,每个虚拟页需要经过页表的转换才能找到对应的物理页面。页表中关于内存页的访问权限 就是由vm_page_prot决定的。vm_flags则偏向于 整个虚拟内存区域的访问权限 以及行为规范。anon_vma/vm_file/vm_pgoff:都和 虚拟内存映射 有关。虚拟内存区域 可以映射到 物理内存 ,也可以映射到 文件 ,映射到物理内存上我们称之为匿名映射,映射到文件中我们称之为文件映射。
- 匿名映射
anon_vma:调用mmap,此时mmp会在 文件映射和匿名映射区 分配一块vma内存。这块匿名映射区域就用struct anon_vma结构表示。 - 文件映射
vm_file/vm_pgoff:vm_file用来关联被映射的文件。vm_pgoff表示映射进虚拟内存中的文件内容在文件中的偏移。
- 匿名映射
vm_private_data:存储 VMA 中的私有数据vm_ops:指向针对虚拟内存区域VMA的相关操作的函数指针。1
2
3
4
5
6
7struct vm_operations_struct {
void (*open)(struct vm_area_struct * area);
void (*close)(struct vm_area_struct * area);
vm_fault_t (*fault)(struct vm_fault *vmf);
vm_fault_t (*page_mkwrite)(struct vm_fault *vmf);
..... 省略 .......
}open:指定的虚拟内存区域 加入 到进程虚拟内存空间时调用。close:虚拟内存区域 VMA 从进程虚拟内存空间中被 删除 时调用fault:当进程访问虚拟内存时,访问的页面不在物理内存中,可能是未分配物理内存也可能是被置换到磁盘中,这时就会产生缺页异常,fault函数就会被调用。

vma 在内核中是如何被组织的?
1 | struct vm_area_struct { |
vm_next/vm_prev:内核通过 双向链表 将 虚拟内存区空间 中的这些虚拟内存区域vm_area_struct进行串联起来。vm_prev/vm_next分别指向 前驱节点 以及 后驱节点。所有vma节点按照 低地址 到 高地址 的增长方向排序。mmap:双向链表的头节点。vm_mm:指向了所属的虚拟内存空间mm_struct。vm_rb:同样的vm_area_struct在内核中有两种组织方式:双向链表 用于高效遍历,红黑树 用于高效查找。

4️⃣ 编译好的二进制文件如何加载到进程虚拟内存
C++ 程序在编译、链接后,会生成 ELF 可执行目标文件。这个 ELF 文件格式图如下:

磁盘文件中的段我们叫做
Section,内存中的段我们叫做Segment,也就是内存区域。
ELF 文件中的这些 Section 会在 进程运行之前加载到内存 并映射到 **内存中的 Segment**。通常是多个 Section 映射到一个 Segment。

ELF 格式的二进制文件中的 Section 是如何加载并映射进虚拟内存空间的呢?
内核通过 load_elf_binary 函数进行实现。作用是:
- 加载内核执行它
- 启动第一个用户态进程
init也执行它 fork()之后,调用exec()来执行另一个程序也执行它当
exec运行一个二进制程序的时候,除了解析ELF的格式之外,另外一个重要的事情就是建立上述提到的内存映射。
1 | static int load_elf_binary(struct linux_binprm *bprm) { |
