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