Linux操作系统学习笔记(八)任务空间管理

一. 前言

  从本文开始,我们进入内存部分的学习。首先会接着前面的任务task_struct讲解任务空间管理结构体mm_struct,并简单介绍物理内存和虚拟内存的相关知识,关于详细的基础知识和概念可以参照CSAPP一书,这里不会做过多的赘述,而是默认在已了解其映射关系的基础上进行的学习。在后文中,会继续介绍物理内存的管理以及用户态和内核态的内存映射。

二. 基本概念梳理

  • CPU、缓存、内存、主存的架构是源于越快的设备越贵,因此出于节约(qiong)考虑设计了多层架构,CPU中有了MMU
  • 物理内存有限,多进程共享物理内存存在安全问题,因此出现了虚拟内存的设计
  • 虚拟内存根据ELF的结构进行了相应的设计,存在堆、映射区、栈、数据段等部分
  • 考虑到虚拟内存的结构,出现了堆的申请即动态内存
  • 虚拟内存为每个进程分配单独的地址空间,映射到物理内存上执行,因此有了物理内存和虚拟内存的映射方法:页
  • 为了管理虚拟内存,出现了页表和多级页表
  • 为了加速映射,出现了CPU中的TLB
  • 为了满足共享的需求,出现了内存映射中的共享内存
  • 由于内存碎片的存在,出现了碎片管理的设计以及垃圾回收器

三. 进程内存管理

  对于一个进程来说,需要考虑用户态和内核态两部分需要存储在内核内的各个结构

  用户态包括

  • 代码段
  • 全局变量
  • 常量字符串
  • 函数栈,包括函数调用,局部变量,函数参数等
  • 堆:malloc 分配的内存等
  • 内存映射,如 glibc 的调用, glibc 的代码是以 so 文件的形式存在的,也需要放在内存里面。

  内核态包括

  • 内核部分的代码
  • 内核中全局变量
  • task_struct
  • 内核栈
  • 在内核里面也有动态分配的内存
  • 虚拟地址到物理地址的映射表

  进程在内核态中通过task_struct管理,而task_struct中关于内存有如下成员变量

1
2
3
4
struct mm_struct		*mm;
struct mm_struct *active_mm;
/* Per-thread vma caching: */
struct vmacache vmacache;

  其中mm_struct结构体也较为复杂,我们将分步介绍。首先我们来看看内核态和用户态的地址划分。这里highest_vm_end存储当前虚拟内存地址的最大地址,而task_size则是用户态的大小。

1
2
3
4
5
6
struct mm_struct {
......
unsigned long task_size; /* size of task vm space */
unsigned long highest_vm_end; /* highest vma end address */
......
}

  task_size定义如下,从注释可见用户态分配了4G虚拟内存中的3G空间,而64位因为空间巨大因此在内核态和用户态中间还保留了空闲区域进行隔离,用户态仅使用47位,即128TB。内核态同样分配128TB,位于最高位。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#ifdef CONFIG_X86_32
/*
* User space process size: 3GB (default).
*/
#define TASK_SIZE PAGE_OFFSET
#define TASK_SIZE_MAX TASK_SIZE
/*
config PAGE_OFFSET
hex
default 0xC0000000
depends on X86_32
*/
#else
/*
* User space process size. 47bits minus one guard page.
*/
#define TASK_SIZE_MAX ((1UL << 47) - PAGE_SIZE)
#define TASK_SIZE (test_thread_flag(TIF_ADDR32) ? \
IA32_PAGE_OFFSET : TASK_SIZE_MAX)
......

