(转载请注明出处,发现问题请联系raymond_rule@hotmail.com)

  公司现有的针对AHCI Controller的测试子系统运行模式是:向Controller写入cmdàController向硬盘发送cmdà等待一段固定时间à查看cmd执行结果à向Controller写入下一个cmd……,这个过程有很明显的效率瓶颈:等待固定时间和硬盘执行cmd耗时。而硬盘执行cmd的耗时是无法人为控制的,因此只能从“等待固定时间”方面找突破口。

  目前能想到的几种解决办法:

  一. 不等待固定时间,而是采用一种类似“异步通知”的方式,当cmd执行完毕后,自动“通知”系统开始发送下一个cmd。

  二. 联想到AHCI Controller具有32个cmd slot,因此可以利用此特征来并行执行32个cmd(其实最多31个),这样测试效率会提高30倍。

  本文就第二种方式展开探索,记录尝试解决问题的详细过程。(结果是:第二种方式不可行)

   从上一篇文章(requestATA cmd的转换过程)了解到:Kernel向AHCI Controller寄存器中写入cmd需要经过两处可能引起写入失败的地方,一是从request queue中摘取request并判断是否可向下层转发的过程,二是在ata_scsi_translate()中申请cmd slot的过程。而在第二处位置,能否申请到cmd slot的决定因素是AHCI Controller是否有空闲的cmd slot,因此可以不考虑。

下面,从第一处位置入手,继续深入看scsi_request_fn()的实现:

 

  从代码来看,从queue中摘取request并继续下发的条件是:queue中有request,并且scsi_dev_queue_ready()返回1,而scsi_dev_queue_ready()主要调用scsi_device_busy():

 

  因此,若device_busy>=queue_depth或者device_blocked为真,则scsi_dev_queue_ready()返回0,从而scsi_request_fn()无法下发request。

1. sdev->device_busy的来龙去脉

初始值:

  在scsi_alloc_sdev()分配scsi_device结构体内存时,device_busy自动初始化为0.

增加:

  在scsi_request_fn()中,每下发一个request时,就把device_busy递增1。

减小:

  每当收到完成cmd的中断,就会最终调用scsi_device_unbusy()来把device_busy减1。

  可以看出:实际上,device_busy跟踪记录Controller当前正在处理的cmd数量。

2. sdev->queue_depth的来龙去脉

初始值:

  在scsi_alloc_sdev()中,调用scsi_adjust_queue_depth()来把queue_depth的初始值设为cmd_per_lun,cmd_per_lun是由ahci_sht来指定的(初始为ATA_SHT_CMD_PER_LUN=1)。

初次调整:

  在初始化时,如果enable了NCQ,则会在ata_scsi_dev_config()中调用scsi_adjust_queue_depth()把queue_depth调整到与NCQ depth相符;否则,使用初始值。

运行时调整:

  在系统运行期间,可以通过sysfs来实时地调整queue_depth的大小:/sys/device/pcixxxxxxxxx/…../queue_depth

 

  其中change_queue_depth指向ata_scsi_change_queue_depth(),注意在ata_scsi_change_queue_depth()中,如果在disable NCQ的情况下,是不允许把queue_depth设置成大于1的数值的。

 

        

3. device_blocked的来龙去脉

初始值:

  在scsi_alloc_sdev()分配scsi_device结构体内存时,device_blocked自动初始化为0。

改为非0值:

  Kernel可能会调用到__scsi_queue_insert(),从而将device_blocked设为max_device_blocked(max_device_blocked的初始值为SCSI_DEFAULT_DEVICE_BLOCKED,随后会被设置成固定值1)。可能调用__scsi_queue_insert()的情况有两种:一是完成某个cmd时,二是在调用scsi_dispatch_cmd()时。

  当完成某个cmd进行中断处理时,根据完成cmd的status,传入good_bytes参数:

 

  当调用scsi_dispatch_cmd()时,会根据scsi_device_blocked()的返回值来选择是否调用scsi_queue_insert()。

 

  综合这两种情况来看,都不属于常态,device_blocked在大多数情况下其值都是0。

恢复为0值:

  当某个cmd完成时,会调用scsi_finish_cmd(),在该函数一开始就会把device_blocked恢复为0。

  从上面的分析来看,device_blocked标记device的一种状态,用以提示上层:当前的device是否可以接受cmd。

  至此,综合上面的分析,如果我们这样修改Kernel:

         a. Disable NCQ。增加ATA_HORKAGE_NONCQ标识

         b. 增加queue_depth使之大于1。修改sysfs接口,使之能够echo大于1的数。

  那么,就应该能使scsi_dev_queue_ready()返回1,从而让request向下发送(即转换成SCSI cmd、ATA cmd),最终达到向AHCI Controller的多个cmd slot都写入cmd的目的。

