当前位置:   article > 正文

SCSI子系统(一)

scsi子系统

一、概述

1. Linux SCSI 子统分层的架构

  • 高层: 代表各种SCSI设备类型的驱动,高层 “认领” 低层驱动发现的SCSI设备,为他们分配设备名,将对设备的I/O转换为SCSI命令,交由低层驱动处理;
  • 中间层: 接下来的是中间层,也称为公共层或统一层,在这一层包含SCSI堆栈的高层和低层的一些公共服务函数。高层和低层通过调用中间层的函数来实现其功能,而中间层在执行过程中,也需要调用高层和低层注册的回调函
    数做一些个性化处理。
  • 低层: 代表的是适用于SCSl的物理接口的实际驱动器,例如各个厂商为其特定的主机适配器(也被称为主机总线适配器,Host Bus Adapter)开发的驱动。低层驱动的主要作用是发现连接到主机适配器后面的SCSI设备,在内存中为它们建立好数据结构,并提供消息传递接口,将SCSI命令的接收与发送解释为主机适配器的操作;
    在这里插入图片描述

2. Linux SCSI 模型

  • 主机适配器: 连接主机I/O总线(通常是PCI总线)和存储I/O总线(这里是SCI总线)。一台计算机可以有多个主机适配器,而主机适配器可以控制一个(即所谓的单通道适配器)或多个(即所谓的多通道适配器)SCSI总线;
  • 存储I/O总线(SCI总线): 一条总线可以有多个目标节点(目标设备)与之相连;
  • 目标节点(目标设备): 并且一个目标节点可以有多个逻辑单元,SCSI磁盘整体就是一个目标节点;
  • 逻辑单元(LUN): SCI设备描述符表示具体的逻辑单元,SCSI磁盘中可有多个逻辑单元,统一由磁盘控制器控制,这些逻辑单元才是真正作为I/O终点的存储设备;

在这里插入图片描述
在Linux中,使用四元组《host : channel : id : lun》来唯一定位·个SCI设备。

  • host: 主机适配器在计算机内部的编号,实现方式:在系统中定义一个全局变量,每发现一个主机适配器,将这个全局变量值赋值给host编号,同时递增该全局变量;
  • channel: SCSI通道编号(SCSI总线编号),这是相对主机适配器来说的,由主机适配器的固件来维护;
  • id: 在总线上的目标节点标识符;
  • lun: 在目标节点内的逻辑单元编号;

对于并行SCSI总线,其所能挂接的设备数目是受物理层,即总线宽度;可分为:
(1)8地址窄SCSI:1个SCSI启动器(主机适配器)+ 1 ~ 7个目标节点(id范围:0 ~ 6)
(2)16地址宽SCSI:1个SCSI启动器(主机适配器)+ 1 ~ 15个目标节点(id范围:0 ~ 6 或 8 ~ 15)

channel和id 是纯粹逻辑上的概念,在找到目标节点后,由低层(主机适配器)驱动为这个设备分配唯一的channel和id值 。

2. SCSI子系统功能

  1. 探测SCSI设备,在内存建立科工设备驱动使用的核心结构;
  2. 在sysfs文件系统中构建SCSI子系统的目录树;
  3. SCSI高层驱动绑定SCSI设备,在内存中构建对应的核心结构;
  4. 提供错误恢复API,在SCSI命令错误和超时后被调用。

备注: SCSI子系统(包括高层、中间层和低层)的源代码位于 drivers/scsi 下,头文件在 include/scsi

二、SCSI 子系统对象之间的关系

在这里插入图片描述
scsi_Host、scsi_target、scsi_device分别描述Linux SCSI模型中的主机适配器、目标节点、逻辑单元,scsi_host_template表示SCSI主机适配器模板。

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

1. scsi_host_template:SCSI主机适配器模板

它给出了相同型号主机适配器的公用内容,例如,队列深度、SCI命令处理回调函数、错误恢复回调函数等。主机适配器的分配要依照“主机适配器模板”。
struct scsi_host_template 数据结构(来自文件include/scsi/scsi_host.h)


