Linux Kernel-Zswap 1.功能、流程与原理

yyi
yyi
2023-08-01 / 0 评论 / 156 阅读 / 正在检测是否收录...
温馨提示:
本文最后更新于2023年08月08日,已超过286天没有更新,若内容或图片失效,请留言反馈。

功能、流程与原理

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

评论 (0)

取消