1️⃣ 进程的虚拟地址空间

2️⃣ 内核如何管理进程?

内核通过 task_strcut 来对进程进行管理

  因为在进程描述符 task_struct 结构中,有一个专门描述 进程虚拟地址空间 的内存描述符 mm_struct。由于每个进程都有唯一的 mm_struct 结构体,因此每个进程的虚拟地址空间都是 独立的

1
2
3
4
5
6
7
8
9
10
struct task_struct {
// 进程id
pid_t pid;
// 用于标识线程所属的进程 pid
pid_t tgid;
// 用于标识线程所属的进程 pid
pid_t tgid;
// 内存描述符表示进程虚拟地址空间
struct mm_struct *mm;
};

子进程是如何被创建的?

  • 调用 fork() 函数创建进程的时候, 子进程地址空间mm_struct 结构会随着子进程描述符 task_struct 的创建而创建。
  • copy_process 函数中创建 task_struct 结构,并拷贝父进程的相关资源到子进程的 task_struct 结构里,其中就包括拷贝父进程的虚拟内存空间 mm_struct 结构。
  • copy_mm 函数首先会将父进程的虚拟内存空间 current->mm 赋值给指针 oldmm
    • 通过 fork() 创建的子进程, 通过 dup_mm() 函数将 父进程的虚拟内存空间 以及 相关页表 拷贝到子进程的 mm_struct 结构中。最后将拷贝出来的 mm_struct 赋值给子进程的 task_struct 结构。
    • 通过 clone() 创建的子进程,通过 mmget() 增加父进程的引用计数并直接将 父进程的虚拟内存空间以及相关页表直接赋值给子进程。这样一来父进程和子进程的虚拟内存空间就变成共享的了。

内核线程与用户线程

内核线程和用户态线程的区别: 内核线程没有相关的内存描述符 mm_struct ,内核线程对应的 task_struct 结构中的 mm 域指向 Null,所以 内核线程之间调度是不涉及地址空间切换

当一个内核线程被调度时,它会发现自己的虚拟地址空间为 Null。它不会访问用户态的内存,但是它会访问内核内存,内核会将调度之前的上一个用户态进程的虚拟内存空间 mm_struct 直接赋值给内核线程,因为内核线程不会访问用户空间的内存,它仅仅只会访问内核空间的内存,所以直接复用上一个用户态进程的虚拟地址空间就可以避免为内核线程分配 mm_struct 和相关页表的开销,以及避免内核线程之间调度时地址空间的切换开销。

3️⃣ 进程虚拟地址布局 mm_struct

内核 是通过 mm_struct 来表示 进程的虚拟内存布局。接下来,将详细讲解 mm_struct 是进行表示的。

mm_struct 结构体简要表示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
struct mm_struct { 
unsigned long task_size; /* size of task vm space */
unsigned long start_code, end_code
unsigned long start_data, end_data;
unsigned long start_brk, brk;
unsigned long start_stack;
unsigned long arg_start, arg_end;
unsigned long 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 */

...... 省略 ........
};
  • task_size:定义了 用户态地址空间内核态地址空间 之间的分界线。
  • start_code/end_code代码段起始位置结束位置

    程序编译后的二进制文件中的机器码被加载进内存之后就存放在这里。

  • start_data/end_data:定义 数据段起始位置结束位置

    程序编译后的二进制文件中存放的全局变量和静态变量被加载进内存中就存放在这里。

  • bss:存放 未被初始化全局变量静态变量

    这些变量在加载进内存时会生成一段 0 填充的内存区域 (BSS 段), BSS 段的大小是固定的。

  • start_brk/brkstart_brk 定义 堆的起始位置brk 定义 *堆当前的结束位置**。
  • mmp_base: 定义 内存映射区的起始地址

    进程运行时所依赖的动态链接库中的代码段,数据段,BSS 段以及调用 mmap 映射出来的一段虚拟内存空间就保存在这个区域。

  • start_stack栈的起始位置,在 RBP 寄存器中存储。

    栈的结束位置也就是栈顶指针 stack pointerRSP 寄存器中存储。

  • arg_start/arg_end参数列表 的位置。
  • env_start/env_end环境变量的位置。

    arg/env 都位于栈中的最高地址处。

  • total_vm/locked_vm/pinned_vm 等等:虚拟内存与物理内存映射内容相关的统计变量。

vm_area_struct

mm_struct 的简要介绍中,每一个 不可能只用 unsing long 进行表示。实际上是通过 vm_area_struct 结构体来描述这些虚拟内存区域 vma

