块设备提出原因:
对于块设备的操作:读取块->修改读出的数据-->擦除块-->把数据写入块。为了提高效率,我们对于块设备采取一种措施:先合并请求,然后在根据合并后的请求来操作块设备,这样可以使得块设备的操作次数降到最低。例如对同一块的不同扇区读取N此,通过和并就变为1次,那么对于前面提到的四个步骤可以得到很高的效率。
块设备h和用户交互:用户层-->文件系统--->块设备驱动。
基于块设备提出的原因:就是要实现块设备合并相同请求之后再处理,所用的算法叫做:电梯调度算法。
第一件事情就是讨论内核如何合并和处理请求的。
1、如何合并请求?合并成功的条件是什么?
使用请求队列和bio结构体:
struct request_queue
{
/*
* Together with queue_head for cacheline sharing
*/
struct list_headqueue_head; //指向队列请求request 链表,也就是链表头指针
}
struct request {
struct list_head queuelist; //用于构造请求队列链表
struct bio *bio; //每个队列请求包含一个或者多个bio结构,这些bio就是合并的结果,具有共同请求。此处指向bio队列第一个
struct bio *biotail; //指向这条队列bio链表的最后一个
struct request_queue *q; //指向队列链表的链表头指针
}
struct bio {
struct bio *bi_next;/* request queue link */ //用于构造bio链表
struct bio_vec *bi_io_vec; //指向bio_vec链表,bio对应块设备上一段连续空间的请求,bio中包含的多个bio_vec用来指出这个请求对应的每页数据内存
....
}
struct bio_vec {
struct page *bv_page; //bio指向的数据内存有多页,每一页对应一个bio_vec
unsigned int bv_len;
unsigned int bv_offset;
};
每个请求就有一个bio结构体,如果请求满足合并要求,就通过bio的结构体的bi_next把这些同请求bio链接为一条链表,这条链表由一个请求队列request 的bio成员指针指向。
所以我们操作请求的时候就可以通过操作request来操作满足同一请求的bio了,这就是合并的结果。并且请求队列也有多条,也是链表,链表头由request_queue->queue_head指向。
当然,对于块设备的数据操作要先封装为bio结构体,把bio结构体和其他待处理bio结构体进行合并,如果可以合并,万事大吉;如果不行那么就需要产生一个新的请求request了,最后就是扫面所有request链表,执行里面的bio了。
如何封装一个bio:
这个内核已经帮我们解决,当我们操作一个快设备节点,那么文件系统会转换为对ll_rw_block的操作,在这个函数里面会调用submit_bh帮我们封装一个bio并提交,具体见下面:
int submit_bh(int rw, struct buffer_head * bh) //bh->b_data指向数据区,bh->b_size表示buffer_head映射的大小,bh->b_page指向buffer_head映射的页地址
bio = bio_alloc(GFP_NOIO, 1); //分配一个bio结构体
bio->bi_io_vec[0].bv_page = bh->b_page; //指向buffer_head映射的页地址
bio->bi_io_vec[0].bv_len = bh->b_size;
bio->bi_io_vec[0].bv_offset = bh_offset(bh); //有效数据相对于buffer_head映射的页地址的偏移值
submit_bio(rw, bio); //提交bio
2、提交之后,我们接下来就需要在提交函数里合并同个请求:
submit_bio(int rw, struct bio *bio)
generic_make_request(bio);
__generic_make_request(bio);
ret = q->make_request_fn(q, bio); //合并请求函数__make_request,见下面
对于make_request_fn需要我们用户调用blk_init_queue初始化队列,该初始化过程重点有提供了(很重要):
init_timer(&q->unplug_timer); //初始化定时器,用于触发执行请求函数
INIT_WORK(&q->unplug_work, blk_unplug_work); //工作队列:blk_unplug_work就是请求执行函数
q->request_fn = rfn; //请求处理函数,后面有用到
blk_queue_make_request(q, __make_request);
q->make_request_fn = mfn; //这个就是用来合并请求函数,看看如何合并的。
__make_request
el_ret = elv_merge(q, &req, bio); //使用电梯算法尝试合并bio,如果失败就要添加新请求队列,见下面
init_request_from_bio(req, bio); //初始化新队列,并且把bio挂上去
add_request(q, req); //请求队列放到链表
具体操作可参考文章:http://blog.csdn.net/iceshirley/article/details/3997630;这篇文章分析很好。
这里说说合并成功的要求是什么:
if ((bio->bi_rw & REQ_DISCARD) != (rq->bio->bi_rw & REQ_DISCARD)) //数据没有被设置为丢弃标志
return 0;
if (bio_data_dir(bio) != rq_data_dir(rq)) //读写方向一样
return 0;
if (rq->rq_disk != bio->bi_bdev->bd_disk || rq->special) //合并的请求属于同一个设备
return 0;
还有其他,看不懂,具体看elv_rq_merge_ok函数;
3、处理请求,所有的请求队列是连成一条链表的,具体看:
struct request {
struct list_head queuelist; //组成链表
}
struct request_queue
{
/*
* Together with queue_head for cacheline sharing
*/
struct list_headqueue_head; //指向上面链表的链表头
}
执我们在处理请求的时候就遍历链表,然后把链表的每一个请求里的所有bio按统一请求处理:
第一种情况:(每一次新建请求队列的时候,可能会触发。条件是请求队列数达到q->unplug_thresh)
__make_request是我们在把bio合并到请求的时候调用的。当我们的请求队列队列数达到q->unplug_thresh书目的时候,就调用执行请求:
__make_request
add_request(q, req);
__elv_add_request(q, req, ELEVATOR_INSERT_SORT, 0);
elv_insert(q, rq, where);
if (nrq >= q->unplug_thresh)
__generic_unplug_device(q);
q->request_fn(q);
generic_unplug_device
__generic_unplug_device(q);
q->request_fn(q); //这个在前面说过了,见blk_init_queue函数
blk_init_queue函数的rfn参数。 这个函数属于编程者提供的。
第二种情况:
猜想要有个定时器,时间一到就不过请求队列过少都必须执行(如果我们的请求队列没有达到那么大),这是必然的,具体如下:
我们在初始化队列的时候:
blk_init_queue
blk_init_queue_node
blk_alloc_queue_node
INIT_WORK(&q->unplug_work, blk_unplug_work);//工作队列
工作队列启动条件:
void blk_unplug_timeout(unsigned long data)
{
struct request_queue *q = (struct request_queue *)data;
trace_block_unplug_timer(q);
kblockd_schedule_work(q, &q->unplug_work); //启动工作队列
}
blk_unplug_timeout是一个定时函数,在blk_init_queue调用的时候调用:
blk_queue_make_request(q, __make_request);
q->unplug_timer.function = blk_unplug_timeout;
那什么时候启动?当然在bio合并到请求的时候:
__make_request
blk_plug_device(q);
mod_timer(&q->unplug_timer, jiffies + q->unplug_delay);
总结一下:
对于块设备而言,用户空间的数据经过文件系统传给块设备驱动的时候,文件系统会把操作通过ll_rw_block函数吧这些数据封装为一个bio,然后通过__make_request函数去遍历request_queue的queue_head链表头指向的清求队列request,寻求和相同请的bio合并,如果bio可以合并到队列的就OK,合并如果不能的就新建新的请求队列并且串入之前存在的队列链表。对于请求队列的执行在__make_request函数里启动一个定时器,这个定时器时间一到就执行请求队列,当然还有一种情况之情请求队列就是队列数达到unplug_thresh时候也会执行。
到这里很清楚,对于初始化队列函数blk_init_queue可以说是我们编程的核心函数:
init_timer(&q->unplug_timer); //初始化定时器,用于触发执行请求函数
INIT_WORK(&q->unplug_work, blk_unplug_work); //工作队列:blk_unplug_work就是请求执行函数
q->request_fn = rfn; //请求处理函数
blk_queue_make_request(q, __make_request);
q->make_request_fn = mfn; //这个就是__make_request函数。
1、分配(alloc_disk)、设置(主次设备号、操作函数集)和提交结构体gendisk(add_disk);
2、blk_init_queue函数调用,传入参数有一个是请求处理函数
3、实现第二步的请求处理函数。
4、设置容量
5、设置柱面、扇区信息。
总结:
对于设备文件的操作最终转为请求队列上的BIO结构体,
执行真正的操作就是执行操作请求队列链表的每条请求队列上的所有相同请求的BIO。(在我们的请求处理函数里实现),例子如下:
static void do_viocd_request(struct request_queue *q)
{
struct request *req;
while ((rwreq == 0) && ((req = blk_fetch_request(q)) != NULL)) { //blk_fetch_request取出一个请求
if (req->cmd_type != REQ_TYPE_FS)
__blk_end_request_all(req, -EIO);
else if (send_request(req) < 0) {
pr_warning("unable to send message to OS/400!\n");
__blk_end_request_all(req, -EIO); //调用blk_end_request_all完成request
}
rwreq++;
}
}