device-mapper 块级重删(dm dedup) <3>代码结构(1)

三、代码结构(1) 基础构架

逻辑推理地看源码是学习代码最清晰的方法,这样对代码的记忆会提高很多。

能够从复杂的代码结构中找到逻辑关系也是非常重要的一个技能。

device-mapper 块级重删(dm dedup) <3>代码结构(1)
以上是dm dedup的主要代码逻辑关系。
因为其主要的设计已经在上一篇有介绍过了,所以我们这里直接分析代码流程。

四、代码结构(1) I/O入口 dm_dedup_map

1、dm_dedup_map:这个是从dm.c->dm_dedup.c主要调用接口
device-mapper 块级重删(dm dedup) <3>代码结构(1)

① chunk data的对其切分
首先要解释的是:图中chunk bio的过程,是由dm.c中的split_and_process_bio实现的

    while (ci.sector_count && !error) {
            error = __split_and_process_non_flush(&ci);
            if (current->bio_list && ci.sector_count && !error) {

                struct bio *b = bio_split(bio, bio_sectors(bio) - ci.sector_count,
                              GFP_NOIO, &md->queue->bio_split);
                ci.io->orig_bio = b;
                bio_chain(b, bio);
                ret = generic_make_request(bio);
                break;
            }
        }

这段其中比较核心的是split大的BIO变成以某个方式对齐
看明白如何对齐split的,就必须对_split_and_process_non_flush进行分析

static int __split_and_process_non_flush(struct clone_info *ci)
{
    struct bio *bio = ci->bio;
    struct dm_target *ti;
    unsigned len;
    int r;

    ti = dm_table_find_target(ci->map, ci->sector);
    if (!dm_target_is_valid(ti))
        return -EIO;

    if (unlikely(__process_abnormal_io(ci, ti, &r)))
        return r;

    if (bio_op(bio) == REQ_OP_ZONE_REPORT)
        len = ci->sector_count;
    else
        len = min_t(sector_t, max_io_len(ci->sector, ti),
                ci->sector_count);

    r = __clone_and_map_data_bio(ci, ti, ci->sector, &len);
    if (r < 0)
        return r;

    ci->sector += len;
    ci->sector_count -= len;

    return 0;
}

首先I/O对齐split中有比较重要的就是几个问题:
① 到底是如何切分BIO的?
切分这个读者应该都很容易看懂,就是不断去ci->sector += len和ci->sector_count -= len;
通过将ci->sector不断通过len增加,然后ci->sector_count总量不断减少
制造一个个被split的sub_BIOs。
② 为什么说切分是对齐的?
这就涉及到len的大小,这里我们举个例子:
bio:【bi_sector:3 size=8】应该被切分为什么样子?
如果按照size=4去切分?那应该对其后的结果是:
3%4 = 3 ,bio_split_1 = [1_bi_sector:3,1_size=1],t_size = 8-1=7;bi_sector:4;
4%4 =0, bio_split_2 = [2_bi_sectoer:4,2_size=4],t_size= 7-4=3;bi_secor:8;
8%4 = 0,bio_split_3 = [3_bi_sectoer:8,3_size=3],t_size= 3-4=-1;bi_secor:11;
其实这里我们演算出来的规律,正是max_io_len的代码的逻辑关系:

static sector_t max_io_len(sector_t sector, struct dm_target *ti)
{
    sector_t len = max_io_len_target_boundary(sector, ti);
    sector_t offset, max_len;

    /*
     * Does the target need to split even further?
     */
    if (ti->max_io_len) {
        offset = dm_target_offset(ti, sector);
        if (unlikely(ti->max_io_len & (ti->max_io_len - 1)))
            max_len = sector_div(offset, ti->max_io_len);
        else
            max_len = offset & (ti->max_io_len - 1);
        max_len = ti->max_io_len - max_len;

        if (len > max_len)
            len = max_len;
    }

    return len;
}

