本篇主要是跟随《Linux内核设计与实现》一书,了解进程地址空间相关的技术,从源码的角度理解Linux里进程和线程的区别。
一些概念
- 进程地址空间由进程可寻址的虚拟内存组成,所以它是逻辑内存,不是物理内存;
- “平坦”,指的是地址空间范围是一个独立的连续区间;
- 进程地址空间中任何有效地址都只能位于唯一的区域:栈、对象代码、全局变量、被映射的文件等;
在之前的笔记《进程管理》一篇的补充章节中,本人已经提到了进程的布局,但《Linux内核设计与实现》的划分更细腻:
- 可执行文件代码的内存映射,代码段(text section);
- 可执行文件的已初始化全局变量的内存映射,数据段(data section);
- 包含未初始化全局变量的内存映射,也就是bss段的零页(页面信息全是0值);
- 用于进程用户空间栈的零页内存映射;
- 每一个诸如C库或者动态链接库等共享库的代码段,数据段和bss也会被载入进程的地址空间;
- 任何内存映射文件;
- 任何共享内存段;
- 任何匿名内存映射,比如由malloc()分配的内存;
内存描述符
和进程描述符一样,内存描述符包含了和地址空间有关的全部信息,它被表示为一个mm_struct结构体。
mm_users vs mm_count:
mm_users表示正在使用该地址的进程数目,是mm_struct的用户数,也就是使用该地址空间的线程数;mm_count表示mm_struct结构体的主引用计数。通过同时具有这两个计数,内核可以区分使用地址空间的进程数和引用计数。
mmap vs mm_rb:
这两个结构体都可以描述地址空间的全部内存区域,mmap是链表,遍历元素更高效;mm_rb是红黑树,搜索元素更高效,下图展示了基本原理:
所有的mm_struct结构体通过自身的mmlist连接在一个双向链表中,受元素是init_mm内存描述符,代表了init进程的地址空间,操作需要加锁防止并发。
分配内存描述符
进程描述符task_struct的mm域存放着进程使用的内存描述符。
进程:fork()利用copy_mm()复制
父进程内存描述符
线程:调用clone()时,设置CLONE_VM标志,使父进程和子进程共享地址空间;
接下来看看具体代码中是如何实现的:
|
|
tsk代表待创建进程/线程的进程描述符,一旦clone_flags & CLONE_VM
为true,那么代码最终跳转到good_mm:
处,可见tsk的mm只是简单指向了父进程内存描述符oldmm的内容。
clone_flags & CLONE_VM
为false,则会执行mm = dup_mm(tsk);
,下面继续追踪这段代码:
|
|
调用dup_mm()后tsk不再是单纯的引用父进程的内存描述符,而是复制了父进程的内存描述符,如同书中描绘的那样“The mm_struct structure is allocated from the mm_cachep slab cache via the allocate_mm() macro in kernel/fork.c.”
mm_struct与内核线程
内核线程又称为守护进程,没有进程地址空间,内核线程对应的进程描述符
mm域为空。内核线程可以定义为没有用户上下文的进程。这些“小知识”在前几篇已经阐述过。
首先,内核线程不需要有自己的“独家”描述符和页表。但是在具体被调度后,还是需要访问一些数据的,比如页表。所以内核线程将直接使用前一个进程的内存描述符,内核线程一文提到:“为强调用户空间部分不能访问,mm设置为空指针。但由于内核必须知道用户空间当前包含了什么,所以在active_mm中保存了指向mm_struct的一个指针来描述它。”
内核线程不访问用户空间的内存。
虚拟内存区域
内存区域由vm_area_struct结构体描述,也称作虚拟内存区域:virtual memoryAreas,VMAs。
每个内存区域都拥有一致的属性,比如访问权限等。
内存区域的树型结构和内存区域的链表结构
mmap和mm_rb保存的元素就是各个虚拟内存区域。
- mmap指针指向vm_area_struct内存对象构成的链表;
- mm_rb指向红黑树的根节点,地址空间中每一个vm_area_struct结构体通过自身的vm_rb连接到树中;
页表
页表的目的是完成物理内存和虚拟内存的映射;
地址转换需要将虚拟地址分段,使每段虚拟地址都作为一个索引指向页表,而页表指向下一级别的页表活着指向最终的物理页面;
利用多级页表能够节约地址转换需占用的存放空间,Linux对所有体系结构使用三级页表管理;
为了加快搜索内存中的物理地址,多数体系结构实现了一个翻译后缓冲器(TLB),当访问虚拟地址时,处理器首先检查TLB是否缓存了该地址到物理地址的映射;
书中提到的未来改进,架构页表页通过COW处理,可以消除fork()操作中页表拷贝所带来的消耗;
下图给出了一个三级页表的架构图:
小结
阅读《Linux内核设计与实现》第十五章旨在了解Linux实现进程地址空间的代码结构,了解了不少相关技术(比如进程和线程)的实现细节。
参考
《Linux内核设计与实现(第三版)》
《Linux Kernel Development, 3rd Edition》
Linux Kernel: Why are we using two variables mm_users and mm_count in mm_struct?
linux/v4.4/source/kernel/fork.c
问题
mm_count指的是本进程的引用不算而其它进程或者其它进程的线程的引用算?
mm_count和复制父进程的内存描述符有关?