一步一图带你深入理解 Linux 虚拟内存管理( 九 )

由于本小节中我们举的示例是通过fork() 函数创建子进程的情形 , 所以这里大家先占时忽略 if (clone_flags & CLONE_VM) 这个条件判断逻辑 , 我们先跳过往后看~~
copy_mm函数首先会将父进程的虚拟内存空间 current->mm 赋值给指针 oldmm 。然后通过 dup_mm 函数将父进程的虚拟内存空间以及相关页表拷贝到子进程的 mm_struct 结构中 。最后将拷贝出来的 mm_struct 赋值给子进程的 task_struct 结构 。

通过 fork() 函数创建出的子进程 , 它的虚拟内存空间以及相关页表相当于父进程虚拟内存空间的一份拷贝 , 直接从父进程中拷贝到子进程中 。
而当我们通过 vfork 或者 clone 系统调用创建出的子进程 , 首先会设置 CLONE_VM 标识 , 这样来到 copy_mm 函数中就会进入if (clone_flags & CLONE_VM)条件中 , 在这个分支中会将父进程的虚拟内存空间以及相关页表直接赋值给子进程 。这样一来父进程和子进程的虚拟内存空间就变成共享的了 。也就是说父子进程之间使用的虚拟内存空间是一样的 , 并不是一份拷贝 。
子进程共享了父进程的虚拟内存空间 , 这样子进程就变成了我们熟悉的线程 , 是否共享地址空间几乎是进程和线程之间的本质区别 。Linux 内核并不区别对待它们 , 线程对于内核来说仅仅是一个共享特定资源的进程而已 。
内核线程和用户态线程的区别就是内核线程没有相关的内存描述符 mm_struct  , 内核线程对应的 task_struct 结构中的 mm 域指向 Null , 所以内核线程之间调度是不涉及地址空间切换的 。
当一个内核线程被调度时 , 它会发现自己的虚拟地址空间为 Null , 虽然它不会访问用户态的内存 , 但是它会访问内核内存 , 聪明的内核会将调度之前的上一个用户态进程的虚拟内存空间 mm_struct 直接赋值给内核线程 , 因为内核线程不会访问用户空间的内存 , 它仅仅只会访问内核空间的内存 , 所以直接复用上一个用户态进程的虚拟地址空间就可以避免为内核线程分配 mm_struct 和相关页表的开销 , 以及避免内核线程之间调度时地址空间的切换开销 。
父进程与子进程的区别 , 进程与线程的区别 , 以及内核线程与用户态线程的区别其实都是围绕着这个 mm_struct 展开的 。
现在我们知道了表示进程虚拟内存空间的 mm_struct 结构是如何被创建出来的相关背景 , 那么接下来笔者就带大家深入 mm_struct 结构内部 , 来看一下内核如何通过这么一个 mm_struct 结构体来管理进程的虚拟内存空间的 。
5.1 内核如何划分用户态和内核态虚拟内存空间通过 《3. 进程虚拟内存空间》小节的介绍我们知道 , 进程的虚拟内存空间分为两个部分:一部分是用户态虚拟内存空间 , 另一部分是内核态虚拟内存空间 。
一步一图带你深入理解 Linux 虚拟内存管理

文章插图
那么用户态的地址空间和内核态的地址空间在内核中是如何被划分的呢?
这就用到了进程的内存描述符 mm_struct 结构体中的 task_size 变量 , task_size 定义了用户态地址空间与内核态地址空间之间的分界线 。
struct mm_struct {unsigned long task_size; /* size of task vm space */}通过前边小节的内容介绍 , 我们知道在32 位系统中用户态虚拟内存空间为 3 GB , 虚拟内存地址范围为:0x0000 0000 - 0xC000 000。

经验总结扩展阅读