
一、硬件建模
硬件层面的总线或者控制器,在文档里称之为总线或者控制器;硬件层面的设备在文档里称之为设备。
软件层面的总线,在文档里称之为bus,对应着structbus_type类型;软件层面的设备称之为device,对应着struce device类型。
硬件各级设备的睡眠和唤醒顺序,决定了软件上的设备父子关系。例如sd,emmc先sleep,sdio才能sleep。
硬件上的连接关系决定了软件上扫描顺序。例如pcie现有rc虚拟bridge,再扫描一级总线上的各种外设,扫到了bridge才能再递归扫描下一级总线上的外设。
硬件总线上传输的信息的封装方式,决定了多级设备的驱动,各自处理的范围。例如ufs驱动负责upiu等等处理,scsi子系统负责scsi命令的处理。

soc芯片内部有host(0),host(1)...host(k)这些有scsi功能的控制器。
这些host分别连接着片外的scsi设备device(0)...device(k)外设。为了形象点,host(1)没有接任何scsi外设。
每个device内部有若干个channel,每个channel下面有若干个id,每个id下面有若干个lun。
这些lun就是可以接受scsi命令的实体,例如可以是硬盘,cdrom,磁带等等,也可以是一些可以接收特殊scsi命令的wlun。
1. Host(0-k)
表示可以发送和接收scsi命令的控制器。图示中的host(0),host(1)是一个示意图,框图以描述host(k)为主
一个控制器对应一个外设;也可以不接任何外设。
2. device(0-k)

3. channel+id
关于channel和id,我目前没有在scsi协议里面找到任何关于它们的描述。
这里个人理解的channel和id更多的是要给底层各种驱动程序一个灵活性。软件上channel和id给device内部构造了一个树形图,而众多的lun是这个树上的叶子节点。关于channel和id的处理,scsi是交给底层驱动去处理的,scsi仅仅只是用这些来给lun的struce device做命名,并在发送scsi命令时把lun所属的channel和id信息交给了驱动。更详细的信息在后面的数据结构图里面会描述。
channel和id对scsi而言没有实质意义,因此linuxscsi子系统创造了一个target的概念,如下图:

上面的图用不同的颜色画出了三个target,target的名字是host编号,channel编号和id编号组合命名,因此上面三个target的名字分别是(k:0:0)(k:0:1) (k:0:...)。以此类推,这张图后面还可以框出更多的target。
注意channel编号和id编号是底层驱动自行管理的,而host编号(也就是前k)则是linux scsi子系统自行管理(参看static DEFINE_IDA(host_index_ida)(drivers/scsi/host.c))
引入target概念后,每个device内部可以看成被分为被多个target,每个target下面接着多个lun。硬件拓扑图可以画成下面的样子:

4. lun
每个target下面挂接着多个lun。
lun是能够接收scsi命令的主体。例如可以是一个物理硬盘,一个光驱;对ufs而言是ufs固件虚拟出来的rpmb,boot0,boot1;也有一些lun不是物理实体但是能接收scsi命令,也被看作为lun,例如report luns可以响应scsi report luns command返回设备lun总数等信息。因此软件上每个lun在linux的通用块设备层都有独享的一个request queue。注意下,有时候一个硬盘通过GPT或者MBR分为多个逻辑分区;它们是一个lun里面,共用一个request queue。关于逻辑分区,不在linux scsi子系统中处理,这里不深究。
注意每个lun都从host对应的物理总线通路发送和接收数据。这个也涉及到通用块设备层的配置,后面会讲。
二、Linux scsi子系统软件模型
接下来会基于linux设备驱动模型描述scsi子系统的框图。看到这里的小伙伴接下来需要有linux设备驱动模型的基础知识背景了。
由于linux scsi子系统代码庞大,直接说代码,会把人绕进去,这里会通过linux设备驱动模型中的bus,class,device框图来描述linux scsi子系统框架,后面会结合框架,指出子系统中的关键代码和位置。
1.图示说明

2.主要bus和class

3. Host,target,lun设备建模
(1) host(0-k)
Scsi子系统内部针对每个host控制器在linux子系统内部创建两个structdevice结构体:sdhost_dev和shost_gendev。

sdhost_dev寄存”scsi_host” class上面,并通过attr显示一些host相关的属性。

