Linux 内存是如何被分配和回收的?

9次阅读

malloc仅分配虚拟内存,物理页在首次读写触发缺页异常时才由伙伴系统分配;若空闲页不足,则在page fault阶段立即启动内存回收。

Linux 内存是如何被分配和回收的?

内存分配:从 malloc 到物理页框,中间发生了什么

linux 进程调用 malloc 申请内存时,实际只在虚拟地址空间划出一块区域,**并不立即分配物理内存**——这是 lazy allocation(惰性分配)的核心。真正触发物理页分配的,是第一次读写该地址时引发的缺页异常(page fault),内核此时才通过伙伴系统(buddy system)从对应 zone 中找一个空闲页框,建立页表映射。

关键点在于:用户态看到的是虚拟地址,而内核按需把物理页“贴”上去。如果此时空闲页不足,就会立刻进入回收流程——不是等 malloc 返回失败,而是卡在 page fault 阶段。

  • 对象(slab 或 slub 分配器,复用已缓存的对象,避免频繁拆分/合并页
  • 大块内存(如 mmap 分配)可能直接从伙伴系统拿整页或多页,不经过 slab
  • 匿名页(堆、栈)没有磁盘后备,回收时必须写入 swap;文件页(page cache)若干净(未修改),可直接丢弃;若脏,则需先回写磁盘

内存回收触发:三个水位线(WMARK_MIN/WMARK_LOW/WMARK_HIGH)怎么管事

内核为每个内存 zone 维护三条水位线,它们不是固定值,而是随系统总内存动态计算:WMARK_MIN 是底线,低于它就触发同步的直接回收(direct reclaim),当前进程会被卡住;WMARK_LOW 是警戒线,唤醒 kswapd 后台线程开始异步回收;WMARK_HIGH 是目标线,kswapd 回收到这里就停手。

常见误解是“swap 开了就没事”,其实只要空闲页跌破 WMARK_MIN,哪怕 swap 还有空间,也会先卡住进程做 direct reclaim——因为换入换出太慢,内核宁可阻塞也要抢出几页。

  • 可通过 cat /proc/zoneinfo | grep -A 10 "Node 0, zone.*Normal" 查看各 zone 实时水位
  • 调整 /proc/sys/vm/min_free_kbytes 可间接改变所有水位线(增大它会抬高 WMARK_MIN,让回收更早启动,但会减少可用内存)
  • 不要盲目调高 min_free_kbytes:在 64GB 内存机器上设成 2GB,等于凭空吃掉 3% 的内存,对容器密集场景尤其不友好

回收干了啥:LRU 链表 + 页面分类 + 不同策略

回收不是随机扫内存,而是按页面类型和活跃度分层处理:每个 zone 有两组 LRU 链表(Active/Inactive),再按页面性质拆成四类:anon_inactive(不活跃匿名页)、anon_activefile_inactivefile_active。回收优先从 anon_inactivefile_inactive 里取页。

区别对待是因为:文件页可丢弃或回写,成本低;匿名页只能 swap 或压缩(如果启用了 zswap)。而 LRU 并非纯时间排序——内核会根据页面被访问频率(通过 pgrefillpgdeactivate 等计数器)动态升降页面在链表中的位置。

  • 脏文件页(file_dirty)必须先回写到磁盘才能回收,这会拖慢整个回收周期,也是 IO 尖峰的常见源头
  • zswap(内存压缩)默认不启用,需手动加载模块并配置 zswap.enabled=1;它把 anon_inactive 压缩后存在内存中,避免写 swap,但会增加 CPU 开销
  • 回收过程会跳过被 mlock() 锁住的页、内核线程页、以及某些驱动独占的 DMA 页——这些是回收黑名单

OOM 杀手不是第一道防线,而是最后兜底

当 direct reclaim 跑完一轮仍凑不够页,内核就判定为 Out-of-Memory,并启动 oom_killer。它不看 CPU 占用,只算每个进程实际占用的物理内存页数(RSS),再叠加 oom_score_adj 调整分——数值越高的进程越容易被杀。

注意:OOM 触发前,系统往往已出现明显症状:top%wa(IO wait)飙升、dmesg 打印 “Out of memory: Kill process ...”、/proc/meminfoInactive(anon) 持续接近零。这时再查进程 RSS 已经晚了,得倒推是谁长期霸占 anon_active 链表不释放。

  • ps aux --sort=-%mem | head -10 快速定位内存大户,但要注意 RSS 包含共享库,未必是真实“罪魁”
  • 真正有效的是 cat /proc/[pid]/smaps | awk '/^Pss:/ {sum+=$2} END {print sum}',它统计 PSS(Proportional Set Size),按共享比例折算后更准
  • 容器环境要特别注意:cgroup v1 的 memory limit 不触发 OOM Killer,而是直接 throttle 进程,表现为卡死无响应,而非被 kill

水位线、LRU 链表、回收路径、OOM 触发条件——这几层环环相扣,改错一个参数(比如只调 swappiness)解决不了根本问题;真正要调的,往往是应用自身的内存使用模式,比如避免长生命周期缓存无淘汰策略。

text=ZqhQzanResources