Linux 块设备驱动分析(二)

Linux 块设备驱动分析(一)
Linux 块设备驱动分析(二)
Linux 块设备驱动分析(三)

块IO请求的处理过程

页高速缓存(page cache)是Linux内核实现磁盘缓存,它主要用来减少对磁盘的I/O操作。具体地讲,是通过把磁盘中的数据缓存到物理内存中,把对磁盘的访问变为对物理内存的访问。

如读取一个文件,首先会先检查你读的那一部分文件数据是否在高速缓存中,如果在,则放弃访问磁盘,而直接从内存中读取,如果不在,则去磁盘中读取。

对于写操作,应用如果是以非SYNC方式写的话,写的数据也只是进内存,然后由内核帮忙在适当的时机回写进硬盘。

内存的页最终是要转化为硬盘里面真实要读写的位置的。在Linux里面,用于描述硬盘里面要真实操作的位置与page cache页的映射关系的数据结构是bio,定义如下:


struct bio {
	struct bio		*bi_next;	
	struct block_device	*bi_bdev;     //块设备
	struct bvec_iter	bi_iter;      //磁盘位置信息等
	unsigned short		bi_vcnt;	  //bio_vec数组的大小
	struct bio_vec		*bi_io_vec;	  //bio_vec数组
	
	......

};

struct bvec_iter {
	sector_t		bi_sector;	     //块I/O操作在磁盘上的起始扇区
	unsigned int	bi_size;	     //还没有传输的字节数

	unsigned int	bi_idx;		     //bio_vec数组的当前索引

	unsigned int    bi_bvec_done;	//bi_io_vec[bi_idx]完成的字节数
}

struct bio_vec {
	struct page		*bv_page; //该bio_vec对应的page描述符
	unsigned int	bv_len;   //数据大小
	unsigned int	bv_offset; //数据在页面中的偏移
};

每个bio对应的硬盘里面一块连续的位置 ,可能对应着page cache的多页,或者一页,所以它里面会有一个bio_vec数组。

在这里插入图片描述
文件系统构造好bio之后,调用通用块层提供的submit_bio函数,向通用块层提交I/O请求。bio进入通用块层后,将会由I/O调度层进行合并等操作,所谓I/O请求合并就是将进程内或者进程间产生的在物理地址上连续的多个IO请求合并成单个IO请求一并处理,从而提升IO请求的处理效率。

比如有bio1(对应磁盘A的第100-101块,写操作)和bio2(对应磁盘A的第102-103块,写操作),那么这两个bio是可以合并为一个request。
在这里插入图片描述
submit_bio函数定义如下:

blk_qc_t submit_bio(struct bio *bio)
{
	
	......

	//generic_make_request函数会调用request_queue的make_request_fn成员函数,制造请求
	return generic_make_request(bio)
}

块设备有个请求队列:

struct gendisk {
	
	......
	struct request_queue *queue; //请求队列
	......
};


struct request_queue {
	......
	struct elevator_queue	*elevator;     // I/O电梯调度器
	......
	request_fn_proc		*request_fn;       //处理request的回调函数
	make_request_fn		*make_request_fn;  //make_request_fn回调函数
	......
};

显然,请求队列的make_request_fn回调函数是由块设备驱动设置的。一般,驱动程序会调用blk_init_queue函数申请并初始化一个请求队列,其make_request_fn回调函数会设置成blk_queue_bio。我们就分析这个函数:

static blk_qc_t blk_queue_bio(struct request_queue *q, struct bio *bio)
{
	struct blk_plug *plug;
	int el_ret, where = ELEVATOR_INSERT_SORT;
	struct request *req;
	unsigned int request_count = 0;

	
	
	if (!blk_queue_nomerges(q)) {

		//尝试与进程本地的plug队列里的request合并
		if (blk_attempt_plug_merge(q, bio, &request_count, NULL))
			return BLK_QC_T_NONE;
	} else
		request_count = blk_plug_queued_count(q); 

	/* request_count保存着进程本地的plug队列的request数目 */

	spin_lock_irq(q->queue_lock);

	/* 不能合并到plug队列里的request,则尝试合并到I/O电梯调度器(request_queue->elevator_queue)
     的调度队列
   */
	el_ret = elv_merge(q, &req, bio);
	......

get_rq:
	//都不能合并的话,产生一个新的request
	req = get_request(q, bio->bi_opf, bio, GFP_NOIO);
	......

	//通过bio初始化这个新的request
	init_request_from_bio(req, bio);

	......

	plug = current->plug;
	if (plug) {

		if (!request_count)
			trace_block_plug(q);
		else {
			/* 如果进程本地的plug队列的request数目达到BLK_MAX_REQUEST_COUNT(16)个
         则把进程本地的plug队列的request泄洪到I/O电梯调度器的调度队列
       */
			if (request_count >= BLK_MAX_REQUEST_COUNT) {
				blk_flush_plug_list(plug, false); //泄洪
				trace_block_plug(q);
			}
		}
		//把新的request插入到进程本地的plug队列
		list_add_tail(&req->queuelist, &plug->list);
		blk_account_io_start(req, true);
	} else {
		......
	}

	return BLK_QC_T_NONE;
}

