一、深入理解 Linux 虚拟内存管理
当程序运行起来之后就变成了进程,而这些业务数据结构的引用在进程的视角里全都都是虚拟内存地址,因为进程无论是在用户态还是在内核态能够看到的都是虚拟内存空间,物理内存空间被操作系统所屏蔽进程是看不到的。
进程通过虚拟内存地址访问这些数据结构的时候,虚拟内存地址会在内存管理子系统中被转换成物理内存地址,通过物理内存地址就可以访问到真正存储这些数据结构的物理内存了。随后就可以对这块物理内存进行各种业务操作,从而完成业务逻辑。
防止多进程运行时造成的内存地址冲突,内核引入了虚拟内存地址,为每个进程提供了一个独立的虚拟内存空间,使得进程以为自己独占全部内存资源。
1. 进程虚拟内存空间
1. 首先程序代码编译成二进制文件存放在磁盘中。所以在进程运行之前,这些存放在二进制文件中的机器码需要被加载进内存中,而用于存放这些机器码的虚拟内存空间叫做代码段。
2.在程序代码中定义大量的全局变量和静态变量,在程序编译之后也会存储在二进制文件中,在程序运行之前,这些全局变量被加载进内存中供程序访问。所以在虚拟内存空间中也需要一段区域来存储这些全局变量。
- 那些在代码中被指定了初始值的全局变量和静态变量在虚拟内存空间中的存储区域我们叫做数据段。
- 那些没有指定初始值的全局变量和静态变量在虚拟内存空间中的存储区域我们叫做 BSS 段。这些未初始化的全局变量被加载进内存之后会被初始化为 0 值。
- 这些全局变量和静态变量都是在编译期间就确定的
代码段和数据段,它们是从程序的二进制文件中直接加载进内存中的,BSS 段中的数据也存在于二进制文件中,因为内核知道这些数据是没有初值的,所以在二进制文件中只会记录 BSS 段的大小,在加载进内存时会生成一段 0 填充的内存空间。
3. 程序在运行过程中还需要依赖动态链接库,这些动态链接库以 .so 文件的形式存放在磁盘中,比如 C 程序中的 glibc,里边对系统调用进行了封装 。这些动态链接库也有自己的对应的代码段,数据段,BSS 段,也需要一起被加载进内存中。
文件映射与匿名映射区域。1. 进程运行时所依赖的动态链接库中的代码段,数据段,BSS 段就加载在这里。2. 用于内存文件映射的系统调用 mmap,会将文件与内存进行映射,那么映射的这块内存(虚拟内存)也需要在虚拟地址空间中有一块区域存储。注意:在文件映射与匿名映射区的地址增长方向是从高地址向低地址增长。
这些1. 动态链接库中的代码段,数据段,BSS 段,以及 2. 通过 mmap 系统调用映射的共享内存区,在虚拟内存空间的存储区域叫做文件映射与匿名映射区。
2. Linux 进程虚拟内存空间
目前的 64 位系统下只使用了 48 位来描述虚拟内存空间,寻址范围为 2^48 ,所能表达的虚拟内存空间为 256TB。
其中低 128 T 表示用户态虚拟内存空间,高 128 T 表示内核态虚拟内存空间,
用户态虚拟内存空间与内核态虚拟内存空间之间形成了一段空洞,我们把这个空洞叫做 canonical address 空洞。
3. 进程虚拟内存空间的管理
通过 fork() 函数创建出的子进程,它的虚拟内存空间以及相关页表相当于父进程虚拟内存空间的一份拷贝,直接从父进程中拷贝到子进程中。
当我们通过 vfork 或者 clone 系统调用创建出的子进程,会将父进程的虚拟内存空间以及相关页表直接赋值给子进程。父进程和子进程的虚拟内存空间就变成共享的了。也就是说父子进程之间使用的虚拟内存空间是一样的,并不是一份拷贝。
子进程共享了父进程的虚拟内存空间,这样子进程就变成了线程,是否共享地址空间几乎是进程和线程之间的本质区别。Linux 内核并不区别对待它们,线程对于内核来说仅仅是一个共享特定资源的进程而已。
内核线程和用户态线程的区别就是内核线程没有相关的内存描述符 mm_struct ,内核线程对应的 task_struct 结构中的 mm 域指向 Null,所以内核线程之间调度是不涉及地址空间切换的。
内核线程不会访问用户空间的内存,它仅仅只会访问内核空间的内存,所以直接复用上一个用户态进程的虚拟地址空间就可以避免为内核线程分配 mm_struct 和相关页表的开销,以及避免内核线程之间调度时地址空间的切换开销。
所以:父进程与子进程的区别,进程与线程的区别,以及内核线程与用户态线程的区别其实都是围绕着这个 mm_struct 展开的。
4. 内核如何划分用户态和内核态虚拟内存空间
内核中采用了一个叫做内存描述符的 mm_struct 结构体来表示进程虚拟内存空间的全部信息。并通过 task_size 域来划分用户态虚拟内存空间和内核态虚拟内存空间。
struct mm_struct {
unsigned long task_size; /* size of task vm space */
unsigned long start_code, end_code, start_data, end_data;
unsigned long start_brk, brk, start_stack;
unsigned long arg_start, arg_end, env_start, env_end;
unsigned long mmap_base; /* base of mmap area */
unsigned long total_vm; /* Total pages mapped */
unsigned long locked_vm; /* Pages that have PG_mlocked set */
unsigned long pinned_vm; /* Refcount permanently increased */
unsigned long data_vm; /* VM_WRITE & ~VM_SHARED & ~VM_STACK */
unsigned long exec_vm; /* VM_EXEC & ~VM_WRITE & ~VM_STACK */
unsigned long stack_vm; /* VM_STACK */
...... 省略 ........
}
当内存吃紧的时候,有些页可以换出到硬盘上,而有些页因为比较重要,不能换出。locked_vm 就是被锁定不能换出的内存页总数,pinned_vm 表示既不能换出,也不能移动的内存页总数。
data_vm 表示数据段中映射的内存页数目,exec_vm 是代码段中存放可执行文件的内存页数目,stack_vm 是栈中所映射的内存页数目,这些变量均是表示进程虚拟内存空间中的虚拟内存使用情况。
5.内核如何管理虚拟内存区域
vm_area_struct结构体描述了虚拟内存区域 VMA(virtual memory area)。
struct vm_area_struct {
unsigned long vm_start; /* Our start address within vm_mm. */
unsigned long vm_end; /* The first byte after our end address
within vm_mm. */
/*
* Access permissions of this VMA.
*/
pgprot_t vm_page_prot;
unsigned long vm_flags;
struct anon_vma *anon_vma; /* Serialized by page_table_lock */
struct file * vm_file; /* File we map to (can be NULL). */
unsigned long vm_pgoff; /* Offset (within vm_file) in PAGE_SIZE
units */
void * vm_private_data; /* was vm_pte (shared mem) */
/* Function pointers to deal with this struct. */
const struct vm_operations_struct *vm_ops;
}
虚拟内存区域 VMA 由许多的虚拟页 (page) 组成,每个虚拟页需要经过页表的转换才能找到对应的物理页面。页表中关于内存页的访问权限就是由 vm_page_prot 决定的。