Linux iscsi/target 内核模块框架详细解析

(一)scsi 简介

SCSI(Small Computer System Interface),即小型计算机系统接口。SCSI 是一种智能的通用接口标准。一种用于计算机和智能设备之间(硬盘、软驱、光驱、打印机、扫描仪等)系统级接口的独立处理器标准。它是各种计算机与外部设备之间的接口标准。
SCSI 是个多任务接口,设有母线仲裁功能。挂在一个 SCSI 母线上的多个外设可以同时工作。SCSI 上的设备平等占有总线。SCSI 接口可以同步或异步传输数据。
最初的 SCSI 标准的最大同步传输速率为5MB/s(SCSI-1,又名Narrow SCSI,1986年,最大支持7个设备,时钟频率为5MHz),后来的 SCSI II 规定了 2 种提高速度的选择。一种为提高数据传输的频率,即 Fast SCSI(1994年,最大支持7个设备),由于频率提高一倍,达10MB/s(10MHz);另一种提高速度的选择是传输频率提高一倍的同时也增大数据通路的宽度,由8位增至16位,即Wide SCSI,其最大同步传输速度为20MB/s (时钟频率为10MHz ,1996年,最大支持15个设备)。
SCSI 类型
(1)SCSI-1
1986 年ANSI标准,X3.131-1986,SCSI-1是最原始的版本,异步传输的频率为3MB/S,同步传输的频率为5MB/s。
(2)SCSI-2
1994 年ANSI标准,X3.131-1994,早期的SCSI-2,称为FastSCSI,通过提高同步传输的频率使据传输速率从原有的5MB/s提高为10MB/s,支持8位并行数据传输,可连7个外设。后来出现的WideSCSI,支持16位并行数据传输,数据传输率也提高到了20MB/s,可连16个外设。此版本的SCSI使用一个50针的接口,主要用于扫描仪、CD-ROM驱动器及老式硬盘中。
(3)SCSI-3
发展到 SCSI-3,已经过于复杂,不适合作为单独的一个标准;在某次修订中,T10委员会决定把 SCSI 分为更小的单元;并且将某些单元的维护工作移交给其他组织。SCSI-3是这些子标准的集合, 表示第三代SCSI。

SCSI 协议层

  1. 特定设备命令集:包括磁盘设备的 SCSI 块命令(SBC,SCSI Block Commands)等。
  2. 基础命令集:所有 SCSI 设备都必须实现的基础命令(SPC,SCSI Primary Commands)。
  3. SCSI 传输协议:iSCSI,FCP。
  4. 物理连接:光纤通道,internet 架构模型:定义了SCSI系统模型和各单元的功能分工。

SCSI 报文类型
SCSI Payload

  • SCSI 命令/响应
  • 任务管理请求/响应
  • SCSI 读写(Data-In,Data-Out)
  • Ready To Transfer

SCSI 和 ISCSI Payload

  • 异步消息

iSCSI Payload

  • Text 请求/响应
  • 登录请求/响应
  • 注销请求/响应
  • SNACK 请求(selective negative ack)
  • Reject
  • NOP-Out/NOP-In

初始化阶段 scsi 协议协商交互
(1)查询存储信息

  • 针对 LUN0 进行命令查询
  • 查询 LUN0 的 SN 等一系列信息生成控制卷
  • 控制卷的作用是承载客户端第一次 report luns 命令

(2)扫盘

  • INQURY:查询盘的基本属性,VID/PID/SN 等
  • Mode Sense(请求检测指令):查询盘的其他基本属性,写保护开关,写缓存开关等
  • Mgmt Protocol In:查询盘支持的 SCSI 命令字
  • TUR:查询盘是否准备好
  • READ CAPACITY:查询盘的容量
  • READ:一般是读盘开始位置,一般属于盘的元数据区

SCSI 子系统命令分类:
与存储相关的 SCSI 命令一般是在 SCSI Architecture Model(SAM),SCSI Primary Commands(SPC)和 SCSI Block Commands(SBC)中定义:

  • SAM 定义 SCSI 系统模型、SCSI 标准集的功能性分区,以及适用于所有 SCSI 实现和实现标准的需求。
  • SPC 定义对所有 SCSI 设备模型通用的行为。
  • SBC 定义命令集扩展,以方便操作 SCSI 直接访问块设备。

每个 SCSI 命令都由 Command Descriptor Block(CDB)描述,它定义 SCSI 设备执行的操作。SCSI 命令涉及到用于向 SCSI 设备传输数据(或从中输出数据)的数据命令,以及用于设置 SCSI 设备的配置参数的非数据命令。

下面是一些常用的 SCSI 命令:

  • Inquiry:请求目标设备的摘要信息
  • Test/Unit/Ready:检测目标设备是否准备好进行传输
  • READ:从 SCSI 目标设备传输数据
  • WRITE:向 SCSI 目标设备传输数据
  • Request Sense:请求最后一个命令的检测数据
  • Read Capacity:请求存储容量信息

所有 scsi 命令都要以操作代码的第一个字节为开端,以表明它所代表的操作。并且所有 scsi 命令都要包含一个控制字节。这个字节通常是该命令的最后一个字节,用于表示与供应商相关的信息等等。

(二)scsi 子系统架构

scsi 子系统是块设备驱动器之一。
与其他子系统不同的是:scsi 子系统是一种分层的架构,共分为三层。
最上层是 scsi 和主要设备类型的驱动器的最高接口。
中间层是公共层或统一层,这一层包含 scsi 堆栈的较高层和较低层的一些公共服务。
最底层是适用于 scsi 的物理接口的实际驱动器。

最上层有:disk driver(sd 磁盘驱动),tape drvier(st 磁带驱动),CD-ROM drvier(sr 软盘驱动),Generic drvier(sg 通用驱动)。
最底层(Lower level drvier【LLD】):Fibre channel drvier(fc 光纤通道驱动),iscsi drvier(iscsi 驱动),Bridge drvier。

scsi 子系统代码在 linux/drviers/scsi/目录中,头文件位于linux/include/scsi/目录中。

linux 的 scsi 模型基于传统的并行 scsi。主机适配器连接主机 I/O 总线(通常是 PCI 总线)和存储 I/O 总线(例如 scsi 总线)。一台计算机可以有多个主机适配器,而主机适配器可以控制一个(单通道适配器)或多个(多通道适配器)scsi 总线。一条总线可以有多个目标节点与之相连,并且一个目标节点可以有多个逻辑单元。

在 linux 中,用 scsi 设备描述符表示具体的逻辑单元,和通常说到的 scsi 设备不一样,scsi 磁盘整体而言是目标节点,而磁盘内部可以有多个逻辑单元,统一由磁盘控制器控制,这些逻辑单元才是真正的 I/O 终点的存储设备。但是,为了简单,大多数厂商生产的磁盘只做了基本实现,即实现了 LUN0。

主机 I/O 总线
 -------------------------------------------------										    
  					  |
					  |
			       主机适配器
			 channel-0 channel-1 
				 |          |
存储 I/O 总线     |          +---------------
 ----------------+--------------------------------
	         |				   |
	      目标设备0           目标设备1
	 lun0 lun1 lun2        lun0...lun3 

linux 中使用四元组host:channel:id:lun来唯一定位一个 scsi 设备。
host 为主机适配器在计算机内的编号,一般实现方式是:在系统中定义一个全局变量,每发现一个主机适配器,将这个全局变量赋值给 host 编号,同时递增该全局变量。

  • channel 为 scsi 通道编号,或者为 scsi 总线编号,这个相对于主机适配器而言,应该由主机适配器的固件来维护。
  • id 为目标节点标识符。
  • lun 为目标节点内的逻辑单元编号。

scsi 子系统的主要功能是:

  • 探测 scsi 设备,在内存建立可供设备驱动使用的核心数据结构。
  • 在 sysfs 文件系统中构造 scsi 子系统的目录树。
  • scsi 最上层驱动绑定 scsi 设备,在内存中构建对应的核心数据结构。
  • 提供错误恢复 api,在 scsi 命令错误和超时后被调用。
scsi 最上层

