page_reclaim

简述

能够回收的内存:

  • 进程的代码段、数据段、栈以及堆

  • 通过系统调用 mmap() 的各种匿名页/文件页

  • 内核的各种cache,如slab cache, inode cache, dentry cache, page cahce等

不能够回收的内存:

  • 内核代码段、数据段、内核调用 kmalloc()/vmalloc() 申请的内存、内核线程占用的内存

  • 应用程序主动调用mlock锁定的页

为什么不能够回收这部分内存? 不是技术上实现不了,而是这样做得不偿失,因为频繁换入换出和缺页异常处理非常影响性能

回收内存方式

Linux Kernel 有三个内存水位线,分别是 high、low、min

  • 当系统内存低于 low 水位线时,唤醒 kswapd 内核线程进行周期性回收内存。 kswapd 是一个内核线程,在系统初始化过程中调用 kswapd_init() 来创建

  • 当系统内存低于 min 水位线时,进行紧急回收内存,如 __alloc_pages_direct_reclaim()

  • 应用层手动触动回收内存,如 /proc/sys/vm/drop_caches

当进行内存回收时,需要回收到什么时候才停止?

内存水位线达到 high 并且能够分配指定 order 阶的页时,内存回收结束,kswapd 重新进入睡眠状态。

三者的函数调用关系如下:

详细解析

kswapd_init() 为每一个 node 创建一个 kswapd 内核线程

kswapd() 从指定的 node 中回收内存

try_to_free_pages() 从指定的 zonelist 中依次回收不同 node (即 zone->zone_pgdat) 中的内存

drop_caches_sysctl_handler() 回收各种 cache,比如 slab cache

shrink_node() 从指定的 node 中依次回收不同 mem_cgroup 中的 LRU 页 与 slab cache

其中,mem_cgroup_iter() 获得不同的 mem_cgroup, mem_cgroup_lruvec() 获得指定 mem_cgroup 的 lruvec

shrink_lruvec() 依次回收各种类型的 LRU 页

shrink_list() 回收 LRU active/inactive 链表中的页

如果是LRU active链表,从 LRU active 链表中获得一定数量的页,并且从 LRU active 链表中 删除,然后依次获得每一页,如果是可执行的文件页,将此页加入 active 链表中,否则, 清除 active 标志并且加入 inactive 链表中。最后将 active/inactive 链表都加入对应的 LRU active/inactive 链表中

如果是LRU inactive链表,从 LRU inactive 链表中获得一定数量的页,并且从 LRU inactive 链表中删除,然后依次获得每一页

  • 如果有 referenced,将页设置成 active 属性,将 active 链表(有 active 属性的页) 加入对应的 LRU active 链表中,否则,

  • 如果支持 demote 功能,将此页进行 demote 操作,否则,

  • 如果是匿名页,有 swapbacked,但是不存在 swapcache,为此匿名页分配 swap space, 并且将此匿名页加入 swap cache中,同时将 swp_entry_t 保存在 page.private 中

  • 来到此处的页都属于 page cache 或 swap cache

通过反向映射 RMAP 取消所有映射(如果是脏页,执行 writeback 动作), 从 address_space.xarray 中删除此页,并且将页回收到 buddy 子系统中。

shrink_slab() 回收 slab cache

如果 enable mem_cgroup 功能以及 mem_cgroup 不是 root_mem_cgroup, 调用 shrink_slab_memcg() 从 shrinker_idr 中查找 shrinker,最后调用 do_shrink_slab()

否则,直接从 shrinker_list 链表中查找 shrinker,最后同时调用 do_shrink_slab()

do_shrink_slab() 调用用户注册的 shrinker 回调函数, shrinker.count_objects 返回能够回收的个数, shrinker.scan_objects 进行回收,返回值为扫描期间释放的个数

比如: 当 mount 某个文件系统类型的 block 时,会创建 super block,即 alloc_super() 注册 一个 shrinker(主要是:shrinker.scan_objects 回调函数)。

如果在系统内存资源不够的情况下,进行内存回收,调用 shrink_slab() 来遍历所有注册的 shrinker,然后调用 shrinker.scan_objects 回调函数 将文件系统目前空闲的 inode cache、 dentry cache 释放回 slab 中。

如果释放后,slab 刚好能有空闲的一整页,就可以将此空闲页释放回 buddy 子系统中

Q: 如何判断文件系统的 inode cache 能够空闲能够释放?

A: inode->i_count 等于 0,并且此 inode 对应的 address_space.xarray 为空时, 即此 inode cache 能够释放的,源码如下:

其中,在回收 page cache 时,调用 __remove_mapping() 会将 page cache 从 addresss_space.xarray 中删除

标志 folio 为 referenced 或 unreferenced 状态, 此 folio 处于 inactive 或 active 链表中

主要有三种情况,如下:

  • inactive,unreferenced -> inactive,referenced

  • inactive,referenced -> active,unreferenced

  • active,unreferenced -> active,referenced

返回 folio referenced 的次数

当 folio 是 Anonymous Page时, 通过反向映射的 rmap_walk_anon() 得到所有映射到 folio 的 Anonymous VMA, 然后调用 folio_referenced_one() 判断对应的每一个 Anonymous VMA 最近有没有被 referenced?如果有,folio referenced 的次数加一; 否则,不作任何处理。

anon page 调用 folio_lock_anon_vma_read() 尝试获得 anon_vma->root->rwsem lock, 如果失败,folio_referenced() return -1

file page 调用 rmap_walk_file() 尝试获得 mapping->i_mmap_rwsem lock, 如果失败,folio_referenced() return -1

零散知识点

  • 如何从将某一页从 LRU inactive 链表移到到 active 链表?

调用 folio_check_references(),如果返回 PAGEREF_ACTIVATE,代表需要将页 从 LRU inactive 链表移到到 active 链表,所以调用 folio_set_active() 将页设置成 active 属性,最后调用 move_pages_to_lru() 才真正将页 从 LRU inactive 链表移到到 active 链表

将页从 LRU active 链表移到到 inactive 链表,同理所得

  • /proc/vmstat

pageoutrun 代表 kswapd 启动次数

allocstall 代表 direct reclaim 启动次数

参考

页面回收的基本概念

Last updated

Was this helpful?