目录

SCSI子系统对象

1 scsi_host_template:SCSI主机适配器模板

2 Scsi_Host:SCSI主机适配器

3 scsi_target:SCSI目标节点

4 scsi_device:SCSI逻辑设备

5 scsi_cmnd:SCSI命令


SCSI子系统对象

下图SCSI子系统对象之间的关系。Scsi_Host、scsi_target和scsi_device分别描述的是Linux SCSI模型中的主机适配器、目标节点和逻辑单元,而scsi_host_template表示的是SCSI主机适配器模板。

 在Linux系统中,可以安装多个相同型号的主机适配器,这些主机适配器被同一个低层驱动来管理。当PCI子系统通过ID匹配,或者手工方式,添加每一个SCSI主机适配器时,SCSI低层驱动为它分配一个Scsi_Host描述符。因此,如果系统中有多个SCSI主机适配器,系统中就存在同样多个Scsi_Host描述符。SCSI主机适配器描述符包含两部分,一部分可供SCSI中间层使用,另一部分则为SCSI低层驱动专用。两部分的空间一次性分配。

在分配好SCSI主机适配器描述符后,将它添加到系统中,然后启动探测过程。探测到的scsi_target通过siblings链入主机适配器以_targets为表头的链表。而探测到的scsi_device将host域指向该主机适配器,而且一方面通过siblings链入主机适配器以_devices为表头的链表,另一方面通过same_target_siblings链入SCSI目标节点以devices为表头的链表。SCSI目标节点描述符和SCSI设备描述符各自有为SCSI低层驱动专用的信息,都通过hostdata域指向它们。

SCSI各个核心结构的关系。一般来说一种类型的SCSI低层驱动可以驱动多个SCSI主机适配器。每个主机适配器可以挂接多个SCSI目标节点,每个目标节点中至多可以有多个逻辑设备。对于SCSI并行接口,目标节点数最多为7或15,这取决于SCSI总线的宽度,对于SCSI磁盘,逻辑设备数最多为8。

1 scsi_host_template:SCSI主机适配器模板

scsi_host_template结构中的域(来自文件include/scsi/scsi_host.h)

scsi_host_template,从字面上理解是“SCSI主机适配器模板”,它给出了相同型号主机适配器的公用内容,例如,队列深度、SCSI命令处理回调函数、错误恢复回调函数等。主机适配器的分配要依照“主机适配器模板”。也就是说,SCSI低层驱动先定义scsi_host_template,然后在发现每一个可以管理的主机适配器后,“刻印”出一个Scsi_Host。(scsi_host_template给中间层提供函数调用接口)

最新的驱动由PCI子系统负责检测它所能支持的主机适配器。每个SCSI设备驱动需要定义一个scsi_host_template描述符。在驱动加载过程中,以它为模板,为每个支持的主机适配器创建一个Scsi_Host结构。

在定义模板时,最核心的是实例化其中的回调函数,正是通过这些回调函数规范了由它“刻印”出来的主机适配器的行为。

1.int (* queuecommand)(struct scsi_cmnd *, void (*done)(struct scsi_cmnd*));

queuecommand函数用于将SCSI命令块排入低层设备驱动的队列。第一个参数为指向SCSI命令描述符的指针;第二个参数为指向完成回调函数的指针。在驱动程序完成处理命令之后,done回调函数被调用。如果函数返回0,则主机适配器已经接受了该命令。done函数必须在驱动处理完命令之后被调用,也可以在queuecommand返回之前对这条命令调用done,但这时必须返回0。主机适配器也可以拒绝这条命令,这种情况下,不应该改动这条命令,同时也不能调用done。

2.int (* transfer_response)(struct scsi_cmnd *, void (*done)(struct scsi_cmnd*));

一般情况下,主机适配器作为SCSI启动器使用,但是如果有目标器驱动支持,主机适配器也可以作为SCSI目标器使用。基于STGT的目标器驱动必须实现这个回调函数。STGT核心在处理完SCSI命令时,调用它让LLD(向启动器端)传送响应。第一个参数为指向SCSI命令描述符的指针,第二个参数为指向完成回调函数的指针。

4.int (* eh_abort_handler)(struct scsi_cmnd *);

 int (* eh_device_reset_handler)(struct scsi_cmnd *);•

int (* eh_target_reset_handler)(struct scsi_cmnd *);•

int (* eh_bus_reset_handler)(struct scsi_cmnd *);•

int (* eh_host_reset_handler)(struct scsi_cmnd *);