shost_gendev则挂在“scsi”bus上面。“scsi” bus的match函数(scsi_bus_match)不允许shost_gendev有driver对应。所以目前只有一些attribute可以在用户空间使用。

实例:



6个shost_gendev和6个shost_dev设备,对应着硬件上的6个host控制器。
这6个控制器是sata控制器,并且都是一个同一个pcie外设扩展出来的,共享一个pcie设备带宽。
sata驱动创建了ata1-ata6无总线挂靠的虚拟设备,Sata驱动不是这里讨论的内容。但是sata在扫描完port之后,对每个port通过下面的函数创建了硬件host对应的struct device:
Pci_dev添加到pci_bus
(2) target
在scsi内部针对每个target创建了一个名字为“targetk:m:n”的device结构体,其中k是host编号,m是channel编号,n是id编号。


这个targe device也被挂在到”scsi”bus总线上。“scsi” bus的match函数(scsi_bus_match)不允许target不有driver,所以目前只有一些attribute可以在用户空间使用。

(3) lun
Scsi子系统针对每个lun创建了两个device对象,分别是sdev_gendev和sdev_dev。

它们的名字都是k:m:n:lunN,其中k是host编号,m是channel编号,n是id编号,lunN是lun编号。例如

sdev_gendev挂在”scsi” bus上,它会触发bus驱动,驱动会通过sdev_gendev->type字段,来判断该device是否和自己匹配。
例如ufs,ssd的device会触发名为“sd”的驱动。sd驱动会给匹配上的lun,在用户空间创建对应的block设备节点,类似于sda,sdb这些(sda1,sda2是sda上GPT或者MBR搞出来的逻辑分区,不属于scsi内容)。例如


sdev_dev是挂在名为”scsi_device”的class上,用作它用。其中比较重要的sg.c驱动,它在这个class上注册了interface(callback),当有device挂在这个class上时,interface会被调用,从而间接的创建对应的char设备。sg比较特殊,它会不加区分的给所有进来的lun创建一个对应的字符设备到用户空间,类似于sg0,sg1。

(4) 公版驱动

这些驱动都通过scsi_register_driver注册到”scsi” bus上,如果我们写一个自定义设备驱动,也可以这样放置到scsi子系统里。
这些公版驱动有针对硬盘的,磁带的,光驱的,扫描仪,ROM等等各种设备的驱动。
基本上这些驱动都会在自己的probe里面去查看sdev_gendev->type字段,判断该device是否和自己匹配。例如:




三、主体代码描述
子系统初始化:驱动bus的建立,子设备驱动的挂载。
外设扫描:对于scsi而言就是把device侧的所有lun扫描出来。
通路建立:建立子设备驱动和device之间的连接,对于scsi而言就是公版外设驱动和lundevice之间的通路。Scsi子系统是借助block通用块设备层完成这部分工作。
休眠唤醒:对于scsi而言,休眠过程是lun->target->host,唤醒过程是反过来。这个决定了host是爷爷辈设备,targe是父设备,lun是子设备,所有的公版驱动都是子设备驱动。
1.子系统初始化

(1) Scsi_init_queue:
(2)Scsi_init_procfs:

(3) Scsi_init_devinfo


(4) Scsi_init_sysctl
(5) Scsi_init_hosts和scsi_sysfs_register
2.子设备驱动加载

3.外设扫描
以host为单位进行scan。它会把host对应的device下面所有的target和lun全扫描出来。
以target为单位触发scsi进行scan。它会把target下面所有的lun全扫出来
以lun为单位触发scsi进行scan。它会扫描特定lun。
通过/proc/scsi/scsi触发特定的target或lun的scan。
通过host对应user空间设备的属性”scan”节点触发特定的target或lun的scan。
(1) Host扫描

(2) Target和lun扫描

先从0开始for循环扫描channel
在每个channel循环下面再for循环扫描每个id
在每个id下面再for循环扫描所有的lun
伪码如下:


