diff --git a/notes/计算机操作系统 - 内存管理.md b/notes/计算机操作系统 - 内存管理.md index 0f04c33b..4d12f5d1 100644 --- a/notes/计算机操作系统 - 内存管理.md +++ b/notes/计算机操作系统 - 内存管理.md @@ -17,13 +17,20 @@ * [A1 页面置换模拟](#a1-页面置换模拟) * [A2 缺页次数](#a2-缺页次数) * [A3 进程的地址空间布局](#a3-进程的地址空间布局) + * [A4 页表占用的内存](#a4-页表占用的内存) + * [A5 申请超过物理内存](#a5-申请超过物理内存) * [参考资料](#参考资料) ## 虚拟内存 -虚拟内存的目的是为了让物理内存扩充成更大的逻辑内存,从而让程序获得更多的可用内存。此外,每个进程都有独立的地址空间,一个进程无法直接访问另一个进程的内存,操作系统还可以按页设置读写和执行权限。 +虚拟内存指的是让每个进程看到一个独立、连续的地址空间,程序中使用的都是虚拟地址,访问内存时由硬件和操作系统翻译成物理地址。它主要解决了以下问题: + +- 扩充内存:只有用到的部分才需要放在物理内存中,暂时不用的可以换出到磁盘,程序可以比物理内存大; +- 隔离和保护:每个进程都有独立的地址空间,一个进程无法直接访问另一个进程的内存,操作系统还可以按页设置读写和执行权限; +- 简化内存管理:每个程序都按相同的地址布局链接和加载,不用关心物理内存中哪些位置空闲,分到的物理内存也不必连续; +- 共享:不同进程的虚拟页可以映射到同一块物理内存,例如共享库的代码、fork 之后写时复制的页。 为了更好的管理内存,操作系统将内存抽象成地址空间。每个程序拥有自己的地址空间,这个地址空间被分割成多个块,每一块称为一页。这些页被映射到物理内存,但不需要映射到连续的物理内存,也不需要所有页都必须在物理内存中。当程序引用到不在物理内存中的页时,硬件触发缺页中断,由操作系统将缺失的页装入物理内存,再重新执行失败的指令。 @@ -45,6 +52,8 @@ 64 位系统的地址空间很大,如果用一张页表覆盖全部地址,页表本身就会非常大,因此使用多级页表,只为用到的地址分配下一级页表。例如 x86-64 通常使用 4 级页表,48 位虚拟地址分为 4 个 9 位的页表索引和 12 位的页内偏移。 +48 位地址、4 KB 的页共有 2^36 个页,每个页表项 8 字节,一张覆盖全部地址的单级页表要 512 GB,而且每个进程一张。4 级页表中每张表正好占一页(512 项 × 8 字节),某一级的表项为空,它下面的整棵子树都不用分配。例如一个刚启动的小程序,页表只占 44 KB;mmap 256 MB 并逐页写入后,新增的 65536 个页表项占 512 KB,页表共 556 KB(Ubuntu 24.04 实测,见[附录 A4](#a4-页表占用的内存))。代价是 TLB 未命中时要依次访问 4 级页表,所以 TLB 的命中率对性能很重要,使用 2 MB 的大页时,同样多的 TLB 表项可以覆盖更多内存。 + MMU 查页表时发现页不在内存中,或者这次访问违反了页的权限(例如写只读页),就会触发缺页异常(Page Fault),由操作系统处理: 1. 保存现场,取得引发缺页的虚拟地址; @@ -57,6 +66,8 @@ MMU 查页表时发现页不在内存中,或者这次访问违反了页的权 例如用 mmap 申请 256 MB 的匿名内存后,进程的常驻内存(RSS)几乎不变;逐页写入一遍,触发 65536 次次要缺页(256 MB / 4 KB),RSS 增加到 257 MB,再写一遍就不再缺页。fork 出的子进程把这些页再写一遍,又触发 65536 次缺页,每次复制一页,父进程中的值不受影响(Ubuntu 24.04 实测,见[附录 A2](#a2-缺页次数))。这也说明申请内存只是分配了虚拟地址,真正用到时才占用物理内存。 +因此申请比物理内存还大的内存也可能成功,结果取决于 vm.overcommit_memory:默认值 0 用启发式规则拒绝明显过大的申请,即一次申请超过物理内存与交换区的总和;1 总是允许;2 不允许所有进程申请的总量超过交换区加上一定比例(默认 50%)的物理内存。申请成功不代表能用完,例如在内存上限为 512 MB 的容器中(overcommit_memory 为 1),malloc 1024 GB 也会成功,但逐 MB 写入 2 GB 时,写到 384 MB 之后不久进程就被内核的 OOM Killer 杀死,退出码 137(实测,见[附录 A5](#a5-申请超过物理内存))。 + ## 页面置换算法 在程序运行过程中,如果要访问的页面不在内存中,就发生缺页中断从而将该页调入内存中。此时如果内存已无空闲空间,系统必须从内存中调出一个页面到磁盘对换区中来腾出空间。 @@ -234,6 +245,52 @@ $ ulimit -s 再运行一次,各区域的起始地址都变了(例如 main 为 0xaaaabc070a18、local 为 0xffffd6a9311c),上下顺序不变。 +### A4 页表占用的内存 + +同一个容器(aarch64 内核同样使用 4 KB 的页和 4 级页表),从 /proc/self/status 读 VmSize(虚拟内存大小)、VmRSS 和 VmPTE(页表占用的内存): + +```c +show("启动后"); +char *p = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); // 256 MB +madvise(p, size, MADV_NOHUGEPAGE); +show("mmap 256 MB 后"); +for (size_t i = 0; i < size; i += page) p[i] = 1; +show("逐页写入后"); +``` + +```text +启动后 VmSize: 2252 kB +启动后 VmRSS: 1056 kB +启动后 VmPTE: 44 kB +mmap 256 MB 后 VmSize: 264396 kB +mmap 256 MB 后 VmRSS: 1184 kB +mmap 256 MB 后 VmPTE: 44 kB +逐页写入后 VmSize: 264396 kB +逐页写入后 VmRSS: 263328 kB +逐页写入后 VmPTE: 556 kB +``` + +### A5 申请超过物理内存 + +同一个容器,启动时限制内存为 512 MB 且不使用交换区(docker run --memory=512m --memory-swap=512m)。Docker Desktop 的虚拟机中 vm.overcommit_memory 为 1,与 Ubuntu 默认的 0 不同,这个设置不能在容器中修改,因此没有实测模式 0。程序依次 malloc 4 GB、16 GB 和 1024 GB,再 malloc 2 GB 并逐 MB 写入,每 128 MB 打印一次 RSS: + +```text +vm.overcommit_memory = 1 + total used free shared buff/cache available +Mem: 7837 545 6490 0 965 7292 +Swap: 1023 0 1023 +malloc( 4 GB) 成功,RSS 3 MB +malloc( 16 GB) 成功,RSS 3 MB +malloc(1024 GB) 成功,RSS 3 MB +已写入 128 MB,RSS 131 MB +已写入 256 MB,RSS 259 MB +已写入 384 MB,RSS 387 MB +Killed +退出码 137 +``` + +模式 0 的规则见 Linux 6.10 的 mm/util.c:__vm_enough_memory 中 OVERCOMMIT_GUESS 分支只在一次申请的页数超过 totalram_pages() + total_swap_pages 时返回失败。 + ## 参考资料 - Tanenbaum A S, Bos H. Modern Operating Systems [M]. Pearson, 2014. @@ -242,3 +299,5 @@ $ ulimit -s - [getrusage(2) — Linux manual page](https://man7.org/linux/man-pages/man2/getrusage.2.html) - [proc_pid_maps(5) — Linux manual page](https://man7.org/linux/man-pages/man5/proc_pid_maps.5.html) - [mallopt(3) — Linux manual page](https://man7.org/linux/man-pages/man3/mallopt.3.html) +- [Overcommit Accounting — The Linux Kernel documentation](https://docs.kernel.org/mm/overcommit-accounting.html) +- [proc_pid_status(5) — Linux manual page](https://man7.org/linux/man-pages/man5/proc_pid_status.5.html)