该层是内核(设备级)最高级别接口。由一组驱动器组成,比如块设备(scsi 磁盘和 scsi cd-rom)和字符设备(scsi 磁带和 scsi generic)。最上层接受来自更上层(如 VFS)的请求并将其转换成 scsi 请求。上层负责完成 scsi 命令并将状态信息通知更上层。
scsi 磁盘驱动器在 linux/drviers/scsi/sd.c中实现。 scsi 磁盘驱动器通过调用register_blkdev进行自初始化并通过scsi_register_driver提供一组函数以表示所有 scsi 设备。其中sd_probe和sd_init_command是很重要的两个回调函数。只要新的 scsi 设备附加到系统, scsi 中间层就会调用 sd_probe函数。sd_probe函数可以决定此设备是否由 scsi 磁盘驱动器管理,如果是:就创建新的scsi_disk结构来表示它。sd_init_command函数将来自文件系统层的请求转变层 scsi 读或者写命令(为完成这个 I/O 请求,sd_rw_intr 会被调用)。
scsi 磁带驱动器在linux/drviers/scsi/sr.c中实现。磁带驱动器是顺序存取设备,会通过register_chrdev_region将自身注册为字符设备。scsi 磁带驱动器还提供了一个 probe 函数,称为st_probe。该函数会创建一种新磁带设备并将其添加到称为scsi_tapes的向量。scsi 磁带驱动器的独特之处在于,如果可能,它可以直接从用户空间执行 I/O 传输。否则,数据会通过驱动器缓冲被分段。
scsi CD-ROM 驱动器在linux/drivers/scsi/sr.c中内实现。CD-ROM 驱动器是另一种块设备并为 scsi 磁盘驱动器提供类似的函数集。sr_probe函数可用来创建scsi_sd结构以表示 CD-ROM 设备,并用register_cdrom注册此 CD-ROM。scsi 磁带驱动器还会导出sr_init_command,以将请求转换成 scsi CD-ROM 读或写请求。
scsi generic 驱动器在linux/drivers/scsi/sg.c内实现。该驱动器允许用户应用程序向设备发送 scsi 命令(比如格式化、模式感知或诊断命令)。通过sg3utils包还可以从用户空间利用 scsi generic 驱动器。这个用户空间包包括多种实用工具,可用来发送 scsi 命令和解析这些命令的响应。

scsi 中间层

该层是上层和底层的公共服务层(部分实现在linux/drivers/scsi/scsi.c)。它提供了很多上层和底层使用的函数,因而可以充当两层间的连接层。中间层很重要,它抽象化了较低层驱动器(LLD)的实现,可以在linux/drviers/scsi/hosts.c中部分地实现。这意味着可以以同样的方式使用带不同接口的Fibre Channel主机总线适配器(HBA)。
底层驱动器注册和错误处理都由 scsi 中间层提供。中间层还提供了上层和底层的 scsi 命令排队。scsi 中间层的一个重要功能是将来自上层的命令请求转换成 scsi 请求。它也负责管理特定于 scsi 的错误恢复。
中间层可以连接 scsi 子系统的上层和底层。它接受对 scsi 事物的请求并对这些请求进行排队以便处理(如linux/drviers/scsi/scsi_lib.c中所示)。当这些命令完成后,它接受来自 LLD 的 scsi 响应并通知上层此请求已完成。
中间层最重要职责之一是错误和超时处理。如果 scsi 命令没有在合理的时间内完成或者 scsi 请求返回错误,中间层就会管理错误或者重新发送此请求。中间层还可以管理上层恢复,比如请求 HBA(LLD)或 scsi 设备重置。scsi 错误和超时处理在linux/drivers/scsi/scsi_error.c中实现。

scsi 较低层

在最底层的是一组驱动器,称为 scsi 低层驱动器。它们是一些可与物理设备(比如 HBA)链接的特定驱动器。LLD 提供了自公共中间层到特定于设备的 HBA 的一种抽象。每个 LLD 都提供了到特定底层硬件的接口,但所使用的到中间层的接口却是一组标准接口。
较低层包含大量代码,原因是它要负责处理各种不同的 scsi 适配器类型。例如,Fibre Channel协议包含了针对 Emulex 和 QLogic 的各种适配器的 LLD。面向 Adaptec 和 LSI 的 SAS 适配器的 LLD 也包括在内。最后还有就是 iscsi 驱动。

(三)scsi 子系统对象

有三个核心的对象,分别对应三个数据结构:struct Scsi_Hoststruct scsi_targetstruct scsi_device
Scsi_Host描述 scsi 模型中的主机适配器。
scsi_target描述 scsi 模型中的目标节点。
scsi_device描述 scsi 模型中的逻辑单元。
另一个核心的数据结构是struct 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。

scsi_host_template: scsi 主机适配器模版

在linux/include/scsi/scsi_host.h中定义。

该定义给出了相同类型主机适配器的公用内容,例如:队列深度,scsi 命令处理回调函数,错误恢复回调函数等。主机适配器的分配要依照主机适配器模版。意思是:scsi 低层驱动先定义scsi_host_template,然后在发现每一个可以管理的主机适配器后,复制出一个Scsi_Host(scsi_host_template给中间层提供函数回调接口)。

一般来说由 pci 子系统来负责检测它所能支持的主机适配器。每个 scsi 设备驱动需要定义一个scsi_host_tempalte描述符。在驱动加载过程中,以它为模版,为每个支持的主机适配器创建一个Scsi_Host结构。

该模版有如下一些重要的回调定义:
(1)int (* queuecommand)(struct Scsi_Host *, struct scsi_cmnd *);
queuecommand回调用于将 scsi 命令块排入底层设备驱动的队列。
(2)

	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_abort_handler放弃给定的命令。
eh_device_reset_handler使 scsi 设备复位。
eh_target_reset_handler使目标节点复位。
eh_bus_reset_handler使 scsi 总线复位。
eh_host_reset_handler使主机适配器复位。
(3)

int (* slave_alloc)(struct scsi_device *);
int (* slave_configure)(struct scsi_device *);
void (* slave_destroy)(struct scsi_device *);

大多数 scsi 设备都有专门的私有数据,该系列回调用于对设备私有数据进行处理。
slave_alloc在扫描到一个新的 scsi 设备后调用,用户可以在这个回调中为设备分配结构或进行初始化。
slave_configure在接收到 scsi 设备的INQUIRY响应后调用,用户可以在这个回调中设置队列深度,修改设备标志位等工作。
slave_destory在销毁这个设备之前被调用,可以在这个回调中释放前面分配的内存等。
(4)

int (* target_alloc)(struct scsi_target *);
void (* target_destroy)(struct scsi_target *);

和slave类似,不过它们是用于对目标节点的专有数据进行配置。
target_alloc在发现一个新的目标器后调用,通常在这个回调中分配特定的数据结构,并进行初始化。
target_destory在目标节点被销毁之前被调用,可以在这个回调中释放之前分配的内存等。
(5)

int (* scan_finished)(struct Scsi_Host *, unsigned long);
void (* scan_start)(struct Scsi_Host *);

如果主机适配器有能力自己发现挂接到它的目标器,而不需要扫描整条总线,就可以实现该类回调,并调用scsi_scan_host。scsi_start回调在 scsi 中间层准备好,开始扫描前被调用,而scan_finished被定期调用,知道扫描已经结束或超时。

Scsi_Host: scsi 主机适配器

在linux/include/scsi/scsi_host.h中定义。
struct Scsi_Host *scsi_host_alloc(struct scsi_host_template *sht, int privsize);用于申请一个 Scsi_Host结构体。
在一般系统中,scsi 主机适配器为一块基于 pci 总线的拓展卡或者为一个 scsi 控制器芯片。每个 scsi 主机适配器可以存在多个通道,一个通道实际拓展了一条 scsi 总线。每个通道可以连接多个 scsi 目标节点,具体连接的数量与 scsi 总线带宽能力有关,或者受具体 scsi 协议的限制。
结构体中的一些重要定义:
(1)

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

系统为扫描发现的每个主机适配器创建一个Scsi_Host描述符。hostt指向创建它所依据的模版。
(2)

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

主机适配器后面的目标节点被链入__targets链表中,逻辑设备被链入__devices链表中。
(3)

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

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