来个整体的流程图:
在这里插入图片描述

电梯排序

当各个进程本地的plug list里面的request满了,就要泄洪,以排山倒海之势进入的,不是最终的设备驱动,而是I/O电梯调度器。
在这里插入图片描述
进入调度器进行电梯调度,其实目的有:

  1. 进一步的合并request
  2. 把request对硬盘的访问变得顺序化

在请求队列里有一成员elevator:

struct request_queue {
	......
	struct elevator_queue	*elevator; //IO调度器
	......
};

struct elevator_queue
{
	struct elevator_type *type; 
	void *elevator_data;
	......
	DECLARE_HASHTABLE(hash, ELV_HASH_BITS);
};

struct elevator_type
{
	......
	struct elevator_ops ops; //调度器的操作集
	......
};

struct elevator_ops
{
	......
	//调度器出口函数,用于将调度器内的IO请求派发给设备驱动
	elevator_dispatch_fn *elevator_dispatch_fn;

	//调度器入口函数,用于向调度器添加IO请求
	elevator_add_req_fn *elevator_add_req_fn;
	......
};

内核提供了3个I/O电梯调度器,Noop、Deadline和CFQ调度器,默认的调度器是CFQ。CFQ调度器为系统内的所有任务分配均匀的I/O带宽, 提供一个公平的工作环境, 在多媒体应用中, 能保证音、 视频及时从磁盘中读取数据。

可以通过类似如下的命令, 改变一个设备的调度器:

echo SCHEDULER > /sys/block/DEVICE/queue/scheduler

接下来就来分析blk_flush_plug_list函数:

void blk_flush_plug_list(struct blk_plug *plug, bool from_schedule)
{
	struct request_queue *q;
	unsigned long flags;
	struct request *rq;
	LIST_HEAD(list);
	unsigned int depth;

	......

	list_splice_init(&plug->list, &list);

	//对蓄流链表中的requset进行排序,以扇区大小进行排序
	list_sort(NULL, &list, plug_rq_cmp);

	......

	local_irq_save(flags);
	//循环的取出蓄流链表中的requset
	while (!list_empty(&list)) {
		rq = list_entry_rq(list.next);
		list_del_init(&rq->queuelist);
	
		......

		//向调度器中添加IO请求
		if (rq->cmd_flags & (REQ_PREFLUSH | REQ_FUA))
			__elv_add_request(q, rq, ELEVATOR_INSERT_FLUSH); 
		else
			__elv_add_request(q, rq, ELEVATOR_INSERT_SORT_MERGE);

		depth++;
	}

	......
	if (q)
		//调用request_queue->request_fn函数处理请求
		queue_unplugged(q, depth, from_schedule); 

	local_irq_restore(flags);
}

__elv_add_request:

void __elv_add_request(struct request_queue *q, struct request *rq, int where)
{

	
	rq->q = q;

	......

	switch (where) {
	
	......

	case ELEVATOR_INSERT_SORT_MERGE:
		//尝试与调度队列中的request合并,不能合并则落入ELEVATOR_INSERT_SORT分支
		if (elv_attempt_insert_merge(q, rq))  
			break;
	case ELEVATOR_INSERT_SORT:
		......
		if (rq_mergeable(rq)) { 
			elv_rqhash_add(q, rq);
			if (!q->last_merge)
				q->last_merge = rq;
		}

		//调用elevator_ops->elevator_add_req_fn函数将新来的request插入到调度队列合适的位置
		q->elevator->type->ops.elevator_add_req_fn(q, rq);
		break;

	......
}

request_queue的request_fn成员函数由驱动程序提供,该函数的主要工作是,从请求队列中提取出请求,然后进行硬件上的数据传输,传输完数据后,调用__blk_end_request_all函数报告请求处理完成。下面给出一个request_fn示例:

static void do_request(struct request_queue *q)
{
	struct request *req;
	struct bio *bio;
	//从request_queue提取request
	while ((req = blk_fetch_request(q)) != NULL) 
	{
		
		if(req->cmd_type != REQ_TYPE_FS)	//请求类型不是来自fs
		{
			printk(KERN_ALERT"Skip non-fs request\n");
			__blk_end_request_all(req, -EIO); //报告出错
			break;
		}

		__rq_for_each_bio(bio, req)	//遍历request的bio
			ramblk_xfer_bio(bio);  //处理单个bio
		
		//报告请求处理完成
		__blk_end_request_all(req, 0);
		
	}
}

提取请求的函数为blk_fetch_request:

struct request *blk_fetch_request(struct request_queue *q)
{
	struct request *rq;

	/* 返回下一个要处理的请求(由I/O调度器决定),如果没有请求则返回NULL 
     request_queue->queue_head链表不空,则从该链表取出request,如果空,则会
     调用elevator_ops->elevator_dispatch_fn函数,让I/O调度器派发request到queue_head链表
   */
	rq = blk_peek_request(q);
	if (rq)
		blk_start_request(rq); //启动请求,将请求从请求队列中移除
	return rq;
}

总结下blk_flush_plug_list函数的处理流程:
在这里插入图片描述

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值