热门IT资讯网

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

发表于:2024-11-22 作者:热门IT资讯网编辑
编辑最后更新 2024年11月22日,三、代码结构(1) 基础构架逻辑推理地看源码是学习代码最清晰的方法,这样对代码的记忆会提高很多。能够从复杂的代码结构中找到逻辑关系也是非常重要的一个技能。以上是dm dedup的主要代码逻辑关系。因为

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

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

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


以上是dm dedup的主要代码逻辑关系。
因为其主要的设计已经在上一篇有介绍过了,所以我们这里直接分析代码流程。

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

1、dm_dedup_map:这个是从dm.c->dm_dedup.c主要调用接口

① 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 个人发布,公众号发布:存储之谷】,如需转载,请于本人联系,谢谢。

0