Zswap4BSD接入 BSD Swap

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

这一步是比较艰巨的,上一篇文章里虽然看上去都是fanyi的锅,实际上问题是我都是处理一些迁移好的依赖(by linuxkpi),而fanyi需要弄一个没有的依赖(没有那么好接,需要读一下源码)

到了我们这,Linux和FreeBSD的swap区别还是蛮大,我将会首先梳理Linux中是如何在swap之前先调用frontswap的,再梳理FreeBSD的swap到底是怎么运转的,最后再选择一种合理的方式(尽量照抄Linux)把frontswap接入到 BSD 中。

Frontswap的一探究竟

Frontswap作为一个中间层,为优化方式提供了一种接口,使得页面在真正换出到swap设备之前可以先进入frontswap,如果frontswap成功,则不必再换出。

从下层接口看起

struct frontswap_ops {
    void (*init)(unsigned); /* this swap type was just swapon'ed */
    int (*store)(unsigned, pgoff_t, struct page *); /* store a page */
    int (*load)(unsigned, pgoff_t, struct page *); /* load a page */
    void (*invalidate_page)(unsigned, pgoff_t); /* page no longer needed */
    void (*invalidate_area)(unsigned); /* swap type just swapoff'ed */
};

这里没加参数名字,但是那个unsigned都是type, pgoff_t 都是offset就好了。现在frontswap只有一个driver,即zswap,所以我们都会结合zswap来说。

在我理解,一个type对应的是一个swap设备,或者说,我们每次做swapon系统调用的时候,那个对应的文件就是一个设备,当成一个逻辑设备就好了(毕竟万物皆文件嘛)。

在这种基础上,我们就很好理解了,frontswap要求driver可以区分不同的设备,根据不同的设备初始化、插入、加载和删除页面。以及可以失效一整个设备的所有页面。

上层接口

extern void frontswap_init(unsigned type, unsigned long *map);
extern int __frontswap_store(struct page *page);
extern int __frontswap_load(struct page *page);
extern void __frontswap_invalidate_page(unsigned, pgoff_t);
extern void __frontswap_invalidate_area(unsigned);

相对应的,frontswap也为上层提供了一系列接口,其中参数大差不差,store和load会提取page中的private字段,最后拆成type和offset,再塞给下层,这可能是我们要在过程中关注的一点,

  • 一个页面在什么时候会获得这个private,
  • 它是怎么跟pid和虚拟内存的offset对应上的?
  • 我们在FreeBSD里如何把swap的vm_object和offset 跟这个private对应起来

    我们好像没有那么多地方能存放数据。总不能把zswap改成二重索引的树套树。

除此之外,frontswap还在对应swap设备的控制结构swap_info_struct中留下了一个unsigned long *类型的参数map,用于中间层校验当前的page是否在frontswap中存在,及相应的是否有效

上层接口到下层接口

1. Driver的注册

在做zswap的时候我们已经很明显的看到了,frontswap用register函数注册相关的控制结构

int frontswap_register_ops(const struct frontswap_ops *ops)
{
    if (frontswap_ops)
        return -EINVAL;

    frontswap_ops = ops;
    static_branch_inc(&frontswap_enabled_key);
    return 0;
}

注册的过程伴随着swap设备的swapon,但是存在一种情况即backend driver还没准备好,我们就要往里写了,所以frontswap的每个操作都会在操作前首先检测注册情况

2. init

void frontswap_init(unsigned type, unsigned long *map)
{
    struct swap_info_struct *sis = swap_info[type];

    VM_BUG_ON(sis == NULL);
    if (WARN_ON(!map))
        return;
    frontswap_map_set(sis, map);

    if (!frontswap_enabled())
        return;
    frontswap_ops->init(type);
}

这个map是个bitmap,用来判断一个页面是不是在frontswap里,初始化的时候,要新建一个map赋给这个设备的swap_info

3. test & set & clear

听起来就像bit操作,实际上也确实是,这两个函数根据给定的swap_info和offset,把map的第offset位返回/置1/清除

4. store & load

终于到了比较核心的地方

int __frontswap_store(struct page *page)
{
    int ret = -1;
    swp_entry_t entry = { .val = page_private(page), };
    int type = swp_type(entry);
    struct swap_info_struct *sis = swap_info[type];
    pgoff_t offset = swp_offset(entry);
    if (__frontswap_test(sis, offset)) {
        __frontswap_clear(sis, offset);
        frontswap_ops->invalidate_page(type, offset);
    }
    ret = frontswap_ops->store(type, offset, page);
    if (ret == 0) {
        __frontswap_set(sis, offset);
        inc_frontswap_succ_stores();
    } else {
        inc_frontswap_failed_stores();
    }
    return ret;
}

就和我们之前说的一样,首先从private中拆出来private字段,拆成type和offset,验证当前页面是否在frontswap中已经出现了,如果出现,首先干掉,然后再把页面塞进去

int __frontswap_load(struct page *page)
{
    int ret = -1;
    swp_entry_t entry = { .val = page_private(page), };
    int type = swp_type(entry);
    struct swap_info_struct *sis = swap_info[type];
    pgoff_t offset = swp_offset(entry);
    if (!__frontswap_test(sis, offset))
        return -1;

    ret = frontswap_ops->load(type, offset, page);
    if (ret == 0)
        inc_frontswap_loads();
    return ret;
}

