内核使用大量的缓存、异步、优化机制来提高IO性能,这一点Block IO子系统也不例外。以最常见的buffered IO为例,所有需要写入磁盘的数据起初都是存在Page Cache中,由文件系统来管理,显然,这样的IO是基于task_struct的,在当前版本中,内核使用task_struct->buffer_head来管理task_struct提交的IO请求。然后,借助write_back机制,在合适的时机将存储了task_struct IO数据的脏页下刷到磁盘,即write_back线程调用Block IO子系统提供的submit_bio()接口。然而,这样的bio也不是直接转换为CDB直接落盘,Block IO子系统也有自己的异步、优化机制,这就是本文要讨论的重点,具体的,IO路径中Block层路段有以下几个重要的话题:
- pluging /unpluging 机制
- IO Scheduler的优化策略
- Block层与SCSI层衔接
pluging/unpluging机制使通用块层的request批量下发到Blk-core层,并进一步下发到底层设备。这个机制说起来也简单,FS下发的bio描述了一个IO的源头与目的地,Block层想要对其进行优化,上层提交一次request就发送一次CDB显然是不行的。而要优化,无非两种前提:池(缓存)使用空间为优化提供机会,异步使用时间为优化提供机会。这里,数据已经在内核空间的主存中,再缓存一次显然是浪费资源,所以,为了优化,Block层使用了异步机制为优化创造条件,也就是pluging/unpluging机制。
具体地,每个设备都有自己的水位线:BLK_MAX_REQUEST_COUNT,表示该设备一次性可以处理的request数量,为了最大限度的提高硬件性能,同时为IO调度器提供优化的机会,绝大多数意欲下发到底层设备的request(IO barrier request除外)都会先进入到task_struct->plug->list缓存起来,待request数量足够的时候,一次性蓄在其中的request加入到elv中优化,再转变为CDB下发到设备,而这掌控蓄放的塞子plug,就由下发线程负责,每下发一个request,都会检查是否达到了警戒水位,如此,便实现了异步下发。对下发的bio/request来说,核心函数是blk_queue_bio().
pluging
submit_bio() //blk-core.c generic_make_request(struct bio * bio) //static私有方法 bio_list_init() //同一时刻只允许进程有一个active do{q->make_request_fn() }while(bio) //blk_queue_bio() where = ELEVATOR_INSERT_SORT get_request() //failed to merge, get a new request req=init_request_from_bio() //根据bio初始化request if plug: //塞子塞住的情况下 list_add_tail(&req->queuelist, &plug->list) //add request to plug->list
unpluging
submit_bio() //blk-core.c generic_make_request(struct bio * bio) //static私有方法 bio_list_init() //同一时刻只允许进程有一个active do{q->make_request_fn() }while(bio) //blk_queue_bio() where = ELEVATOR_INSERT_SORT get_request() //failed to merge, get a new request req=init_request_from_bio() //根据bio初始化request if unplug add_acct_request(q, req, where) __elv_add_request(q, rq, where) ELEVATOR_INSERT_SORT elv_rqhash_add(q, rq) q->elevator->type->ops.elevator_add_req_fn(q, rq); //noop_add_request list_add_tail(&rq->queuelist, &nd->queue); //noop,nd->queue只是一个list_head __blk_run_queue() __blk_run_queue_uncond() q->request_fn(q) //回调scsi_request_fn() blk_peek_request(q); __elv_next_request return list_entry_rq(q->queue_head.next) or elevator_dispatch_fn //noop_dispatch,.=>request_queue q->prep_rq_fn(q, rq) //回调scsi_prep_fn() cmd = scsi_get_cmd_from_req(sdev, req) cmd = scsi_get_command() cmd = __scsi_get_command() cmd = scsi_host_alloc_command() cmd = kmem_cache_zalloc() INIT_DELAYED_WORK() list_add_tail(&cmd->list, &dev->cmd_list) req->special = cmd //双向引用 cmd->request = req cmd->cmnd = req->cmd scsi_setup_cmnd() scsi_setup_fs_cmnd() scsi_cmd_to_driver(cmd)->init_command(cmd); //回调sd_init_command() sd_setup_discard_cmnd() //根据request->cmd_flags 4选1 sd_setup_write_same_cmnd() sd_setup_flush_cmnd() sd_setup_read_write_cmnd() //针对普通读写设置scsi_cmnd->cmnd cmd = req->special cmd->scsi_done = scsi_done; scsi_dispatch_cmd(cmd) host->hostt->queuecommand(host, cmd)
IO Scheduler的优化策略
在一个IO的生命周期中,IO 调度器的作用主要体现在以下几个3个方面:
- 合并bio到现有请求队列已有的request
- 合并request到现有请求队列已有的request
- 对不能合并的request在其添加时进行重排等优化处理
下述接口是blk-core层为不同IO 调度器提供的接口,需要其根据需求来实现,本文不讨论这些IO调度器具体的实现,重点讨论这些接口含义以及在IO生命周期中的调用时机。
对于一个IO调度队列来说,其在内核中最能体现自己价值的方式就是对elevator_ops的实现。
type | field | description |
---|---|---|
elevator_merge_fn* | elevator_merge_fn | 查找request_queue中可以和传入bio合并的request,如果可以合并,返回值取ELEVATOR_FRONT_MERGE或ELEVATOR_BACK_MERGE,并将相应的request通过输入参数带出。如果不可合并,返回值为ELEVATOR_NO_MERGE |
elevator_merged_fn* | elevator_merged_fn | 在调度器有请求被合并时调用 |
elevator_merge_req_fn* | elevator_merge_req_fn | 两个request被合并时调用 |
elevator_allow_merge_fn* | 判定bio可以被安全合并到现有request时调用 | |
elevator_bio_merged_fn* | elevator_bio_merged_fn | |
elevator_dispatch_fn* | elevator_dispatch_fn* | 将准备好的request们移到派发队列request_queue |
elevator_add_req_fn* | elevator_add_req_fn | 向调度器中添加一个新请求时调用 |
elevator_activate_req_fn* | elevator_activate_req_fn | 在块设备驱动首次看到一个请求时被调用,IO调度器可以用这个回调来确定请求执行从什么时候开始 |
elevator_deactivate_req_fn* | elevator_deactivate_req_fn | 在块设备决定要延迟一个请求,把它重新排入队列时调用 |
elevator_completed_fn* | elevator_completed_fn | 请求完成时被调用 |
elevator_request_list_fn* | elevator_former_req_fn | 该函数按照磁盘排序顺序在给定请求前面的哪一个请求,被用来查找合并的可能性 |
elevator_request_list_fn* | elevator_latter_req_fn | |
elevator_init_icq_fn* | elevator_init_icq_fn | |
elevator_exit_icq_fn* | elevator_exit_icq_fn | |
elevator_set_req_fn* | elevator_set_req_fn | |
elevator_put_req_fn* | elevator_put_req_fn | |
elevator_may_queue_fn* | elevator_may_queue_fn | |
elevator_init_fn* | elevator_init_fn | 为队列分配elv-specific的空间,是elv的私有数据 |
elevator_exit_fn* | elevator_exit_fn | |
elevator_registered_fn* | elevator_registered_fn |
在IO过程中,主要的调用点如下:
submit_bio() //blk-core.c generic_make_request(struct bio * bio) //static私有方法 bio_list_init() //同一时刻只允许进程有一个active do{q->make_request_fn() }while(bio) //blk_queue_bio() elv_merge() //attempt to merge bio to an existed request elv_bio_merged() //elevator_bio_merged_fn(); attempt_back_merge() elv_merged_request() //elevator_merged_fn(); //attempt to merge request to an existed one submit_bio() //blk-core.c generic_make_request(struct bio * bio) //static私有方法 bio_list_init() //同一时刻只允许进程有一个active do{q->make_request_fn() }while(bio) //blk_queue_bio() where = ELEVATOR_INSERT_SORT if unplug add_acct_request(q, req, where) __elv_add_request(q, rq, where) ELEVATOR_INSERT_SORT elv_rqhash_add(q, rq) q->elevator->type->ops.elevator_add_req_fn(q, rq); //noop_add_request list_add_tail(&rq->queuelist, &nd->queue); //noop,nd->queue只是一个list_head __blk_run_queue() __blk_run_queue_uncond() q->request_fn(q) blk_peek_request(q); __elv_next_request return list_entry_rq(q->queue_head.next) or elevator_dispatch_fn //noop_dispatch,.=>request_queue
与SCSI 子系统接口
submit_bio() //blk-core.c generic_make_request(struct bio * bio) //static私有方法 bio_list_init() //同一时刻只允许进程有一个active do{q->make_request_fn() }while(bio) //blk_queue_bio() __blk_run_queue() __blk_run_queue_uncond() q->request_fn(q) blk_peek_request(q); __elv_next_request return list_entry_rq(q->queue_head.next) or elevator_dispatch_fn //noop_dispatch,.=>request_queue q->prep_rq_fn(q, rq) //scsi_prep_fn() or cmd = scsi_get_cmd_from_req(sdev, req) cmd = scsi_get_command() cmd = __scsi_get_command() cmd = scsi_host_alloc_command() cmd = kmem_cache_zalloc() INIT_DELAYED_WORK() list_add_tail(&cmd->list, &dev->cmd_list) req->special = cmd //双向引用 cmd->request = req cmd->cmnd = req->cmd cmd = req->special cmd->scsi_done = scsi_done; scsi_dispatch_cmd(cmd) host->hostt->queuecommand(host, cmd)