(3) 各种scan入口
以host为单位进行scan:
scsi_scan_host
以target为单位触发scsi进行scan:
scsi_scan_target
以lun为单位触发scsi进行scan:
scsi_add_device或者__scsi_add_device
通过/proc/scsi/scsi:
scsi_scan_host_selected
通过host对应user空间设备的属性“scan”节点:
scsi_scan_host_selected
4.通路建立:借助block层
通过blk_mq_alloc_tag_set注册一个blk_mq_tag_set。注册时我们要提供一堆钩子函数给通用块设备层,处理block发下来的request请求。
通过blk_mq_init_queue并以blk_mq_tag_set为参数为每个能独立处理block请求的实体申请一个request_queue。这样所有的request_queue都和tag_set关联起来了。
第一步是在创建shost_devgen或者shost_dev的地方做的。

第二步是在创建sdev_dev或者sdev_devgen的地方做的。





5.休眠唤醒
休眠唤醒是驱动的一部分,包括PM(suspendresume),runtime PM,也有shutdown,remove等。以休眠为例:在”scsi” bus上那些公版driver实现了子设备的休眠唤醒操作。这个级别的驱动操作的都是lun设备,因此这个级别的驱动是基于scsi命令对设备进行操作。那些更底层的操作例如断开link,给外设断电等是更底层的父设备们去完成的。例如
硬盘驱动sd.c在休眠的时候,给lun发送了scsiSYNCHRONIZE_CACHE命令,要求lun把缓存数据回写到硬盘防止断电丢失,并发送了start_stop命令要求lun进入低功耗状态。

光盘驱动sr.c在连休眠唤醒没有实现,只有一个runtime pm操作啥也没做,可能它的功耗是由光驱自动控制的,不需要软件参与。

6.底层驱动注册

7.关于channel和id的使用
ufs驱动支持一1个channel和1个id

sata驱动里面,支持2个channel,16个id,每个id下面就1个lun


8.标准外设驱动

(1) Sd.c
由于sd.c比较常用,这里把sd.c单独拿出来描述下,其余的外设驱动都大同小异,不再复述。Sd.c它操作的是硬盘,ssd等以sect为单位进行读取写入的存储设备。
该驱动的名字是“sd”, ”sd”内部创建了一个”scsi_disk”的class

“sd”会针对每个匹配上的sdev_gendev,做alloc_disk和device_add_disk操作。也就是说在user空间创建对应的块设备节点,例如sda,sdb这些节点。 “sd”也会在”scsi_disk”class上创建和sdev_gendev同名的device,会有对应group attr和其对应做一些操作。 关于sd设备驱动特别说明 Sd设备驱动本身是块设备驱动,它需要使用block相关的request_queue来发送块设备相关请求给lun,前面讲到lun和host之间的沟通是通过block层来完成的,每个lun有自己独有的request_queue,因此sd驱动直接把这个request_queue拿来用之,把这个request_queue和本地申请的alloc_disk进行绑定。sda,sdb这些块设备就可以直接通过request_queue给lun发送请求。

(2) Sg.c
Sg存在的唯一目的,是使用ioctl命令,例如rpmb的操作,FFU固件升级等操作,都是通过ioctl方式完成。
由于无论sg还是sd,还是别的什么scsi外设驱动创建出来用户态设备节点,最终都是通过lun对应的request_queue来完成发送scsi命令,所以sg能做的事情,其它节点也能做,因此有的平台没有打开sg编译开关。

四、数据链表结构
硬件拓扑结构是一个树形。在linuxscsi子系统里面,host,target,lun对应的scsi_host, scsi_target, scsi_device也是一个树形链表结构,如下图:

五、总结:
Linux中复杂的驱动子系统的设计基本类似与scsi子系统,先有一个物理设备拓补模型,再设计对应的软件驱动模型。
有时为了软件操作有条理,或者为了软件方便扩展,也或者为了代码解耦,可以把物理设备拓补模型进行虚拟扩展。例如:1上述虚拟出的target设备. 2 pcie也会把抽象的bus信号线虚拟出一个pci_bus class设备等等。目的就是软件上采用分级驱动形式,把复杂的操作简化。
设备模型建好后,再实现设备扫描和PM相关的功能,各层驱动负责各层操作,尽量不越界操作,减少代码耦合。例如sd.c不关心device是怎么放到scsi总线上来的(这个device可能是某个pcie插槽接的板卡,板卡再外接的一个外设;也可能是soc ufs控制器上的外设)sd.c驱动只管基于scsi command做好自己分内的操作就可以兼容这些设备。
外部硬件的连接方式像搭积木,分级驱动也可以以最小代价像积木一样应对硬件的变化。





