在SubSySScSi – PreFAce一文中已经讨论了硬件原理,并给出了下图的Scsi与内核关系的关系,本文主要讨论SCSI子系统的关键角色-关键的类与关键的对象。
内核支持的总线众多,每一种总线上挂载的设备又多种多样,涉及的核心类与对象千差万别,但总的来说,根据bus-device-driver模型,在一个总线子系统中,其核心类与对象大体可以分为以下6类:
- 对总线的抽象->struct bus_type scsi_bus_type
- 对总线控制器的抽象->struct scsi_host_template, struct Scsi_Host
- 对总线控制器驱动的抽象->None
- 对总线设备的抽象->struct scsi_target, struct scsi_device, struct scsi_disk
- 对总线设备驱动的抽象->struct scsi_driver
- 对命令的抽象->struct scsi_cmnd
他们之间的关系可以用下图简要表示:
核心的对象有:scsi_bus_type, sdev_class, shost_class, sd_disk_class。,这些对象在内核初始化SCSI子系统时候即创建好,包括总线级的实例scsi_bus_type以及为各种SCSI对象准备的class,它们负责将后续注册到内核的SCSI相关对象通过sysfs融入内核设备驱动框架中,进而被用户使用,个中关系可以用下图简要表示。
scsi_bus_type
scsi_bus_type,封装了SCSI总线级的共性资源,比如总线名、匹配函数等等。一如所有的bus_type实例,通过其上的设备链表与驱动链表,管理着SCSI总线中的”设备”及其”驱动”,有意思的是,在SCSI子系统中,不论是Scsi_Host实例、还是scsi_target实例抑或scsi_device实例,都被看作”设备”被挂接到scsi_bus_type的”设备链表”中,虽然只有scsi_device实例在”驱动链表”有其对应的驱动。
474 struct bus_type scsi_bus_type = { 475 .name = "scsi", 476 .match = scsi_bus_match, 477 .uevent = scsi_bus_uevent, 478 #ifdef CONFIG_PM 479 .pm = &scsi_bus_pm_ops, 480 #endif 481 };
这里很少的域被初始化了,scsi_bus_match()提供总线匹配方法,按照match的约定,可以匹配成功时返回非0,失败返回0,其实质只有下面一句,可见SCSI设备对象与SCSI驱动的匹配不依靠name,id等机制,只要设备可用且没有被其他驱动匹配,即可被当前调用的驱动匹配。
scsi_bus_match() return (sdp->inq_periph_qual == SCSI_INQ_PQ_CON)? 1: 0;
SCSI子系统在首先通过适配器发送INQUARY命令探测总线上的设备,设备如果可用即以SCSI_INQ_PQ_CON应答之,SCSI子系统即在其设备对象中将该应答存入scsi_device->inq_periph_qual域,总线即根据响应是否为SCSI_INQ_PQ_CON来决定匹配结果。
此外,注意到,这里并没有初始化probe,即是说,SCSI子系统不提供总线级的初始化,而当”驱动”或”设备”注册到内核的时候,一旦匹配,最终都会调用really_probe,其逻辑整理如下:
really_probe(struct device* dev, struct device_driver* drv) dev->driver = drv driver_sysfs_add(dev) dev->pm_domain->activate(dev) if dev->bus->probe dev->bus->probe(dev) else if drv->probe drv->probe(dev) driver_bound(dev):add dev to driver's device list return
即是说,如果没有总线级的初始化,really_probe()就会执行driver中的probe。
shost_class/sdev_class/sdisk_class
这个class是内核用来组织SCSI总线下的资源的,shost用于组织sysfs中的scsi_host对象,sdev_class用于组织内核中的scsi_device对象,sdisk_class用于组织内核中的scsi_disk对象。
scsi_host_template, Scsi_Host。
一个SCSI适配器在内核中的抽象由两个类互相协作完成:scsi_host_template和Scsi_Host。
scsi_host_template
内核DD框架生动的体现了内核OOP的设计,比如bus_type类封装了”总线级”的共性资源,Scsi_Host封装了”适配器级”的共性资源,比如收发消息的方法、硬件规格等。对于大部分总线,这两层封装已经足够,对于多通过板卡接入系统的SCSI适配器,往往在中间还有一层共性特点:位于同一张板卡上的SCSI适配器相同,也就具有很多共性特征。而有共性即可抽象,内核将这部分共性抽象出来,封装到scsi_host_template中。在构造scsi_host的时候,只需要根据scsi_host_template中共性的部分以及自己特性的部分”刻印”出一个scsi_host对象即可。以PCIe RAID卡为例,作为PCIe设备,RAID卡的驱动就会首先为这些SCSI适配器构造一个共同的scsi_host_template对象,再为每个SCSI适配器构造相应的Scsi_Host对象,省时省力。
scsi_host_template域中,最重要的当属queuecommand域,queuecommand()是SCSI子系统LLDD的上边界,HLDD准备好SCSI命令的封装之后,通过中间层的接口发送命令,而中间层最终就是通过queuecommand将命令传递到LLDD,由它将SCSI命令由”封装”转变为底层操作下发到SCSI设备,比如,在PCIe RAID卡中,queuecommand()最终转换为对PCIe空间的写,RAID卡固件识别到这些数据后,从中取出SCSI命令的部分,下发到位于其上的SCSI适配器芯片中。
scsi_host
scsi_host描述了一个SCSI主机适配器,对应到主板上就是SCSI控制器芯片。添加到一个SCSI主机适配器到内核,实质就是构造一个scsi_host实例并注册到内核。scsi_host包含两部分,一部分供SCSI中间层使用,另一部分供SCSI低层驱动专用,两部分一同分配,在分配好scsi_host后,将它添加到系统中,启动总线探测过程。SCSI适配器可以看作一个SCSI总线的”根”,负责系统与其上总线设备的通信,而Scsi_Host作为适配器的抽象,自然需要管理其上的所有设备的相应的抽象(scsi_target,scsi_device),它们之间的关系可以用下图表示:
- 探测到的TARGET通过scsi_target->siblings链入scsi_host->_target链表;
- 探测到的LUN设置scsi_device->host指向该scsi_host,并通过scsi_device->siblings链入scsi_host->_devices链表,同时,通过scsi_device->same_target_siblings链入scsi_target->devices链表。
此外,scsi_host,scsi_target和scsi_device均使用hostdata指向供低层驱动使用的资源。
scsi_disk
在一个SCSI总线下说”设备”,可以有两种设备:target和lu,在LLDD中,就使用scsi_target和scsi_device作为target和lun的抽象。而对应于SCSI总线使用的< Initiator:Channel:Target:LUN >来标识一个逻辑设备,内核使用scsi_device中的< host:channel:id:lun >四个域来唯一标识一个scsi_device对象。
按照SCSI协议的方式标识了一个LUN往往还不够,内核还需要在更”高”的层次上标识一个LUN,比如这个LUN的功能,对于最常见的SCSI设备–SCSI磁盘,内核使用scsi_disk来抽象,其上承gendisk实例,下启scsi_device和scsi_driver,可以说,三者之中,只要知其一,便可找到另外两个,
scsi_driver
在不同的物理环境下,SCSI总线适配器的驱动的组织多有不同(甚至不需要驱动,仅把相应的资源和方法封装在设备对象中即可,因为无需用户使用适配器),此处仅讨论SCSI设备驱动,其定义如下,SCSI子系统HLDD中的诸多驱动均是该类的实例,比如sd驱动中的sd_template(sd.c),sr驱动中的sr_template(sr.c),st驱动中的st_template(st.c)等
//include/scsi/scsi_driver.h 11 struct scsi_driver { 12 struct device_driver gendrv; 14 void (*rescan)(struct device *); 15 int (*init_command)(struct scsi_cmnd *); 16 void (*uninit_command)(struct scsi_cmnd *); 17 int (*done)(struct scsi_cmnd *); 18 int (*eh_action)(struct scsi_cmnd *, int); 19 };
–12–>如所有其他的驱动对象,scsi_driver中内嵌了device_driver类,如前文所述,驱动与设备一旦匹配,即回调其中的probe()
–14–>重现探测的回调方法
–15–>初始化SCSI命令回调的方法
–17–>SCSI命令执行完毕时回调的方法
–18–SCSI命令执行出错时回调的错误处理方法
驱动和设备一旦匹配,即构造scsi_disk对象到内核
scsi_cmnd
在i2c总线子系统中,这种抽象由i2c_msg提供,而在SCSI总线子系统中,这种抽象由scsi_cmnd提供。scsi_cmnd发于SCSI子系统中间层,传到SCSI低层。传递到SCSI子系统的每个IO请求都会被转换为一个scsi_cmnd对象,而一个scsi_cmnd最终又会被转换为一个SCSI命令。除了SCSI CDB本身之外,scsi_cmnd还封装了与之相关的上下文信息,比如数据缓冲区,完成回调函数以及关联的块设备请求(request)等。