(4)
```c
struct Scsi_Host {
unsigned short max_cmd_len;// 可以接受的 scsi 命令的最大长度
}

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

struct Scsi_Host {
   struct task_struct    * ehandler;   // 错误恢复线程
}

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

struct Scsi_Host {
struct deviceshost_gendev, shost_dev;
}

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

实现系统中所有主机适配器scsi_host的遍历。例如通过 proc 文件系统"热插入"一个 scsi 设备时,内核实现就需要从给定的主机适配器编号查找到Scsi_Host描述符。实际上,主机适配器的遍历借助于驱动模型中类的设备链表。确切地说,每个Scsi_Host都通过内嵌的类设备被链入到shost_class的设备链表中。

Scsi_Host的shost_gendev被挂入scsi_bus_type -> bus_type(linux 中对应 bus 为 “scsi”)。

Scsi_Host的shost_dev被挂入shost_class -> class(linux 中对应 class 为 “scsi_host”)。

scsi_target: scsi 目标节点

在linux/incluide/scsi/scsi_device.h中定义。
scsi_scan_target:扫描目标节点及可能包含的所有 luns。
scsi_alloc_target:寻找一个已存在的目标节点或者分配一个新的目标节点。
一个scsi_target结构表示一个 scsi 目标节点,这个结构可以被用于表示只有一个逻辑单元或有多个逻辑单元的设备。
结构体中的一些重要定义:

(1)

struct scsi_target {
struct list_headsiblings;
struct list_headdevices;
}

每个 scsi 目标节点用scsi_target表示,通过siblings挂到Scsi_Host主机适配器的__targets中。
而devices则是scsi_device设备的链表头。
(2)

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

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

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函数。
(4)

struct scsi_target {
struct devicedev;
}

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

scsi_devcie: scsi 逻辑设备

在linux/incluide/scsi/scsi_device.h中定义。

scsi_devcie表示逻辑设备,代表的是 scsi 磁盘的逻辑单元。scsi_devcie所代表的设备可能物理上并不属于操作系统所运行的系统,例如它可能是另一个存储设备上的STAT/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_headdevices;// 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_devcie表示的是 scsi 逻辑设备,它通过siblings链入到所连接的主机适配器的逻辑设备链表中,通过same_target_siblings链入到所属的目标节点的逻辑设备链表中。

结构体中的一些重要定义:
(1)

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

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

struct scsi_device {
struct Scsi_Host *host;
}

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

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

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

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指针指向。
(5)

struct scsi_device {
char type;
}

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

scsi_cmnd: scsi 命令

在linux/scsi/scsi_cmnd.h中定义。

scsi 命令有几方面的含义:

  • 一种是 scsi 规范定义的 scsi 命令,即表示为字节串的命令描述符(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 命令的上下文。

结构中的关键定义:
(1)

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中的错误恢复链表中。
(2)

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 中间层填入。
(3)

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域被低层驱动用来指向完成函数。

scsi 设备探测

有三种方法:

  1. 有的主机适配器实现将拓扑发现的工作放在主机适配器固件(HBA 固件)中执行,例如某些带 RAID 功能的主机适配器。它必须同时提供一种消息传递机制,让主机适配器驱动知道拓扑发现已经完成,并从主机适配器固件获得发现的所有设备,将它们添加到操作系统。例如 LSI 的 MPT 驱动。
  2. 其他的主机适配器实现则是由主机适配器驱动(HBA 驱动)来负责拓扑发现。分两种情况:第一种不需要调用 scsi 中间件提供的服务完成。例如 SAS(串行 scsi)和 FC(光纤通道 Fibre Channel)协议,按照自己的方式扫描总线,将发现的 scsi 设备添加到系统。第二种是 iscsi 协议,不需要拓扑发现过程,而是直接用户指定和 iscsi 目标器建立会话连接时手工添加 scsi 设备。
  3. 最后一种也是由主机适配器驱动来负责拓扑发现,并且拓扑发现过程通过 scsi 中间件提供的服务完成。大多数支持传统 SPI(scsi 并行接口)协议的主机适配器采用该方法。
scsi 系统初始化
SCSI子系统总是被编译到Linux内核的,其初始化入口函数为init_scsi,在文件drivers/scsi/scsi.c中。它使用宏subsys_initcall定义,依次调用:

• 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系统控制表;

• scsi_sysfs_register——注册SCSI总线类型以及sdev_class类;

• scsi_netlink_init——初始化SCSI传输netlink接口,netlink是Linux内核与用户空间进行通信的一种机制。
iscsi 与用户态通信

用户态一般可以使用 iscsiadm 程序负责 iscsi target 的扫描,iscsi 启动段初始化及登陆管理操作,而 iscsiadm 主要与 iscsid 后台进程通信发送命令完成登陆等管理。iscsid 通过 netlink 机制与内核中的scsi_transport_iscsi.ko通信完成所有 scsi 相关操作。

scsi_transport_iscsi提供iscsi_register_transport完成回调操作注册,主要向scsi_transport_iscsi提供struct iscsi_transport方法。

scsi_transport_iscsi提供了 sysfs 查看注册到该系统的子系统:

/sys/class/iscsi_transport
/sys/class/iscsi_endpoint
/sys/class/iscsi_iface
​
/sys/class/iscsi_host
/sys/class/iscsi_connection
/sys/class/iscsi_session
​
/sys/bus/iscsi_flashnode

另外提供了iscsi_eh工作队列处理 iscsi 的错误恢复等操作。

该模块使用 netlink 与用户态 iscsid 程序通信完成 iscsi 相关操作,而底层我们只需要注册struct iscsi_transport方法。

这里介绍部分常用数据结构

struct iscsi_transport {
  unsigned int caps;	// 该 iscsi 提供的能力,供用户读取。
  
  // 创建一个新的 iSCSI 会话对象
  struct iscsi_cls_session *(*create_session) (struct iscsi_endpoint *ep,
					uint16_t cmds_max, uint16_t qdepth,
					uint32_t sn);
  // 摧毁一个已经存在的 iSCSI 会话对象
  void (*destroy_session) (struct iscsi_cls_session *session);
  // 创建一个新的 iSCSI 连接
  struct iscsi_cls_conn *(*create_conn) (struct iscsi_cls_session *sess,
				uint32_t cid);
  // 将此连接与现有的 iSCSI 会话和指定的传输描述符关联
  int (*bind_conn) (struct iscsi_cls_session *session,
			  struct iscsi_cls_conn *cls_conn,
			  uint64_t transport_eph, int is_leading);
  // 摧毁一个不活跃的连接
  void (*destroy_conn) (struct iscsi_cls_conn *conn);
  // 设置 iSCSI 参数
  int (*set_param) (struct iscsi_cls_conn *conn, enum iscsi_param param,
			  char *buf, int buflen);
  // 设置连接为可操作状态
 	int (*start_conn) (struct iscsi_cls_conn *conn);
  // 暂停/恢复/终止连接
	void (*stop_conn) (struct iscsi_cls_conn *conn, int flag);
  // 发送 iSCSI PDU,登陆,登出,NOP-Out,拒绝,文本等信息。
  int (*send_pdu) (struct iscsi_cls_conn *conn, struct iscsi_hdr *hdr,
			 char *data, uint32_t data_size);
  // 在恢复超时期间通知 LLD(底层驱动-> iscsi_tcp) 一个块
  void (*session_recovery_timedout) (struct iscsi_cls_session *session);
  // 初始化 iscsi_task 和所有相关内部数据结构
  int (*init_task) (struct iscsi_task *task);
  // 请求 LLD 传输 cmd 任务
  int (*xmit_task) (struct iscsi_task *task);
  // 请求 LLD 清除 task
  void (*cleanup_task) (struct iscsi_task *task);
};

iscsi_tcp 模块有如下初始化代码注册struct iscsi_transport:

static struct iscsi_transport iscsi_sw_tcp_transport = {
	.owner			= THIS_MODULE,
	.name			= "tcp",
	.caps			= CAP_RECOVERY_L0 | CAP_MULTI_R2T | CAP_HDRDGST
				  | CAP_DATADGST,
	/* session management */
	.create_session		= iscsi_sw_tcp_session_create,
	.destroy_session	= iscsi_sw_tcp_session_destroy,
	/* connection management */
	.create_conn		= iscsi_sw_tcp_conn_create,
	.bind_conn		= iscsi_sw_tcp_conn_bind,
	.destroy_conn		= iscsi_sw_tcp_conn_destroy,
	.attr_is_visible	= iscsi_sw_tcp_attr_is_visible,
	.set_param		= iscsi_sw_tcp_conn_set_param,
	.get_conn_param		= iscsi_sw_tcp_conn_get_param,
	.get_session_param	= iscsi_session_get_param,
	.start_conn		= iscsi_conn_start,
	.stop_conn		= iscsi_sw_tcp_conn_stop,
	/* iscsi host params */
	.get_host_param		= iscsi_sw_tcp_host_get_param,
	.set_host_param		= iscsi_host_set_param,
	/* IO */
	.send_pdu		= iscsi_conn_send_pdu,
	.get_stats		= iscsi_sw_tcp_conn_get_stats,
	/* iscsi task/cmd helpers */
	.init_task		= iscsi_tcp_task_init,
	.xmit_task		= iscsi_tcp_task_xmit,
	.cleanup_task		= iscsi_tcp_cleanup_task,
	/* low level pdu helpers */
	.xmit_pdu		= iscsi_sw_tcp_pdu_xmit,
	.init_pdu		= iscsi_sw_tcp_pdu_init,
	.alloc_pdu		= iscsi_sw_tcp_pdu_alloc,
	/* recovery */
	.session_recovery_timedout = iscsi_session_recovery_timedout,
};

static int __init iscsi_sw_tcp_init(void)
{
	if (iscsi_max_lun < 1) {
		printk(KERN_ERR "iscsi_tcp: Invalid max_lun value of %u\n",
		       iscsi_max_lun);
		return -EINVAL;
	}

	iscsi_sw_tcp_scsi_transport = iscsi_register_transport(
						&iscsi_sw_tcp_transport);
	if (!iscsi_sw_tcp_scsi_transport)
		return -ENODEV;

	return 0;
}

完成iscsi_register_transport注册后,用户态即可通过 iscsiadm 工具和 iscsi_tcp 模块通信,完成扫描登陆等操作了。

(4)iscsi_tcp 远端设备发现流程(主要源码分析,较长)

iscsi 分为 initiator 和 target:

initiator 作为客户端负责会话,连接,登录等管理。

iscsi 使用 target 作为后端存储管理,响应前端请求。

Initiator用一种叫做CDB(Command Descriptor Blocks)的数据结构封装请求。

target 需要包括一个 SCSI port,任务分发器和逻辑单元:

  • SCSI port用来连接SCSI通道,任务分发器负责将客户端的请求分到到适当的逻辑单元,而逻 辑单元才是真正处理任务的实体
  • 通常一个逻辑单元都支持多任务环境,所以需要包含一个任务管理器和真正响应请求的服务器

两种会话类型:

  • Dicovery session
    用于发现 target,只支持三种iSCSI报文
    • Login Req / Rsp
    • Text Req(Send Target) / Rsp
    • Logout Req / Rsp
  • Normal session
    • 登录阶段
      • 初始化登录阶段
      • 安全认证阶段
      • 操作协商阶段
    • 完整功能阶段
    • 退出阶段

在initiator和target之间有两种参数协商方法

  • – 登录过程中的参数协商,通过Login Req和Login Rsp协商参数
  • – 完整功能态下(已经登录)的参数协商,通过Text Req和Text Rsp进行参数协商
  • – 这两种协商方法,报文的数据段语法相同
  • – 有些参数只能在登录过程协商

两种编码方式

  • iqn(iSCSI qualified name)
    • 类型
    • 日期(拥有组织名的日期) ​
    • 组织名 ​
    • 组织内部唯一的标志符 ​
    • 实例:iqn.2001-04.com.h3c:storage.tape1.sys1.xyz
  • eui(主要用于FC设备接入iSCSI网络)
    • 类型EUI-64 标识符(如果是FC设备,那么就是FC的WWN)
    • 实例:eui.02004567A425678D
  • 一个iSCSI Node只能有一个名字(?),但可以有多个地址 • 一个名字可以有多个IP地址;多个名字也可以共用一个IP地址
  • iSCSI的认证机制是基于名字的
1 会话创建

首先第一步是由 iscsid 进程创建一个新的会话,通过 netlink 机制会调用到 iscsi_tcp 的iscsi_sw_tcp_session_create函数。
该函数会创建一个新的主机适配器并注册到系统中。

static struct iscsi_cls_session *
iscsi_sw_tcp_session_create(struct iscsi_endpoint *ep, uint16_t cmds_max,
			    uint16_t qdepth, uint32_t initial_cmdsn)
{
	struct iscsi_cls_session *cls_session;
	struct iscsi_session *session;
	struct iscsi_sw_tcp_host *tcp_sw_host;
	struct Scsi_Host *shost;
	int rc;
...
...
	shost = iscsi_host_alloc(&iscsi_sw_tcp_sht,
				 sizeof(struct iscsi_sw_tcp_host), 1); -------------------------------1if (!shost)
		return NULL;
  // 将创建的 scsi_transport_template 存储到该主机适配器,后续 iscsi_tcp 通过该回调可以去初始化或者启动 pdu 相关操作。
	shost->transportt = iscsi_sw_tcp_scsi_transport;
	shost->cmd_per_lun = qdepth;
	shost->max_lun = iscsi_max_lun;
	shost->max_id = 0;
	shost->max_channel = 0;
	shost->max_cmd_len = SCSI_MAX_VARLEN_CDB_SIZE;

  // 对主机适配的最大命令数量进行矫正。
	rc = iscsi_host_get_max_scsi_cmds(shost, cmds_max);
	if (rc < 0)
		goto free_host;
	shost->can_queue = rc;

  // 将主机适配添加到系统中,包括与 blk 相关变量的绑定和初始化,以及 device sysfs 相关文件创建。
	if (iscsi_host_add(shost, NULL))
		goto free_host;

	cls_session = iscsi_session_setup(&iscsi_sw_tcp_transport, shost,
					  cmds_max, 0,
					  sizeof(struct iscsi_tcp_task) +
					  sizeof(struct iscsi_sw_tcp_hdrbuf),
					  initial_cmdsn, 0); -----------------------------------------------2if (!cls_session)
		goto remove_host;
	session = cls_session->dd_data;
	tcp_sw_host = iscsi_host_priv(shost);
	tcp_sw_host->session = session;

	if (iscsi_tcp_r2tpool_alloc(session))
		goto remove_session;
	return cls_session;
...
...
}

(1)首先分配一个管理主机适配器的struct Scsi_Host结构,其中struct iscsi_sw_tcp_host是 iscsi_tcp 管理的私有数据结构,如下:

struct iscsi_sw_tcp_host {
	struct iscsi_session	*session;
};

会在绑定会话与连接时赋值。

iscsi_sw_tcp_sht是 iscsi_tcp 主机适配器的主机适配器模版,如下:

static struct scsi_host_template iscsi_sw_tcp_sht = {
	.module			= THIS_MODULE,
	.name			= "iSCSI Initiator over TCP/IP",
	.queuecommand           = iscsi_queuecommand,
	.change_queue_depth	= scsi_change_queue_depth,
	.can_queue		= ISCSI_TOTAL_CMDS_MAX,
	.sg_tablesize		= 4096,
	.max_sectors		= 0xFFFF,
	.cmd_per_lun		= ISCSI_DEF_CMD_PER_LUN,
	.eh_timed_out		= iscsi_eh_cmd_timed_out,
	.eh_abort_handler       = iscsi_eh_abort,
	.eh_device_reset_handler= iscsi_eh_device_reset,
	.eh_target_reset_handler = iscsi_eh_recover_target,
	.use_clustering         = DISABLE_CLUSTERING,
	.slave_alloc            = iscsi_sw_tcp_slave_alloc,
	.slave_configure        = iscsi_sw_tcp_slave_configure,
	.target_alloc		= iscsi_target_alloc,
	.proc_name		= "iscsi_tcp",
	.this_id		= -1,
	.track_queue_depth	= 1,
};

iscsi_host_alloc代码如下:

struct Scsi_Host *iscsi_host_alloc(struct scsi_host_template *sht,
				   int dd_data_size, bool xmit_can_sleep)
{
	struct Scsi_Host *shost;
	struct iscsi_host *ihost;

  // 见下面 scsi_host_alloc 代码
	shost = scsi_host_alloc(sht, sizeof(struct iscsi_host) + dd_data_size);
	if (!shost)
		return NULL;
  // Scsi_Host 的尾部依次是 libiscsi 的 iscsi_host 私有数据结构和 iscsi_tcp 的 iscsi_sw_tcp_host 私有数据结构
	ihost = shost_priv(shost);

  // 如果 LLD 支持将来自工作队列的 I/O 进行排队那么会创建 iscsi_q_x 工作线程管理 xmit_task。
	if (xmit_can_sleep) {
		snprintf(ihost->workq_name, sizeof(ihost->workq_name),
			"iscsi_q_%d", shost->host_no);
		ihost->workq = create_singlethread_workqueue(ihost->workq_name);
		if (!ihost->workq)
			goto free_host;
	}
...
	ihost->state = ISCSI_HOST_SETUP;
	ihost->num_sessions = 0;
	init_waitqueue_head(&ihost->session_removal_wq);
	return shost;
...
}

// scsi_host_alloc 是对 Scsi_Host 的默认分配和初始化,在这里还添加了一个私有数据 struct iscsi_host。
// 那么在这里 struct Scsi_Host 除了保存了 iscsi_tcp 模块的私有数据还保存了 libiscsi 模块的私有数据 iscsi_host。
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;

	shost = kzalloc(sizeof(struct Scsi_Host) + privsize, gfp_mask);
	if (!shost)
		return NULL;

	shost->shost_state = SHOST_CREATED;
...
	// 全局变量,每分配一个主机适配器则增加其值。  
	index = ida_simple_get(&host_index_ida, 0, 0, GFP_KERNEL);
	if (index < 0)
		goto fail_kfree;
	shost->host_no = index;
...
  // 一些默认值设定,后续会在与底层驱动匹配通信时覆盖掉默认值。
	/* 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;
...
	device_initialize(&shost->shost_gendev);
	dev_set_name(&shost->shost_gendev, "host%d", shost->host_no);
  // 在 /sys/bus/scsi/hostx/ 中可以看到该 bus
	shost->shost_gendev.bus = &scsi_bus_type;
  // 在 /sys/devices/platform/hostx/iscsi_host/ 中可以看到该设备
	shost->shost_gendev.type = &scsi_host_type;

  // shost_gendev 是 shost 的父类
	device_initialize(&shost->shost_dev);
	shost->shost_dev.parent = &shost->shost_gendev;
  // 在 /sys/class/scsi_host/hostx/ 中可以看到该设备类
	shost->shost_dev.class = &shost_class;
	dev_set_name(&shost->shost_dev, "host%d", shost->host_no);
	shost->shost_dev.groups = scsi_sysfs_shost_attr_groups;

  // 每个主机适配器都会启动一个 scsi_error_handler 线程,用于错误恢复处理。
  // 线程名是 scsi_eh_x (x 根据主机适配器编号分配)
	shost->ehandler = kthread_run(scsi_error_handler, shost,
			"scsi_eh_%d", shost->host_no);
...
	// 每个主机适配器有一个scsi_tmf_x 的工作队列(Task management function work queue)
  // 一般用于 scsi 的错误终止任务使用。
	shost->tmf_work_q = alloc_workqueue("scsi_tmf_%d",
					    WQ_UNBOUND | WQ_MEM_RECLAIM,
					   1, shost->host_no);
...
	scsi_proc_hostdir_add(shost->hostt);
	return shost;
...
}

(2)iscsi_session_setup是会话相关结构体分配和初始化。会话使用struct iscsi_cls_session结构管理,并且 iscsi_tcp 对应会话有两个私有数据会存放在iscsi_cls_session中,分别是struct iscsi_tcp_task和struct iscsi_sw_tcp_hdrbuf。

struct iscsi_cls_session {
  // 链入全局链表中管理
	struct list_head sess_list;		/* item in session_list */
  // 对应的 iscsi_transport_template
	struct iscsi_transport *transport;
...
  // 会话相关工作队列。
	struct work_struct block_work;
	struct work_struct unblock_work;
	struct work_struct scan_work;
	struct work_struct unbind_work;

  // 会话恢复相关管理。
	/* recovery fields */
	int recovery_tmo;
	bool recovery_tmo_sysfs_override;
	struct delayed_work recovery_work;

	unsigned int target_id;
	bool ida_used;

	/*
	 * pid of userspace process that created session or -1 if
	 * created by the kernel.
	 */
	pid_t creator;
	int state;
	int sid;				/* session id */
	void *dd_data;				/* LLD private data */
	struct device dev;	/* sysfs transport/container device */
};

// 会话创建
struct iscsi_cls_session *
iscsi_session_setup(struct iscsi_transport *iscsit, struct Scsi_Host *shost,
		    uint16_t cmds_max, int dd_size, int cmd_task_size,
		    uint32_t initial_cmdsn, unsigned int id)
{
	struct iscsi_host *ihost = shost_priv(shost);
	struct iscsi_session *session;
	struct iscsi_cls_session *cls_session;
	int cmd_i, scsi_cmds;
	unsigned long flags;

	spin_lock_irqsave(&ihost->lock, flags);
	if (ihost->state == ISCSI_HOST_REMOVED) {
		spin_unlock_irqrestore(&ihost->lock, flags);
		return NULL;
	}
  // 增加该主机适配器的会话数量
	ihost->num_sessions++;
	spin_unlock_irqrestore(&ihost->lock, flags);
...
  // 见下面
	cls_session = iscsi_alloc_session(shost, iscsit,
					  sizeof(struct iscsi_session) +
					  dd_size);
	if (!cls_session)
		goto dec_session_count;
...
	// 每个会话有一个命令池
	/* initialize SCSI PDU commands pool */
	if (iscsi_pool_init(&session->cmdpool, session->cmds_max,
			    (void***)&session->cmds,
			    cmd_task_size + sizeof(struct iscsi_task)))
		goto cmdpool_alloc_fail;
...
  // 为每个会话进行初始化并添加到系统
	if (iscsi_add_session(cls_session, id))
		goto cls_session_fail;

	return cls_session;
...
}

// 其中 struct iscsi_session 属于会话的私有数据,管理会话的各类私有信息,如超时,命令数量,密码,用户等等信息。
struct iscsi_cls_session *
iscsi_alloc_session(struct Scsi_Host *shost, struct iscsi_transport *transport,
		    int dd_size)
{
	struct iscsi_cls_session *session;

	session = kzalloc(sizeof(*session) + dd_size,
			  GFP_KERNEL);
	if (!session)
		return NULL;

	session->transport = transport;
	session->creator = -1;
	session->recovery_tmo = 120;
	session->recovery_tmo_sysfs_override = false;
	session->state = ISCSI_SESSION_FREE;
  // 当 block_work 被激活时会启动该 work, 记录恢复超时
	INIT_DELAYED_WORK(&session->recovery_work, session_recovery_timedout);
	INIT_LIST_HEAD(&session->sess_list);
  // 用于在设置会话登陆并启动 I/O(conn_start)
	INIT_WORK(&session->unblock_work, __iscsi_unblock_session);
  // 当连接需要 stop 时会调用
	INIT_WORK(&session->block_work, __iscsi_block_session);
  // 会话摧毁时调用
	INIT_WORK(&session->unbind_work, __iscsi_unbind_session);
  // 理论上 iscsi_tcp 不会使用
	INIT_WORK(&session->scan_work, iscsi_scan_session);
...
	ISCSI_DBG_TRANS_SESSION(session, "Completed session allocation\n");
	return session;
}

到这里一个会话便创建完成并将相关数据结构添加到系统中,并向用户空间返回会话 id,主机适配器 id 和创建者 pid:

static int
iscsi_if_create_session(struct iscsi_internal *priv, struct iscsi_endpoint *ep,
			struct iscsi_uevent *ev, pid_t pid,
			uint32_t initial_cmdsn,	uint16_t cmds_max,
			uint16_t queue_depth)
{
	struct iscsi_transport *transport = priv->iscsi_transport;
	struct iscsi_cls_session *session;
	struct Scsi_Host *shost;

	session = transport->create_session(ep, cmds_max, queue_depth,
					    initial_cmdsn);
	if (!session)
		return -ENOMEM;

	session->creator = pid;
	shost = iscsi_session_to_shost(session);
	ev->r.c_session_ret.host_no = shost->host_no;
	ev->r.c_session_ret.sid = session->sid;
	ISCSI_DBG_TRANS_SESSION(session,
				"Completed creating transport session\n");
	return 0;
}
2 连接创建

接着会创建一个连接并与相关 sock 绑定,这里会调用iscsi_sw_tcp_conn_create。

同样该函数会创建一个新的连接数据结构并注册到系统中。

该函数返回一个新的struct iscsi_cls_conn结构体。

这里不具体展开源码,只分析有几个重点地方:

iscsi_sw_tcp_conn_create
	-> iscsi_tcp_conn_setup
  	-> iscsi_conn_setup
  		-> iscsi_create_conn

在iscsi_create_conn中,会新分配一个struct iscsi_cls_conn结构体,并且将新分配的cls_conn链如全局connlist链表。

可以在/sys/class/iscsi_connection/connectionx:x中看到连接相关信息。

在iscsi_conn_setup中,会在struct iscsi_conn的transport_timer启动一个 timer(iscsi_check_transport_timeouts),该 timer 会在 conn_start 时启动。该 timer 用于检测 ping,接收超时等。

初始化了xmitwork工作任务,调用iscsi_xmitworker。当发送 pdu,下发 scsi 命令,写 sock 时会启动该xmitwork用于任务命令队列任务。当执行 iscsi task 任务时会启动tmf_timer(iscsi_tmf_timedout)。该 timer 检测 iscsi task 的是否超时。

初始化onn->ehwait,用于 xmit 任务线程睡眠。

完成会话结构创建最后向用户空间返回对应的会话 id,连接 id。

static int
iscsi_if_create_conn(struct iscsi_transport *transport, struct iscsi_uevent *ev)
{
	struct iscsi_cls_conn *conn;
	struct iscsi_cls_session *session;

	session = iscsi_session_lookup(ev->u.c_conn.sid);
	if (!session) {
		printk(KERN_ERR "iscsi: invalid session %d.\n",
		       ev->u.c_conn.sid);
		return -EINVAL;
	}

	conn = transport->create_conn(session, ev->u.c_conn.cid);
	if (!conn) {
		iscsi_cls_session_printk(KERN_ERR, session,
					 "couldn't create a new connection.");
		return -ENOMEM;
	}

	ev->r.c_conn_ret.sid = session->sid;
	ev->r.c_conn_ret.cid = conn->cid;

	ISCSI_DBG_TRANS_CONN(conn, "Completed creating transport conn\n");
	return 0;
}
3 绑定会话

第三步是绑定会话调用iscsi_sw_tcp_conn_bind。

	case ISCSI_UEVENT_BIND_CONN:
		session = iscsi_session_lookup(ev->u.b_conn.sid);
		conn = iscsi_conn_lookup(ev->u.b_conn.sid, ev->u.b_conn.cid);

		if (conn && conn->ep)
			iscsi_if_ep_disconnect(transport, conn->ep->id);
...
		ev->r.retcode =	transport->bind_conn(session, conn,
						ev->u.b_conn.transport_eph,
						ev->u.b_conn.is_leading);
...

通过会话 id 找到对应的 session,并且通过会话 id 和连接 id 找到对应的 conn。最后调用iscsi_sw_tcp_conn_bind完成会话与连接的绑定。

static int
iscsi_sw_tcp_conn_bind(struct iscsi_cls_session *cls_session,
		       struct iscsi_cls_conn *cls_conn, uint64_t transport_eph,
		       int is_leading)
{
	struct iscsi_session *session = cls_session->dd_data;
	struct iscsi_conn *conn = cls_conn->dd_data;
	struct iscsi_tcp_conn *tcp_conn = conn->dd_data;
	struct iscsi_sw_tcp_conn *tcp_sw_conn = tcp_conn->dd_data;
	struct sock *sk;
	struct socket *sock;
	int err;

	/* lookup for existing socket */
	sock = sockfd_lookup((int)transport_eph, &err);
	if (!sock) {
		iscsi_conn_printk(KERN_ERR, conn,
				  "sockfd_lookup failed %d\n", err);
		return -EEXIST;
	}

  // 向 session 中填充对应的 cls_conn。
	err = iscsi_conn_bind(cls_session, cls_conn, is_leading);
	if (err)
		goto free_socket;

	spin_lock_bh(&session->frwd_lock);
	/* bind iSCSI connection and socket */
  // 连接结构体填充对应的 sock
	tcp_sw_conn->sock = sock;
	spin_unlock_bh(&session->frwd_lock);

	/* setup Socket parameters */
	sk = sock->sk;
	sk->sk_reuse = SK_CAN_REUSE;
	sk->sk_sndtimeo = 15 * HZ; /* FIXME: make it configurable */
	sk->sk_allocation = GFP_ATOMIC;
	sk_set_memalloc(sk);

  // 填充 conn 相关的 tcp 回调
  // sk->sk_data_ready = iscsi_sw_tcp_data_ready;
	// sk->sk_state_change = iscsi_sw_tcp_state_change;
	// sk->sk_write_space = iscsi_sw_tcp_write_space;
	iscsi_sw_tcp_conn_set_callbacks(conn);
	tcp_sw_conn->sendpage = tcp_sw_conn->sock->ops->sendpage;
	/*
	 * set receive state machine into initial state
	 */
	iscsi_tcp_hdr_recv_prep(tcp_conn);
	return 0;

free_socket:
	sockfd_put(sock);
	return err;
}
4 pdu target 通信

当会话与连接绑定后,用户态即可通过ISCSI_UEVENT_SEND_PDU发送 pdu 数据到 target,获取相关 target 信息。

主要流程如下:

ISCSI_UEVENT_SEND_PDU
	-> send_pdu(iscsi_conn_send_pdu)
		-> alloc_pdu(iscsi_sw_tcp_pdu_alloc)
  	-> init_task(iscsi_prep_mgmt_task)
			-> iscsi_tcp_task_init
  		-> init_pdu(iscsi_sw_tcp_pdu_init)
		-> xmit_task(iscsi_tcp_task_xmit)
			-> xmit_pdu(iscsi_sw_tcp_pdu_xmit)
iscsi_tcp_cleanup_task

alloc_pdu:发送一个 pdu 首先是分配一个 pdu,主要从 conn 的 cmd_pool 中取出一个 cmd 管理区并且返回。

init_task -> init_pdu: 主要对该 cmd task 的 pdu 区进行初始化(与 scsi 协议 cdb 相关?)

最终通过iscsi_sw_tcp_pdu_xmit提交数据发送到 target。

iscsi_sw_tcp_pdu_xmit
	-> iscsi_sw_tcp_xmit
  	-> iscsi_sw_tcp_xmit_segment
  		-> sendpage

如果需要接收数据则会在初始化 pdu 的iscsi_segment_init_linear注册 done 回调,用于完成时接收相关数据。

5 启动连接(扫描 target 设备)

这里调用iscsi_conn_start:

int iscsi_conn_start(struct iscsi_cls_conn *cls_conn)
{
	struct iscsi_conn *conn = cls_conn->dd_data;
	struct iscsi_session *session = conn->session;
...
  // 一些会话的验证
	if ((session->imm_data_en || !session->initial_r2t_en) &&
	     session->first_burst > session->max_burst) {
		iscsi_conn_printk(KERN_INFO, conn, "invalid burst lengths: "
				  "first_burst %d max_burst %d\n",
				  session->first_burst, session->max_burst);
		return -EINVAL;
	}

	if (conn->ping_timeout && !conn->recv_timeout) {
		iscsi_conn_printk(KERN_ERR, conn, "invalid recv timeout of "
				  "zero. Using 5 seconds\n.");
		conn->recv_timeout = 5;
	}

	if (conn->recv_timeout && !conn->ping_timeout) {
		iscsi_conn_printk(KERN_ERR, conn, "invalid ping timeout of "
				  "zero. Using 5 seconds.\n");
		conn->ping_timeout = 5;
	}

	spin_lock_bh(&session->frwd_lock);
	conn->c_stage = ISCSI_CONN_STARTED;
	session->state = ISCSI_STATE_LOGGED_IN;
	session->queued_cmdsn = session->cmdsn;

	conn->last_recv = jiffies;
	conn->last_ping = jiffies;
	if (conn->recv_timeout && conn->ping_timeout)
		mod_timer(&conn->transport_timer,
			  jiffies + (conn->recv_timeout * HZ));

	switch(conn->stop_stage) {
	case STOP_CONN_RECOVER:
		/*
		 * unblock eh_abort() if it is blocked. re-try all
		 * commands after successful recovery
		 */
		conn->stop_stage = 0;
		conn->tmf_state = TMF_INITIAL;
		session->age++;
		if (session->age == 16)
			session->age = 0;
		break;
	case STOP_CONN_TERM:
		conn->stop_stage = 0;
		break;
	default:
		break;
	}
	spin_unlock_bh(&session->frwd_lock);

  // 启动 unblock_work 工作队列,调用 __iscsi_unblock_session
	iscsi_unblock_session(session->cls_session);
  // 完成初始化
	wake_up(&conn->ehwait);
	return 0;
}

static void __iscsi_unblock_session(struct work_struct *work)
{
...
	/*
	 * The recovery and unblock work get run from the same workqueue,
	 * so try to cancel it if it was going to run after this unblock.
	 */
	cancel_delayed_work(&session->recovery_work);
	spin_lock_irqsave(&session->lock, flags);
  // 会话登陆标记
	session->state = ISCSI_SESSION_LOGGED_IN;
	spin_unlock_irqrestore(&session->lock, flags);
	/* start IO */
  // 标记所有 blk 层 I/O cmd 都被允许执行了
	scsi_target_unblock(&session->dev, SDEV_RUNNING);
...
	ISCSI_DBG_TRANS_SESSION(session, "Completed unblocking session\n");
}

到这里之后 iscisd 会通过 pdu 与 target 通信交换信息,包括设备 type 扫描等等,最终确认的设备会在主机适配器下被发现,并挂载到。之后即可通过 blk 层通过主机适配器模版访问 target。

这里具体的 pdu 交互协议未深入分析,主要分析 target 设备如何被 sd 上层驱动匹配识别并管理。

第一步:当 iscsid 与 target 完成首次通信后,即可由用户态程序手动写来扫描 target 设备:

~# echo "- - -" > /sys/class/scsi_host/hostx/scan

用户态通过写/sys/class/scsi_host/hostx/scan文件即可手动触发扫描过程,- - -分别表示channel id,target id,lun id,如果是-则表示任意扫描,触发代码如下:

static ssize_t
store_scan(struct device *dev, struct device_attribute *attr,
	   const char *buf, size_t count)
{
	struct Scsi_Host *shost = class_to_shost(dev);
	int res;

	res = scsi_scan(shost, buf);
	if (res == 0)
		res = count;
	return res;
};
static DEVICE_ATTR(scan, S_IWUSR, NULL, store_scan);

static int scsi_scan(struct Scsi_Host *shost, const char *str)
{
	char s1[15], s2[15], s3[17], junk;
	unsigned long long channel, id, lun;
	int res;

	res = sscanf(str, "%10s %10s %16s %c", s1, s2, s3, &junk);
	if (res != 3)
		return -EINVAL;
  // 如果 sx 是 "-",channel 等于 ~0
	if (check_set(&channel, s1))
		return -EINVAL;
	if (check_set(&id, s2))
		return -EINVAL;
	if (check_set(&lun, s3))
		return -EINVAL;
  // 这里根据主机 scsi_transport 是否有实现 user_scan 来调用扫描,
  // 这里调用的是 iscsi_register_transport 注册,那么会赋值回调 iscsi_user_scan。
	if (shost->transportt->user_scan)
		res = shost->transportt->user_scan(shost, channel, id, lun);
	else
		res = scsi_scan_host_selected(shost, channel, id, lun,
					      SCSI_SCAN_MANUAL);
	return res;
}

static int iscsi_user_scan(struct Scsi_Host *shost, uint channel,
			   uint id, u64 lun)
{
	struct iscsi_scan_data scan_data;
	// 根据刚才用户空间传入的值开始扫描用户会话,channel = ~0,id = ~0,lun = ~0
	scan_data.channel = channel;
	scan_data.id = id;
	scan_data.lun = lun;
	scan_data.rescan = SCSI_SCAN_MANUAL;

	return device_for_each_child(&shost->shost_gendev, &scan_data,
				     iscsi_user_scan_session);
}

static int iscsi_user_scan_session(struct device *dev, void *data)
{
	struct iscsi_scan_data *scan_data = data;
	struct iscsi_cls_session *session;
	struct Scsi_Host *shost;
	struct iscsi_cls_host *ihost;
	unsigned long flags;
	unsigned int id;
...
	session = iscsi_dev_to_session(dev);
...
  shost = iscsi_session_to_shost(session);
	ihost = shost->shost_data;
...
  id = session->target_id;
...
  // 在 iscsi_tcp 中 target_id 未赋值, ISCSI_MAX_TARGET = -1,
  // 开始进行扫描...(此时 channel = 0,id = 0,lun = ~0)
	if (id != ISCSI_MAX_TARGET) {
		if ((scan_data->channel == SCAN_WILD_CARD ||
		     scan_data->channel == 0) &&
		    (scan_data->id == SCAN_WILD_CARD ||
		     scan_data->id == id))
			scsi_scan_target(&session->dev, 0, id,
					 scan_data->lun, scan_data->rescan);
	}
...
}

scsi_scan_target
  -> __scsi_scan_target

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);
	blist_flags_t bflags = 0;
	int res;
	struct scsi_target *starget;

  // 对于 iscsi_tcp, this_id = -1
  // 某些 scsi 主机适配器会占用一个 target id
	if (shost->this_id == id)
		/*
		 * Don't scan the host adapter
		 */
		return;
  
  // 这里不展开 scsi_alloc_target。
  // scsi_alloc_target 用于分配一个新的或者寻找一个已存在的 scsi_target 目标节点
  // channel = 0, id = 0, lun = ~0
  // 首先会分配一个新的 scsi_target 设备并初始化赋值 channel 和 id。
  // 接着从主机适配器寻找是否存在相同 channel 和 id 的目标节点存在则释放申请的
  // 目标节点并返回已存在的目标节点。不存在则将新的目标节点加入主机适配器的 __target 链表,并返回。
  starget = scsi_alloc_target(parent, channel, id);
...
	/*
	 * Scan LUN 0, if there is some response, scan further. Ideally, we
	 * would not configure LUN 0 until all LUNs are scanned.
	 */
  // 扫描 lun0,如果有,则会继续扫描所有其他的 lun,直到扫描完所有 luns。
	res = scsi_probe_and_add_lun(starget, 0, &bflags, NULL, rescan, NULL);
	if (res == SCSI_SCAN_LUN_PRESENT || res == SCSI_SCAN_TARGET_PRESENT) {
		if (scsi_report_lun_scan(starget, bflags, rescan) != 0)
			/*
			 * The REPORT LUN did not scan the target,
			 * do a sequential scan.
			 */
			scsi_sequential_lun_scan(starget, bflags,
						 starget->scsi_level, rescan);
	}
}

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

  /*
	 * The rescan flag is used as an optimization, the first scan of a
	 * host adapter calls into here with rescan == 0.
	 */
	sdev = scsi_device_lookup_by_target(starget, lun);
	if (sdev) {
    // 如果对应的 scsi_device(lun)存在,并且不是首次扫描那么直接返回 SCSI_SCAN_LUN_PRESENT。
		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;
			else
				scsi_device_put(sdev);

			if (bflagsp)
				*bflagsp = scsi_get_device_flags(sdev,
								 sdev->vendor,
								 sdev->model);
			return SCSI_SCAN_LUN_PRESENT;
		}
		scsi_device_put(sdev);
	} else
		sdev = scsi_alloc_sdev(starget, lun, hostdata); ---------------------------1if (!sdev)
		goto out;

	result = kmalloc(result_len, GFP_ATOMIC |
			((shost->unchecked_isa_dma) ? __GFP_DMA : 0));
	if (!result)
		goto out_free_sdev;

	if (scsi_probe_lun(sdev, result, result_len, &bflags)) ----------------------2goto out_free_result;
  
 	// 到这里,说明查询对端 lun 已经完成,并且成功探测以及初始化了相关 scsi_device 设备信息。
...
	/*
	 * result contains valid SCSI INQUIRY data.
	 */
  // 这里返回了向 target 查询的返回数据。
	if ((result[0] >> 5) == 3) {
		/*
		 * For a Peripheral qualifier 3 (011b), the SCSI
		 * spec says: The device server is not capable of
		 * supporting a physical device on this logical
		 * unit.
		 *
		 * For disks, this implies that there is no
		 * logical disk configured at sdev->lun, but there
		 * is a target id responding.
		 */
    // 外设限定符(= 3), scsi 规范描述设备服务器不能在这个lun上支持物理设备。
    // 所以返回 SCSI_SCAN_TARGET_PRESENT,表示 target 响应了,但是不能
    // 作为一个可以被支持的物理设备被 attch。
...
		res = SCSI_SCAN_TARGET_PRESENT;
		goto out_free_result;
	}
  
	/*
	 * Some targets may set slight variations of PQ and PDT to signal
	 * that no LUN is present, so don't add sdev in these cases.
	 * Two specific examples are:
	 * 1) NetApp targets: return PQ=1, PDT=0x1f
	 * 2) USB UFI: returns PDT=0x1f, with the PQ bits being "reserved"
	 *    in the UFI 1.0 spec (we cannot rely on reserved bits).
	 *
	 * References:
	 * 1) SCSI SPC-3, pp. 145-146
	 * PQ=1: "A peripheral device having the specified peripheral
	 * device type is not connected to this logical unit. However, the
	 * device server is capable of supporting the specified peripheral
	 * device type on this logical unit."
	 * PDT=0x1f: "Unknown or no device type"
	 * 2) USB UFI 1.0, p. 20
	 * PDT=00h Direct-access device (floppy)
	 * PDT=1Fh none (no FDD connected to the requested logical unit)
	 */
  // 有些 target 会设置 PQ 和 PDT 表示没有 LUN 存在,因此这里是对该定义的判断。
	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"));
		res = SCSI_SCAN_TARGET_PRESENT;
		goto out_free_result;
	}
...
  // 协议验证没有问题,现在把发现的 lun 进行实例化,以便可以访问。
	res = scsi_add_lun(sdev, result, &bflags, shost->async_scan); --------------3if (res == SCSI_SCAN_LUN_PRESENT) {
		if (bflags & BLIST_KEY) {
			sdev->lockable = 0;
			scsi_unlock_floptical(sdev, result);
		}
	}
...
}

(1)scsi_alloc_sdev分配一个新的scsi_device,并且对 io 进行初始化:

static struct scsi_device *scsi_alloc_sdev(struct scsi_target *starget,
					   u64 lun, void *hostdata)
{
	struct scsi_device *sdev;
	int display_failure_msg = 1, ret;
	struct Scsi_Host *shost = dev_to_shost(starget->dev.parent);

	sdev = kzalloc(sizeof(*sdev) + shost->transportt->device_size,
		       GFP_ATOMIC);
...
  // 初始化当前 scsi_device 的 channel,id,lun 编号。
	sdev->id = starget->id;
	sdev->lun = lun;
	sdev->channel = starget->channel;
...
  // 设备的初始化状态,lun 被添加时变为 SDEV_RUNNING。
  sdev->sdev_state = SDEV_CREATED;
...
  // LLD 驱动事件工作任务,当用户态向 LLD 发送事件时启动。
	INIT_WORK(&sdev->event_work, scsi_evt_thread);
  // 处理 blk 的 io 请求队列。
	INIT_WORK(&sdev->requeue_work, scsi_requeue_run_queue);
...
  /*
	 * Some low level driver could use device->type
	 */
	sdev->type = -1;
...
  // 存储设备的 blk 请求,由 scsi 来获取并执行。
  sdev->request_queue = scsi_mq_alloc_queue(sdev);
...
  return sdev;
...
}

(2)使用标准 scsi 命令向 target 查询探测单个 LUN:(INQUIRY 命令,具体 scsi 协议命令可以查看include/scsi/scsi_proto.h)

static int scsi_probe_lun(struct scsi_device *sdev, unsigned char *inq_result,
			  int result_len, blist_flags_t *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;
...

	/* Each pass gets up to three chances to ignore Unit Attention */
	for (count = 0; count < 3; ++count) {
		int resid;
		// 包装一个 scsi 协议命令
		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 协议封装成 pdu 通过 tcp 发送给 target。
    // 这里会使用刚才创建的 scsi_device->request_queue,通过 block 层发送 io 请求,
    // 最终会调用 iscsi_tcp 的主机适配器模版发送给 target,这里不具体展开,后续分析 io 请求代码。
		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. 
			 */
      // 对端设备未就绪,如果返回码符合条件那么重试 INQUIRY 请求,最多重试三次。
			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.
			 */
      // 一些 workaround。
			if (resid == try_inquiry_len)
				continue;
		}
		break;
	}

  // 有正确响应,开始下一步的验证。
	if (result == 0) {
    // scsi 规范规定查询供应商,产品,和修订字符串必须完全由图形 ASCII 字符组成,并在右侧用空格填充。
    // 不是所有厂商都遵循该规则,所以这里对字符串进行处理替换。
		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 */

		/*
		 * Get any flags for this device.
		 *
		 * XXX add a bflags to scsi_device, and replace the
		 * corresponding bit fields in scsi_device, so bflags
		 * need not be passed as an argument.
		 */
    // 将厂商,model 等信息转换为 bflags。
		*bflags = scsi_get_device_flags(sdev, &inq_result[8],
				&inq_result[16]);

		/* When the first pass succeeds we gain information about
		 * what larger transfer lengths might work. */
		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;
			// 还需要更多数据,继续向 target 发送查询请求。
			/* If more data is available perform the second pass */
			if (next_inquiry_len > try_inquiry_len) {
				try_inquiry_len = next_inquiry_len;
				pass = 2;
				goto next_pass;
			}
		}
	} else if (pass == 2) {
		sdev_printk(KERN_INFO, sdev,
			    "scsi scan: %d byte inquiry failed.  "
			    "Consider BLIST_INQUIRY_36 for this device\n",
			    try_inquiry_len);

		/* If this pass failed, the third pass goes back and transfers
		 * the same amount as we successfully got in the first pass. */
		try_inquiry_len = first_inquiry_len;
		pass = 3;
		goto next_pass;
	}
  
	/* If the last transfer attempt got an error, assume the
	 * peripheral doesn't exist or is dead. */
	if (result)
		return -EIO;
...
	/*
	 * The scanning code needs to know the scsi_level, even if no
	 * device is attached at LUN 0 (SCSI_SCAN_TARGET_PRESENT) so
	 * non-zero LUNs can be scanned.
	 */
  // 确定 scsi 协议版本?
	sdev->scsi_level = inq_result[2] & 0x07;
	if (sdev->scsi_level >= 2 ||
	    (sdev->scsi_level == 1 && (inq_result[3] & 0x0f) == 1))
		sdev->scsi_level++;
	sdev->sdev_target->scsi_level = sdev->scsi_level

	/*
	 * If SCSI-2 or lower, and if the transport requires it,
	 * store the LUN value in 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;
}

(3)分配并完全初始化一个scsi_device

static int scsi_add_lun(struct scsi_device *sdev, unsigned char *inq_result,
		blist_flags_t *bflags, int async)
{
...
	/*
	 * Copy at least 36 bytes of INQUIRY data, so that we don't
	 * dereference unallocated memory when accessing the Vendor,
	 * Product, and Revision strings.  Badly behaved devices may set
	 * the INQUIRY Additional Length byte to a small value, indicating
	 * these strings are invalid, but often they contain plausible data
	 * nonetheless.  It doesn't matter if the device sent < 36 bytes
	 * total, since scsi_probe_lun() initializes inq_result with 0s.
	 */
  // 这里保存了最小的查询信息,包括 Vendor,Product,Revision 字符串。
	sdev->inquiry = kmemdup(inq_result,
				max_t(size_t, sdev->inquiry_len, 36),
				GFP_ATOMIC);
...
	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 emulation layer device.  This is a hack to work around
		 * the SATL power management specifications which state that
		 * when the SATL detects the device has gone into standby
		 * mode, it shall respond with NOT READY.
		 */
		sdev->allow_restart = 1;
	}
  
...
  // 根据 bflags 对 scsi_device 设备进行初始化和字段填充。
...
  // 根据 target 配置 sdev->type。
  sdev->type = (inq_result[0] & 0x1f);
...
	/*
	 * Maximum 512 sector transfer length
	 * broken RA4x00 Compaq Disk Array
	 */
	if (*bflags & BLIST_MAX_512)
		blk_queue_max_hw_sectors(sdev->request_queue, 512);
	/*
	 * Max 1024 sector transfer length for targets that report incorrect
	 * max/optimal lengths and relied on the old block layer safe default
	 */
	else if (*bflags & BLIST_MAX_1024)
		blk_queue_max_hw_sectors(sdev->request_queue, 1024);
...
	/* set the device running here so that slave configure
	 * may do I/O */
	mutex_lock(&sdev->state_mutex);
  // scsi_device 设备可以运行。
	ret = scsi_device_set_state(sdev, SDEV_RUNNING);
	if (ret)
		ret = scsi_device_set_state(sdev, SDEV_BLOCK);
	mutex_unlock(&sdev->state_mutex);
...
	/*
	 * Ok, the device is now all set up, we can
	 * register it and tell the rest of the kernel
	 * about it.
	 */
  // 现在设备已经准备好了,可以注册并通知内核其他组件了。
	if (!async && scsi_sysfs_add_sdev(sdev) != 0)
		return SCSI_SCAN_NO_RESPONSE;

	return SCSI_SCAN_LUN_PRESENT;
}

// 将该设备添加进系统
int scsi_sysfs_add_sdev(struct scsi_device *sdev)
{
...
  // 寻找 sdev 对应的设备 handler,这里可以确认是 alua,将会
  // 调用 alua_bus_attach
	scsi_dh_add_device(sdev);

  // 根据之前 sdev 下的 device 匹配对应的驱动,
  // 之前 sdev->type 已赋值,当我们进入 scsi sd probe (driver/scsi/sd.c)时将会成功匹配。
	error = device_add(&sdev->sdev_gendev);
	if (error) {
		sdev_printk(KERN_INFO, sdev,
				"failed to add device: %d\n", error);
		return error;
	}
...
}

error = device_add(&sdev->sdev_gendev);
	-> sd_probe
 
// 当对 device 进行驱动初始化期间和每一个新的 scsi 设备连接到系统时,
// 每个 scsi 设备都会调用这个一次。
static int sd_probe(struct device *dev)
{
	struct scsi_device *sdp = to_scsi_device(dev);
	struct scsi_disk *sdkp;
	struct gendisk *gd;
	int index;
	int error;

	scsi_autopm_get_device(sdp);
	error = -ENODEV;
	if (sdp->type != TYPE_DISK &&
	    sdp->type != TYPE_ZBC &&
	    sdp->type != TYPE_MOD &&
	    sdp->type != TYPE_RBC)
		goto out;

#ifndef CONFIG_BLK_DEV_ZONED
	if (sdp->type == TYPE_ZBC)
		goto out;
#endif
	// 当 type 符合上述类型时则属于 sd 驱动管理。
	SCSI_LOG_HLQUEUE(3, sdev_printk(KERN_INFO, sdp,
					"sd_probe\n"));
    
	error = -ENOMEM;
	sdkp = kzalloc(sizeof(*sdkp), GFP_KERNEL);
	if (!sdkp)
		goto out;
	// 分配一个全新的 gendisk,SD_MINORS 如果激活了 debug,则是 16 开始,
  // lsblk 可以看到 target 的设备号是 8:16 开始。
	gd = alloc_disk(SD_MINORS);
	if (!gd)
		goto out_free;

	index = ida_alloc(&sd_index_ida, GFP_KERNEL);
	if (index < 0) {
		sdev_printk(KERN_WARNING, sdp, "sd_probe: memory exhausted.\n");
		goto out_put;
	}

	error = sd_format_disk_name("sd", index, gd->disk_name, DISK_NAME_LEN);
	if (error) {
		sdev_printk(KERN_WARNING, sdp, "SCSI disk (sd) name length exceeded.\n");
		goto out_free_index;
	}
}

到这里一个 lun 设备成功加入到系统中,后续再完成一些设备的读写检测即可进行 i/o 访问。io 请求通过 blk request 调用到 iscsi_queuecommand。

  • 20
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Linux iSCSI是一种用于在计算机之间传输块级数据的协议。它允许将存储设备(如硬盘驱动器)通过网络连接到远程计算机,并使其在本地计算机上看起来像本地存储设备一样。这样,远程计算机可以像使用本地存储设备一样访问和操作远程存储设备。 要在Linux上使用iSCSI,您需要进行以下步骤: 1. 安装iSCSI软件:使用以下命令安装与iSCSI相关的软件: ```shell yum install iscsi yum install iscsi-initiator-utils.x86_64 ``` 2. 配置iSCSI Initiator:编辑iSCSI Initiator的配置文件以指定远程存储设备的位置和访问凭据。您可以使用以下命令编辑配置文件: ```shell vi /etc/iscsi/initiatorname.iscsi ``` 在文件中,您需要设置以下参数: - InitiatorName:指定本地计算机的iSCSI Initiator名称。 - TargetAddress:指定远程存储设备的IP地址或主机名。 - TargetName:指定远程存储设备的iSCSI Target名称。 - Username和Password:如果需要身份验证,则指定访问远程存储设备所需的用户名和密码。 3. 连接到远程存储设备:使用以下命令连接到远程存储设备: ```shell iscsiadm -m discovery -t sendtargets -p <远程存储设备的IP地址或主机名> iscsiadm -m node -T <远程存储设备的iSCSI Target名称> -l ``` 4. 验证连接:使用以下命令验证与远程存储设备的连接: ```shell iscsiadm -m session ``` 您应该能够看到已建立的iSCSI会话。 请注意,这只是一个简单的介绍和演示,实际使用iSCSI可能涉及更多的配置和设置。具体的步骤和命令可能因您的操作系统和iSCSI实现而有所不同。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值