在计算机系统启动过程中,操作系统会扫描默认的PCI根总线,从而触发了PCI设备扫描的过程,开始构建PCI设备树。SCSI主机适配器作为PCI设备会被PCI总线驱动层扫描到(PCI设备的扫描采用配置空间访问的方式),扫描到SCSI主机适配器之后,操作系统开始加载SCSI主机适配器驱动,SCSI主机适配器驱动就是上面所说的低层驱动。SCSI主机适配器驱动根据SCSI主机适配器模板分配SCSI主机适配器描述符,并添加到系统。在此之后或与此同时,还需要启动通过SCSI主机适配器扩展出来的下一级总线——SCSI总线的扫描。
1.PCI总线扫描主机适配器,并加载主机适配器驱动,低层驱动。
2.SCSI总线扫描SCSI设备,并加载高层驱动sd_probe。
SCSI子系统的驱动模型设备关系
SCSI总线扫描的目的是通过协议特定或芯片特定的方式探测出挂接在主机适配器后面的目标节点和逻辑单元,为它们在内存中构建相应的数据结构,将它们添加到系统之中。不同的主机适配器实现可能采取不同的拓扑发现和设备添加机制。这里列举三种主要的方法。
1.有的主机适配器实现将拓扑发现的工作放在主机适配器固件(HBA Firmware)中执行,例如某些带RAID功能的主机适配器。它必须同时提供一种消息传递机制,让主机适配器驱动知道拓扑发现已经完成,并从主机适配器固件获得发现的所有设备,将它们添加到操作系统。例如LSI的MPT驱动就是这样做的,参见文件driver/message/fusion的实现。(主机适配器固件(HBA Firmware)中执行)
2.其他的主机适配器实现则由主机适配器驱动(HBA Driver)来负责拓扑发现。这又区分为两种:第一种不需要调用SCSI中间层提供的服务完成。例如SAS(串行SCSI,Serial Attached SCSI)和FC(光纤通道,Fibre Channel)协议,按照自己的方式扫描总线,将发现的SCSI设备添加到系统,而iSCSI协议,根本没有拓扑发现过程,直接在用户指定和iSCSI目标器建立会话连接时手工添加SCSI设备。(主机适配器驱动自己定义实现扫描逻辑,比如pm8001)
3.最后一种方式也是由主机适配器驱动来负责拓扑发现的,并且拓扑发现过程通过SCSI中间层提供的服务完成。大多数支持传统的SPI(SCSI并行接口)协议的主机适配器都采取这种方式,它也是我们讨论的重点。(scsi中间层提供扫描逻辑)
下面我介绍scsi中间层提供扫描逻辑
对于SPI协议,SCSI主机适配器驱动可以调用SCSI中间层提供的扫描算法完成SCSI总线设备的扫描,扫描过程可以描述如下:
第一步:
SCSI中间层依次以可能的ID和LUN构造INQUIRY命令,之后将这些INQUIRY命令提交给块I/O子系统,后者又最终将调用SCSI中间层的策略例程,再次提取到SCSI命令结构后,调用SCSI低层驱动的queuecommand回调函数实现。(SCSI中间层依次以可能的ID和LUN构造INQUIRY命令)
对于给定ID的目标节点,如果它在SCSI总线上存在,那么它一定要实现对LUN0的INQUIRY响应。也就是说,如果向某个ID的目标节点的LUN0发送INQUIRY命令,SCSI低层没有收到响应,SCSI中间层就可以知道具有该ID的目标器不存在。
如果SCSI低层收到响应,SCSI中间层可以采用两种方法探测这个目标节点的其他LUN:它或者向LUN0发送REPORT LUNS命令,并向报告存在的LUN发送INQUIRY命令,或依次向各个LUN尝试发送INQUIRY命令,检查是否能收到响应,最终SCSI中间层能够得到SCSI域中的所有连接的逻辑设备及其信息。(向某个ID的目标节点的LUN0发送INQUIRY命令,得到响应后,依次向每个LUN发送INQUIRY命令)
1 探测流程入口SCSI中间层提供扫描SCSI总线的服务函数是scsi_scan_host,它采用的就是上面描述的向各个
scsi_scan_host函数调用流程
函数scsi_scan_host()代码(摘自文件drivers/scsi/scsi_scan.c)
SCSI中间层为SCSI低层提供了可供调用的公共函数。scsi_scan_host函数用于对指定的SCSI适配器进行扫描,它的实现中只提供了适用于SCSI并行接口(SPI)协议的扫描逻辑。对于如SAS、FC等,如果扫描要由主机适配器驱动来负责,那么它也应该调用scsi_scan_host,通过它进而调用底层注册的回调函数以执行特定的扫描逻辑。(shost->hostt->scan_start /shost->hostt->scan_finished)
void scsi_scan_host(struct Scsi_Host *shost)
{
struct async_scan_data *data;
if (strncmp(scsi_scan_type, "none", 4) == 0)
return;//判断scsi_scan_type是否被设定为none,若是,则函数可以直接退出了。
if (scsi_autopm_get_host(shost) < 0)
return;
data = scsi_prep_async_scan(shost);//异步扫描
if (!data) {
do_scsi_scan_host(shost);//同步扫描
scsi_autopm_put_host(shost);
return;
}
async_schedule(do_scan_async, data);
}
scsi_scan_host的扫描由定义在同一文件中的静态变量scsi_scan_type决定。它给出了扫描类型,可取值为none、sync和async,分别表示不进行扫描、执行同步扫描和异步扫描。这个变量的值可在加载SCSI中间层模块时通过模块参数设定。如果未设定,则采用取决于编译选项默认值。如果在编译时指定CONFIG_SCSI_SCAN_ASYNC,则默认值为async;否则默认值为sync。
scsi_prep_async_scan为异步扫描作准备。当然,如果是同步扫描,则不需要任何准备,这时,它返回NULL。
do_scsi_scan_host函数同步扫描逻辑的代码如下:
static void do_scsi_scan_host(struct Scsi_Host *shost)
{
if (shost->hostt->scan_finished) {
unsigned long start = jiffies;
if (shost->hostt->scan_start)
shost->hostt->scan_start(shost);
while (!shost->hostt->scan_finished(shost, jiffies - start))
msleep(10);
} else {
scsi_scan_host_selected(shost, SCAN_WILD_CARD, SCAN_WILD_CARD,
SCAN_WILD_CARD, 0);
}
}
如果驱动需要执行特定的扫描逻辑,则需要注册回调函数scan_finished和scan_start。检查scsn_finished函数指针是否为NULL,若否,说明主机适配器驱动定义了自己的扫描逻辑。调用scan_start回调函数开始扫描,循环调用scan_finished检查扫描过程是否该结束。结束的可能有两种:扫描成功完成或者扫描超时。
上面的逻辑适用于使用SAS协议或FC协议的主机适配器。而对于使用SPI协议的主机适配器,它的探测过程是“标准化”的,完全可以并且已经在SCSI中间层实现,这就调用的scsi_scan_host_selected函数,其代码如下图所示:
函数scsi_scan_host_selected()代码(摘自文件drivers/scsi/scsi_scan.c)