(但是经过实际试验发现,这种做法是不可行的)

  按照上面的方案,修改Kernel,进行实验,结果是Kernel依然不会向AHCI Controller的多个cmd slot写入cmd,即不会出现某一时刻cmd slot中有多个cmd并存的情形。这是什么原因呢?继续看Kernel,从中找答案。

1. 在查看Kernel处理cmd的流程时,会发现如下的注释:

ata_std_qc_defer()的注释:

 

ata_qc_issue()中的注释:

 

  从这些只言片语中,可以看出:non-NCQ命令不能与其他任何命令共存,并且upper layer只根据queue depth来决定是否向下发送cmd(这是我们之前的修改所关注的),而low layer需要自己负责保证同一时刻只有一个cmd是outstanding的。

2. 下层driver如何保证只有一个outstanding cmd呢?

  下层driver利用函数接口qc_defer()的返回值来限制真正向AHCI Controller的cmd slot写入cmd的数目。截取ata_scsi_translate()部分代码如下:

 

  从上面代码可以看出,把SCSI cmd转换成了ATA cmd之后(A部分),还需要经过是否defer的判断(B部分),才能最终将ATA cmd写入AHCI Controller的寄存器中(C部分)。qc_defer指向的函数是:ahci_pmp_qc_defer(),通常会调用到ata_std_qc_defer():

 

  如果要发送的cmd是non-NCQ cmd,那么只有当ata_tag_valid()返回0,并且sactive==0时,该cmd才会被写入Controller寄存器;如果是NCQ cmd,则只需要ata_tag_valid()返回0,该cmd就会被写入寄存器。其他情形,返回ATA_DEFER_LINK,从而不会把该cmd写入Controller寄存器中。因此,ata_tag_valid()与active_tag是关键所在。

ata_tag_valid()定义:

 

  其中,ATA_MAX_QUEUE定义为32。也就是说如果形参tag是小于32的,则表示该tag有效,ata_tag_valid()返回1;否则,返回0。

active_tag值的变化情况:

初始值:

  在初始化ata_link时,active_tag赋值为ATA_TAG_POISON(即0xfafabfcfdU)

当把cmd写入寄存器时:

  对于non-NCQ cmd,就会把该cmd所占用的tag计入active_tag。如果是NCQ cmd,则保留active_tag的初始值(0xfafbfcfdU),并且把该cmd对应的tag保存在sactive中。

 

当完成一个cmd时:

  当Driver收到完成cmd的中断后,会调用__ata_qc_complete(),如果完成的是non-NCQ cmd,则把active_tag恢复成初始值ATA_TAG_POISON;否则不修改active_tag的值(即依然保持为0xfafbfcfdU)。

  综合上述分析,可以看出:active_tag的作用是跟踪目前的non-NCQ cmd所在的cmd slot。当已经有non-NCQ cmd写入Controller寄存器中时,ata_tag_valid()返回1,则ata_std_qc_defer()就会返回ATA_DEFER_LINK,从而使得该cmd不会被立即写入Controller的寄存器中。这就是上面实验会失败的原因。

 

3. 当non-NCQ cmd被defer后,接下来会发生什么?

  non-NCQ cmd被defer后,当然不能被写入Controller,但该cmd是由request queue摘除的request一步步转换而来,如果不能向硬盘发送,那么,当然会被重新加入到queue中。

ac_defer()返回ATA_DEFER_LINK引起ata_scsi_translate()返回SCSI_MLQUEUE_DEVICE_BUSY,沿着函数调用栈一步步向上追溯,queuecommand()也返回SCSI_MLQUEUE_DEVICE_BUSY。

 

  再继续追溯,会发现该cmd对应的timer等都会一一被清理,同时还会设置device_blocked,这样下次调用scsi_request_fn()时,scsi_dev_queue_ready()就会返回0,就不必再进行摘取request、开始计时等动作,而是直接返回。总之被defer的cmd会被重新放入request queue中,静静地等待下一次被取出。

最后的思考:

  当Controller的cmd slot中有non-NCQ cmd时,无法再写入一个non-NCQ cmd的规定到底是基于什么原因?Controller到底是否运行Driver写入多个non-NCQ cmd slot?如果Controller允许这样做,那么可以实现利用多个cmd slot来提高I/O performance的目的。

posted on 2015-05-05 17:01  pxdbxq  阅读(894)  评论(1)    收藏  举报