C Tutorials-02-内存管理(深入理解 malloc)

@tldr:

  • 如果申请空间< 128K,则尝试上推 brk 指针,(这种管理空闲内存的方式是典型的“bump-point 和 free-list” 结合的方式,JVM管理 TLAB 的方式类似)当然也不一定每次都需要上推 brk 指针,还会在 free-list 里做 first-fit 查找空闲块
  • 如果> 128K, 则直接在 mmap区(内存映射区)分配

深入理解malloc

void *ptr = malloc(N) 之后发生了什么?

从操作系统角度来看,进程分配内存有两种方式,分别由两个系统调用完成:brk 和 mmap(不考虑共享内存)。

1、do_brk() 系统调用通过调整堆中的 brk 指针大小来增加或者回收堆内存
2、mmap 是在进程的虚拟地址空间中(堆和栈中间,称为文件映射区域的地方)找一块空闲的虚拟内存。

内核中使用 start_brk 标识的起始位置,brk 标识堆当前的结束位置。当堆申请新的内存空间时,只需要将 brk 指针增加对应的大小,回收地址时减少对应的大小即可。 // 参考 struct mm_struct 结构体的解析

这两种方式分配的都是虚拟内存,没有分配物理内存。在第一次访问已分配的虚拟地址空间的时候,发生缺页中断,操作系统负责分配物理内存,然后建立虚拟内存和物理内存之间的映射关系。

▷ 情况 1:malloc 小于128k 的内存,会调用 do_brk() 进行分配内存,将 brk 指针往高地址推,给进程分配了 N bytes 的线性地址区域(VM) ,此时系统并没有随即分配物理内存, 进程也没有占用 N bytes 的物理内存. // 这也表明了, 你时常在使用 top 的时候 VIRT 值增大, 而 RES 值却不变的原因.

但是 do_brk() 也不一定每次都上推 brk 指针,如果在 start_brk - brk 地址之间有未使用的内存块(这些内存块可能是被 free 过了,但是页框没有被回收,参考 free),会优先使用这部分内存块;

当第一次通过指针使用此内存页, 在 RAM 中找不到与之相对应的 页框(page frame,也即物理内存). 发生 缺页异常, 系统灵敏地捕获这一异常, 进入缺页异常处理阶段:接下来, 系统会分配一个页框映射给它, 我们把这种情况(被访问的页还没有被放在任何一个页框中, 内核分配一新的页框并适当初始化来满足调用请求) 称为 Demand Paging.

页框:对应4K大小的物理页,详见 [[../21.Operating-System/01.RAM.1.内存寻址]] 「页帧」

过了很长一段时间, 通过 *ptr 再次引用内存第一页. 若系统在 RAM 找不到它映射的页框(可能交换至磁盘了). 也会发生缺页异常, 进入缺页异常处理:系统会分配页框(page frame), 找到备份在磁盘的那“页”, 并将它换入内存(其实因为换入操作比较昂贵, 所以预换入多页. 这也表明某些文档说:”vmstat 某时出现不少 si 并不能意味着物理内存不足”).

通过指针使用到了 N bytes 的第二页. 参见第一次访问 N bytes 第一页, “Demand Paging”

凡是类似这种会迫使进程去睡眠(很可能是由于当前磁盘数据填充至页框所花的时间), 阻塞当前进程的缺页异常处理称为主缺页(major falut), 也称为 大缺页. 相反, 不会阻塞进程的缺页, 称为次缺页(minor fault).

▷ 情况 2:malloc 函数分配内存大于128K(可由 M_MMAP_THRESHOLD 选项调节),那就不是去推 brk 指针了,而是利用 mmap 系统调用,从堆和栈的中间分配一块虚拟内存(也即内存映射区)。内存映射区即 Mapping Area : [[../21.Operating-System/APUE.03b.进程的虚拟内存管理]]

▷ free 的过程:

  • 如果是在内存映射区分配的内存(大于 128K),指针对应的虚拟内存和物理内存一起释放;
  • 如果是在堆区分配的内存(小于 128K),这时情况会变得不同,如果用 malloc 依次申请了 a、b、c 三块内存(这时候 brk 指针指向的是 c 的末端),如果 free(b),这时候 b 的虚拟内存和物理内存都没有被释放,因为 c 内存块还在使用中所以无法直接移动 brk 指针,这样就产生了内存碎片

    如果这时又新 malloc 了一块内存,b 还可以重复使用,如果 b 比这次 malloc 分配的区域小,那么原来 b 区域要分裂为两块,此时还是有碎片。

    在 free 的时候,发现最大地址空闲内存超过128K(可由 M_TRIM_THRESHOLD 选项调节),执行内存紧缩操作(trim)

因为 Heap 区都是小于128k 的细碎内存块, 上面的链表可以防止反复申请/释放带来的内存碎片, 但 mmap 对应的区域都是大块(大于128K)的内存, 所以不用采用上面的机制.

▷ 主缺页异常处理过程示意图,参见 Handling Page Fault, 当系统进入缺页异常流程后(do_page_fault),最终会调用 alloc_pages 申请真正的物理内存页 @link 伙伴系统

@ref: