第三章:内存管理

内存管理框架

​ 内存管理子系统的架构如下图所示,分为用户空间、内核空间和硬件 3 个层面。

内存管理框架

用户空间

​ 应用程序使用 malloc() 申请内存,使用 free() 释放内存。

​ malloc() 和 free() 是 glibc 库的内存分配器 ptmalloc 提供的接口, ptmalloc 使用系统调用 brk 或 mmap 向内 核以页为单位申请内存 , 然后划分成小内存块分配给应用程序。

内核空间

​ (1)内核空间的基本功能

​ 虚拟内存管理负责从进程的虚拟地址空间分配虚拟页, sys_brk 用来扩大或收缩堆, sys_mmap 用来在内存映射区域分配虚拟页, sys_munmap 用来释放虚拟页。

内核使用延迟分配物理内存的策略,进程第 1 次访问虚拟页的时候,触发页错误异常,页错误异常处理程序从页分配器申请物理页,在进程的页表中把虚拟页映射到物理页。

内存分配的函数调用

​ 页分配器负责分配物理页,当前使用的页分配器是伙伴分配器。

​ 内核空间提供了把页划分成小内存块分配的块分配器,提供分配内存的接口 kmalloc() 和释放内存的接口 kfree(), 支持 3 种块分配器: SLAB 分配器、 SLUB 分配器和 SLOB 分配器。

​ 在内核初始化的过程中,页分配器还没准备好,需要使用临时的引导内存分配器分配内存。

​ (2)内核空间的扩展功能

不连续页分配器提供了分配内 存的接口 vmalloc 和释放内存的接口 vfree, 在内存碎片化的时候,申请连续物理页的成功率很低,可以申请不连续的物理页,映射到连续的虚拟页,即虚拟地址连续而物理地址不连续。

​ **连续内存分配器 (Contiguous Memory Allocator, CMA) **用来给驱动程序预留一段连续的内存,当驱动程序不用的时候,可以给进程使用;当驱动程序需要使用的时候,把进程占用的内存通过回收或迁移的方式让出来,给驱动程序使用。

