B09 —— 内存不够用时会发生什么:kswapd 和 Page Frame Reclaim
金句:当内存开始吃紧,系统会悄悄地把不常用的东西搬到磁盘——这就是 swapping。
内存压力的来源
Linux 系统总是在内存接近满时就开始”整理”——把不常用的页面(page)换出到磁盘,为新的分配腾出空间。
什么时候会触发内存回收:
1. 水位线(Watermark)检测:
- 内存有三个水位:pages_high, pages_min, pages_low
- 当可用内存 < pages_low,开始回收
- 当 < pages_min,强制回收(Direct Reclaim)
2. 分配请求失败(GFP_ATOMIC):
- 分配请求在 fast path 失败
- 触发 direct reclaim(同步回收)
- 进程会阻塞等待
3. 周期性回收(kswapd):
- kswapd 是内核线程,持续运行
- 当内存低于 pages_high 时开始
- 异步回收,慢慢把内存降到 pages_high 以上
watermark 定义(/proc/sys/vm/):
watermark_scale_factor:默认 10(即 pages_high = 最低 + 总内存/10)
min_free_kbytes:最小保留内存(用于紧急情况)kswapd:内存回收的后台守护进程
kswapd 的工作方式:
kswapd 是 Linux 的页面回收线程:
- 每个 NUMA 节点一个 kswapd(kswapd0, kswapd1, ...)
- 处于休眠状态(TASK_INTERRUPTIBLE)
- 被唤醒的条件:可用内存 < pages_high
kswapd 的回收算法:
while (需要回收) {
// 1. 尝试回收冷页面(最近未被访问)
// 遍历 LRU(Least Recently Used)链表
// 页面引用计数 = 1(只有 page cache 持有)→ 可以回收
// 2. 如果不够,扫描更老的页面
// pageout():写回脏页(到磁盘),释放干净页
// 3. 如果仍然不够,触发 OOM Killer
// 选择最"坏"的进程杀掉,释放其页面
}
回收的优先级(从低到高):
1. anonymous cold pages(匿名冷页:进程的堆/栈)
2. file-backed cold pages(文件冷页:mmap 的文件)
3. file-backed dirty pages(脏文件页:需要先写回磁盘)
4. anonymous hot pages(匿名热页:不太容易回收)LRU(Least Recently Used)链表
Linux 用 LRU 链表来追踪页面的访问热度:
struct zone {
// 活动页面链表(Active)
struct list_head active_list; // 最近被访问的页面
unsigned long nr_active; // 活动页面数量
// 非活动页面链表(Inactive)
struct list_head inactive_list; // 最近未被访问的页面
unsigned long nr_inactive; // 非活动页面数量
// 链表按类型分类:
// file-backed vs anonymous
// referenced vs unreferenced
};
LRU 链表的工作流程:
1. 页面访问时:
page_referenced() → 如果被访问,加入/保持在 active_list
如果未访问,在 active_list 中时间够长,移到 inactive_list
2. 回收时(kswapd):
优先从 inactive_list 取页面(更容易回收)
检查 referenced 位 → 如果没人引用 → 回收
如果有引用(被进程持有)→ 不能回收
3. anonymous vs file-backed 的优先级:
anonymous:需要 swapout(写磁盘),成本高
file-backed:干净页直接回收,脏页写回后回收
→ file-backed 通常比 anonymous 先回收Page Frame Reclaim Algorithm(PFRA)
PFRA(页面框回收算法)的详细流程:
Step 1:检查内存水位
if (free_pages > pages_high) {
// 内存充足,休眠
schedule();
return;
}
Step 2:开始扫描(scan)
for (each page in LRU) {
// 检查 page 的类型和状态
if (PageAnon(page) && !PageSwapBacked(page)) {
// anonymous page,需要 swapout
// 写页到 swap 设备
// 释放物理页
}
else if (PageDirty(page)) {
// 脏文件页,需要写回
// 调用 filemap_write() 写盘
// 写完后释放
}
else {
// 干净的文件页
// 直接释放回 buddy system
}
if (sufficient_memory_reclaimed()) break;
}shrink_page_list() 的实现:
int shrink_page_list(struct list_head *page_list, ...)
{
LIST_HEAD(ret_pages);
int nr_reclaimed = 0;
list_for_each_entry(page, page_list, lru) {
// 尝试锁定页面(避免并发访问)
if (!trylock_page(page))
continue;
// 检查引用计数
// page_count(page) = 1 表示只有 page cache 持有
if (page_count(page) == 1 && !PageDirty(page)) {
// 干净且无引用 → 直接释放
unlock_page(page);
free_page(page);
nr_reclaimed++;
continue;
}
// 如果是 anonymous,需要写 swap
if (PageAnon(page)) {
if (!PageSwapBacked(page)) {
// 首次回收 anonymous page
// 添加到 swap
add_to_swap(page);
}
// 写页到 swap
swap_writepage(page);
}
// 如果是脏文件页
if (PageDirty(page)) {
// 写回磁盘
writepage(page);
}
unlock_page(page);
}
return nr_reclaimed;
}swappiness:匿名页 vs 文件页的回收偏好
swappiness 参数(/proc/sys/vm/swappiness):
swappiness = 60(默认值,0~100)
含义:
- 100:优先回收 anonymous pages(匿名页优先)
- 0:尽量不回收 anonymous pages(只回收文件页)
- 60:60% 的扫描时间用于 anonymous,40% 用于 file-backed
实际效果:
swappiness 高 → anonymous 更容易被 swapout
swappiness 低 → anonymous 保留更久,文件页先被回收
不同场景的选择:
- 服务器(文件缓存为主):swappiness = 10~30
- 桌面(匿名内存多):swappiness = 60~100
- 数据库(大量 anonymous heap):swappiness = 1 或 0(避免 swap)
swappiness 调优:
$ sysctl vm.swappiness=10
或者在 /etc/sysctl.conf 中:
vm.swappiness = 10OOM Killer:当回收不够时
当 kswapd 和 direct reclaim 都无法释放足够的内存时,
Linux 会调用 OOM Killer(Out of Memory Killer):
触发条件:
- 可用内存 < pages_min(强制回收水位)
- direct reclaim 失败
- 分配请求无法满足(GFP_NOFS/GFP_NOIO 等)
选择进程的策略(oom_score):
1. 内存消耗(RSS):
- 内存消耗越多,越容易被选中杀掉
2. 存活时间(oom_score_adj):
- 动态调整:recently oom 的进程降低 score
- /proc/PID/oom_score_adj 可以手动调整(-1000 到 1000)
- -1000 = 完全免疫,1000 = 更容易被杀
3. 其他因素:
- 进程优先级(低优先级更容易)
- 进程是 root 还是普通用户
- 进程是否是系统服务(systemd, kernel threads)
OOM Killer 的执行:
- 选择最"坏"的进程
- 发送 SIGKILL(无条件杀掉)
- /var/log/messages 记录(dmesg | grep -i "out of memory")观察 OOM Killer:
$ dmesg | grep -i "out of memory"
[ 123.456] Out of memory: Killed process 1234 (my-program)
total-vm:2048576kB, anon-rss:1048576kB, file-rss:0kB
$ cat /proc/PID/status | grep -i vm
VmPeak: 1048576 kB
VmSize: 1048576 kB
VmRSS: 512000 kB
VmSwap: 256000 kB总结
- 内存回收触发:水位线检测(pages_high/low/min),分配失败触发 direct reclaim
- kswapd:内核线程,异步回收,当内存 < pages_high 时开始工作
- LRU 链表:active/inactive,按匿名/文件、干净/脏、引用/非引用分类
- PFRA 流程:检查页面类型 → 匿名写 swap,脏文件写盘,干净文件直接释放
- swappiness:0~100,优先 anonymous(100)还是 file-backed(0),服务器调低
- OOM Killer:kswapd 不够时杀掉最”坏”进程(最高 oom_score),发送 SIGKILL
下篇预告(B10):fork 进程时,页面是怎么”复制”的?Copy-On-Write 机制——为什么 fork 很快,为什么修改内存才会真正复制。
关注公众号「AI不着急」,回复”资料”获取内存学习路线图。
评论