eh_×××系列函数用于错误恢复处理,它们都有唯一的参数,为指向需要执行错误恢复的SCSI命令描述符的指针,如果错误恢复动作成功返回SUCCESS,否则返回FAILED。其中eh_abort_handler放弃给定的命令,eh_device_reset_handler使SCSI设备复位,eh_target_reset_handler使目标节点复位,eh_bus_reset_handler使SCSI总线复位,eh_host_reset_handler使主机适配器复位。关于它们的更详细介绍,参看SCSI错误恢复一节。

5.int (* slave_alloc)(struct scsi_device *);

int (* slave_configure)(struct scsi_device *);

void (* slave_destroy)(struct scsi_device *);

大多数SCSI设备都有专门的私有数据,slave_×××系列函数用于对这些数据进行处理。它们都有唯一的参数,即指向SCSI设备描述符的指针。slave_alloc在扫描到一个新的SCSI设备后调用,用户可以在这个函数中为设备分配结构或者进行初始化。slave_configure在接收到SCSI设备的INQUIRY响应之后调用。用户可以在这个函数中设置队列深度、修改设备标志位等工作。slave_destroy在销毁这个设备之前被调用,可以在这个函数中释放前面两个函数分配的内存等。

6.int (* target_alloc)(struct scsi_target *);

void (* target_destroy)(struct scsi_target *);

target_×××系列函数和slave_×××类似,只不过它们是用于对目标节点的专有数据进行配置的。它们都有唯一的参数,即指向目标节点描述符的指针。target_alloc在发现一个新的目标器后调用,通常在这个函数中分配特定的数据结构,并进行初始化。target_destroy在目标节点被销毁之前被调用,可以在这个函数中释放前一个函数分配的内存等。

7.int (* scan_finished)(struct Scsi_Host *, unsigned long);

 void (* scan_start)(struct Scsi_Host *);

如果主机适配器有能力自己发现挂接到它的目标器,而不需要扫描整条总线,它可以填入scan_×××系列函数,并调用scsi_scan_host。(自己定义扫描逻辑)scan_start函数在SCSI中间层准备好,开始扫描之前被调用,而scan_finished被定期调用,直到扫描已经结束,或超时。

2 Scsi_Host:SCSI主机适配器

Scsi_Host结构中的域(来自文件include/scsi/scsi_host.h)

drivers\scsi\hosts.c

struct Scsi_Host *scsi_host_alloc(struct scsi_host_template *sht, int privsize)

申请一个scsi_host结构体,并对其进行赋值操作。

在很多实际的系统中,SCSI主机适配器为一块基于PCI总线的扩展卡或者为一个SCSI控制器芯片。每个SCSI主机适配器可以存在多个通道,一个通道实际扩展了一条SCSI总线。每个通道可以连接多个SCSI目标节点,具体连接的数量与SCSI总线带载能力有关,或者受具体SCSI协议的限制。

struct Scsi_Host {
struct scsi_host_template *hostt;    //指向用于创建此主机适配器的模板
}

系统为扫描发现的每个主机适配器创建一个Scsi_Host描述符。hostt域指向创建它所依据的模板(也代表驱动)的描述符的指针。

struct Scsi_Host {
	struct list_head	__devices;	//这个scsi适配器的scsi设备链表头
	struct list_head	__targets;	//这个scsi适配器的scsi目标节点链表头
}

这个主机适配器后面的目标节点被链入到以__targets为首的链表,逻辑设备被链入到以__devices为首的链表。

struct Scsi_Host {
    unsigned int max_id;    //目标节点最大编号
    unsigned int max_lun;    //逻辑设备lun最大编号
    unsigned int max_channel;    //最大通道编号
}

max_channel、max_id和max_lun分别表示这个主机适配器的最大通道编号、连接到这个主机适配器的目标节点最大编号和连接到这个主机适配器的逻辑单元最大编号。

struct Scsi_Host {
	unsigned short max_cmd_len;		//可以接受的SCSI命令的最大长度
}

max_cmd_len为主机适配器可以接受的SCSI命令的最大长度。对大多数主机适配器来说,这个值是12,但是有些可以是16,或者为260,如果驱动支持可变长度的命令描述块。如果驱动程序没有专门设置,则这个域的值设定为12。

struct Scsi_Host {
    struct task_struct    * ehandler;  /* Error recovery thread. */    //错误恢复线程
}

每个主机适配器有一个内核线程用于错误恢复处理,ehandler域为指向它的指针。这个错误恢复线程在分配本结构的同时创建。一旦SCSI命令执行出现错误或超时,这个内核线程会被启动,与此同时,会阻塞这个主机适配器,不会有新的SCSI命令被派发,这样当所有SCSI命令都已结束后,就可以开始错误恢复的动作。

struct Scsi_Host {
	struct device		shost_gendev, shost_dev;
}

主机适配器描述符包含两个内嵌的驱动模型设备对象,一个shost_gendev用于将主机适配器关联到SCSI总线类型,另一个shost_dev将主机适配器关联到scsi_host类。