struct scsi_host_template {
	struct module *module;
	const char *name;
	const char *(* info)(struct Scsi_Host *); //   这个函致可以返回开发人员觉得合活的任何有用信息。如果未给定,则使用名字域。状态:可选
	int (* ioctl)(struct scsi_device *dev, int cmd, void __user *arg);
	int (* queuecommand)(struct Scsi_Host *, struct scsi_cmnd *); // 将SCSI命令排入低层设备驱动的队列。SCSI中间层调用该回调两数向HBA发送SCSI俞令
	int (* transfer_response) (struct scsi_cmnd *, void (*done) (struct scsi_cmnd *)); // 主机适配器也可以作为SCSI目标器使用时,基于STGT的目标器驱动必须实现这个回调函数。STGT核心在处理完SCSI命令时,调用它让LLD(向启动器端)传送响应。
	int (* eh_abort_handler)(struct scsi_cmnd *);  // 放弃给定的命令
	int (* eh_device_reset_handler)(struct scsi_cmnd *);  // 使SCSI设备复位
	int (* eh_target_reset_handler)(struct scsi_cmnd *);  // 使SCSI目标节点
	int (* eh_bus_reset_handler)(struct scsi_cmnd *);    // 使SCSI总线复位
	int (* eh_host_reset_handler)(struct scsi_cmnd *);  //  使主机适配器复位
	int (* slave_alloc)(struct scsi_device *);  // 在扫描到一个新的SCSI设备后调用,用户可以在这个函数中为设备分配结构或者进行初始化。
	int (* slave_configure)(struct scsi_device *); // 在接收到SCSI设备的INQUIRY响应之后调用。用户可以在这个函数中设置队列深度、修改设备标志位等工作
	void (* slave_destroy)(struct scsi_device *); // 销毁这个设备之前被调用,可以在这个函数中释放前面两个函数分配的内存等。
	int (* target_alloc)(struct scsi_target *); // 在发现一个新的目标器后调用,通常在这个函数中分配特定的数据结构,并进行初始化。
	void (* target_destroy)(struct scsi_target *);  // 在目标节点被销毁之前被调用,可以在这个函数中释放前一个函数分配的内存等。
	int (* scan_finished)(struct Scsi_Host *, unsigned long); // 如果主机适配器定义了自己的扫描逻辑,则需要实现这个回调函数
	void (* scan_start)(struct Scsi_Host *);  // 如果主机适配器足义了自己的扫描逻辑.则翁要实现这个回调函数
	int can_queue;  // 该域为主机适配器可以同时接受的命令数,必须大于零
	int this_id; //   在很多情况下,尤其是支持断开重连时,主机适配器在SCSI总线上占用一个ID。这是.必须预留该ID值.如果只有一个启动器,并且hos缺少ID,可将该域设为-1
	unsigned int max_sectors; // 主机适配器单个SCSI命令能访问扇区的最大数目
	short cmd_per_lun; //   允许排入连接到这个主机适配器的SCSI设备的最大命令数目,即队列深度
	unsigned int cmd_size;
	struct scsi_host_cmd_pool *cmd_pool; // 月于分配SCSI命令结构和SenseData感测数据缓冲区的存储池结构
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27

每个SCSI设备驱动需要定义一个scsi_host_template描述符。在驱动加载过程中,以它为模板,为每个支持的主机适配器创建一个Scsi_Host结构

2. Scsi_Host:SCSI主机适配器

SCSI主机适配器为一块基于PCI总线的扩展卡或者为一个SCSI控制器芯片。每个SCSI主机适配器可以存在多个通道,一个通道实际扩展了一条SCSI总线。每个通道可以连接多个SCSI目标节点,具体连接的数量与SCSI总线带载能力有关,或者受具体SCSI协议的限制。
struct Scsi_Host 数据结构(来自文件include/scsi/scsi_host.h)

struct Scsi_Host {
	struct list_head	__devices; // 这个主机适配器的SCSI设备链表
	struct list_head	__targets; // 这个主机适配器的目标节点链表
	struct list_head	eh_cmd_q; // 进入错误恢复的SCSI命令链表的表头
	struct task_struct    * ehandler;  // 错误恢复线程,在分配本结构的同时创建
	struct completion     * eh_action;  // 在发送一个用于错误恢复的SCSI命令后,需要等待其完成,才能继续。该指针指向此同步目的的完成变量
	wait_queue_head_t       host_wait; //   SCSI 设备错误恢复等待队列。在SCSI 设备错误恢复过程中,要操作SCSI 设备的进程将在此队列等待,直到恢复完成
	struct scsi_host_template *hostt; // 指向用于创建这个主机适配器的模板的指针
	struct scsi_transport_template *transportt; // 指向SCSI传输层模板的指针
	unsigned int host_no;  // 系统范围内唯一编号,用于标识这个主机适配器
	unsigned int max_channel; // 这个主机适配器的最大通道(Bus)编号
	unsigned int max_id; // 连接到这个主机适配器的目标节点最大编号
	u64 max_lun; // 连接到这个主机适配器的逻辑单元最大编号
	unsigned int unique_id; // 用于主机适配器相互区别的唯一标识特
	unsigned short max_cmd_len; // 主机适配器可以接受的SCSI命令的级大长度。对大多数主机适配器来说,这个值是12,但是有些可以是16,或者为260,默认12。
	int this_id; // 这个主机适配器的SCSI ID
	int can_queue; // 该城为主机适配器可以同时接受的命令数,必须大于霉
	short cmd_per_lun; // 允许排入连接到这个主机适配器的SCSI设备的最大命令教目,即队列深度.
	unsigned long cmd_serial_number; // 用来为命令分配序列号
	enum scsi_host_state shost_state; // 主机适配器的状态
	struct device		shost_gendev; // 这个主机适配器的内嵌通用设备,SCSI设备通过这个域链入SCSI总线类型(scsi_bus_type)的设备链表
	struct device		shost_dev; // 这个主机适配器的内嵌类设备,SCSI设备通过这个域链入SCSI主机适配器类 (shost_class)设备链表
	void *shost_data; // 指向另外分配的传输层教据(如果有)
	struct device *dma_dev; // 指向用来进行DMA的物理总线设备的指针,仅为.虚拟,主机适祝器所需
	unsigned long hostdata[0]  // 用以保存主机适配器的专有数据,只被SCSI低层驱动使用。
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26

系统为扫描发现的每个主机适配器创建一个Scsi_Host描述符。

主机适配器描述符包含两个内嵌的驱动模型设备对象,一个用于将主机适配器关联到SCSI总线类型,另一个将主机适配器关联到scsi_host类。如下图:
在这里插入图片描述

3. scsi_target:SCSI目标节点

scsi_target结构可以被用于表示只有一个逻辑单元或有多个逻辑单元的设备。
scsi_target描述符只反映了从SCSI子系统的角度来看目标节点的信息。取决于SCSI低层驱动的具体实现,具体的目标节点可能会有自己的私有描述符,由hostdata指针指向。一般来说,私有描述符也直接或间接回指到对应的scsi_target描述符。hostdata域只被SCSI低层驱动使用。
通过设置 /proc/scsi/device_info 参数将SCSI设备的各种信息提供给Linux内核。
struct scsi_target 数据结构(来自文件include/scsi/scsi_device.h)

struct scsi_target {
	struct scsi_device	*starget_sdev_user; //   用在SCSI目标节点一次只允许对一个逻辑单元进行I/O的场合。如果没有I/O,则该域为NULL,否则指向正在进行I/O的SCSI设备
	struct list_head	siblings; // 链接到所属主机适配器的目标节点链表的连接件
	struct list_head	devices; // 这个目标节点的SCSI设备链表
	struct device		dev;  // 这个目标节点的内嵌通用设各
	unsigned int		channel; // 这个目标节点所在的通道号
	unsigned int		id; // 这个目标节点的ID
	unsigned int		single_lun:1;	// 如果为1,表明一次只允许对目标节点的一个逻辑单元进行I/O
	unsigned int		can_queue; // 该域为目标节点可以同时处理的命令数,必须大于零
	enum scsi_target_state	state; // 这个目标节点的状态
	void 			*hostdata; // 指向SCSI口标节点专有教据(如果有的话)的指针
	unsigned long		starget_data[0]; // 用于传输
} 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

SCSI目标节点也有一个内嵌驱动模型设备,它被链入SCSI总线类型(scsi_bus_type)的设备链表,从驱动模型的角度,和SCSI主机适配器一样也被看作是SCSI总线类型上的“普通”设备,具有同等的地位。
在这里插入图片描述

4. scsi_device:SCSI逻辑设备

scsi_device 代表的是SCSI磁盘的逻辑单元(Logical Unit, LU)。操作系统在扫描到连接在主机适配器上的逻辑设备时,创建scsi_device描述符,用于SCSI高层驱动和该设备通信。
struct scsi_device 数据结构(来自文件include/linux/scsi_device.h)

struct scsi_device {
	struct Scsi_Host *host; // 指向所连接主机适配器的指针
	struct request_queue *request_queue; // 指向这个SCSI设备的请求队列的指针
	struct list_head    siblings;  // 链接到所属主机适配器的SCSI设备链表的连接件
	struct list_head    same_target_siblings; // 链接到所属目标节点的SCSI设备链表的连接件
	struct list_head cmd_list; // 使用中的SCSI命令结构的队列
	unsigned short queue_depth;	// 队列深度,即允许排入队列的最多命令数
	unsigned short max_queue_depth;	/* max queue depth */
	unsigned int id, channel; 这个SCSI设备所在目标节点的ID 和 所在通道号
	u64 lun; // 这个SCSI设备的LUN编号
	unsigned int manufacturer;	/* Manufacturer of device, for using * vendor-specific cmd's */
	unsigned sector_size; //SCSI设各的硬件扇区长度(以字节为单位)。通过Read Capacity命令读到
	void *hostdata;	// 指向SCSI 逻辑设备专有数据(如果有)的指针
	unsigned char type; // SCSI设备的类型,SCSI高层驱动将根据这个域判断是否支持该设备。
	char scsi_level; // 实现SCSI规范的版本号,利用INQUIRY命令获得
	unsigned char inquiry_len; // INQUIRY字符串的有效长度
	unsigned char * inquiry;	// 指向该SCSI设备的SCSI INQUIRY响应报文字符串的指针
	const char * vendor;	// 指向SCSI INQUIRY响应报文中的厂商标识符的指针
	const char * model;	// 指向SCSI INQUIRY响应报文中的产品标识符的指针
	const char * rev;		// 指向SCSI INQUIRY响应报文中的产品修正号的指针
	struct scsi_target      *sdev_target;  // 指向这个设各所属的目标器,只用于Single_lun的目标设备
	struct device		sdev_gendev, // 这个SCSI设备的内嵌通用设备,SCSI设备通过这个域链入SCSI总找类型scsi_bus_type的设备链表
				sdev_dev; //   这个SCSI设备的内嵌类设备,SCSI设备通过这个域链入SCSI设备类(sdev_class的设备链表)
	struct execute_work	ew; /* used to get process context on put */
	struct work_struct	requeue_work;
	struct scsi_device_handler *handler;
	void			*handler_data; // 某些设备可以绑定特定的设备处理函数,例如在激活设备时傲一些处理.或实现自己的sense data数据检查等
	enum scsi_device_state sdev_state; // 这个SCSI设备的状态
	unsigned long		sdev_data[0]; // 用于传输层
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30

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

5. scsi_cmnd:SCSI命令

scsi_cmnd 发源于SCSI中间层,传递到SCSI低层驱动。每个I/O请求会被创建一个scsi_cmnd,但scsi_cmnd并不一定必然是I/O请求。scsi_cmnd最终又被落实到一个SCSI命令(Command Descriptor Block, CDB)。scsi_cmnd包括:CDB、数据缓冲区、感测数据缓冲区、完成回调函数,以及所关联的块设备驱动层请求等,是SCSI中间层执行SCSI命令的上下文。
struct scsi_cmnd 数据结构(来自文件include/scsi/scsi_cmnd.h)

struct scsi_cmnd {
	struct scsi_request req; // scsi命令请求
	struct scsi_device *device; // 指向命令所属SCSI设备的描述符的指针
	struct list_head list;  // 链入到所属SCSI设备的命令链表的连接件
	struct list_head eh_entry; // 链入到主机适配器的错误恢复链表eh_cmd_q的连接件
	int eh_eflags;		/* Used by error handlr */
	unsigned long serial_number; // SCSI命令的序号.标识了一个诸求.可用于错误恢复和调试目的
	int retries; // 已经重试的次数
	int allowed; // 可允许的重试次数
	unsigned short cmd_len; // SCSI命令的长度
	enum dma_data_direction sc_data_direction; // SCSI 命令的数据传输方向
	unsigned char *cmnd; // 指向SCSI规范格式的命令字符串的指针
	struct scsi_data_buffer sdb; // SCSI命令的数据缓冲区
	struct scsi_data_buffer *prot_sdb; // SCSI命令的保护信息缓冲区
	unsigned transfersize;	// 传输单位,(应该等于硬件扇区长度)
	struct request *request;	// 指向对应的块设各驱动层请求描述符(如果有的话〕的指针
	unsigned char *sense_buffer; // SCSI命令的感侧数据缓冲区
	void (*scsi_done) (struct scsi_cmnd *); // 被低层驱动用来指向完成函数,中间层和较高层代码不使用。低层驱动通常将它设置为queuecommand函数中传入的done参教
	int result;		// 从低层驱动返回的状态码
	int flags;		/* Command flags */
	unsigned char tag;	/* SCSI-II queued command tag */
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

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

三、SCSI子系统初始化

SCSI子系统初始化入口函数为init_scsi,在文件drivers/scsi/scsi.c中。依次调用:
● scsi_init_queue——初始化聚散列表等所需要的存储池;
● scsi_init_procfs——初始化proc文件系统中与SCSI有关的目录项;
● scsi_init_devinfo——设置SCSI动态设备信息列表;
● scsi_init_hosts——注册shost_class类,这将在sys/class/目录下创建scsi_host子目录;
● scsi_init_sysctl——注册SCSI系统控制表;实际会在/proc/sys目录下创建dev/scsi/logging_level节点,用来修改scsi的打印级别
● scsi_sysfs_register——注册SCSI总线类型以及sdev_class类;在/sys/bus/下注册scsi总线和在/sys/class/下创建scsi_device
● scsi_netlink_init——初始化SCSI传输netlink接口,netlink是Linux内核与用户空间进行通信的一种机制。

static int __init init_scsi(void)
{
	int error;

	error = scsi_init_queue();
	if (error)
		return error;
	error = scsi_init_procfs();
	if (error)
		goto cleanup_queue;
	error = scsi_init_devinfo();
	if (error)
		goto cleanup_procfs;
	error = scsi_init_hosts();
	if (error)
		goto cleanup_devlist;
	error = scsi_init_sysctl();
	if (error)
		goto cleanup_hosts;
	error = scsi_sysfs_register();
	if (error)
		goto cleanup_sysctl;

	scsi_netlink_init();

	printk(KERN_NOTICE "SCSI subsystem initialized\n");
	return 0;

cleanup_sysctl:
	scsi_exit_sysctl();
cleanup_hosts:
	scsi_exit_hosts();
cleanup_devlist:
	scsi_exit_devinfo();
cleanup_procfs:
	scsi_exit_procfs();
cleanup_queue:
	scsi_exit_queue();
	printk(KERN_ERR "SCSI subsystem failed to initialize, error = %d\n",
	       -error);
	return error;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42

四、添加主机适配器到系统

SCSI低层驱动是面向主机适配器的,低层驱动被加载时,首先要添加主机适配器。添加主机适配器包括两部分的内容,为主机适配器分配数据结构,将主机适配器添加到系统。SCSI中间层为此提供了两个公共函数:scsi_host_alloc和scsi_add_host。

1. 函数scsi_host_alloc ()代码(摘自文件drivers/scsi/hosts.c)

struct Scsi_Host *scsi_host_alloc(struct scsi_host_template *sht, int privsize)
{
	struct Scsi_Host *shost;
	gfp_t gfp_mask = GFP_KERNEL;
	int index;

	if (sht->unchecked_isa_dma && privsize)
		gfp_mask |= __GFP_DMA;
  // 一次性分配SCSI主机适配器的公有部分和私有部分空间
	shost = kzalloc(sizeof(struct Scsi_Host) + privsize, gfp_mask);
	if (!shost)
		return NULL;

	shost->host_lock = &shost->default_lock;
	spin_lock_init(shost->host_lock);
	shost->shost_state = SHOST_CREATED;
	INIT_LIST_HEAD(&shost->__devices);//初始化&shost->__devices链表用于链接host下所有的scsi_device
	INIT_LIST_HEAD(&shost->__targets); //初始化&shost->__targets链表用于链接host下所有的scsi_target
	INIT_LIST_HEAD(&shost->eh_cmd_q);//初始化&shost->eh_cmd_q链表用于链接host下所有的error command
	INIT_LIST_HEAD(&shost->starved_list);
	init_waitqueue_head(&shost->host_wait);
	mutex_init(&shost->scan_mutex);

	index = ida_simple_get(&host_index_ida, 0, 0, GFP_KERNEL);//shost->host_no,通过idx子系统为host分配一个ID
	if (index < 0)
		goto fail_kfree;
	shost->host_no = index;

	shost->dma_channel = 0xff;

	/* These three are default values which can be overridden */
	shost->max_channel = 0;
	shost->max_id = 8;
	shost->max_lun = 8;

	/* Give each shost a default transportt */
	shost->transportt = &blank_transport_template;

	/*
	 * All drivers right now should be able to handle 12 byte
	 * commands.  Every so often there are requests for 16 byte
	 * commands, but individual low-level drivers need to certify that
	 * they actually do something sensible with such commands.
	 */
	shost->max_cmd_len = 12;
	shost->hostt = sht;//通过sht模板来初始化新创建的Scsi_host
	shost->this_id = sht->this_id;
	shost->can_queue = sht->can_queue;
	shost->sg_tablesize = sht->sg_tablesize;
	shost->sg_prot_tablesize = sht->sg_prot_tablesize;
	shost->cmd_per_lun = sht->cmd_per_lun;
	shost->unchecked_isa_dma = sht->unchecked_isa_dma;
	shost->use_clustering = sht->use_clustering;
	shost->no_write_same = sht->no_write_same;

	if (shost_eh_deadline == -1 || !sht->eh_host_reset_handler)
		shost->eh_deadline = -1;
	else if ((ulong) shost_eh_deadline * HZ > INT_MAX) {
		shost_printk(KERN_WARNING, shost,
			     "eh_deadline %u too large, setting to %u\n",
			     shost_eh_deadline, INT_MAX / HZ);
		shost->eh_deadline = INT_MAX;
	} else
		shost->eh_deadline = shost_eh_deadline * HZ;

	if (sht->supported_mode == MODE_UNKNOWN)
		/* means we didn't set it ... default to INITIATOR */
		shost->active_mode = MODE_INITIATOR;
	else
		shost->active_mode = sht->supported_mode;

	if (sht->max_host_blocked)
		shost->max_host_blocked = sht->max_host_blocked;
	else
		shost->max_host_blocked = SCSI_DEFAULT_HOST_BLOCKED;

	/*
	 * If the driver imposes no hard sector transfer limit, start at
	 * machine infinity initially.
	 */
	if (sht->max_sectors)
		shost->max_sectors = sht->max_sectors;
	else
		shost->max_sectors = SCSI_DEFAULT_MAX_SECTORS;

	/*
	 * assume a 4GB boundary, if not set
	 */
	if (sht->dma_boundary)
		shost->dma_boundary = sht->dma_boundary;
	else
		shost->dma_boundary = 0xffffffff;

	shost->use_blk_mq = scsi_use_blk_mq;
	shost->use_blk_mq = scsi_use_blk_mq || shost->hostt->force_blk_mq;
 
	device_initialize(&shost->shost_gendev);//初始化Scsi_Host内嵌通用设备描述符和设备描述符
	dev_set_name(&shost->shost_gendev, "host%d", shost->host_no);
	shost->shost_gendev.bus = &scsi_bus_type;//在scsi_bus下面创建节点, 如:/sys/bus/scsi/devices/host0
	shost->shost_gendev.type = &scsi_host_type;

	device_initialize(&shost->shost_dev);
	shost->shost_dev.parent = &shost->shost_gendev;//shost_gendev为shost_dev的父设备
	shost->shost_dev.class = &shost_class;///sys/class/scsi_host/host0
	dev_set_name(&shost->shost_dev, "host%d", shost->host_no);
	shost->shost_dev.groups = scsi_sysfs_shost_attr_groups;

	shost->ehandler = kthread_run(scsi_error_handler, shost,
			"scsi_eh_%d", shost->host_no);
	if (IS_ERR(shost->ehandler)) {
		shost_printk(KERN_WARNING, shost,
			"error handler thread failed to spawn, error = %ld\n",
			PTR_ERR(shost->ehandler));
		goto fail_index_remove;
	}

	shost->tmf_work_q = alloc_workqueue("scsi_tmf_%d",
					    WQ_UNBOUND | WQ_MEM_RECLAIM,
					   1, shost->host_no);
	if (!shost->tmf_work_q) {
		shost_printk(KERN_WARNING, shost,
			     "failed to create tmf workq\n");
		goto fail_kthread;
	}
	scsi_proc_hostdir_add(shost->hostt);// 在proc文件系统中为这个主机适配器添加一个目录
	return shost;

 fail_kthread:
	kthread_stop(shost->ehandler);
 fail_index_remove:
	ida_simple_remove(&host_index_ida, shost->host_no);
 fail_kfree:
	kfree(shost);
	return NULL;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 功能描述:
    scsi_host_alloc函数分配一个新的Scsi_Host描述符并进行基本的初始化。

  • 参数:
    ● struct scsi_host_template *sht:指向SCSI主机适配器模板的指针;
    ● int privsize:驱动私有数据分配的额外字节数;

  • 返回值:
    指向Scsi_Host描述符的指针;或者在失败时返回NULL。

一次性分配SCSI主机适配器的公有部分和私有部分空间:
在这里插入图片描述
主机适配器sysfs文件系统的子目录树:
在这里插入图片描述
标记①:为主机适配器目录名host#,其中#为主机适配器编号;
标记②:为目标节点目录名target#:#:#,其中#:#:#分别为主机适配器编号、通道编号和ID编号;
标记③:为SCSI设备目录名#:#:#:#,其中#:#:#分别为主机适配器编号、通道编号、ID编号,以及逻辑单元编号。

2. 函数scsi_add_host ()代码(摘自文件include/scsi/scsi_host.h)

在scsi_host_alloc函数被调用后,主机适配器还不会被公开给SCSI中间层,直到scsi_add_host函数被调用。

static inline int __must_check scsi_add_host(struct Scsi_Host *host, struct device *dev)
{
	return scsi_add_host_with_dma(host, dev, dev);
}
  • 1
  • 2
  • 3
  • 4
  • 功能描述:
    将分配的Scsi_Host主机适配器描述符添加到scsi总线设备模型中。

  • 参数:
    struct scsi_host_template *sht:指向主机适配器描述符的指针;
    struct device *dev:指向这个SCSI主机适配器在驱动模型中的父设备的device描述符,它决定了这个SCSI主机适配器在sysfs文件系统中的位置,可以为NULL;

  • 返回值:
    成功返回0,错误返回非0值。

3. 函数scsi_add_host_with_dma ()代码(摘自文件drivers/scsi/hosts.c)

int scsi_add_host_with_dma(struct Scsi_Host *shost, struct device *dev,
			   struct device *dma_dev)
{
	struct scsi_host_template *sht = shost->hostt;
	error = scsi_init_sense_cache(shost);
	// can_queue主机适配器可以同时接受的命令数必须大于0
	if (!shost->can_queue) {
		goto fail;
	}
	// 确定主机适配器在sysfs中的位置,内嵌device地址赋值给主机适配器内嵌通用设备的parent域。   
	// 如果传入的dev参数为NULL,我们就把它放在sys/devices/platform/目录下,platform设备是某些特定平台专有的外围设备。
	if (!shost->shost_gendev.parent)
		shost->shost_gendev.parent = dev ? dev : &platform_bus;
	if (!dma_dev)
		dma_dev = shost->shost_gendev.parent;

	// 主机适配器的DMA设备,它只是用于虚拟主机适配器环境下
	shost->dma_dev = dma_dev;

	pm_runtime_get_noresume(&shost->shost_gendev);
	pm_runtime_set_active(&shost->shost_gendev);
	pm_runtime_enable(&shost->shost_gendev);
	device_enable_async_suspend(&shost->shost_gendev);

	// 将主机适配器的内嵌类设备添加到系统中
	error = device_add(&shost->shost_gendev);
	
	scsi_host_set_state(shost, SHOST_RUNNING);
	get_device(shost->shost_gendev.parent);

	device_enable_async_suspend(&shost->shost_dev);

	error = device_add(&shost->shost_dev);
	
	get_device(&shost->shost_gendev);

	if (shost->transportt->host_size) {
		shost->shost_data = kzalloc(shost->transportt->host_size,
					 GFP_KERNEL);
		if (shost->shost_data == NULL) {
			error = -ENOMEM;
			goto out_del_dev;
		}
	}

	if (shost->transportt->create_work_queue) {
		snprintf(shost->work_q_name, sizeof(shost->work_q_name),
			 "scsi_wq_%d", shost->host_no);
		shost->work_q = create_singlethread_workqueue(
					shost->work_q_name);
		if (!shost->work_q) {
			error = -EINVAL;
			goto out_free_shost_data;
		}
	}
	
	// 将主机适配器添加到子系统,包括为主机适配器属性添加对应的文件。在/sys/class/scsi_host/host0下
	error = scsi_sysfs_add_host(shost);
	
	// 用于proc文件系统为该主机适配器创建目录项,为兼容早期版本,允许通过proc文件系统对主机适配器进行某些配置,或者显示一些信息
	scsi_proc_host_add(shost);
	
	scsi_autopm_put_host(shost);
	return error;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65

主机适配器的 内嵌通用设备 和 内嵌类设备 的初始化已经全部完成。将SCSI子系统的驱动模型设备关系整理如下图:
在这里插入图片描述

五、SCSI设备探测

1. SCSI设备探测总体流程

在这里插入图片描述**SCSI总线扫描的目的:**是通过协议特定或芯片特定的方式探测出挂接在主机适配器后面的目标节点和逻辑单元,为它们在内存中构建相应的数据结构,将它们添加到系统之中。
主机适配器实现可能采取不同的拓扑发现和设备添加机制:
● 主机适配器实现将拓扑发现的工作放在主机适配器固件(HBA Firmware)中执行,例如某些带RAID功能的主机适配器。
● 由主机适配器驱动(HBA Driver)来负责拓扑发现,不需要调用SCSI中间层提供的服务完成。(SAS、FC协议)
● 由主机适配器驱动(HBA Driver)来负责拓扑发现,通过SCSI中间层提供的服务完成。大多数支持传统的SPI(SCSI并行接口)协议的主机适配器都采取这种方式,它也是我们讨论的重点。

2. SCSI总线设备的扫描过程描述

在这里插入图片描述

3. SCSI设备探测和添加流函数介绍

探测函数的调用流程:
在这里插入图片描述

(1)函数scsi_scan_host ()代码(摘自文件drivers/scsi/scsi_scan.c)

SCSI中间层提供扫描SCSI总线的服务函数是scsi_scan_host,它采用的就是上面描述的向各个<channel, id, lun>发送INQUIRY命令的方式。

void scsi_scan_host(struct Scsi_Host *shost)
{
	struct async_scan_data *data;
  // 扫描类型,可取值为none、manual、sync和async,分别表示不进行扫描、手动扫描、执行同步扫描和异步扫描。
  // 加载SCSI中间层模块时通过模块参数设定,未设定,则采用在编译时指定CONFIG_SCSI_SCAN_ASYNC,则默认值为async。
	if (strncmp(scsi_scan_type, "none", 4) == 0 ||
	    strncmp(scsi_scan_type, "manual", 6) == 0)
		return;

	data = scsi_prep_async_scan(shost); // 异步扫描作准备, 同步扫描,则不需要任何准备
	if (!data) {
		do_scsi_scan_host(shost); // 同步扫描逻辑
		return;
	}
	async_schedule(do_scan_async, data); // 异步扫描逻辑

	/* scsi_autopm_put_host(shost) is called in scsi_finish_async_scan() */
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

同步扫描函数do_scsi_scan_host ()代码(摘自文件drivers/scsi/scsi_scan.c)

static void do_scsi_scan_host(struct Scsi_Host *shost)
{
   // 检查scsn_finished函数指针是否为NULL,若否,说明主机适配器驱动定义了自己的扫描逻辑, 适用于使用SAS协议或FC协议的主机适配器
	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中间层实现
		scsi_scan_host_selected(shost, SCAN_WILD_CARD, SCAN_WILD_CARD,
				SCAN_WILD_CARD, 0);
	}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

(2)函数scsi_scan_host_selected ()代码(摘自文件drivers/scsi/scsi_scan.c)

  • 功能描述:
    扫描选定的主机适配器

  • 参数:
    ● struct Scsi_Host *shost: 指向SCSI主机适配器描述符的指针;
    ● unsigned int channel: 通道编号;
    ● unsigned int id:ID编号;
    ● u64 lun:LUN编号;
    ● enum scsi_scan_mode rescan:0表示第一次扫描,为1表示重新扫描。

其中channel、id和LUN 都可以为具体的值,或者为通配符(SCAN_WILD_CARD宏,值为~0)表示需要尝试所有的可能值。

  • 返回值:
    函数返回0表示成功;否则返回负的错误码。
int scsi_scan_host_selected(struct Scsi_Host *shost, unsigned int channel,
			    unsigned int id, u64 lun,
			    enum scsi_scan_mode rescan)
{
	SCSI_LOG_SCAN_BUS(3, shost_printk (KERN_INFO, shost,
		"%s: <%u:%u:%llu>\n",
		__func__, channel, id, lun));
   // 对传入的<channel, id, lun>参数进行检查,它要求:
   // ● 通配符,表示扫描所有的channel、id或lun;
   // ● 指定值,表示扫描特定的channel、id或lun,这种情况下,所指定的值必须在有效范围内。
	if (((channel != SCAN_WILD_CARD) && (channel > shost->max_channel)) ||
	    ((id != SCAN_WILD_CARD) && (id >= shost->max_id)) ||
	    ((lun != SCAN_WILD_CARD) && (lun >= shost->max_lun)))
		return -EINVAL;

	if (!shost->async_scan) // 在此之前可能已经发起过异步扫描
		scsi_complete_async_scans(); // 等待异步扫描完成
  // 扫描所有可能的通道,或特定编号的通道
	if (scsi_host_scan_allowed(shost)) {
		if (channel == SCAN_WILD_CARD)
			for (channel = 0; channel <= shost->max_channel;
			     channel++)
				scsi_scan_channel(shost, channel, id, lun,
						  rescan);
		else
			scsi_scan_channel(shost, channel, id, lun, rescan);
	}

	return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30

(3)函数scsi_scan_channel ()代码(摘自文件drivers/scsi/scsi_scan.c)

  • 功能描述:
    扫描选定的通道

  • 参数:
    同 scsi_scan_host_selected

  • 返回值:
    同 scsi_scan_host_selected

static void scsi_scan_channel(struct Scsi_Host *shost, unsigned int channel,
			      unsigned int id, u64 lun,
			      enum scsi_scan_mode rescan)
{
	uint order_id;
  // 扫描所有可能的目标节点,或特定ID值的目标节点
	if (id == SCAN_WILD_CARD)
		for (id = 0; id < shost->max_id; ++id) {
			if (shost->reverse_ordering)
				order_id = shost->max_id - id - 1;
			else
				order_id = id;
			__scsi_scan_target(&shost->shost_gendev, channel,
					order_id, lun, rescan);
		}
	else
		__scsi_scan_target(&shost->shost_gendev, channel,
				id, lun, rescan);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

(4)函数__scsi_scan_target ()代码(摘自文件drivers/scsi/scsi_scan.c)

  • 功能描述:
    扫描选定的目标节点

  • 参数:
    同 scsi_scan_host_selected

  • 返回值:
    同 scsi_scan_host_selected

static void __scsi_scan_target(struct device *parent, unsigned int channel,
		unsigned int id, u64 lun, enum scsi_scan_mode rescan)
{
	struct Scsi_Host *shost = dev_to_shost(parent);
	int bflags = 0;
	int res;
	struct scsi_target *starget;

	if (shost->this_id == id) // 要扫描的ID等于本主机适配器的ID
		/*
		 * Don't scan the host adapter
		 */
		return;

	starget = scsi_alloc_target(parent, channel, id); // 分配一个目标节点结构
	if (!starget)
		return;

	if (lun != SCAN_WILD_CARD) {
		// 扫描特定lun值的逻辑单元。
		scsi_probe_and_add_lun(starget, lun, NULL, NULL, rescan, NULL);
		goto out_reap;
	}

	// 扫描所有的逻辑单元,先调用scsi_probe_and_add_lun探测LUN0。对LUN0的INQUIRY响应是所有目标节点都强制实现的;
	// 当然,这个位置上可以有逻辑单元,也可以没有逻辑单元
	res = scsi_probe_and_add_lun(starget, 0, &bflags, NULL, rescan, NULL);
	// SCSI_SCAN_LUN_PRESENT 表示的是这个位置上有逻辑单元,
	// SCSI_SCAN_TARGET_PRESENT 表示目标节点有响应,但是这个位置上没有逻辑单元。
	// 如果有对LUN0的INQUIRY 响应,进而探测其他lun编号的逻辑单元:
	if (res == SCSI_SCAN_LUN_PRESENT || res == SCSI_SCAN_TARGET_PRESENT) {
		// 向lun0发送REPORT LUN命令,这个命令返回目标节点已经实现的lun编号
		if (scsi_report_lun_scan(starget, bflags, rescan) != 0)
			//  如果向lun0发送REPORT LUN命令失败,我们只能采取最笨拙的办法,从1开始到最大编号,对于每个lun进行探测
			scsi_sequential_lun_scan(starget, bflags, starget->scsi_level, rescan);
	}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37

scsi_report_lun_scan和scsi_sequential_lun_scan函数最终都调用scsi_probe_and_add_lun对指定编号的lun进行探测。

(5)函数scsi_probe_and_add_lun()代码(摘自文件drivers/scsi/scsi_scan.c)

  • 功能描述:
    扫描选定的LUN逻辑单元, scsi_probe_and_add_lun函数是一个基础函数。

  • 参数:
    ● starget:指向目标节点描述符的指针;
    ● lun:逻辑单元编号;
    ● bflagsp:输出参数,如果不为NULL,通过它返回SCSI设备的标志;
    ● sdevp:输出参数,如果不为NULL,通过它返回指向SCSI设备描述符的指针;
    ● rescan:0表示第一次扫描,为1表示重新扫描;
    ● hostdata:被传递给scsi_alloc_sdev函数,一般用于添加别的部分发现的设备到系统时。

  • 返回值:
    ● SCSI_SCAN_NO_RESPONSE 表示不能分配或设置scsi_device;
    ● SCSI_SCAN_TARGET_PRESENT 表示目标节点有响应,但是这个位置上没有逻辑单元;
    ● SCSI_SCAN_LUN_PRESENT 表示已经分配并初始化一个新的scsi_device。

static int scsi_probe_and_add_lun(struct scsi_target *starget,
				  u64 lun, int *bflagsp,
				  struct scsi_device **sdevp,
				  enum scsi_scan_mode rescan,
				  void *hostdata)
{
	struct scsi_device *sdev;
	unsigned char *result;
	int bflags, res = SCSI_SCAN_NO_RESPONSE, result_len = 256;
	struct Scsi_Host *shost = dev_to_shost(starget->dev.parent);

	// 在目标节点描述符的devices链表中查找对应这个lun的SCSI设备是否已经存在
	sdev = scsi_device_lookup_by_target(starget, lun);
	if (sdev) {
	  // 在内存中已经找到SCSI设备,并且rescan参数为1
		if (rescan != SCSI_SCAN_INITIAL || !scsi_device_created(sdev)) {
			SCSI_LOG_SCAN_BUS(3, sdev_printk(KERN_INFO, sdev,
				"scsi scan: device exists on %s\n",
				dev_name(&sdev->sdev_gendev)));
			if (sdevp)
				*sdevp = sdev; // 返回该SCSI设备描述符的指针
			if (bflagsp)
				*bflagsp = scsi_get_device_flags(sdev, sdev->vendor, sdev->model); // 返回SCSI设备的标志
			return SCSI_SCAN_LUN_PRESENT; // 返回SCSI_SCAN_LUN_PRESENT
		}
	} else
	   // 如果在内存中没有找到对应的SCSI设备,先新分配一个SCSI设备描述符。
		sdev = scsi_alloc_sdev(starget, lun, hostdata);

   // 接下来需要发送各种SCSI命令,先从内存保留的DMA区域中分配一个256个字节的缓冲区用于保存响应结果
	result = kmalloc(result_len, GFP_KERNEL | ((shost->unchecked_isa_dma) ? __GFP_DMA : 0));
   // 发送SCSI INQUIRY命令探测逻辑单元
	if (scsi_probe_lun(sdev, result, result_len, &bflags))
		goto out_free_result;
   // scsi_probe_lun 通过参数返回了SCSI设备的标志, 设置该函数的输出参数。
	if (bflagsp)
		*bflagsp = bflags;
	 // 结果缓冲区中包含有效的响应数据。
	 // 响应的外围设备限定符(Peripheral Qualifier)为3 (011b),那么说明在这个位置上没有逻辑单元
	if ((result[0] >> 5) == 3) {
		SCSI_LOG_SCAN_BUS(2, sdev_printk(KERN_INFO, sdev, "scsi scan:"
				   " peripheral qualifier of 3, device not"
				   " added\n"))
		if (lun == 0) {
			SCSI_LOG_SCAN_BUS(1, {
				unsigned char vend[9];
				unsigned char mod[17];

				sdev_printk(KERN_INFO, sdev,
					"scsi scan: consider passing scsi_mod."
					"dev_flags=%s:%s:0x240 or 0x1000240\n",
					scsi_inq_str(vend, result, 8, 16),
					scsi_inq_str(mod, result, 16, 32));
			});
		}
		// 返回SCSI_SCAN_TARGET_PRESENT给调用者,表示目标节点有响应,但是这个位置上没有逻辑单元。
		res = SCSI_SCAN_TARGET_PRESENT;
		goto out_free_result;
	}
	// 用别的值组合来表明某个位置上不存在逻辑单元,例如NetApp目标节点在响应中使用外围设备限定符为0x01和外围设备类型为0x1F
	if (((result[0] >> 5) == 1 || starget->pdt_1f_for_no_lun) &&
	    (result[0] & 0x1f) == 0x1f &&
	    !scsi_is_wlun(lun)) {
		SCSI_LOG_SCAN_BUS(3, sdev_printk(KERN_INFO, sdev,
					"scsi scan: peripheral device type"
					" of 31, no device added\n"));
		// 返回SCSI_SCAN_TARGET_PRESENT给调用者,表示目标节点有响应,但是这个位置上没有逻辑单元。
		res = SCSI_SCAN_TARGET_PRESENT; 
		goto out_free_result;
	}
	// 已经在这个位置上找到了一个有效的逻辑单元, 调用scsi_add_lun将它添加到系统
	res = scsi_add_lun(sdev, result, &bflags, shost->async_scan);
	if (res == SCSI_SCAN_LUN_PRESENT) {
		if (bflags & BLIST_KEY) {
			sdev->lockable = 0;
			scsi_unlock_floptical(sdev, result);
		}
	}

 out:
	return res;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82

(6)函数scsi_probe_lun ()代码(摘自文件drivers/scsi/scsi_scan.c)

  • 功能描述:
    发送SCSI INQUIRY命令探测逻辑单元
  • 参数:
    ● sdev:指向SCSI设备描述符的指针;
    ● inq_result:指向用于保存响应数据的结果缓冲区;
    ● result_len:缓冲区长度;
    ● bflags:输出参数,用于获得SCSI设备的标志;
  • 返回值:
    ● 函数返回0表示成功,否则返回错误码。
static int scsi_probe_lun(struct scsi_device *sdev, unsigned char *inq_result,
			  int result_len, int *bflags)
{
	unsigned char scsi_cmd[MAX_COMMAND_SIZE];
	int first_inquiry_len, try_inquiry_len, next_inquiry_len;
	int response_len = 0;
	int pass, count, result;
	struct scsi_sense_hdr sshdr;

	*bflags = 0;
	// 第一轮使用36字节的最保守的标准传输长度构造命令,除非sdev->inquiry_len指定了一个不同的值。
	first_inquiry_len = sdev->inquiry_len ? sdev->inquiry_len : 36;
	try_inquiry_len = first_inquiry_len;
	pass = 1;

 next_pass:
	/* 每一轮有三次机会以忽略 Unit Attention */
	for (count = 0; count < 3; ++count) {
		int resid;
		// 构造一个 INQUIRY 命令
		memset(scsi_cmd, 0, 6);
		scsi_cmd[0] = INQUIRY;
		scsi_cmd[4] = (unsigned char) try_inquiry_len;
		memset(inq_result, 0, try_inquiry_len);
		// 执行SCSI INQUIRY命令都是调用scsi_execute_req函数
		// scsi_execute_req是一个同步阻塞函数,在SCSI命令执行完成之前,当前线程将被挂起。
		result = scsi_execute_req(sdev,  scsi_cmd, DMA_FROM_DEVICE,
					  inq_result, try_inquiry_len, &sshdr,
					  HZ / 2 + HZ * scsi_inq_timeout, 3,
					  &resid);
		if (result) {
			/*
			 * not-ready to ready transition [asc/ascq=0x28/0x0]
			 * or power-on, reset [asc/ascq=0x29/0x0], continue.
			 * INQUIRY should not yield UNIT_ATTENTION
			 * but many buggy devices do so anyway. 
			 */
			if ((driver_byte(result) & DRIVER_SENSE) &&
			    scsi_sense_valid(&sshdr)) {
				if ((sshdr.sense_key == UNIT_ATTENTION) &&
				    ((sshdr.asc == 0x28) ||
				     (sshdr.asc == 0x29)) &&
				    (sshdr.ascq == 0))
					continue;
			}
		} else {
			/*
			 * if nothing was transferred, we try
			 * again. It's a workaround for some USB
			 * devices.
			 */
			if (resid == try_inquiry_len)
				continue;
		}
		break;
	}
   // SCSI命令已经完成,并且已将响应数据保存到结果缓冲区
	if (result == 0) {
		scsi_sanitize_inquiry_string(&inq_result[8], 8);
		scsi_sanitize_inquiry_string(&inq_result[16], 16);
		scsi_sanitize_inquiry_string(&inq_result[32], 4);

		response_len = inq_result[4] + 5;
		if (response_len > 255)
			response_len = first_inquiry_len;	/* sanity */

		// 用户可能需要为某些类型的SCSI设备提供一些额外的标志,帮助Linux内核正确设置这些设备
		// 这些标志记录在以全局变量scsi_dev_info_list为表头的链表中,
		// 通过INQUIRY响应中的 厂商标识符 和 产品标识符 在链表中查找SCSI设备的标志,作为后续对SCSI设备进行处理的依据
		*bflags = scsi_get_device_flags(sdev, &inq_result[8],	&inq_result[16]);

		// 第一轮,获取返回数据中的支持的传输长度
		if (pass == 1) {
			if (BLIST_INQUIRY_36 & *bflags)
				next_inquiry_len = 36;
			else if (sdev->inquiry_len)
				next_inquiry_len = sdev->inquiry_len;
			else
				next_inquiry_len = response_len;

			// 如果第一轮成功,则以返回的数据长度再次构造命令,进行第二轮尝试
			if (next_inquiry_len > try_inquiry_len) {
				try_inquiry_len = next_inquiry_len;
				pass = 2;
				goto next_pass;
			}
		}

	} else if (pass == 2) {
		/* 如果第二轮失败,则使用最初的数据长度构造命令,进行第三轮尝试。 */
		try_inquiry_len = first_inquiry_len;
		pass = 3;
		goto next_pass;
	}

	/* 如果第三轮失败,则返回错误。 */
	if (result)
		return -EIO;

	/* 不要报告比设备有效数据更多的数据 */
	sdev->inquiry_len = min(try_inquiry_len, response_len);

	if (sdev->inquiry_len < 36) {
		if (!sdev->host->short_inquiry) {
			shost_printk(KERN_INFO, sdev->host,
				    "scsi scan: INQUIRY result too short (%d),"
				    " using 36\n", sdev->inquiry_len);
			sdev->host->short_inquiry = 1;
		}
		sdev->inquiry_len = 36;
	}

	sdev->scsi_level = inq_result[2] & 0x07;	// 获取版本
	if (sdev->scsi_level >= 2 || // SCSI-2
	    (sdev->scsi_level == 1 && (inq_result[3] & 0x0f) == 1))
		sdev->scsi_level++;
	sdev->sdev_target->scsi_level = sdev->scsi_level; // 保存版本

	// 如果是 SCSI-2 或更低,如果传输需要,则将LUN值存储在CDB[1]中。
	sdev->lun_in_cdb = 0;
	if (sdev->scsi_level <= SCSI_2 &&
	    sdev->scsi_level != SCSI_UNKNOWN &&
	    !sdev->host->no_scsi2_lun_in_cdb)
		sdev->lun_in_cdb = 1;

	return 0;
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128

由于SCSI规范的发展,SCSI INQUIRY命令有多个版本,体现在返回的响应数据格式和数据长度不相同。所有的SCSI设备都需要支持36字节的标准传输长度,有的设备会返回额外的数据,在标准数据格式中指明其长度。
为此,SCSI INQUIRY命令分3轮执行,每一轮有三次机会以忽略Unit Attention, SCSI inquiry命令的尝试方案如表4-6所示。
在这里插入图片描述除第一轮外,如果成功,都应该返回成功;只是第一轮成功需要修改长度后重新尝试。
除第二轮外,如果错误,都应该返回错误;只是第二轮错误需要恢复标准长度后重试。

在这里插入图片描述

(7)函数scsi_add_lun ()代码(摘自文件drivers/scsi/scsi_scan.c)

  • 功能描述:
    根据SCSI响应数据以及前面过程中的标志设置来初始化SCSI设备描述符各个域的,将选定的LUN逻辑单元SCSI设备添加到系统
  • 参数:
    ● sdev:指向SCSI描述符的指针;
    ● inq_result:上面获得的INQUIRY响应数据;
    ● bflags:SCSI设备的标志;
    ● async:1表示异步添加到sysfs;
  • 返回值:
    ● SCSI_SCAN_NO_RESPONSE 表示不能分配或设置scsi_device;
    ● SCSI_SCAN_LUN_PRESENT 表示已经分配并初始化一个新的scsi_device。
static int scsi_add_lun(struct scsi_device *sdev, unsigned char *inq_result,
		int *bflags, int async)
{
	int ret;
	sdev->inquiry = kmemdup(inq_result,
				max_t(size_t, sdev->inquiry_len, 36),
				GFP_KERNEL);
	if (sdev->inquiry == NULL)
		return SCSI_SCAN_NO_RESPONSE;

	sdev->vendor = (char *) (sdev->inquiry + 8);
	sdev->model = (char *) (sdev->inquiry + 16);
	sdev->rev = (char *) (sdev->inquiry + 32);

	if (strncmp(sdev->vendor, "ATA     ", 8) == 0) {
		/* Sata仿真层设备。这是一个围绕SATL电源管理规范工作的hack,该规范规定,当SATL检测到设备已进入待机模式时,应响应NOT READY。*/
		sdev->allow_restart = 1;
	}

	if (*bflags & BLIST_ISROM) {
		sdev->type = TYPE_ROM;
		sdev->removable = 1;
	} else {
		sdev->type = (inq_result[0] & 0x1f);
		sdev->removable = (inq_result[1] & 0x80) >> 7;

		if (scsi_is_wlun(sdev->lun) && sdev->type != TYPE_WLUN) {
			sdev_printk(KERN_WARNING, sdev,
				"%s: correcting incorrect peripheral device type 0x%x for W-LUN 0x%16xhN\n",
				__func__, sdev->type, (unsigned int)sdev->lun);
			sdev->type = TYPE_WLUN;
		}
	}

	if (sdev->type == TYPE_RBC || sdev->type == TYPE_ROM) {
				if ((*bflags & BLIST_REPORTLUN2) == 0)
			*bflags |= BLIST_NOREPORTLUN;
	}

	sdev->inq_periph_qual = (inq_result[0] >> 5) & 7;
	sdev->lockable = sdev->removable;
	sdev->soft_reset = (inq_result[7] & 1) && ((inq_result[3] & 7) == 2);

	if (sdev->scsi_level >= SCSI_3 ||(sdev->inquiry_len > 56 && inq_result[56] & 0x04))
		sdev->ppr = 1;
	if (inq_result[7] & 0x60)
		sdev->wdtr = 1;
	if (inq_result[7] & 0x10)
		sdev->sdtr = 1;

	sdev_printk(KERN_NOTICE, sdev, "%s %.8s %.16s %.4s PQ: %d "
			"ANSI: %d%s\n", scsi_device_type(sdev->type),
			sdev->vendor, sdev->model, sdev->rev,
			sdev->inq_periph_qual, inq_result[2] & 0x07,
			(inq_result[3] & 0x0f) == 1 ? " CCS" : "");

	if ((sdev->scsi_level >= SCSI_2) && (inq_result[7] & 2) &&
	    !(*bflags & BLIST_NOTQ)) {
		sdev->tagged_supported = 1;
		sdev->simple_tags = 1;
	}

	/*
	 * Some devices (Texel CD ROM drives) have handshaking problems
	 * when used with the Seagate controllers. borken is initialized
	 * to 1, and then set it to 0 here.
	 */
	if ((*bflags & BLIST_BORKEN) == 0)
		sdev->borken = 0;

	if (*bflags & BLIST_NO_ULD_ATTACH)
		sdev->no_uld_attach = 1;

	/* 显然,需要选择一些真正坏掉的设备(与SCSI标准相反),而不需要断言ATN */
	if (*bflags & BLIST_SELECT_NO_ATN)
		sdev->select_no_atn = 1;

	/* 最大512扇区传输长度损坏的RA 4x00 Compaq磁盘阵列 */
	if (*bflags & BLIST_MAX_512)
		blk_queue_max_hw_sectors(sdev->request_queue, 512);
	/* 对于报告错误的最大/最优长度并依赖于旧块层安全默认值的目标,最大1024扇区传输长度 */
	else if (*bflags & BLIST_MAX_1024)
		blk_queue_max_hw_sectors(sdev->request_queue, 1024);

	/* 有些设备可能不希望在添加设备时自动发出启动命令。*/
	if (*bflags & BLIST_NOSTARTONADD)
		sdev->no_start_on_add = 1;

	if (*bflags & BLIST_SINGLELUN)
		scsi_target(sdev)->single_lun = 1;

	sdev->use_10_for_rw = 1;

	/* 有些设备不喜欢REPORT SUPPORTED OPERATION CODES,会简单地超时导致sd_mod init花费非常非常长的时间 */
	if (*bflags & BLIST_NO_RSOC)
		sdev->no_report_opcodes = 1;

	/* 设置运行在这里的设备,以便从配置可以做I/O */
	mutex_lock(&sdev->state_mutex);
	ret = scsi_device_set_state(sdev, SDEV_RUNNING);
	if (ret)
		ret = scsi_device_set_state(sdev, SDEV_BLOCK);
	mutex_unlock(&sdev->state_mutex);

	if (ret) {
		sdev_printk(KERN_ERR, sdev,
			    "in wrong state %s to complete scan\n",
			    scsi_device_state_name(sdev->sdev_state));
		return SCSI_SCAN_NO_RESPONSE;
	}

	if (*bflags & BLIST_NOT_LOCKABLE)
		sdev->lockable = 0;

	if (*bflags & BLIST_RETRY_HWERROR)
		sdev->retry_hwerror = 1;

	if (*bflags & BLIST_NO_DIF)
		sdev->no_dif = 1;

	if (*bflags & BLIST_UNMAP_LIMIT_WS)
		sdev->unmap_limit_for_ws = 1;

	sdev->eh_timeout = SCSI_DEFAULT_EH_TIMEOUT;

	if (*bflags & BLIST_TRY_VPD_PAGES)
		sdev->try_vpd_pages = 1;
	else if (*bflags & BLIST_SKIP_VPD_PAGES)
		sdev->skip_vpd_pages = 1;

	transport_configure_device(&sdev->sdev_gendev);

	sdev->use_rpm_auto = 0;
	sdev->autosuspend_delay = SCSI_DEFAULT_AUTOSUSPEND_DELAY;
	// 主机适配器模板实现了slave_configure回调函数,它允许主机适配器驱动对SCSI设备执行特定的初始化操作,如调整SCSI设备的队列深度等
	if (sdev->host->hostt->slave_configure) {
		ret = sdev->host->hostt->slave_configure(sdev);
		if (ret) {
			if (ret != -ENXIO) {
				sdev_printk(KERN_ERR, sdev,
					"failed to configure device\n");
			}
			return SCSI_SCAN_NO_RESPONSE;
		}
	}

	if (sdev->scsi_level >= SCSI_3)
		scsi_attach_vpd(sdev);

	sdev->max_queue_depth = sdev->queue_depth;

  // 如果主机适配器在执行的是异步扫描,则先忽略这一步,在异步扫描函数scsi_finish_async_scan完成后再做
  // 将SCSI设备及对应的目标节点添加到sysfs文件系统,并创建对应的属性文件
	if (!async && scsi_sysfs_add_sdev(sdev) != 0)
		return SCSI_SCAN_NO_RESPONSE;

	return SCSI_SCAN_LUN_PRESENT;
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152
  • 153
  • 154
  • 155
  • 156
  • 157
  • 158
  • 159
  • 功能描述:
    将SCSI设备及对应的目标节点添加到sysfs文件系统,并创建对应的属性文件。这个过程中会涉及SCSI设备的两个内嵌设备:内嵌通用设备和内嵌类设备。
  • 参数:
    ● sdev:指向SCSI设备描述符的指针;
  • 返回值:
    ● 零-成功,非零-错误码
int scsi_sysfs_add_sdev(struct scsi_device *sdev)
{
	int error, i;
	struct request_queue *rq = sdev->request_queue;
	struct scsi_target *starget = sdev->sdev_target;

	error = scsi_target_add(starget); // 目标节点添加到sysfs文件系统
	if (error)
		return error;

	error = scsi_dh_add_device(sdev);  // SCSI设备添加到sysfs文件系统
	
	error = device_add(&sdev->sdev_gendev); // 内嵌通用设备添加到sysfs文件系统

	device_enable_async_suspend(&sdev->sdev_dev); 
	error = device_add(&sdev->sdev_dev); // 内嵌类设备添加到sysfs文件系统

	transport_add_device(&sdev->sdev_gendev);

	error = bsg_register_queue(rq, &sdev->sdev_gendev, NULL, NULL);
	/* add additional host specific attributes */
	if (sdev->host->hostt->sdev_attrs) {
		for (i = 0; sdev->host->hostt->sdev_attrs[i]; i++) {
		  // 各种属性文件被放在内嵌通用设备对应的目录下
			error = device_create_file(&sdev->sdev_gendev, sdev->host->hostt->sdev_attrs[i]);
		}
	}
	return error;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29

最终,如果SCSI设备有效,那么它相关的数据结构(预先分配的目标节点和SCSI设备描述符)必然会被创建并且保留在内存中,否则移除预分配的相关数据结构。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/一键难忘520/article/detail/1003471
推荐阅读
相关标签
  

闽ICP备14008679号