load也差不多,这边如果检测到不在,那就直接返回失败,否则load一下。

至于后面的两个invalid,我们等到他们被调用的时候再看好了。

那它在哪被调用呢?别急,我们先去看看Linux的Swap吧

Linux Swap的浅述

swap应该是大家都听过的概念,废话不多说,不管是linux还是unix,只有匿名页面在需要内存回收的时候,需要放在swap分区中,以便重新访问的时候调入内存,不然只能OOM Kill了,而以文件做backend的页面可能是不需要换出的(可读写页面可能会分情况,比如出现了CoW之后再设置为可以换出的页面)

什么时候发生swap?

  • 周期性检查,保证内存有充足空间:mm/vmscan.c - kswapd()
  • 直接内存回收,当内存不够用的时候: mm/page_alloc.c - __alloc_pages_slowpath

我们关心的第一个问题:swp_entry_t 从哪里来,又到哪里去

来看一张图

img

swp_entry就是一个long,和一个PTE等长,正好可以放进一个页表作为一个页表项,当swap out的时候,linux在回收物理页面之前先修改这个PTE,这样下次访问到这个PTE的时候就会触发一个Page Fault,再通过这个swp_entry_t找到原来这个页面在swap中的位置,进而换出,再把这个表项修改成真的PTE。

PTE的如下三种情况

  • 全为0:该页不属于进程的地址空间,或相应的页框尚未分配给进程。
  • 最低位为0,其余位不全为0:该页现在在swap分区中。
  • 最低位为1:present位为1,表示该页在内存中。直接转换。

至于FreeBSD怎么办,我们一会再想,反正现在是知道这个entry是咋来的了

swap是怎么调用frontswap的?失败了怎么办?

什么时候init

SYSCALL_DEFINE2(swapon, const char __user *, specialfile, int, swap_flags) {
    // ...
    if (IS_ENABLED(CONFIG_FRONTSWAP))
        frontswap_map = kvcalloc(BITS_TO_LONGS(maxpages),
                     sizeof(long),
                     GFP_KERNEL);
    // ...
    enable_swap_info(p, prio, swap_map, cluster_info, frontswap_map);
    // ...
}

static void enable_swap_info(struct swap_info_struct *p, int prio,
                unsigned char *swap_map,
                struct swap_cluster_info *cluster_info,
                unsigned long *frontswap_map)
{
    if (IS_ENABLED(CONFIG_FRONTSWAP))
        frontswap_init(p->type, frontswap_map);
}

swapon的时候,这个调用链路实在太明确啦,就不说了,

  • 因此我们大概也是要找到FreeBSD对应的Swapon,幸运的是,这个应该不是很难。

什么时候Store

肯定是先Store再load啦,我们先看Store好了。

在mm/page_io.c下

int swap_writepage(struct page *page, struct writeback_control *wbc)
{
    // some other code
    if (frontswap_store(&folio->page) == 0) {
        folio_start_writeback(folio);
        folio_unlock(folio);
        folio_end_writeback(folio);
        return 0;
    }
    __swap_writepage(&folio->page, wbc);
    return 0;
}

如果走frontswap成功,就不走后面的write_page了,调用start_writeback,然后结束流程。

然而这块很显然出现了一个问题,folio_xx_writeback是用来干啥的呢?按理说,我们是在swap之前就把该unmap的已经unmap掉了,最核心的写入部分也搞完了,应该只能有点收尾工作,不能有writeback啊?这个问题稍后再看,我们把frontswap看完

什么时候load

读取的部分是+号位有点长

void swap_readpage(struct page *page, bool synchronous, struct swap_iocb **plug)
{
    struct swap_info_struct *sis = page_swap_info(page);
    bool workingset = PageWorkingset(page);
    unsigned long pflags;
    bool in_thrashing;

    VM_BUG_ON_PAGE(!PageSwapCache(page) && !synchronous, page);
    VM_BUG_ON_PAGE(!PageLocked(page), page);
    VM_BUG_ON_PAGE(PageUptodate(page), page);

    /*
     * Count submission time as memory stall and delay. When the device
     * is congested, or the submitting cgroup IO-throttled, submission
     * can be a significant part of overall IO time.
     */
    if (workingset) {
        delayacct_thrashing_start(&in_thrashing);
        psi_memstall_enter(&pflags);
    }
    delayacct_swapin_start();

    if (frontswap_load(page) == 0) {
        SetPageUptodate(page);
        unlock_page(page);
    } else if (data_race(sis->flags & SWP_FS_OPS)) {
        swap_readpage_fs(page, plug);
    } else if (synchronous || (sis->flags & SWP_SYNCHRONOUS_IO)) {
        swap_readpage_bdev_sync(page, sis);
    } else {
        swap_readpage_bdev_async(page, sis);
    }

    if (workingset) {
        delayacct_thrashing_end(&in_thrashing);
        psi_memstall_leave(&pflags);
    }
    delayacct_swapin_end();
}

别看这里多了几个if else,其实store只是把这几个if-else包了一层,如果失败自动回归原流程,好像也没啥需要特别注意的。

至于range_free(invalidate)什么的,我们倒是不着急,先把核心做出来吧.

0

评论 (0)

取消