③ 明白了切分的方法,那么还有一个问题就是,max_io_len的n%splt_size的ti>max_io_len是多少呢?
按照多大切分的我们也需要搞明白一下。
这个过程很简单,大概的过程就是向上推找到这个值的赋值,初始含义和可配置的地方。

最终看到这个值是在dm_dedup_ctr里传的一个参数block_size所决定的,也就是块大小。
这个block_size值得就是hash index的单位,在dm_dedup里它内约束在了4k到1M的区间内.
#define MIN_DATA_DEV_BLOCK_SIZE (4 1024)
#define MAX_DATA_DEV_BLOCK_SIZE (1024
1024)

OK ,目前我们约定俗成的认为它就是page size 4k,那么这样就很好理解了。
这样被对齐split后的bio,为什么要对齐split,主要是为了对齐split bio能够对应一个pbn,这样就可以以某个pbn的hash来代表它。

② 多线程处理每个chunk_bio

static int dm_dedup_map(struct dm_target *ti, struct bio *bio)
{
    dedup_defer_bio(ti->private, bio);

    return DM_MAPIO_SUBMITTED;
}

static void dedup_defer_bio(struct dedup_config *dc, struct bio *bio)
{
    struct dedup_work *data;

    data = mempool_alloc(dc->dedup_work_pool, GFP_NOIO);
    if (!data) {
        bio->bi_error = -ENOMEM;
        bio_endio(bio);
        return;
    }

    data->bio = bio;
    data->config = dc;

    INIT_WORK(&(data->worker), do_work);

    queue_work(dc->workqueue, &(data->worker));
}

这个代码原理非常简单,用mempool申请work,用queue_work去分发请求到各个cpu。
这里如果想做的更好一点,可以做一个cpu池,在创建设备的时候可让配置其cpu亲和,单cpu命令队列深度(最大IO合并的大小)。

static void process_bio(struct dedup_config *dc, struct bio *bio)
{
    int r;

    if (bio->bi_opf & (REQ_PREFLUSH | REQ_FUA) && !bio_sectors(bio)) {
        r = dc->mdops->flush_meta(dc->bmd);
        if (r == 0)
            dc->writes_after_flush = 0;
        do_io_remap_device(dc, bio);
        return;
    }

    switch (bio_data_dir(bio)) {
    case READ:
        r = handle_read(dc, bio);
        break;
    case WRITE:
        r = handle_write(dc, bio);
    }

    if (r < 0) {
        bio->bi_error = r;
        bio_endio(bio);
    }
}

最后解析一下bio读写的方向然后去给handle_read和handle_write去分发请求。

如果认真看的读者,应该已经清楚明白了,map的流程就是:dm_bio(大bio)被以block_size对齐split后带多cpu处理的一个流程。
这里是dm-dedup的发动机,很多人可能要问,为什么这里要做成异步处理的形式,为什么不直接就在上层派发dm_bio的task里就把dedup的工作做完?
我认为这里这么做,主要是考虑到了dedup算hash index需要大量的时间,所以高并发情况下这个程序最终表现出的性能,可能都在多个cpu在计算hash上面。
如果在dm_bio的task里面做hash ,相当于没有流水线并发能力,单线程在算hash,计算就会是io性能的瓶颈,这里比较好的解决了这个问题,但是这里没有很好的考虑到I/O合并(如果I/O不能合并,可能会造成巨大的I/O latency),和各个cpu的请求队列深度均衡问题。

【本文只在51cto博客作者 “底层存储技术” https://blog.51cto.com/12580077 个人发布,公众号发布:存储之谷】,如需转载,请于本人联系,谢谢。

原创文章,作者:Maggie-Hunter,如若转载,请注明出处:https://blog.ytso.com/185313.html

(0)
上一篇 2021年11月4日
下一篇 2021年11月4日

相关推荐

发表回复

登录后才能评论