Linux 虚拟内存为每个进程提供受保护的虚拟地址空间,同时允许内核共享文件、过量提交地址空间、回收物理页并将内存放置在 NUMA 节点上。讨论假定存在内存管理单元(MMU);没有 MMU 的系统使用 Linux 独立的 nommu 实现。
转换和 TLB #
每次加载、存储和指令获取都使用虚拟地址。硬件页表将地址转换为物理页帧加偏移量。 Linux 用五个层级表示层次结构:
PGD— 页全局目录P4D— 页第四级目录PUD— 页上级目录PMD— 页中间目录PTE— 页表项
硬件层级较少的架构会将未使用的 Linux 层级折叠到相邻层级。CPU 会在转换后备缓冲器(TLB)中缓存转换,因为每次访问都遍历多级页表的成本很高。更改映射需要使每个可能执行受影响地址空间的 CPU 上的过期转换失效。
基础页大小取决于架构和内核配置。较高页表层级的映射可以覆盖更大的页面,从而降低页表内存和 TLB 压力。Linux 提供两种独立的大页机制:
- HugeTLB 预留或管理显式大页,通常通过
hugetlbfs或MAP_HUGETLB使用;容量和分配失败属于其运维契约。 - 透明大页(THP) 根据内核策略提升符合条件的映射,不提供单独的应用映射 ABI。提升和压缩可能引入延迟,因此工作负载应进行测量,而不是假定一定有收益。
地址空间和映射 #
每个用户空间地址空间由 mm_struct 描述。内核线程通常没有独立的用户空间 mm;架构特定的执行会在需要时使用活动地址空间上下文。内核用 vm_area_struct(VMA)表示连续的虚拟内存区域。VMA 记录半开地址范围、权限、标志、后备对象和缺页操作。现代内核使用 maple tree 索引 VMA。
execve(2) 为可执行文件和共享库段创建映射。其他常见 VMA 包括栈、堆、匿名
mmap(2) 映射、文件支持的映射、共享内存和 vDSO。mmap(2) 预留虚拟范围及其访问策略;映射时不需要分配或读取所有物理页。
mprotect(2) 更改 VMA 权限,munmap(2) 移除映射。权限检查同时需要合适的 VMA 和合适的页表项。因此,保护页、不可执行映射和地址空间布局随机化都是强制机制,而不仅是记账。
缺页、按需分页和写时复制 #
由于转换缺失或尝试访问违反当前条目权限,缺页会将控制权转移给内核。缺页处理程序找到 VMA 并决定访问是否合法:
- 次要缺页无需存储 I/O 即可解决,例如映射驻留的页缓存页或安装新分配的零填充页。
- 主要缺页需要先进行存储 I/O,执行才能继续。
- 无效地址或不允许的访问通常生成
SIGSEGV;某些对象特定的失败(例如无法访问文件支持的页面)会生成SIGBUS。
匿名内存最初可以映射共享的只读零页。写入会触发缺页并获得私有物理页。fork(2) 之后,父子进程通常通过只读映射共享页面。第一个写入者触发写时复制缺页,获得私有页,并保持另一个映射不变。写时复制使面向 exec() 的子进程执行 fork() 成本较低,但大型 fork 之后的写入可能很昂贵。
文件支持的映射和 read(2) 使用页缓存。干净的缓存页可以回收,因为内核可以重新读取文件。脏共享映射必须先写回,之后其帧才能复用。私有文件映射对写入使用写时复制,因此其修改页是匿名页,而不是对后备文件的修改。
物理分配和 NUMA #
Linux 使用页描述符描述物理帧,并按 NUMA 节点和 zone 划分内存。zone 编码硬件限制,例如 DMA 可用内存,或适用的 32 位系统中未由内核永久映射的内存。NUMA 节点拥有 zone、空闲列表、统计信息和回收状态,因为访问延迟取决于请求 CPU 和内存组。
伙伴分配器提供物理连续的页块。零阶是一个基础页;第 n 阶是 2^n 个连续页面。SLAB 系列分配器(通常是 SLUB)从伙伴分配器获取页面,并将其划分为固定大小的内核对象缓存。
kmalloc() 为分配器支持的大小返回物理连续的内核内存。vmalloc() 从不连续的物理页面构造虚拟连续的内核范围,因此其分配成本和设备 DMA 属性不同。应根据设备寻址要求选择驱动和 DMA API,而不是相互替代。
回收、回写、交换和 OOM #
物理内存压力会导致回收。内核可以:
- 丢弃干净的页缓存页;
- 写回脏文件页,然后丢弃它们;
- 在交换可用时交换符合条件的匿名页;以及
- 调用 shrinker 回收文件系统元数据等可回收缓存。
每个 zone 都有水位线。跨越低水位线会唤醒 kswapd 进行后台回收。更严重压力下的分配可以进入直接回收,并阻塞到有足够页面可用为止。回收策略和多代 LRU 的可用性取决于内核配置和版本;应检查目标内核,而不是假定使用某个 LRU 实现。
固定页面、大量内核分配、不可回收的设备缓冲区以及策略保护的页面都会降低可回收容量。碎片也可能在空闲内存充足时阻止高阶分配。压缩会迁移可移动页面,形成连续的空闲块;kcompactd 可以在后台运行它,分配任务也可以同步执行压缩。THP 和 DMA 是此类块的常见使用者。
当回收和压缩无法满足分配时,内核会应用内存不足策略。OOM 处理可能是全局的、限定在内存 cgroup 中的,或受失败分配上下文约束。OOM kill 是恢复路径,并不证明被选中的进程就是内存增长的原始来源。在更改 oom_score_adj 之前,应调查 cgroup 限制、内存压力、分配模式和固定内存。
观察内存行为 #
应结合使用多个接口,因为每个接口报告不同层级:
cat /proc/meminfo
cat /proc/vmstat
cat /proc/pressure/memory
cat /proc/$$/maps
cat /proc/$$/smaps_rollup
cat /proc/$$/numa_maps
vmstat 1/proc/PID/maps 列出 VMA。smaps 增加每个 VMA 的 RSS、比例集大小、匿名、文件、交换和页大小记账;smaps_rollup 汇总这些信息。numa_maps 显示局部性和策略。访问 /proc/PID/pagemap 受到限制,因为物理映射信息可能削弱安全边界。
对于缺页率,进程记账通过 getrusage(2) 暴露 ru_minflt 和 ru_majflt,而
/proc/PID/stat 包含每进程的次要和主要缺页计数器。/proc/vmstat 提供系统范围的
pgfault 和 pgmajfault 计数器。应结合延迟和 I/O 指标解释一段时间内的差值:主要缺页计数标识需要 I/O 的缺页路径,但不能独立证明存储设备导致了应用延迟。
/proc/pressure/memory 中的压力停顿信息(PSI)报告任务因内存压力而停顿的时间。将 PSI 与 vmstat、cgroup memory.events 和应用延迟结合,可以区分无害的缓存周转与持续回收或 cgroup 限制压力。
评论
在 GitHub 上阅读或评论(发表评论需要登录)