Linux SCSI 子系统 II

SubSySScSi – PreFAce一文中已经讨论了硬件原理,并给出了下图的Scsi与内核关系的关系,本文主要讨论SCSI子系统的关键角色-关键的类与关键的对象。
内核支持的总线众多,每一种总线上挂载的设备又多种多样,涉及的核心类与对象千差万别,但总的来说,根据bus-device-driver模型,在一个总线子系统中,其核心类与对象大体可以分为以下6类:

  1. 总线的抽象->struct bus_type scsi_bus_type
  2. 总线控制器的抽象->struct scsi_host_template, struct Scsi_Host
  3. 总线控制器驱动的抽象->None
  4. 总线设备的抽象->struct scsi_target, struct scsi_device, struct scsi_disk
  5. 总线设备驱动的抽象->struct scsi_driver
  6. 命令的抽象->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),它们之间的关系可以用下图表示:

  1. 探测到的TARGET通过scsi_target->siblings链入scsi_host->_target链表;
  2. 探测到的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)等。

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.