传统ATA的驱动框架如上图所示,传统的ATA Host架构在PCI总线之上,在PCI总线扫描过程中枚举得到。PCI扫描程序Scan到ATA Host之后会加载该设备的驱动程序,即ATA Host Driver,该Driver亦是一个PCI Device的驱动。ATA Host会被注册到IDE Core Level驱动层,从而生成一条IDE的总线,IDE核心层在对ATA Host初始化完毕之后会扫描该Host,并且加载适合设备的IDE Device Driver。如果Ide Device Driver是一个Ide Disk的驱动,那么ATA磁盘将会被驱成HD设备;如果驱动是Ide-Scsi,那么ATA Device将会被虚拟成一个SCSI Host,并且将该Host加入到Scsi Middle Leve驱动层,同样的原理,Scsi Middle Level驱动层会扫描这个虚拟的Scsi host,然后加载扫描得到设备的驱动,这个驱动通常为scsi disk driver,此时,一个传统IDE设备被驱成了一个SCSI设备。从上述的驱动栈我们可以看出,IDE设备被虚拟成SCSI设备的关键在于IDE Device Driver,在该层对设备进行了虚拟化处理,形成了一条虚拟的SCSI总线,然后再将设备虚拟成了SCSI Device,按照这种思路,我们可以不断的进行设备虚拟和总线层叠扩展。
从上述驱动框架来看,IDE总线层作用并不是很大,因此,完全可以将IDE总线层抛弃,直接采用如下图所示的驱动框架,这也是目前SATA等驱动常用的驱动模型。
在上述驱动模型中,ATA Host的枚举过程与第一个模型保持一致,但是ATA Host Driver会直接将ATA Host注册到SCSI Middle Level层,考虑到ATA协议层与SCSI协议层存在差异,因此,通过LibATA驱动作为SCSI Middle Level与ATA Host之间的转换层,从而可以很好的将ATA Host直接融入到SCSI的驱动体系中来,可以直接将ATA设备驱成SCSI Device。与第一个模型相比,这个模型的驱动栈变浅了,驱动效率提高了,而且可以无缝的将ATA驱动融入到SCSI驱动体系中。在这个驱动模型中,LibATA驱动无疑是最大的功臣。目前,很多SATA Host驱动以及PATA Host的驱动都采用这种模型。
Scsi Disk设备驱动通过Scsi middle level层向scsi host提交请求,scsi disk driver是一个块设备驱动,其调用块设备层的unplug_fn函数处理scsi disk的请求队列。在处理scsi disk请求队列过程中会调用scsi middle level层注册的scsi_request_fn函数实现具体的操作。Scsi_request_fn函数会从io调度队列中获取一个请求,然后将该请求转换成scsi command,最后直接调用scsi_dispatch_cmd函数将scsi命令提交给scsi host。Scsi host与scsi middle level层的接口是queue_command函数,每个scsi host驱动都会向scsi middle level层注册具体的queue_command方法。由于Queue_command函数在不可睡眠的上下文中执行,所以其不能处理复杂的操作,通常的操作是将接收到的scsi命令放置到scsi host维护的处理队列中。如果一个真是的scsi host,而不是一个虚拟的host时,那么在scsi host层可以将scsi command通过DMA的方式传输到scsi disk。上述过程完成了一个io请求的提交过程,对于诸如磁盘这样的设备,在这一过程中需要考虑到存储介质的特性以及应用访问模式的特性,所以需要做一些IO调度的策略,使得scsi磁盘的读写更加满足存储介质的特性。当然也可以在scsi disk的上层实现更加高级的IO管理策略。IO请求的提交可以理解为整个IO过程的前半部,那么后半部就是IO完成的回调过程,下文分析Linux中IO回调路径的具体实现。
当一个IO事件完成之后,scsi disk会采用中断的方式通知scsi host驱动。当scsi host的中断事件发生之后,CPU会执行host的中断服务程序,通常实际的scsi host都会以PCI设备的形式存在,考虑到中断共享问题,在中断服务程序中首先需要进行中断事件的判断,然后根据scsi host的状态寄存器进行具体中断任务的处理。对于读写IO请求,当数据DMA到scsi disk之后会产生DMA结束中断信号,在DMA过程中可以采用聚散DMA(scatter-gather DMA)的技术,因此这一过程不会涉及到数据的内存拷贝,也就是说在读写IO过程中,数据一直处于bio的Page页中(写数据过程中会直接将Page页中的数据DMA到磁盘,读数据过程中直接将磁盘中的数据DMA到bio的Page中,这样的处理机制效率较高)。当host确定完成请求之后,会调用scsi middle level的回调函数,该回调函数就是著名的scsi_done。Scsi_done在queue_command的过程中被提交到scsi host层。在scsi_done函数中直接调用了blk_complete_request函数,该函数通过raise_softirq_irqoff(BLOCK_SOFTIRQ)触发了scsi的软中断。到目前为止,上述过程都在scsi host的中断上半部中执行。中断上半部运行时间不能过长,否则会导致中断事件的丢失。触发软中断之后,中断上半部就可以退出了。在退出上半部之后,CPU将会交给已经触发的scsi软中断服务程序,此时可以看到软中断的服务程序仍然运行在中断上下文,并不是一个可以调度的context。
软中断的执行函数是blk_done_softirq,由于是scsi command引发的中断事件,因此会调用事先注册到请求队列上的scsi_softirq_done函数,完成具体的scsi软中断下半部事件处理。在该函数中会进行一些scsi command执行的正确性判断,如果命令执行错误,那么可以采用重试的方法进行命令的requeue处理,当重试到一定程度之后会将执行错误的scsi命令交给scsi错误处理内核守护进程,进行最后的判决;如果执行成功,那么调用scsi_finish_command函数结束掉scsi命令。在scsi_finish_command函数中调用scsi_io_completion函数结束块级的io request,具体会调用scsi_end_request函数,然后调用blk_end_request函数,最后调到blk_end_io函数。在blk_end_io函数中会结束掉request上的所有bio,结束bio的过程可以调用bio_endio函数。Request中的所有bio都结束之后释放request资源。至此,一个bio所在的request被scsi disk处理完毕之后,通过中断上半部和下半部已经全部处理完毕。这里需要注意的是,IO的所有回调过程都是在中断上下文中处理的,所以在编写IO的回调函数时需要注意睡眠问题,需要考虑内存分配可能带来的睡眠,信号量的使用会导致睡眠,从而使系统崩溃。
通过上述分析,scsi disk的正常IO回调路径涉及的函数描述如下:
IO完成后中断响应->scsi_done
->blk_complete_request
->raise_softirq_irqoff(BLOCK_SOFTIRQ)
->blk_done_softirq
->scsi_softirq_done
->scsi_finish_command
->scsi_io_completion
->blk_end_request
->blk_end_bidi_request
->blk_finish_request
->req.end_io
最近Linux在Scsi middle level方面变化比较大,上述的分析基于较新的Linux-2.6.28版本,和前面的2.6.18版本相比IO回调流程也发生了一定变化。