3.1 用户态内存结构

  在用户态,mm_struct有着以下成员变量

  • mmap_base:内存映射的起始地址
  • mmap_legacy_base:表示映射的基址,在32位中为固定的TASK_UNMAPPED_BASE,而在64位中,存在一个虚拟地址随机映射机制,因此为TASK_UNMAPPED_BASE + mmap_rnd()
  • hiwater_rssRSS的高水位使用情况
  • hiwater_vm:高水位虚拟内存使用情况
  • total_vm:映射的总页数
  • locked_vm:被锁定不能换出的页数
  • pinned_vm:不能换出也不能移动的页数
  • data_vm:存放数据的页数
  • exec_vm:存放可执行文件的页数
  • stack_vm:存放栈的页数
  • arg_lock:引入spin_lock用于保护对下面区域变量们的并行访问
  • start_code 和 end_code: 可执行代码的开始和结束位置
  • start_data 和 end_data :已初始化数据的开始位置和结束位置
  • start_brk :堆的起始位置
  • brk :堆当前的结束位置
  • start_stack :栈的起始位置,栈的结束位置在寄存器的栈顶指针中
  • arg_start 和 arg_end :参数列表的位置,位于栈中最高地址的地方。
  • env_start 和 env_end :环境变量的位置,位于栈中最高地址的地方。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
struct mm_struct {
......
unsigned long mmap_base; /* base of mmap area */
unsigned long mmap_legacy_base; /* base of mmap area in bottom-up allocations */
......
unsigned long hiwater_rss; /* High-watermark of RSS usage */
unsigned long hiwater_vm; /* High-water virtual memory usage */
unsigned long total_vm; /* Total pages mapped */
unsigned long locked_vm; /* Pages that have PG_mlocked set */
atomic64_t 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 */
spinlock_t arg_lock; /* protect the below fields */
unsigned long start_code, end_code, start_data, end_data;
unsigned long start_brk, brk, start_stack;
unsigned long arg_start, arg_end, env_start, env_end;
unsigned long saved_auxv[AT_VECTOR_SIZE]; /* for /proc/PID/auxv */
......
}

  根据这些成员变量,我们可以规划出用户态中各个部分的位置,但是我们还需要一个结构体描述这些区域的属性,即vm_area_struct