实现系统中所有主机适配器scsi_host的遍历。例如通过proc文件系统“热插入”一个SCSI设备时,内核实现就需要从给定的主机适配器编号查找到Scsi_Host描述符。实际上,主机适配器的遍历借助于驱动模型中类的设备链表。确切地说,每个Scsi_Host都通过内嵌的类设备被链入到shost_class的设备链表中。具体查找方法可参见文件drivers/scsi/Host.c中的scsi_host_lookup函数。(struct Scsi_Host *scsi_host_lookup(unsigned short hostnum) ,在shost_class的设备链表中通过主机适配器的number找到对应的scsi_host)

下图SCSI主机适配器的驱动模型对象关系

3 scsi_target:SCSI目标节点

scsi_target结构中的域(来自文件include/scsi/scsi_device.h)

drivers\scsi\scsi_scan.c

scsi_scan_target 作用:scan a target id, possibly including all LUNs on the target.

void scsi_scan_target(struct device *parent, unsigned int channel, unsigned int id, u64 lun, int rescan)

scsi_alloc_target 作用: allocate a new or find an existing target

static struct scsi_target *scsi_alloc_target(struct device *parent,int channel, uint id)

scsi_target结构表示一个SCSI目标节点,这个结构可以被用于表示只有一个逻辑单元或有多个逻辑单元的设备。

struct scsi_target {
	struct list_head	siblings;
	struct list_head	devices;
}

每个SCSI目标节点用scsi_target描述符表示。它通过siblings域链入到所链接主机适配器的目标节点链表(即以__targets为首的链表)中。而devices则是属于这个目标节点的逻辑设备链表的表头。

struct scsi_target {	
	void 			*hostdata; /* available to low-level driver */
}

scsi_target描述符只反映了从SCSI子系统的角度来看目标节点的信息。取决于SCSI低层驱动的具体实现,具体的目标节点可能会有自己的私有描述符,由hostdata指针指向。一般来说,私有描述符也直接或间接回指到对应的scsi_target描述符。hostdata域只被SCSI低层驱动使用。

struct scsi_target {	
		struct scsi_device	*starget_sdev_user;
}

有的目标节点由于设计上的限制,一次只允许对目标节点的一个逻辑单元进行I/O,这样的目标节点被称为“Single LUN”。Linux内核将为具有该限制的目标器设置single_lun域为1。

对于“Single LUN”的目标节点,每个时刻最多只有一个SCSI设备的请求队列在运行,为此在目标节点描述符包含starget_sdev_user域,它指向当前正在进行I/O的SCSI设备;如果没有I/O,则该域为NULL。具体调度的方法,参见文件drivers/scsi/scsi_lib中的scsi_single_lun_run函数。

struct scsi_target {	
	struct device		dev;
}

SCSI目标节点也有一个内嵌驱动模型设备,它被链入SCSI总线类型(scsi_bus_type)的设备链表,SCSI目标节点的驱动模型对象关系如下图。有意思的是,前面看到的SCSI主机适配器描述符,以及后面将要看到的SCSI设备描述符也各自通过内嵌的驱动模型设备链入到这个链表。也就是说,从驱动模型的角度,它们被看作是SCSI总线类型上的“普通”设备,具有同等的地位。

                                                SCSI目标节点的驱动模型对象关系

4 scsi_device:SCSI逻辑设备

scsi_device结构中的域(来自文件include/linux/scsi_device.h)

scsi_device表示的是逻辑设备,代表的是SCSI磁盘的逻辑单元(Logical Unit,LU)。需要注意的是,scsi_device描述符所代表的设备可能物理上并不属于操作系统所运行的系统,例如它可能是另一台存储设备上的SATA/SCSI/SAS磁盘或存储芯片。操作系统在扫描到连接在主机适配器上的逻辑设备时,创建scsi_device描述符,用于SCSI高层驱动和该设备通信。

struct Scsi_Host {
	struct list_head	__devices;	//这个scsi适配器的scsi设备链表头
	struct list_head	__targets;	//这个scsi适配器的scsi目标节点链表头
}
struct scsi_target {
	struct list_head	devices;//scsi_device链表头
}
struct scsi_device {
	struct list_head    siblings;   /* list of all devices on this host */
    struct list_head    same_target_siblings; /* just the devices sharing same target id */
}

严格意义上讲,scsi_device描述符表示的是SCSI逻辑设备,它通过siblings链入到所连接主机适配器的逻辑设备链表中,通过same_target_siblings链入到所属目标节点的逻辑设备链表中。

struct scsi_device {
	void *hostdata;		/* available to low-level driver */
}

