通过 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. 进程虚拟内存空间》小节的介绍我们知道,进程的虚拟内存空间分为两个部分:一部分是用户态虚拟内存空间 , 另一部分是内核态虚拟内存空间 。

文章插图
那么用户态的地址空间和内核态的地址空间在内核中是如何被划分的呢?
这就用到了进程的内存描述符 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。内核态虚拟内存空间为 1 GB,虚拟内存地址范围为:0xC000 000 - 0xFFFF FFFF 。

文章插图
32 位系统中用户地址空间和内核地址空间的分界线在 0xC000 000 地址处,那么自然进程的 mm_struct 结构中的 task_size 为 0xC000 000 。
我们来看下内核在
/arch/x86/include/asm/page_32_types.h
文件中关于 TASK_SIZE 的定义 。/* * User space process size: 3GB (default). */#define TASK_SIZE__PAGE_OFFSET
如下图所示:__PAGE_OFFSET 的值在 32 位系统下为0xC000 000 。
文章插图
而在 64 位系统中,只使用了其中的低 48 位来表示虚拟内存地址 。其中用户态虚拟内存空间为低 128 T,虚拟内存地址范围为:0x0000 0000 0000 0000 - 0x0000 7FFF FFFF F000。
内核态虚拟内存空间为高 128 T,虚拟内存地址范围为:0xFFFF 8000 0000 0000 - 0xFFFF FFFF FFFF FFFF。

文章插图
64 位系统中用户地址空间和内核地址空间的分界线在0x0000 7FFF FFFF F000 地址处,那么自然进程的 mm_struct 结构中的 task_size 为 0x0000 7FFF FFFF F000。
我们来看下内核在
/arch/x86/include/asm/page_64_types.h
文件中关于 TASK_SIZE 的定义 。#define TASK_SIZE(test_thread_flag(TIF_ADDR32) ? \IA32_PAGE_OFFSET : TASK_SIZE_MAX)#define TASK_SIZE_MAXtask_size_max()#define task_size_max()((_AC(1,UL) << __VIRTUAL_MASK_SHIFT) - PAGE_SIZE)#define __VIRTUAL_MASK_SHIFT 47
我们来看下在 64 位系统中内核如何来计算 TASK_SIZE , 在task_size_max() 的计算逻辑中 1 左移 47 位得到的地址是 0x0000800000000000,然后减去一个 PAGE_SIZE (默认为 4K) , 就是 0x00007FFFFFFFF000,共 128T 。所以在 64 位系统中的 TASK_SIZE 为 0x00007FFFFFFFF000。
推荐阅读
- Dubbo 03: 直连式 + 接口工程
- 一篇文章带你了解热门版本控制系统——Git
- 一篇文章带你了解网页框架——Vue简单入门
- 我用canvas带你看一场流星雨
- 一篇文章带你掌握主流办公框架——SpringBoot
- 带你认识JDK8中超nice的Native Memory Tracking
- SpringBoot+Vue3 AgileBoot - 手把手一步一步带你Run起全栈项目
- 带你读AI论文丨ACGAN-动漫头像生成
- 一篇文章带你掌握主流服务层框架——SpringMVC
- 一篇文章带你掌握主流基础框架——Spring