首页
关于
Search
1
2023 年保研经验贴
123 阅读
2
Linux Kernel-THP阅读分析[Hang up]
41 阅读
3
CSAPP 1. 计算机系统漫游
40 阅读
4
Rust入门笔记 2.函数、所有权与复合类型
40 阅读
5
Rust入门笔记 3.切片、向量与字符串
38 阅读
内核
源码分析
Rust
语法
读书笔记
深入理解计算机系统
技术之外
登录
Search
yyi
累计撰写
19
篇文章
累计收到
0
条评论
首页
栏目
内核
源码分析
Rust
语法
读书笔记
深入理解计算机系统
技术之外
页面
关于
搜索到
8
篇与
的结果
2023-08-01
Linux Kernel-Zswap 2.上下层依赖
上下层依赖重要数据结构zswap_poolstruct zswap_pool { // 挂 zswap 所用的内存池 struct zpool *zpool; struct crypto_acomp_ctx __percpu *acomp_ctx; struct kref kref; struct list_head list; struct work_struct release_work; struct work_struct shrink_work; struct hlist_node node; char tfm_name[CRYPTO_MAX_ALG_NAME]; };struct zpool *zpool:指向 zpool 对象的指针,用于表示当前 zswap 池所对应的 zpool。可能需要基于 uma 进一步封装实现。struct crypto_acomp_ctx __percpu *acomp_ctx:指向压缩算法。struct crypto_acomp_ctx { struct crypto_acomp *acomp; struct acomp_req *req; struct crypto_wait wait; u8 *dstmem; struct mutex *mutex; };struct kref kref:用于实现引用计数技术的 kref 对象,用于管理 zswap 池对象的生命周期。当一个 zswap 池对象被创建时,其引用计数值初始化为 1;当该对象被使用时,其引用计数值增加;当不再需要该对象时,其引用计数值减少,当引用计数值降为 0 时,该对象将会被释放。 FreeBSD 支持 refcount 特性,我们认为是可以迁移的。struct list_head list:用于实现链表的 list_head 结构体,用于将当前 zswap 池对象添加到 zswap 的池列表中。普通链表头,可以迁移。struct work_struct release_work:表示一个工作队列对象,用于异步释放当前 zswap 池对象。在该工作队列中执行的操作会在一个单独的内核线程上运行,从而避免阻塞当前进程。struct work_struct shrink_work:表示一个工作队列对象,用于在 zswap 池中的页面数量超过一定阈值时触发缩减操作。该工作队列也是异步执行的,在单独的内核线程上运行。FreeBSD 的 taskqueue 和 workqueue类似,都支持异步调用,可以迁移。struct hlist_node node:表示一个哈希表节点,用于将当前 zswap 池对象添加到 zswap 的池哈希表中。char tfm_name[CRYPTO_MAX_ALG_NAME]:表示保存压缩算法名称的字符数组,用于指定 zswap 池所使用的压缩算法名称。在进行前端交换时,内核将会根据该名称来查找相应的压缩算法,并使用其进行数据压缩处理。zswap_entrystruct zswap_entry { struct rb_node rbnode; pgoff_t offset; int refcount; unsigned int length; struct zswap_pool *pool; union { unsigned long handle; unsigned long value; }; struct obj_cgroup *objcg; };struct zswap_entry 表示单个被压缩页面元数据。struct rb_node rbnode:红黑树节点pgoff_t offset:表示该页面在交换分区中的偏移量,也是红黑树的 idxint refcount:表示对该页面的引用计数,用于保证在页面同时被加载、失效和写回等操作时不会出现条件竞争,从而避免页面被过早地释放。unsigned int length:被压缩后页面长度。struct zswap_pool *pool:标识该页面所属的 zswap 池。union:a) unsigned long handle:压缩页面数据句柄。这个是 zpool 用的。b) unsigned long value:内容相同的空白页面填充值。struct obj_cgroup *objcg:指向 obj_cgroup 结构体的指针,用于标识该页面所属的 cgroup(如果按 cgroup 进行回收)。zswap_treestruct zswap_tree { struct rb_root rbroot; spinlock_t lock; };这个结构体代表了一个 zswap 树的元信息,分别有一个红黑树的根和一个自旋锁。一点对 zswap_pool\entry\tree的理解enrty 是页面单元,包括该页在内存中的位置大小等信息,pool是实际存储 entry 元信息对应页面压缩后数据的内存池,tree 维护了一颗平衡树,用于找到对应 type+offset 的元信息entry。zswap_header一个 ulong其他全局变量一般为 config 或者全局状态量,易于迁移。page篇幅过长,Linux 的基础页面数据结构重要依赖模块内存管理池 zpoolzpool 是 linux 的一类内存管理 API,是 zswap 所依赖的最底层的数据结构。zswap_pool 通过 zpool 的各类内存管理工具,包括申请、映射、收缩等关键操作异步压缩 acompressacompress 是 linux crypto 下的模块,它提供了类似队列式的异步的压缩和解压缩能力同时,crypto 模块还提供了统一的同步方式swap 框架 frontswap提供了一种 swap 接口层,通过实现 frontswap 提供的函数指针,可以自定义swap 的实现方式有了 frontswap,我们只需要关注 swap 后端的设计,并向对应的初始化、存入、取出等接口实现逻辑、提供功能即可。frontswap 是zswap 最重要的依赖之一平衡树 rbtree维护 zswap_tree 的核心数据结构,保证插入和索引 offset 对应的页面的时候的复杂度。工作队列work_queue提供一个异步工作的机制,包括对 pool 的 shrink 和 release引用计数 kref需要一个引用计数器,主要是保证在 pool 和 entry被释放时的内存安全性锁与信号量主要需要关于自旋锁和互斥锁的 aquire/release的实现,避免线程竞争带来的各类安全和数据访问问题。原子操作一般依赖模块认为一般依赖模块仅是为了某些特性准备,不影响 swap 的关键流程scatterlist为了支持 acomp 的参数而依赖,属于间接依赖多 CPU 系列的宏和回调注册有的为了支持 CPU 热插拔事件、有的为了实现 acomp 在多 CPU 上的真并行kmem_cacheentry_cache 的底层申请与管理依赖于 kmem_cache,属于性能优化。
2023年08月01日
16 阅读
0 评论
0 点赞
2023-08-01
Linux Kernel-Zswap 3.迁移到FreeBSD思路
迁移 FreeBSD需求分析:FreeBSD 目前仅支持默认的Swap 操作,为了优化其表现,尝试设计一套类似于 zswap 的机制,使得在FreeBSD 中,当需要进行页面交换时可以把页面交换至硬盘的链路改为压缩后仍存放在内存中需要换入时,可以正确的解压缩页面并换回内存内存空间不足/达到设计的上限时,有合理的机制把当前压缩页面池中的内容踢出,放入硬盘兜底对于已放入硬盘的页面,有合理的机制在换入时可以找到。设计思路:对于 zswap 所依赖的大部分内容,FreeBSD 中都有相应的模块可以替代,部分可能需要进一步封装或 patch,如 zstd 之于acompress,uma 之于 zpool,但是有一个最核心的点 FreeBSD 没有支持,即 Frontswap。我们可以考虑这样的链路:→内核调用换出→Hook劫持至我们的 SwapHook→将相应页面压缩后放入压缩页面池→返回成功,若失败,继续正常的换出流程,否则返回。→内核调用换入→Hook劫持至我们的 SwapHook→寻找相应页面,若不存在,返回失败→解压相应页面,返回内容和成果。过程中的任意失败都会导致进入正常的换入流程。FreeBSD Swap 是如何工作的我们假定从缺页错误开始,一直到如何调用 Swap开始,对我们都是透明的,(好处是很多其他原因触发的缺页错误我们都无需关注)则:在换出时:调用 swap_pager_putpages(),传入参数包括object:指向虚拟内存对象的vm_object_t的指针ma:一个指向要换出的物理页面数组的指针count:ma 中物理页面的数量flags:操作行为rtvals:return values,存储每个换出页面的操作结果。assert 检查 ma 和 object 的一致性解锁 object通过调用方是否为 pageproc 和 VM_PAGER_PUT_SYNC 参数设置情况判断是否要一步操作初始化释放范围开始换出操作:循环处理要换出的每个页面,判断当前剩下的页面数量和单词 IO 可以操作的最大页面数量,判断本次循环要操作的数量获取一个异步写资源,设置 nsw_wcount_async获取本批次要写的页面的空间,为后面向 swap 设备写做准备如果没申请到需要的空间,释放 nsw_wcount_async,(如果为 1)唤醒其他等待的资源,设置本批次返回值为失败对当前虚拟内存对象上锁循环处理本批次的每个页面:赋值当前要处理的页面给 mreq 变量清除页面的 PGA SWAP FREE 标记建立一个元信息,包括当前页所在的 object、在 object 中的 index 和要存放到的 swap block 块号更新操作完成后释放的 swap 空间范围校验 dirty,确认是不是一个脏页设置 swaping 标志解锁这个虚拟内存对象为 buf 结构分配一个空间,使用 uma 获取校验、设置一些标志,我们暂时不关心设置缓冲大小、缓冲块号、页面信息和页数信息计数,把当前这些页面的返回值设为 VM_PAGER_PEND根据是否异步的情况,把对应 buf 发给 swp_pager_strategy函数,并决定是 wait 还是 continue由对应函数进行换出操作释放相关的页面、锁定对应的 object在换入时:调用 swap_pager_getpages(),传入参数包括object:指向虚拟内存对象的vm_object_t的指针ma:一个指向要换入的物理页面的指针count:ma 中物理页面的数量rbehind、rahead:当前页面之前和之后的页面数量锁定当前 object,调用 locked函数检查第一个页面是否在对象的 swap 空间中,并获取前后最多有多少页,如果不在,解锁并返回VM_PAGER_FAIL检查请求的页面数量是否超过设备上的页面范围判断当前 vm_obj 的标记,并根据 SPLIT 和 DEAD 的情况决定是否重新设置 maxahead 和 maxbehiendmaxahead:换入第一个页面后的连续可用页面数量同 ahead,换入页面前的根据 max 和 rahead、rbehind 决定实际要换入的页面范围,目的是排除已经在内存中的页面根据 ahead 和 behind,申请足够数量的页面,把这些页面标记为 VP0_SWAPINPROG根据实际申请到的页面数量,更新 rbehind、rahead 和 count对虚拟内存的页面增加引用计数通过 object 和 index 查询 swap 设备上的 swap 元信息。并 assert 相应页在 swap 设备上解锁 vm_object,为 buf 申请内存,并填充相关数据,具体数据和换出差不多计数把缓冲区和内核 PROC 关联调用 swp_pager_strategy(),把请求借助 buf 发送给 swap 设备锁定虚拟内存对象,while(swap_flag) Sleep 20s等换入操作结束。若超时,打印一些信息解锁对象,遍历页面,检查 valid 属性,如果有问题则返回 ERROR返回 OK经过对 FreeBSD Swap 流程的了解,我们发现除了在之前想到的对 swap 进行 Hook 之外,是否还存在一种方式:自定义一个虚拟的 Swap 设备,向 FreeBSD 注册实现支持blist_alloc()实现支持sw_strategy,接受 buf,执行相关操作。要求能确保在开启之后,该虚拟设备要在 swtailq 的第一位。这样做的好处是显而易见的:我们不必破坏或大量修改 swap 和内核本身,而实现一个虚拟设备驱动就可以完成相应的目标。并且,这其中的相关 flag 及策略判断,FreeBSD 都已经替我们做好了。无论使用在 swap 前 hook,还是使用驱动的方式,我们都有必要对这个 buf 结构体进行一定的了解,以便利用这个结构体的信息进行置换操作。bufbuf定义在 FreeBSD 的 /sys/buf.h中fxr.watson.org: sys/sys/buf.h我们借助源码和 FreeBSD 系统结构手册进行理解handbook这是一个非常庞大的结构体,我们尽量只挑选需要的字段进行理解。可以看到,在 buf 结构体中,有 struct vm_page *b_pages[]和caddr_t b_data依赖迁移:敲定 frontswap 的替代方式后,最重要的工作内容就是如何封装 FreeBSD 所拥有的依赖,使它能适应zswap 的需求,使得可以直接把 zswap 整个文件的核心逻辑都迁移过来或如何修改 zswap 对外部逻辑的调用,使得它能使用 FreeBSD 提供的能力为了合理的安排我的工作,我选择的工作路径是:数据结构迁移→按流程的函数迁移。具体选择封装还是修改视具体情况决定。在分点上自顶向下的递归解决所有依赖问题。数据结构迁移zswap_pool能力梳理:zswap_pool 主要提供了如下能力:提供一个底层数据结构 zpool,使得可以把压缩页面存入并获得 handle和 使用 handler取出对应页面对每个 CPU 提供一个异步压缩上下文,使得可以把数据传入,进行异步压缩和解压缩,此外,还可以提供自定义的压缩算法名字来注册这个异步压缩上下文。其中,这个上下文主要依赖了 crypto_acomp,包括使用它的压缩功能和同步功能。一个引用计数器,防止多线程出现的一些安全问题,保证pool不会被错误释放,并且在 ref 为 0 时可以释放。两个工作项,在需要释放或压缩的时候挂到队列中,进而完成异步的释放或压缩。一个链表头,保证可以 RCU 的遍历所有 zswap_pool现在,我们尝试逐一找到 FreeBSD 中可以支持相关能力的一些模块。底层内存池ZpoolFreeBSD 支持 UMA 内存架构,它实现了 Slab 内存池来管理内核内存的分配与释放,我将基于 UMA,尝试给出 Zpool 的替代方案。UPD:UMA 的 Slab 不支持任意大小的内存池,然而我们还是可以通过一些 Trick 来实现我们想要的东西,详情见 entry 的迁移。为了统一说法,我们会称内存池为 pool,内存池条目为 entry,即使在 FreeBSD 中它们似乎应该成为 zone 和 item。但是在直接出现 FreeBSD 源码的时候,我们选择使用源码说法。uma(9)申请pool:zpool的申请发生在zswap_pool_create() 函数中,它先通过 kzalloc 在内核中申请了一段用于存放 zpool 指针的内存,然后调用zpool_create_pool() 函数获得一个 pool,相关的参数如下:type : 指定使用的池类型,它由 Config 指定,默认为 z3fold。name:池名称,为 “zswap{num_of_pool}”gfp:指导内存分配的标志,这里为 不重试-不告警-允许内核进程回收ops:一些Hook 函数,这里是指导当池空间不足时,调用zswap_writeback_entry进行页面写回,会把需要写回的 handle 传递给该函数。再来看一下 uma 中内存池申请的函数定义name:无需变化size:可以支持设置,我们参考z3fold的内存大小设置,在典型的 4K 页面时可能会设置为 64KB 这个 size 应当是单个对象的 size,根据后面的妥协算法,我们目前采用 4K * 70%其余参数我们暂时认为无需设置当然,仅仅这样申请到了一个 pool(uma 中称为 zone,下同)还不够,我们发现 evict 这个 callback 函数没有用到。幸运的是,uma 提供了**uma**_**zone**_**set**_**maxaction**()函数,支持在 pool 已满并且申请新条目失败时回调。 这里给出了函数和其需要实现的回调函数的定义,看来我们可以非常方便的设置这个回调了。然而它和 zpool 的回调**还不太一样**,它只会返回已满的内存pool。 - 销毁 pool - uma要求在销毁 pool 前,必须释放所有的 entry,在这之后,我们就可以使用 `uma_zdestroy()`函数来释放所有内存 其余和 entry 相关的内存申请操作,在流程迁移里再讨论 异步工作队列 workqueue 与异步工作项 work_structFreeBSD 中,有和 workqueue 极其类似的机制 taskqueue。在 zswap 中,release 和 shrink 队列采用 INIT_WORK 注册工作队列和工作函数,区别是 shrink 预先创建了一个队列,而 release 直接 INIT ,注册到系统工作队列了。同时,shrink 采用 queue_work()方式调用 work,而 release 采用 schedule 方式调用 work,它们的区别是 queue_work会先挂队列,择机执行,而 release 会立即尝试执行。根据资料,这和它们的释放方式有关。但是幸运的是,在迁移过程中我们没有必要对这两种方式过度关心,首先要确保的是迁移的可行性。FreeBSD 采用 taskqueue_create来创建新的队列,主要参数包括队列名字和需要被调用的函数。在 taskqueue 被创建后调用 taskqueue_start_threads()等一系列函数来启动 taskqueue 的线程。采用 taskqueue_enqueue把任务添加到队列里,因此,我们的 work_struct可以直接被修改为 FreeBSD 中的 task。引用计数 kref引用计数作为操作系统中非常常见的功能,FreeBSD 中有类似的机制:refcount API在 zswap 中,主要包括非常基础的 init、put、get_if_not_zero等操作,幸运的是,FreeBSD 中都有类似的、以原子操作实现的功能,其函数名分别为:*refcount**_**init、refcount**_**relese和*refcount**_**acquire**_**if**_**not**_**zero**可以直接调用链表 list链表作为一种极其常用的数据结构,FreeBSD 中当然也有类似的机制:sys/queue.h下定义了 FreeBSD 所使用的链表们,在这里,我们需要一个双向链表。然而zswap使用的是基于 list 后封的 rcu 链表,包括list_for_each_entry_rcu、list_del_rcu、list_add_rcu等。rcu 是 linux 内核的一种机制,它解决了两类问题。reader 不需要在 writer 写时等待,数据一致性得到保证,不会出现半新半旧的情况。这样看,我们有两种方式对它进行迁移。不在乎 rcu 带来的性能优化,直接在链表相关访问前后加 rw 锁。这种方式简单易于移植,但是带来了很显然的性能劣化。使用 FreeBSD 提供的 SMP 机制,完成对 FreeBSD 链表的 rcu 封装。这里我想给出一个简单的基于 rw 锁的示例,(未验证,甚至未验证能否编译)#include <sys/queue.h> #include <sys/rwlock.h> struct data { int val; LIST_ENTRY(data) entries; }; struct rwlock list_rwlock; rw_init(&list_rwlock); LIST_HEAD(head_name, data) head; #define list_for_each_entry_rw(pos, head, member) \ rw_rlock(&list_rwlock); \ for ((pos) = LIST_FIRST((head)); (pos) != NULL; (pos) = LIST_NEXT((pos), member)) void sample() { struct data *p; list_for_each_entry_rw(p, &head, entries) { // read val } rw_runlock(&list_rwlock); }异步压缩 acompFreeBSD 的 crypto 部分可以实现类似acomp 的功能 comment:crypto 似乎要使用设备,暂时排除这种方法。,此外,zlib 库也支持压缩功能,我们主要关注如下内容根据不同的算法名字申请算法很遗憾,根据 FreeBSD 的文档,目前内核 Crypto 模块似乎只支持一种压缩算法,但是我们可以留下未来开发的可能。或者自己实现一些压缩算法。尽可能的支持多核并行压缩调用,如果不能,至少要实现压缩和解压缩功能。对于每个 pool 可以维护一个实例,单 pool 使用实例时要线程安全。快速了解zlib 的使用后,我想尝试如下的方式:ctx 结构struct ctx { z_stream compression_stream; z_stream decompression_stream; struct mtx mutex; };初始化:int ctx_init(struct ctx *ctx) { bzero(&ctx->compression_stream, sizeof(z_stream)); bzero(&ctx->decompression_stream, sizeof(z_stream)); int ret = deflateInit(&ctx->compression_stream, Z_DEFAULT_COMPRESSION); if (ret != Z_OK) { return ret; } ret = inflateInit(&ctx->decompression_stream); if (ret != Z_OK) { deflateEnd(&ctx->compression_stream); return ret; } // 初始化 mutex return Z_OK; }以压缩为例int compress_page(struct crypto_acomp_ctx *ctx, vm_page_t src) { // 映射 src page 到内存 为 src_addr // 申请 dst 空间 // 加锁 int ret = compress2(dst_addr, &dest_len, src_addr, PAGE_SIZE, Z_DEFAULT_COMPRESSION); mtx_unlock(&ctx->mutex); // 解除映射 // free 空间 return ret; }compress2 是同步过程,但是异步的工作队列给了我启发,我想我们可以不太麻烦的自己实现 Linux 内核对应的 crypto_acomp异步过程,步骤如下:ctx 扩充两个字段,一个是工作项 task,一个是等待信号 wait需要压缩时,工作项挂队列,并 suggest 立即执行主线程等待 wait压缩完毕的 callback 中,通过container_of找到对应的结构,进而释放 wait这样就实现了一个简易的异步过程。然而,还是有个工作项需要确认:[ ] 工作队列的异步模式,能否真的满足原 zswap 的构思,即单核同步,多核异步并行?但是我们现在已经完成了这个内容,暂时就把它放在这里最后看吧。### 至此,我想已经基本上给出了 zswap_pool的可行迁移方案,然而由于对 FreeBSD 各内核 API 的了解不够,可能还要进一步学习 FreeBSD 的内核原来来确认确实能满足我们的工作。### 压缩页面表项 zswap_entry暂时忽略了 cgroup 特性,可以作为一个 TODO。struct z_entry_key { vm_object_t object; vm_pindex_t pindex; }; struct zswap_entry { RB_ENTRY(zswap_entry) rbnode; struct z_entry_key rbkey; int refcount; unsigned int length; struct zswap_pool *pool; //uma zone uintptr_t handle; };static inline int zswap_entry_key_cmp(struct z_entry_key k1, struct z_entry_key k2) { /* Compare the vm_object */ if (k1.object < k2.object) return -1; else if (k1.object > k2.object) return 1; /* Compare the pindex */ return k1.pindex < k2.pindex ? -1 : (k1.pindex > k2.pindex ? 1 : 0); } struct zswap_entry *zswap_rb_find_entry(vm_object_t object, vm_pindex_t pindex) { struct rb_node *node = zswap_rb_root.rb_node; int result; while (node) { struct zswap_entry *entry = container_of(node, struct zswap_entry, rb_node); struct z_entry_key key = {.object = object, .pindex = pindex}; result = zswap_entry_key_cmp(key, entry->key); if (result < 0) node = node->rb_left; else if (result > 0) node = node->rb_right; else return entry; } /* Entry not found */ return NULL; }对于 RB_Tree,我想没有必要过多的描述,FreeBSD 内核中已经有红黑树相关的 API 了。不幸的是,在这一步我发现 uma 的 zone 不支持任意大小的块的申请。一点其他的思考:我猜测 zpool 底层应该也是固定大小的块的,当我们需要任意大小的内容时,zpool 提供了一层映射封装,即通过 handle 来把各个分散的块组装起来,这样我猜测 zpool 一定也会浪费一定的空间。---### 补充:关于 z3fold 内存管理在了解 z3fold 之后,我验证了之前的猜想,z3fold 底层也是固定的页面大小,但是 z3fold 把一个页面分为多个固定大小的 chunks,当收到新的内存分配请求时,z3fold 根据请求的大小计算需要 chunks 的数量,尝试在现有的页面中找到足够的连续空闲 chunks,如果找不到,分配一个新的页面容纳请求。---因此,要么我们自定义一个支持任意大小申请、支持失败回调的内存池要么就要规定一个压缩比率,假设我们把压缩率不足 70%的所有页面都直接 swap 到外部设备,压缩率高于 70%的页都放入内存池,我们就可以使用固定 PAGE_SIZE * 70% 的 zone 来实现这个功能了(考虑到我们的目的是 CPU 时间换 IO 时间,我觉得均摊意义上对 CPU 的浪费是可以接受的)在我的设计中,我将会选择第二种方式,当然,70% 这个阈值仍需要实践后来确定,这就是后话了。### 树根结构 zswap_tree树根没什么太多好说的,存放 rb_tree的根和访问树的互斥量。前面都有提到了。到此为止,我认为迁移数据结构整体的框架就已经完成了,后续还需要部分调研和验证工作。---## 流程迁移在流程迁移部分,我会从初始化流程→核心流程(即 frontswap 的 api 函数)→各类回调流程→逐一介绍*关于数据结构操作,目前观察都是同质化很严重的操作(如 rbtree),没有迁移层面的太大难度,后续再查缺补漏。### 初始化在 zswap 中,我认为初始化主要有如下几个部分参数设置zswap_enabled_param_setzswap_compressor_param_set…这一类函数通过实现kernel_param_ops,调用module_param_cb()宏创建相关参数zswap_setup(zswap_init)这个函数初始化了一些数据结构,开辟空间,并且注册部分回调zswap_cpu_comp_prepare这个函数是 CPU 热插拔状态变更的回调,主要用于对 comp 算法的注册。### 参数设置我不是特别了解 FreeBSD 的模块参数设置方式,但是根据我查找到的资料,可以提供一个方式做参数绑定:static int zswap_enabled = 0; SYSCTL_NODE("vm", OID_AUTO, "zswap", CTLFLAG_RW, 0, "Zswap VM sysctls"); TUNABLE_INT("vm.zswap.enabled", &zswap_enabled); SYSCTL_INT("vm.zswap", OID_AUTO, enabled, CTLFLAG_RD, &zswap_enabled, 0, "zswap enabled");这样模块就可以在加载的时候获得这个参数了通过实现一个注册函数,并且引用 DECLARE_MODULE的方式就可以注册这个模块了,并且可以通过注册函数参数的不同来确定当前系统是要 LOAD 还是 UNLOAD,进而获取相关参数并初始化。### 参数设置中的其他动作除了绑定相关的参数内容之外,zswap还做了一些诸如参数验证(比如 compressor 是否存在,pool_type是否存在)、另外,由于 zswap的参数设置可能不会只发生在系统启动时,zswap 还对这种情况做了判断,针对不同的时期做不同的初始化和绑定工作。### Summary因此,在参数设置部分,我们主要关注需要迁移的内容包括向系统注册模块的过程,即 DECLARE_MODULE及初始化函数参数绑定的过程,可能需要 TUNABLE_INT 的特性来设置由于 FreeBSD 可能不包含“其他动作”所判断的 system_state因此需要把不同 State 的处理拆出来,在 DECLARE_MODULE 注册的注册函数中,判断不同的 state我们不再需要注册 compressor,因为统一采用 zlib### Setupsetup 主要包含如下步骤为 zswap_entry申请 CACHE 空间注册 CPU 热插拔申请一个 有兜底逻辑的 Pool创建 shrink 队列设置初始化状态对于 CPU 热插拔,相关方案我们在下一步概述对于 Pool,Shrink,我们在数据结构迁移部分都已给出解决方案所以,本部分主要给出为 zswap_entry申请 CACHE 空间的解决方案给出方案前,我确信 KMEM 的存在是为了管理 zswap_entry这个又小又使用频繁的结构,因为它的结构固定,我不得不想到之前对我们没那么好用的 uma zone 好像更适合当前的场景。我们以zswap_entry为单元对象大小,申请一个 uma zone 来作为它的缓存空间。 具体的方式已经在前文介绍过了。### CPU 热插拔相关的初始化实际上,我觉得这两部分的函数并不是针对热插拔进行处理的,我猜测 Linux 内核应该是在初始化的阶段会主动触发一次 CPU 热插拔的回调,来确保用统一的方式初始化各 CPU 的数据。因此,在我的迁移构思中将会按照这种思路,即不考虑真正的热插拔情况,而是直接考虑对每个 CPU 一次初始化需要 per_cpu的内容。zswap 对于每个 CPU:申请了用于多线程压缩/解压结果的一个空间,大小为 2*PAGESIZE根据压缩算法名字申请了一个压缩实例初始化了压缩的其他内容,包括压缩异步调用的回调(释放 wait)、申请 req 空间等。这样看来,我们只需要找到 FreeBSD 中支持 per-CPU申请空间的特性,并给每个 CPU 申请一个 comp_ctx 和 pDst 就可以了。幸运的是,FreeBSD 显然会有这样的特性,即sys/kernel.h 中的 DPCPU_DEFINE(),用于给每个 CPU 定义变量DPCPU_PTR(var, cpuid)这样看来,这个问题也得到解决了。### 核心流程核心流程其实在主文档中已经描述过了,这里只会给出需要修改的接口处,我将以 store 函数为例,逐一过一遍流程中需要替换的内容,对应的其他核心流程,其实用到的接口都差不多,无需赘述。store参数判断我们目前先忽略了 cgroups 特性对于 透明巨页,暂时忽略,因为我还没有找到资料确定 FreeBSD 是否支持这种 HugePage,我猜测如果出现也可以直接判定 page 结构体的数据。对于各类已满的判定,可以直接迁移entry 申请与填充kcache 已经解决判断页面是否全相同:freeBSD 似乎可以利用 pmap_map() 来映射内存压缩可以通过之前提到的 DPCPU 特性来获取当前 cpu 的 comp_ctx我们不再需要 scatterlist,因为我们实现的加密接口不需要散列表因为我们定义的内存池已经支持回收,所以认为需要把 hdr 和内存拆开,以便未来回收暂时不考虑 movable 的特性存储使用我们定义的内存池进行存储,申请相应的空间因为是固定大小的块,直接把压缩好的内容判断是否符合块大小,然后填充进去即可。挂树挂树以红黑树操作为主,FreeBSD 的基础红黑树能实现我们需要的 insert、find_by_index、erase 等操作。记录、日志FreeBSD 有相应的原子操作,可以提供记录### 回调这部分其实主要关注的就是 zswap_writeback_entry,尤其是其中的写回 swap 设备部分,因为其他部分的依赖其实已经都给出解决方案了。然而,因为 uma 的回调并不会给出一个内存池 LRU 好的 handle,我们可能需要维护另外一个数据结构。这里给出一点不太成熟的构想:我们使用 Treap 平衡树,节点值仍为 index,节点权值为 LRU 参数。对于每次访问:更新该 LRU 参数,使得最近最少访问的节点永远在树顶。这样当回调的时候,我们自然而然的可以选择树顶的这一块内存进行清理和回收。---### 补充:关于 z3fold 的 LRU实际上,z3fold 的 LRU 没有那么复杂,在z3fold_pool中,定义了一个名为 lru 的双向链表,用于存储最近使用顺序排列的 z3fold 页面在 z3fold_alloc中,当找到一个空闲的 z3fold 页面时,如果该页面未在 LRU 列表中,添加到 LRU 列表开头,表示它是最近使用的,如果已经在,则移动到列表开头,保持最近使用顺序。从 LRU 列表末尾回收页面。---言归正传,我觉得这种方式是比较合理的方式,其他方式可能要研究一下 zpool 的 LRU 实现。然而由于 Treap 是基于随机的复杂度,我暂时还没有办法保证这种复杂度的正确。以及当 LRU 参数溢出后应该怎么操作。(但是可以想见的是,达到LRU 参数溢出后所需要的时间量级后,我们完全可以把树重建以保证复杂度和不溢出)好,现在已经确定将要被换出的页面了,接下来要做的事是判断当前 entry 是否已经存在于 swap_cache中,freeBSD 没有提供__read_swap_cache_async函数,但是我们有vm_page_lookup()函数,可以确定我们提供的 offset 是否已经在 swap 中。如果已经在,则和远函数的 ZSWAP_SWAPCACHE_EXIST流程相同,否则,调用vm_page_alloc()函数,并根据结果确定是ZSWAP_SWAPCACHE_NEW还是ZSWAP_SWAPCACHE_FAIL。当结果是ZSWAP_SWAPCACHE_NEW时,证明 swap 空间已经为我们要放回的页面准备好了。接下来进行解压步骤,已经在前文有所叙述。并把解压的结果放在刚刚准备完毕的 swap 空间中。FreeBSD 没有 SetPageUptodate(),但是可以改变 vm_page结构的 flags 实现FreeBSD 没有 SetPageReclaim(),也不需要设置相关标志对于写回,在我阅读了 FreeBSD 的swap 策略后,我发现可以直接通过构造一个 buf+swp_strategy来直接使用其他设备写回,具体方案请参见之前的对 swap 的解析。
2023年08月01日
14 阅读
0 评论
0 点赞
2023-08-01
Linux Kernel-Zswap 1.功能、流程与原理
功能、流程与原理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)等。但是这些函数都较为简单于望文生义,不在此赘述。
2023年08月01日
23 阅读
0 评论
0 点赞
1
2