如果说scsi_device描述符给出的是所有SCSI逻辑设备具有共性的内容,那么一般也会有专有数据,hostdata即为指向SCSI逻辑设备专有数据(如果有的话)的指针。

struct scsi_device {
	struct Scsi_Host *host;
}

SCSI设备描述符的host域为指向所连接主机适配器的指针,从SCSI设备获得所属的目标节点是一个相对较少的操作,所以在SCSI设备描述符中,没有专门定义指向目标节点描述符的指针。

struct scsi_device {
	unsigned int id, channel;
	u64 lun;
}

channel、id和lun域分别表示SCSI设备所在的通道号、所在目标节点的ID以及LUN编号。

struct scsi_device {
	unsigned char * inquiry;	/* INQUIRY response data */
	const char * vendor;		/* [back_compat] point into 'inquiry' ... */
	const char * model;		/* ... after scan; point to static string */
	const char * rev;		/* ... "nullnullnullnull" before scan */
}

有相当一部分域是通过从SCSI设备发送SCSI命令获取到的。例如,在SCSI扫描过程中,将对逻辑设备发送INQUIRY命令,返回的相应信息将保存在动态分配的内存中,由inquiry指针指向。其中,从第8个字节到第15个字节是厂商标识符,由vendor指针指向;从第16个字节到第31个字节是产品标识符,由model指针指向;从第32个字节到第35个字节是产品修正号,由rev指针指向。

struct scsi_device {
		char type;
}

type域记录了SCSI设备的类型,对应标准SCSI INQUIRY响应报文中的外围设备类型(Peripheral Device Type)域。SCSI高层驱动将根据这个域判断是否支持该设备。

struct scsi_device {
	struct device		sdev_gendev,sdev_dev;
}

SCSI设备描述符也包含两个内嵌的驱动模型设备对象(如下图):其中一个将SCSI设备关联到SCSI总线类型,从而和SCSI总线类型的驱动(scsi_driver)链表建立联系;而另一个则将SCSI设备关联到scsi_device类,如sg和ses等接口就是通过它来管理SCSI设备的。

5 scsi_cmnd:SCSI命令

SCSI命令有几方面的含义,本书有时会混用,希望读者能精确区分:

• 一种含义是SCSI规范定义的SCSI命令,即表示为字节串的命令描述块(Command Descriptor Block,CDB),遵循特定格式,一般为固定长度的SCSI命令,例如6字节、10字节、16字节等,也可以为可变长度的SCSI命令;

• 另一种含义指的是scsi_cmnd描述符,它发源于SCSI中间层,传递到SCSI低层驱动。每个I/O请求会被创建一个scsi_cmnd,但scsi_cmnd并不一定必然是I/O请求。scsi_cmnd最终又被落实到一个SCSI命令。除了命令描述块之外,scsi_cmnd包含更丰富的信息,包括数据缓冲区、感测数据缓冲区、完成回调函数,以及所关联的块设备驱动层请求等,是SCSI中间层执行SCSI命令的上下文。

scsi_cmnd结构中的域(来自文件include/scsi/scsi_cmnd.h)

struct scsi_cmnd {
	struct scsi_device *device;
	struct list_head list;  /* scsi_cmnd participates in queue lists */
	struct list_head eh_entry; /* entry for the host eh_cmd_q */
}

SCSI命令是针对SCSI设备而言的,其device域为指向命令所属SCSI设备的描述符的指针,list域为链入到所属SCSI设备的命令链表的连接件。当SCSI命令执行出错,或者超时,我们称故障命令,故障命令以eh_entry域为连接件链入到主机适配器以eh_cmd_q域为表头的错误恢复链表。

struct scsi_cmnd {
	unsigned short cmd_len;
	unsigned char *cmnd;
	struct scsi_data_buffer sdb;
}

scsi_cmnd结构在SCSI中间层和SCSI低层驱动都会用到,其中某些域是两者公用的。例如cmd_len和cmnd域反映的是SCSI规范格式的命令字符串,由SCSI中间层设置,SCSI低层则根据它驱动主机适配器的硬件逻辑,确切地说,是SCSI低层写寄存器,由SCSI固件来驱动硬件逻辑。又如,sdb域给出SCSI命令的数据缓冲区,对于读操作,由SCSI低层驱动填入,对于写操作,则由SCSI中间层填入。

struct scsi_cmnd {
	struct request *request;	/* The command we areworking on */
	/* Low-level done function - can be used by low-level driver to point
	 * to completion function.  Not used by mid/upper level code. */
	void (*scsi_done) (struct scsi_cmnd *);
}

scsi_done域被低层驱动用来指向完成函数,中间层和较高层代码不使用。低层驱动通常将它设置为queuecommand函数中传入的done参数,一般是SCSI中间层提供的scsi_done公共函数。

Logo

更多推荐