B09 —— 内存不够用时会发生什么:kswapd 和 Page Frame Reclaim

金句:当内存开始吃紧,系统会悄悄地把不常用的东西搬到磁盘——这就是 swapping。


内存压力的来源

Linux 系统总是在内存接近满时就开始”整理”——把不常用的页面(page)换出到磁盘,为新的分配腾出空间。

Bash
什么时候会触发内存回收:

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:内存回收的后台守护进程

Bash
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)链表

Bash
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)

Bash
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;
  }

Bash
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 文件页的回收偏好

Bash
swappiness 参数(/proc/sys/vm/swappiness):

swappiness = 60(默认值,0~100)

含义:
  - 100:优先回收 anonymous pages(匿名页优先)
  - 0:尽量不回收 anonymous pages(只回收文件页)
  - 6060% 的扫描时间用于 anonymous,40% 用于 file-backed

实际效果:
  swappiness 高 → anonymous 更容易被 swapout
  swappiness 低 → anonymous 保留更久,文件页先被回收

不同场景的选择:
  - 服务器(文件缓存为主):swappiness = 10~30
  - 桌面(匿名内存多):swappiness = 60~100
  - 数据库(大量 anonymous heap):swappiness = 10(避免 swap)

swappiness 调优:
  $ sysctl vm.swappiness=10

  或者在 /etc/sysctl.conf 中:
  vm.swappiness = 10

OOM Killer:当回收不够时

Bash
当 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 可以手动调整(-10001000)
   - -1000 = 完全免疫,1000 = 更容易被杀

3. 其他因素:
   - 进程优先级(低优先级更容易)
   - 进程是 root 还是普通用户
   - 进程是否是系统服务(systemd, kernel threads)

OOM Killer 的执行:
  - 选择最"坏"的进程
  - 发送 SIGKILL(无条件杀掉)
  - /var/log/messages 记录(dmesg | grep -i "out of memory"

Bash
观察 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不着急」,回复”资料”获取内存学习路线图。

最后修改: 2024年3月16日

作者

评论

发表评论

您的邮箱地址不会被公开。