1
2
3
4
5
6
struct mm_struct {
......
struct vm_area_struct *mmap; /* list of VMAs */
struct rb_root mm_rb;
......
}

  vm_area_struct的具体结构体定义如下所示,实际是通过vm_nextvm_prev组合而成的双向链表,即通过一系列的vm_area_struct来表述一个进程在用户态分配的各个区域的内容。

  • vm_startvm_end表述该块区域的开始和结束为止
  • vm_rb对应一颗红黑树,这颗红黑树将所有vm_area_struct组合起来,便于增删查找。
  • rb_subtree_gap存储当前区域和上个区域之间的间隔,用于后续分配使用。
  • vm_mm指向该结构体所属的vm_struct
  • vm_page_prot管理该页的接入权限,vm_flags为标记位
  • rbrb_subtree_last:有空余位置的区间树结构
  • ano_vma 和 ano_vma_chain:匿名映射。虚拟内存区域可以映射到物理内存,也可以映射到文件,映射到物理内存的时候称为匿名映射,映射到文件需要vm_file指定被映射文件,vm_pgoff存储偏移量。
  • vm_opts:指向该结构体的函数指针,用于处理该结构体
  • vm_private_data:私有数据存储
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
49
50
51
52
53
54
55
56
57
/*
* This struct defines a memory VMM memory area. There is one of these
* per VM-area/task. A VM area is any part of the process virtual memory
* space that has a special rule for the page-fault handlers (ie a shared
* library, the executable area etc).
*/
struct vm_area_struct {
/* The first cache line has the info for VMA tree walking. */
unsigned long vm_start; /* Our start address within vm_mm. */
unsigned long vm_end; /* The first byte after our end address
within vm_mm. */
/* linked list of VM areas per task, sorted by address */
struct vm_area_struct *vm_next, *vm_prev;
struct rb_node vm_rb;
/*
* Largest free memory gap in bytes to the left of this VMA.
* Either between this VMA and vma->vm_prev, or between one of the
* VMAs below us in the VMA rbtree and its ->vm_prev. This helps
* get_unmapped_area find a free area of the right size.
*/
unsigned long rb_subtree_gap;
/* Second cache line starts here. */
struct mm_struct *vm_mm; /* The address space we belong to. */
pgprot_t vm_page_prot; /* Access permissions of this VMA. */
unsigned long vm_flags; /* Flags, see mm.h. */
/*
* For areas with an address space and backing store,
* linkage into the address_space->i_mmap interval tree.
*/
struct {
struct rb_node rb;
unsigned long rb_subtree_last;
} shared;
/*
* A file's MAP_PRIVATE vma can be in both i_mmap tree and anon_vma
* list, after a COW of one of the file pages. A MAP_SHARED vma
* can only be in the i_mmap tree. An anonymous MAP_PRIVATE, stack
* or brk vma (with NULL file) can only be in an anon_vma list.
*/
struct list_head anon_vma_chain; /* Serialized by mmap_sem & page_table_lock */
struct anon_vma *anon_vma; /* Serialized by page_table_lock */
/* Function pointers to deal with this struct. */
const struct vm_operations_struct *vm_ops;
/* Information about our backing store: */
unsigned long vm_pgoff; /* Offset (within vm_file) in PAGE_SIZE
units */
struct file * vm_file; /* File we map to (can be NULL). */
void * vm_private_data; /* was vm_pte (shared mem) */
atomic_long_t swap_readahead_info;
#ifndef CONFIG_MMU
struct vm_region *vm_region; /* NOMMU mapping region */
#endif
#ifdef CONFIG_NUMA
struct mempolicy *vm_policy; /* NUMA policy for the VMA */
#endif
struct vm_userfaultfd_ctx vm_userfaultfd_ctx;
} __randomize_layout;

  对一个mm_struct来说,其众多的vm_area_struct会在ELF文件加载,即load_elf_binary()时构造。该函数在解析ELF文件格式后,就会进行内存映射的建立,主要包括

  • 调用 setup_new_exec,设置内存映射区 mmap_base
  • 调用 setup_arg_pages,设置栈的 vm_area_struct,这里面设置了 mm->arg_start 是指向栈底的,current->mm->start_stack 就是栈底
  • elf_map 会将 ELF 文件中的代码部分映射到内存中来
  • set_brk 设置了堆的 vm_area_struct,这里面设置了 current->mm->start_brk = current->mm->brk,也即堆里面还是空的
  • load_elf_interp 将依赖的 so 映射到内存中的内存映射区域
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

static int load_elf_binary(struct linux_binprm *bprm)
{
......
setup_new_exec(bprm);
......
/* Do this so that we can load the interpreter, if need be. We will
change some of these later */
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);
......
/* Calling set_brk effectively mmaps the pages that we need
* for the bss and break sections. We must do this before
* mapping in the interpreter, to make sure it doesn't wind
* up getting placed where the bss needs to go.
*/
retval = set_brk(elf_bss, elf_brk, bss_prot);
......
elf_entry = load_elf_interp(&loc->interp_elf_ex,
interpreter,
&interp_map_addr,
load_bias, interp_elf_phdata);
......
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;
......
}

3.2 内核态结构

  由于32位和64位系统空间大小差距过大,因此结构上也有一些区别。我们这里分别讨论二者的结构。

3.2.1 32位内核态结构

  内核态的虚拟空间和进程是无关的,即所有进程通过系统调用进入内核后,看到的虚拟地址空间是一样的。如下图所示为32位内核态虚拟空间分布图。

