三、代码结构(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博客作者 “底层存储技术” http://blog.51cto.com/12580077 个人发布,公众号发布:存储之谷】,如需转载,请于本人联系,谢谢。