1
2
3
4
5
6
7
8
9
10
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;
};
  • vm_start/vm_end:指向这块虚拟内存区域的 起始地址终止地址

  • vm_page_prot/vm_flags:表示这块虚拟内存区域的 访问权限行为规范

    虚拟内存区域 VMA 由许多的虚拟页 (page) 组成,每个虚拟页需要经过页表的转换才能找到对应的物理页面。页表中关于内存页的访问权限 就是由 vm_page_prot 决定的。vm_flags 则偏向于 整个虚拟内存区域的访问权限 以及行为规范。

  • anon_vma/vm_file/vm_pgoff:都和 虚拟内存映射 有关。

    虚拟内存区域 可以映射到 物理内存 ,也可以映射到 文件映射到物理内存上我们称之为匿名映射映射到文件中我们称之为文件映射

    • 匿名映射 anon_vma:调用 mmap ,此时 mmp 会在 文件映射和匿名映射区 分配一块 vma 内存。这块匿名映射区域就用 struct anon_vma 结构表示。
    • 文件映射 vm_file/vm_pgoff
      • vm_file 用来关联被映射的文件。
      • vm_pgoff 表示映射进虚拟内存中的文件内容在文件中的偏移。
  • vm_private_data:存储 VMA 中的私有数据

  • vm_ops:指向针对虚拟内存区域 VMA 的相关操作的函数指针。

    1
    2
    3
    4
    5
    6
    7
    struct vm_operations_struct { 
    void (*open)(struct vm_area_struct * area);
    void (*close)(struct vm_area_struct * area);
    vm_fault_t (*fault)(struct vm_fault *vmf);
    vm_fault_t (*page_mkwrite)(struct vm_fault *vmf);
    ..... 省略 .......
    }
    • open:指定的虚拟内存区域 加入 到进程虚拟内存空间时调用。
    • close:虚拟内存区域 VMA 从进程虚拟内存空间中被 删除 时调用
    • fault:当进程访问虚拟内存时,访问的页面不在物理内存中,可能是未分配物理内存也可能是被置换到磁盘中,这时就会产生缺页异常,fault 函数就会被调用。

vma 在内核中是如何被组织的?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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;

struct vm_area_struct *vm_next, *vm_prev;
struct vm_area_struct *mmap;
struct rb_node vm_rb;
struct list_head anon_vma_chain;
struct mm_struct *vm_mm; /* The address space we belong to. */
};
  • vm_next/vm_prev:内核通过 双向链表虚拟内存区空间 中的这些虚拟内存区域 vm_area_struct 进行串联起来。vm_prev/vm_next 分别指向 前驱节点 以及 后驱节点。所有 vma 节点按照 低地址高地址 的增长方向排序。
  • mmap:双向链表的头节点。
  • vm_mm:指向了所属的虚拟内存空间 mm_struct
  • vm_rb:同样的 vm_area_struct 在内核中有两种组织方式:双向链表 用于高效遍历,红黑树 用于高效查找。

4️⃣ 编译好的二进制文件如何加载到进程虚拟内存

C++ 程序在编译、链接后,会生成 ELF 可执行目标文件。这个 ELF 文件格式图如下:

磁盘文件中的段我们叫做 Section,内存中的段我们叫做 Segment,也就是内存区域。

ELF 文件中的这些 Section 会在 进程运行之前加载到内存 并映射到 **内存中的 Segment**。通常是多个 Section 映射到一个 Segment

ELF 格式的二进制文件中的 Section 是如何加载并映射进虚拟内存空间的呢?

内核通过 load_elf_binary 函数进行实现。作用是:

  • 加载内核执行它
  • 启动第一个用户态进程 init 也执行它
  • fork() 之后,调用 exec()来执行另一个程序也执行它

    exec 运行一个二进制程序的时候,除了解析 ELF 的格式之外,另外一个重要的事情就是建立上述提到的内存映射。

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
static int load_elf_binary(struct linux_binprm *bprm) { 
...... 省略 ........
// 设置虚拟内存空间中的内存映射区域起始地址 mmap_base
setup_new_exec(bprm);

...... 省略 ........
// 创建并初始化栈对应的 vm_area_struct 结构。
// 设置 mm->start_stack 就是栈的起始地址也就是栈底,并将 mm->arg_start 是指向栈底的。
retval = setup_arg_pages(bprm, randomize_stack_top(STACK_TOP), executable_stack);

...... 省略 ........
// 将二进制文件中的代码部分映射到虚拟内存空间中
error = elf_map(bprm->file, load_bias + vaddr, elf_ppnt, elf_prot, elf_flags, total_size);

...... 省略 ........
// 创建并初始化堆对应的的 vm_area_struct 结构
// 设置 current->mm->start_brk = current->mm->brk,设置堆的起始地址 start_brk,结束地址 brk。 起初两者相等表示堆是空的
retval = set_brk(elf_bss, elf_brk, bss_prot);

...... 省略 ........
// 将进程依赖的动态链接库 .so 文件映射到虚拟内存空间中的内存映射区域
elf_entry = load_elf_interp(&loc->interp_elf_ex, interpreter, &interp_map_addr, load_bias, interp_elf_phdata);

...... 省略 ........
// 初始化内存描述符
mm_struct current->mm->end_code = end_code;
current->mm->start_code = start_code;
current->mm->start_data = start_data;
current->mm->end_data = end_data;
current->mm->start_stack = bprm->p;
...... 省略 ........
}

参考文章