当上层业务需要和磁盘交互时,包括读写请求,以及设备对象初始化时获取磁盘信息,这些都需要将请求转化为SCSI磁盘可以识别的SCSI命令下发到磁盘,这里就涉及到了SCSI命令的转化和执行。
SCSI命令的执行可以分为两种情况:发自SCSI和发自Block,前者多指在SCSI设备对象初始化过程中,比如SCSI子系统需要转动磁盘、查询磁盘信息等,后者多指源自高层经由Block层下发到SCSI子系统读写命令。本文从这两个角度阐述。
执行发自SCSI的命令
如果命令发自SCSI层,则通过scsi_execute_req()发出,下面是执行scsi_execute_req的调用关系,使用这个接口下发的命令,会被封装到一个request中插入Block层中request_queue中,通过Block层的处理逻辑择时下发,这种设计是一个低层代码调用高层代码例子。
scsi_execute_req() scsi_execute_req_flags() scsi_execute() req = blk_get_request(sdev->request_queue); //从设备的request_queue获取request blk_execute_rq() //insert a request into queue for execution blk_execute_rq_nowait(blk_end_sync_rq) //SCSI调用Block函数,注册blk_end_sync_rq()函数 rq->end_io = done; //即blk_end_sync_rq() __elv_add_request() __blk_run_queue() wait_for_completion_io() //通过完成量等待IO结束,发自SCSI层的SCSI命令独有
执行发自Block的命令
发自Block层的SCSI命令多是读写请求,即Block层通过回调SCSI HLDD的scsi_request_fn()等接口,将Block层的request转化为scsi_cmnd,再通过回调queuecommand()接口下发到SCSI LLDD。然而,无论是Block还是Net等,内核中的IO执行的框架均大体相同,都可看作request-response结构,而实现这种框架,需要三部分工作:
- 准备完成回调,并注册之。完成回调根据返回结果将处理发送的消息,常见的有释放承载请求的buffer,相关标志置位,继续向上层通知处理结果等。
- 发起request。
- 低层执行request并根据执行结果上报response。
这一点,SCSI子系统也不无例外。
为了执行来自Block层的IO请求,SCSI子系统在构造相关对象的时候就必须要注册包括但不限于上述handler的回调接口。下面是SCSI总线扫描过程中的核心函数,可以看出,在构造scsi_device对象之时,便已经将关键的函数注册好,其中,request_queue中两个重要的回调接口:request_fn和softirq_done_fn分别在注册为scsi_request_queue和scsi_softirq_done,前者在执行request时的回调接口,后者是收到response时的回调接口。
scsi_probe_and_add_lun() //为SCSI总线上的LUN在内核中构造相应的scsi_device对象 scsi_alloc_sdev() sdev->request_queue=scsi_alloc_queue(sdev) q = __scsi_alloc_queue(sdev->host, scsi_request_fn) blk_init_queue(request_fn) blk_init_queue_node(rfn) blk_init_alloc_queue_node() blk_init_allocated_queue(rfn) q->request_fn = rfn; //即scsi_request_fn() blk_queue_prep_rq(q, scsi_prep_fn) blk_queue_unprep_rq(q, scsi_unprep_fn) blk_queue_softirq_done(q, scsi_softirq_done) q->softirq_done_fn=fn //scsi_softirq_done被注册为response的回调函数 blk_queue_rq_timed_out(q, scsi_times_out); blk_queue_lld_busy(q, scsi_lld_busy);
Block层的上边界是submit_bio(),本文并不讨论Block层的逻辑,对SCSI命令的执行流程来说,当Block层下刷数据到磁盘的时候,回调request_queue中的接口request_fn(),其已经在设备对象初始化时被注册为
scsi_request_fn(),由它来负责下发数据,其中,另一个注册函数scsi_prep_fn()负责将request封装到scsi_cmnd结构,并最终通过megasas_queue_command()将CDB命令写到PCIe设备规定的地址空间中。
submit_bio() generic_make_request() q->make_request_fn() //blk_queue_bio() blk_flush_plug_list() //蓄流泄流:kblockd queue_unplugged() blk_run_queue_async() __blk_run_queue() __blk_run_queue_uncond() q->request_fn(q) //scsi_request_fn() blk_peek_request() q->prep_rq_fn() //scsi_prep_fn() scsi_setup_cmnd() scsi_setup_fs_cmnd() cmd->scsi_done = scsi_done scsi_dispatch_cmd() queuecommand() //megasas_queue_command() build_and_issue_cmd(instance, scmd) //megasas_build_and_issue_cmd() fire_cmd() //megasas_fire_cmd_gen2() scmd->scsi_done() //Invoke completion on finished SCSI command blk_complete_request() __blk_complete_request() if (list->next == &req->ipi_list) raise_softirq_irqoff(BLOCK_SOFTIRQ);