功能、流程与原理
Source Code : zswap.c
基础功能
zswap 提供了一种 frontswap 的 Hook,使得在内存不足的时候首先调用 zswap 对即将换出的页面进行压缩,并存储在 zswap 的内存池中。由于页面交换的数据量越大,IO 操作越多会导致系统性能越差,zswap 采用了一种 CPU 时间换 IO 时间的策略,通过压缩的方式减小会被换入换出的数据量。
流程
init_zswap()
在 Linux 内核开启 zswap 功能后,init_module()函数就会执行 init_zswap(),首先初始化 zswap_entry_cache,它为未来快速分配内存页面空间提供数据结构基础。
注册 cpu 热插拔通知链,分别对应 zswap_dstmem_prepare回调和zswap_dstmem_dead回调。
调用__zswap_pool_create_fallback注册 zswap 所使用的内存池, 如果创建成功, 把这个 pool 挂到表头
创建一个工作队列,把句柄留在 shrink_wq 中
调用 frontswap_register_ops,对 zswap 的相关函数进行注册(包括 store、load、inval-page、inval-area等函数)
frontswap_register_ops : zswap 在某个版本后,接入了 frontswap API,通过这个函数注册自己的页面压缩、存储、恢复等操作。linux 默认的内存交换机制并不采用 frontswap,开启 frontswap 后就会使用注册的 swap 函数进行 swap 操作
以上的任何一步出错,都会导致 Goto 到清理步骤,并返回错误。
zswap_frontswap_init()
使用 kzalloc 给对应 type 的页面新建一个树节点,并注册到zswap_trees 数组中,初始化对应树的自旋锁
zswap_frontswap_store()
获取对应页面的树元信息,组建一个zswap_header结构,就是一个ulong 数据
对输入参数进行检查,判断:
- 是否为 Huge 页面,若是 reject
- 判断是否启用 zswap 和是否有对应的 tree,否则 reject
- 验证 objcg 是否允许 zswap,否则 shrink
- 判断 zswap pool 是否已满,如果已满,记录并跳转 shrink
- 如果未满但是曾经满过,检查能否接受当前页面,如果能,把满标记去除,否则 reject
准备 entry
- 在 entry_cache里申请一段空间(并且初始化相关信息,如 rb_node等),否则记录失败,reject
- 判断 zswap_same_filled_pages_enabled 选项,如果开启,对同样内容的页(全 0、全 1)进行特殊操作,并跳转到insert_enrty
- 判断zswap_non_same_filled_pages_enabled选项,如果未开启,准备返回错误,freepage
通过zswap_pool_current_get找到当前的 pool
- 判断 pool 是否可用,即判断当前的 kref 是否为 0
- 不可用则 NULL,否则返回
- 若没有可用 pool,freepage
压缩
- 获取当前 CPU 上的acomp_ctx 信息,加互斥锁
- 初始化两个 scatterlist,分别绑定给 page 和output,向 acompress 实例发送一个异步请求,并直接等待对应 acompctx 的 wait,直到异步请求返回。
- 判断压缩是否成功,否则put_dstmem
入池
判断当前的压缩池是否支持动态回收,并以此为依据判断是否要为数据 hdr 预留空间。
理由是:如果支持动态回收,要单独把头部信息分开,方便回收时修改元数据,否则可以把头部和数据放在一起,节省空间。
- 根据压缩池是否支持可移动内存设置 GFP,并根据 GFP 标志申请内存空间。如果未成功,put_dstmem
- 使用 zpool_map_handle将内存映射到 zswap pool 的地址空间,把 hdr 和压缩后数据写入该空间,然后解除映射、释放 acomp_ctx的锁
- 更新offset、handle 和 entry 长度信息
挂树
- 首先判断 objcg 的信息,并进行计数
- 获取对应 rbtree 的锁
- 调用红黑树插入对应 offset 的节点,相应的插入函数会在 offset 重复的时候返回 -EEXIST 枚举,若出现了这种情况,程序会先把相应的重复节点 erase 掉,再处理它的引用计数。并retry 直到返回值不为-EEXIST
- 释放锁、计数、更新压缩池大小
释放
- 解除 acomp 上下文的锁
- 释放 pool 的 kref、释放 objcg 的 kref,返回
- shrink:获取最新的内存池,把 shrink 工作添加到 shrink work 队列,返回 ENOMEM。
zswap_frontswap_load()
查找
- 通过 type 获取树根
- 给树加锁
- 通过 offset 在红黑树上找到对应的 entry,解锁,如果找不到,返回-1(可能已经被写到交换设备中了)
- 判断length 是否为 0,如果为 0 证明是一个内容全相同的页面,fill 之,跳过后面的步骤,进 stat
- 如果 zpool 不支持睡眠映射,则调用 kmalloc 在 KERNEL 中分配一段大小为 entry 的 length 的内存,如果失败,freeentry
解压
- 通过 zpool_map_handler把指定页面映射到 zswap 对应位置,通过zpool_evictable判断之前 store 的时候压没压 hdr 的长度,如果有 hdr,进行跳过
- 如果不支持睡眠映射,把刚刚获得的数据 copy 到之前申请的内存中,解除映射
- 获取 acomp 的上下文,获取 acomp 上下文的锁
- 创建对应的 scatterlist(如果支持睡眠映射,在此处直接映射,否则就要用之前的 copy 过的内存内容),用差不多的方式调用 decompress,等待解压完毕
- 解除锁
- 解除映射或者 free刚刚申请的内存空间
- 计数、获取树的锁、释放节点、释放树的锁
zswap_frontswap_invalidate_page()
- 没什么可说的,从树上删掉一个 entry,做好相关的加锁解锁
zswap_frontswap_invalidate_area()
- 先序遍历整个 type 的树,全干掉。
除了 frontswap 相关 API 的实现,zswap 也需要实现兜底相关的内容
zswap_writeback_entry()
在系统需要回收内存页时,zswap 会通过这段代码向 swap cache 添加一个页面,然后解压并写入到交换设备中。
找到节点并解压的过程类似 load()函数,区别是这里的参数是 zpool 的 handle句柄,程序先通过 handle 映射内存,获得 zswap 的 header,再读取 header信息获得 offset,进而执行后续的解压缩操作。
在获得 offset 后、解压前,还进行了一系列验证和清理相关的操作。
流程之外
流程之外,zswap 提供了诸多管理 pool、entry、tree 的函数,如创建、计数 aquire(get),计数 relese(put)等。但是这些函数都较为简单于望文生义,不在此赘述。
评论 (0)