img
  1. 直接映射区

    前896M为直接映射区,该区域用于和物理内存进行直接映射。虚拟内存地址减去 3G,就得到对应的物理内存的位置。在内核里面,有两个宏:

  • __pa(vaddr) 返回与虚拟地址 vaddr 相关的物理地址;

  • __va(paddr) 则计算出对应于物理地址 paddr 的虚拟地址。

  对于该部分虚拟地址的访问,同样采取分页的方式进行,但是页表地址比较简单,直接一一对应即可。

  在系统启动的时候,物理内存的前 1M 已经被占用了,从 1M 开始加载内核代码段,然后就是内核的全局变量、BSS 等,也是 ELF 里面涵盖的。这样内核的代码段,全局变量,BSS 也就会被映射到 3G 后的虚拟地址空间里面。具体的物理内存布局可以查看 /proc/iomem,具体会因为每个人的系统、配置等产生区别。

  1. high_memory

  高端内存的名字来源于x86架构中将物理地址空间划分三部分:ZONE_DMA、ZONE_NORMAL和ZONE_HIGHMEM。ZONE_HIGHMEM即为高端内存。

  高端内存是内存管理模块看待物理内存的称谓,指的也即896M直接映射区上面的区域。内核中除了内存管理模块外,其余均操作虚拟地址。而内存管理模块会直接操作物理地址,进行虚拟地址的分配和映射。其存在的意义是以32位系统有限的内核空间去访问无限的物理内存空间:借用这段逻辑地址空间,建立映射到想访问的那段物理内存(即填充内核页表),临时用一会,用完后归还。

  1. 内核动态映射空间(noncontiguous memory allocation)

  在VMALLOC_STARTVMALLOC_END之间的区域称之为内核动态映射空间,对应于用户态进程malloc申请内存一样,在内核态可以通过vmalloc来申请。内核态有单独的页表管理,和用户态分开。

  1. 持久内核映射区(permanent kernel mapping)

  PKMAP_BASEFIXADDR_START 的空间称为持久内核映射,这个地址范围是 4G-8M 到 4G-4M 之间。使用 alloc_pages() 函数的时候,在物理内存的高端内存得到 struct page 结构,可以调用 kmap() 将其映射到这个区域。因为允许永久映射的数量有限,当不再需要高端内存时,应该解除映射,这可以通过kunmap()函数来完成。

  1. 固定映射区

  FIXADDR_STARTFIXADDR_TOP(0xFFFF F000) 的空间,称为固定映射区域,主要用于满足特殊需求。

  1. 临时映射区(temporary kernel mapping)

  临时内核映射通过kmap_atomickunmap_atomic实现,主要用于当需要写入物理内存或主存时的操作,如写入文件时使用。

3.2.2 64位内核态结构

  64位内核态因为空间巨大,所以不需要像32位一样精打细算,直接分出很多的空闲区域做保护,结构如下图所示

img
  • 从 0xffff800000000000 开始就是内核的部分,只不过一开始有 8T 的空档区域。
  • __PAGE_OFFSET_BASE(0xffff880000000000) 开始的 64T 的虚拟地址空间是直接映射区域,也就是减去 PAGE_OFFSET 就是物理地址。虚拟地址和物理地址之间的映射在大部分情况下还是会通过建立页表的方式进行映射。
  • VMALLOC_START(0xffffc90000000000)开始到 VMALLOC_END(0xffffe90000000000)的 32T 的空间是给 vmalloc 的。
  • VMEMMAP_START(0xffffea0000000000)开始的 1T 空间用于存放物理页面的描述结构 struct page 的。
  • __START_KERNEL_map(0xffffffff80000000)开始的 512M 用于存放内核代码段、全局变量、BSS 等。这里对应到物理内存开始的位置,减去 __START_KERNEL_map 就能得到物理内存的地址。这里和直接映射区有点像,但是不矛盾,因为直接映射区之前有 8T 的空当区域,早就过了内核代码在物理内存中加载的位置。

总结

  本文比较详细的分析了内存在用户态和内核态的结构,以此为基础,后文可以开始分析内存的管理、映射了。

代码资料

[1] linux/include/linux/mm_types.h

参考资料

[1] wiki

[2] elixir.bootlin.com/linux

[3] woboq

[4] Linux-insides

[5] 深入理解Linux内核

[6] Linux内核设计的艺术

[7] 极客时间 趣谈Linux操作系统

坚持原创,坚持分享,谢谢鼓励和支持