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电梯调度器。
进入调度器进行电梯调度,其实目的有:
- 进一步的合并request
- 把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函数的处理流程: