《Linux内核设计与实现》阅读笔记:(第十五章)进程地址空间

本篇主要是跟随《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是红黑树,搜索元素更高效,下图展示了基本原理:

mmap vs mm_rb

所有的mm_struct结构体通过自身的mmlist连接在一个双向链表中,受元素是init_mm内存描述符,代表了init进程的地址空间,操作需要加锁防止并发。

分配内存描述符

进程描述符task_struct的mm域存放着进程使用的内存描述符。

进程:fork()利用copy_mm()复制父进程内存描述符

线程:调用clone()时,设置CLONE_VM标志,使父进程和子进程共享地址空间;

接下来看看具体代码中是如何实现的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
static int copy_mm(unsigned long clone_flags, struct task_struct *tsk)
{
struct mm_struct *mm, *oldmm;
int retval;
tsk->min_flt = tsk->maj_flt = 0;
tsk->nvcsw = tsk->nivcsw = 0;
#ifdef CONFIG_DETECT_HUNG_TASK
tsk->last_switch_count = tsk->nvcsw + tsk->nivcsw;
#endif
tsk->mm = NULL;
tsk->active_mm = NULL;
/*
* Are we cloning a kernel thread?
*
* We need to steal a active VM for that..
*/
//获取父进程的内存描述符
oldmm = current->mm;
if (!oldmm)
return 0;
/* initialize the new vmacache entries */
vmacache_flush(tsk);
// 判断是否要创建的是线程
if (clone_flags & CLONE_VM) {
atomic_inc(&oldmm->mm_users);
mm = oldmm;
goto good_mm;
}
retval = -ENOMEM;
// 要创建新的进程
mm = dup_mm(tsk);
if (!mm)
goto fail_nomem;
good_mm:
tsk->mm = mm;
tsk->active_mm = mm;
return 0;
fail_nomem:
return retval;
}

tsk代表待创建进程/线程的进程描述符,一旦clone_flags & CLONE_VM为true,那么代码最终跳转到good_mm:处,可见tsk的mm只是简单指向了父进程内存描述符oldmm的内容。

clone_flags & CLONE_VM为false,则会执行mm = dup_mm(tsk);,下面继续追踪这段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
static struct mm_struct *dup_mm(struct task_struct *tsk)
{
struct mm_struct *mm, *oldmm = current->mm;
int err;
mm = allocate_mm();
if (!mm)
goto fail_nomem;
memcpy(mm, oldmm, sizeof(*mm));
if (!mm_init(mm, tsk))
goto fail_nomem;
err = dup_mmap(mm, oldmm);
if (err)
goto free_pt;
mm->hiwater_rss = get_mm_rss(mm);
mm->hiwater_vm = mm->total_vm;
if (mm->binfmt && !try_module_get(mm->binfmt->module))
goto free_pt;
return mm;
free_pt:
/* don't put binfmt in mmput, we haven't got module yet */
mm->binfmt = NULL;
mmput(mm);
fail_nomem:
return NULL;
}

调用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 Kernel怎么管理内存

linux/v4.4/source/kernel/fork.c

内核线程

问题

mm_count指的是本进程的引用不算而其它进程或者其它进程的线程的引用算?

mm_count和复制父进程的内存描述符有关?