在前面的文章中,我们深入探讨了 32 位 Linux 系统虚拟地址映射的基本原理,包括二级页表机制、虚拟内存管理以及用户空间与内核空间的地址映射差异。今天,让我们从 Linux 内核源码的角度,理解操作系统底层是如何管理虚拟内存的。
一、Linux 内核中进程的描述:task_struct
在 Linux 内核中,进程是通过 ** 进程控制块(PCB,Process Control Block)** 来描述的,其数据结构为 task_struct。task_struct 定义在 Linux 内核源码的 include/linux/sched.h 文件中,它包含了大量与进程相关的信息,如进程状态、进程 ID、进程优先级、打开的文件描述符表、信号处理相关信息等。在地址映射的上下文中,task_struct 与虚拟地址空间紧密相关,它为操作系统提供了管理进程内存的关键信息。
// include/linux/sched.h 中的部分task_struct定义示例
struct task_struct {
volatile long state; // 进程状态
pid_t pid; // 进程ID
struct mm_struct *mm; // 指向进程虚拟地址空间的描述结构
// 其他众多成员...
};
进程与线程:统一的 task_struct 管理
在 Linux 内核中,进程和线程的概念在底层都由 task_struct 来表示。线程本质上是一种特殊的进程,它们共享相同的地址空间、文件描述符等资源。通过 clone() 系统调用的不同参数,可以创建进程或线程。如果 clone() 调用时设置了特定的标志位(如 CLONE_VM 表示共享虚拟内存),则创建的是线程;否则,创建的是普通进程,拥有独立的地址空间。
二、Linux 内核中进程虚拟地址空间的描述:mm_struct
task_struct 中的 mm 成员是一个指向 mm_struct 结构体的指针,mm_struct 用于描述进程的整个虚拟地址空间(通常为 4GB,32 位系统)。mm_struct 同样定义在 include/linux/mm.h 文件中,它是内核管理进程虚拟地址空间的核心数据结构。
// include/linux/mm.h 中的部分mm_struct定义示例
struct mm_struct {
pgd_t *pgd; // 指向进程页目录的起始地址
struct vm_area_struct *mmap; // 指向虚拟内存区域链表的头节点
struct vm_area_struct *mmap_cache; // 缓存上一次查找的虚拟内存区域节点
// 其他众多成员...
};
关键成员变量解析
- pgd_t *pgd:该成员指向当前进程的页目录(Page Directory Table,PDT)的起始地址。如我们在之前的文章中提到,CR3 寄存器存储当前进程的页目录基址,而这个基址正是由 mm_struct 中的 pgd 变量给出。通过 pgd,内核可以快速定位到进程的页目录,进而开始虚拟地址到物理地址的转换过程。
- vm_area_struct *mmap:这是一个指向双向链表首节点的指针,该链表用于描述进程虚拟地址空间中的各个区域。每个链表节点都是一个 vm_area_struct 结构体,用于表示一段连续的、具有相同访问权限和属性的虚拟内存区域。例如,进程的代码段、数据段、堆、栈等都由不同的 vm_area_struct 节点来描述。
- vm_area_struct *mmap_cache:基于局部性原理,进程在访问内存时,往往会频繁访问近期使用过的虚拟内存区域。mmap_cache 用于缓存上一次查找的 vm_area_struct 链表节点地址。当下一次查找虚拟地址所属的内存区域时,内核首先从 mmap_cache 开始搜索,如果命中,则可以快速获取到对应的内存区域信息,提高查找效率。
三、vm_area_struct:虚拟内存区域的细节
vm_area_struct 结构体定义了进程虚拟地址空间中的一个区域,它包含了许多重要的成员变量,用于描述该区域的属性和范围。下面是 vm_area_struct 的部分定义(同样在 include/linux/mm.h 中):
// include/linux/mm.h 中的部分vm_area_struct定义示例
struct vm_area_struct {
struct mm_struct *vm_mm; // 指向所属的mm_struct
unsigned long vm_start; // 虚拟内存区域的起始地址
unsigned long vm_end; // 虚拟内存区域的结束地址
struct vm_area_struct *vm_next; // 指向下一个vm_area_struct节点
struct vm_area_struct *vm_prev; // 指向前一个vm_area_struct节点
pgprot_t vm_page_prot; // 该区域的页面保护属性
unsigned long vm_flags; // 该区域的标志位,如可读、可写、可执行等
// 其他众多成员...
};
成员变量与虚拟内存区域的对应关系
- vm_mm:指向该虚拟内存区域所属的 mm_struct,从而将 vm_area_struct 与进程的整个虚拟地址空间描述关联起来。
- vm_start 和 vm_end:这两个变量定义了虚拟内存区域的范围。例如,一个进程的代码段可能从 0x08048000 开始(vm_start),到 0x08049000 结束(vm_end),表示该代码段占用了这一段连续的虚拟地址空间。
- vm_next 和 vm_prev:用于构建双向链表,将各个 vm_area_struct 节点连接起来。通过这两个指针,内核可以遍历进程的整个虚拟地址空间,查找特定虚拟地址所属的区域。
- vm_flags:包含了该虚拟内存区域的各种属性标志。例如,VM_READ 表示该区域可读,VM_WRITE 表示可写,VM_EXEC 表示可执行。这些标志位决定了进程对该区域的访问权限。如果进程试图访问一个不具备相应权限的虚拟内存区域,将触发段错误(Segmentation Fault)。
为了更直观地理解 vm_area_struct 中各参数与虚拟内存区域的关系,我们可以通过以下示意图来说明:
注意这里stack部分,由于其从高地址向低地址扩展,start和end指向不同于其他节点
四、地址映射过程:从虚拟地址到物理地址
当进程执行取指令、读写数据等操作时,需要将虚拟地址转换为物理地址,这个过程由内存管理单元(MMU)完成。在 Linux 内核中,地址映射的流程如下:
- 查找 task_struct:CPU 首先获取当前正在执行的进程的 task_struct。这通常通过硬件寄存器(如进程上下文切换时保存的当前进程信息)来实现。
- 查找 mm_struct:从 task_struct 中获取指向 mm_struct 的指针 mm,从而得到进程虚拟地址空间的描述信息。
- 遍历 vm_area_struct 链表:通过 mm_struct 中的 mmap 指针,开始遍历虚拟内存区域链表。内核在链表中查找目标虚拟地址所属的 vm_area_struct 节点。如果找不到对应的节点,说明该虚拟地址非法,或者进程对该地址没有访问权限,将产生错误(如段错误)。
- 权限检查:一旦找到目标 vm_area_struct 节点,内核检查 vm_flags 中的权限标志,确保进程对该虚拟地址的访问操作(读、写、执行)与节点的权限设置相匹配。如果权限不匹配,同样会产生错误。
- 页表映射:如果地址合法且权限匹配,内核使用 mm_struct 中的 pgd 指针,开始进行页表映射。如前所述,pgd 指向进程的页目录,通过页目录和页表的两级索引,最终找到虚拟地址对应的物理页面,并完成地址转换。
优化措施:红黑树结构
由于虚拟地址到物理地址的转换操作非常频繁,单纯使用链表结构遍历 vm_area_struct 可能效率较低。为了提高查找效率,Linux 内核除了链表结构外,还使用了红黑树(Red - Black Tree)结构来管理 vm_area_struct。红黑树是一种自平衡二叉搜索树,其查找、插入和删除操作的时间复杂度均为 O (log n),相比链表的 O (n) 有显著提升。
在 mm_struct 中,除了 mmap 链表外,还有一个指向红黑树根节点的指针 mm_rb,用于维护红黑树结构。当创建或删除虚拟内存区域时,内核需要同时更新链表和红黑树,以确保两者的一致性。在查找虚拟地址所属的 vm_area_struct 节点时,内核优先尝试从红黑树中查找,如果未找到,再遍历链表。这种双重数据结构的设计,充分发挥了链表和红黑树各自的优势,提高了地址映射的整体效率。
红黑树代码实现可以参考:c++ 手写STL篇(四) 实现红黑树(RB-Tree)_红黑树rbl-CSDN博客
通过深入研究 Linux 内核源码中与地址映射相关的数据结构和实现细节,我们对虚拟地址映射机制有了更深入的理解。task_struct、mm_struct 和 vm_area_struct 等结构体相互协作,构建了一个复杂而高效的内存管理系统。从内核源码的角度理解这些机制,不仅有助于我们优化程序性能,还能为深入研究操作系统内核提供坚实的基础。在后续的博客中,我们将继续探索更多内存管理的高级技术和内核实现细节。