​ 当内存碎片化的时候,找不到连续的物理页,内存碎片整理(“memory compaction” 的意译,直译为“内存紧缩")通过迁移的方式得到连续的物理页。

​ 在内存不足的时候,页回收负责回收物理页,对于没有后备存储设备支持的匿名页, 把数据换出到交换区,然后释放物理页;对于有后备存储设备支待的文件页,把数据写回存储设备,然后释放物理页。如果页回收失败,使用最后一招:内存耗尽杀手 (OOM killer, Out-of-Memory killer), 选择进程杀掉

硬件层面

​ 处理器包含一个称为内存管理单元 (Memory Management Unit, MMU) 的部件,负责把虚拟地址转换成物理地址。

​ 内存管理单元包含一个称为**页表缓存 (Translation Lookaside Buffer, TLB) **的部件, 保存最近使用过的页表映射,避免每次把虚拟地址转换成物理地址都需要查询内存中的页表。

​ 为了解决处理器的执行速度和内存的访问速度不匹配的问题,在处理器和内存之间增加了缓存。缓存通常分为 一级缓存和二级缓存,为了支持并行地取指令和取数据,一级缓存分为数据缓存和指令缓存。

虚拟地址空间布局

虚拟地址空间划分

​ 因为目前应用程序没有那么大的内存需求,所以 ARM64 处理器不支持完全的 64 位虚 拟地址,实际支持情况如下。

​ (1)虚拟地址的最大宽度是 48 位,如图所示。高 16 位是全 1 或全 0 的地址称为规范的地址,两者之间是不规范的地址,不允许使用。

48位虚拟地址空间划分

​ (2)如果处理器实现了 ARMv8.2 标准的大虚拟地址 (Large Virtual Address, LVA) 支持,并且页长度是 64KB, 那么虚拟地址的最大宽度是 52 位。

​ 所有进程共享内核虚拟地址空间,每个进程有独立的用户虚拟地址空间,同一个线程组的用户线程共享用户虚拟地址空间,内核线程没有用户虚拟地址空间。

用户虚拟地址空间布局

​ 用户进程虚拟地址空间包括以下区域。

  1. 代码段、 数据段和未初始化的数据段。
  2. 动态库的代码段、数据段和未初始化的数据段。
  3. 存放动态生成的数据的堆。
  4. 存放局部变量和函数调用的栈。
  5. 存放在栈底部的环境变量和参数字符串。
  6. 把文件区间映射到虚拟地址空间的内存映射区域。

​ 进程是正在执行的程序,是可执行程序的动态实例,它是一个承担分配系统资源的实体,但操作系统创建进程时,会为进程创建相应的内存空间,这个内存空间称为进程的地址空间,每一个进程的地址空间都是独立的!

虚拟内存

​ 当一个进程有了进程的地址空间,那么进程的地址空间就必须被相应的工具所管理,这个工具被称为内存描述符 mm_struct ,它被定义在include/linux/mm_types.h中,在Linux操作系统中是这样管理进程的地址空间的,如下图所示:

mm_struct

​ mm_struct 的部分定义如下:

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
struct mm_struct {
struct vm_area_struct *mmap; /* 指向线性区对象的链表头 */
struct rb_root mm_rb; /* 指向线性区对象的红-黑树 */

/* 在进程地址空间中搜索有效线性地址区间的方法 */
unsigned long (*get_unmapped_area) (struct file *filp,
unsigned long addr, unsigned long len,
unsigned long pgoff, unsigned long flags);

unsigned long mmap_base; /* base of mmap area */
unsigned long mmap_legacy_base; /* base of mmap area in bottom-up allocations */

/* Base adresses for compatible mmap() */
unsigned long mmap_compat_base;
unsigned long mmap_compat_legacy_base;

unsigned long task_size; /* size of task vm space 用户虚拟地址空间的长度 */
unsigned long highest_vm_end; /* highest vma end address */
pgd_t * pgd; /* 指向进程页表起始地址 */

atomic_t mm_users; /* 共享同一个用户虚拟地址空间的进程的数量 */

/*
内存描述符的主使用计数器,每次mm_count递减时,内核都要检查它是否变为0,如果是,就要解除这个内 存描述符,因为不再有用户使用它
*/
atomic_t mm_count;

atomic_long_t nr_ptes; /* 进程中用于pte的页数 */
atomic_long_t nr_pmds; /* 进程中用于pmd的页数 */

int map_count; /* number of VMAs */

spinlock_t page_table_lock; /* 线性区的自旋锁和页表的自旋锁 */

struct list_head mmlist;/* 存放链表相邻元素的地址,第一个元素是init_mm的mm_list字段 */

unsigned long hiwater_rss; /* 进程所拥有的最大页框数 */
unsigned long hiwater_vm; /* 进程线性区中的最大页数 */
unsigned long total_vm; /* 进程地址空间的大小 */
unsigned long locked_vm; /* "锁住"而不能换出的页的个数 */
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; /* 用户态堆栈中的页数 */
unsigned long def_flags; /* 线性区默认的访问标志 */
/* 可执行代码开始地址,结束地址,已初始化数据的开始地址,结束地址 */
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;
/* 开始执行ELF程序时会使用到saved_auxv参数 */
unsigned long saved_auxv[AT_VECTOR_SIZE]; /* for /proc/PID/auxv */
};

​ 进程描述符(task_struct)中和内存描述符(mm_struct)相关的成员:

1
2
3
4
struct mm_struct *mm;  /* 进程的mm指向一个内存描述符
内核线程没有用户虚拟地址空间,所以mm是空指针 */
struct mm_struct *active_mm; /* 进程的active_mm 和 mm总是指向同一个内存描述
内核线程的active_mm在没有运行时是空指针,在运行时指向从上一个进程借用的内存描述符 */