Linux SCSI 子系统

Small Computer Systems Interface (SCSI) 是一组标准集,它定义了与大量设备(主要是与存储相关的设备)通信所需的接口和协议。 Linux® 提供了一种 SCSI 子系统,用于与这些设备通信。Linux 是分层架构的一个很好的例子,它将高层的驱动器(比如磁盘驱动器或光驱)连接到物理接口,比如 Fibre Channel 或 Serial Attached SCSI(SAS).

scsi设备:机器外设总线是计算机内部与外设进行通讯的总线,分为IDE总线,SCSI总线和USB总线.IDE总线是PC机上用得最多的总线,其造价比较便 宜.SCSI总线的速度比IDE总线要快得多,不过造价比较高.IDE总线和SCSI总线一般只于硬盘,光驱和扫描仪等,而USB总线则可以用于更多的外 设,且速度更快.一般来说,这三种外设总线是不可以混合使用的,但如果有总线转换器则可以在一定程度上混合使用,如SCSI总线就可以有向IDE总线进行 转换的转换器.

SCSI-3 的开发开始于 1993 年,现已成为了一组标准集,可以定义协议、命令集和信令方法。在 SCSI-3 中,包含一组命名为 Ultra 的并行 SCSI 标准和基于串行 SCSI 的协议,比如 IEEE 1394 (FireWire)、Fibre Channel, 、Internet SCSI (iSCSI) 和新兴的 SAS。这些标准通过引入存储网络技术(比如 FC-AL 或 iSCSI)改变了传统的存储理念,将数据速率扩展到了 1 Gbit/s,将最大的可寻址设备数增加到了 100 以上,并将最大的电缆长度扩展到了 25 米。图 1 展示了从 1986 至 2007 年 SCSI 的数据速率的变化 .
 

SCSI 传输所采用的协议已经时过境迁,SCSI 命令却保持了最初的元素。SCSI 命令是在 Command Descriptor Block (CDB) 中定义的。CDB 包含了用来定义要执行的特定操作的操作代码,以及大量特定于操作的参数。

SCSI 命令支持读写数据(各有四个变量)以及很多非数据命令,比如 test-unit-ready(设备是否已就绪)、inquiry(检索有关目标设备的基本信息)、read-capacity(检索目标设备的存储容 量)等等。目标设备支持何种命令取决于设备的类型。发起者通过 inquiry 命令识别设备类型。表 1 列出了最常用的 SCSI 命令。

表 1. 常见 SCSI 命令

命令 用途
Test unit ready 查询设备是否已经准备好进行传输
Inquiry 请求设备基本信息
Request sense 请求之前命令的错误信息
Read capacity 请求存储容量信息
Read 从设备读取数据
Write 向设备写入数据
Mode sense 请求模式页面(设备参数)
Mode select 在模式页面配置设备参数


借助大约 60 种可用命令,SCSI 可适用于许多设备(包括随机存取设备,比如磁盘和像磁带这样的顺序存储设备)。SCSI 也提供了专门的命令以访问箱体服务(比如存储箱体内部当前的传感和温度)。
 

Linux 内核中的 SCSI 架构

图 2 显示了 SCSI 子系统在 Linux 内核中的位置。内核的顶部是系统调用接口,处理用户空间调用到内核中合适的目的地的路由(例如 open、read 或 write)。而虚拟文件系统(VFS) 是内核中支持的大多数文件系统的抽象层。它负责将请求路由到合适的文件系统。大多数文件系统都通过缓冲区缓存来相互通信,这种缓存通过缓存最近使用的数据 来优化对物理设备的访问。接下来是块设备驱动器层,它包括针对底层设备的各种块驱动器。SCSI 子系统是这种块设备驱动器之一。

 

与 Linux 内核中的其他主流子系统不同,SCSI 子系统是一种分层的架构,共分为三层。顶部的那层叫做较高层,代表的是内核针对 SCSI 和主要设备类型的驱动器的最高接口。接下来的是中间层,也称为公共层或统一层。在这一层包含 SCSI 堆栈的较高层和较低层的一些公共服务。最后是较低层,代表的是适用于 SCSI 的物理接口的实际驱动器(参见图 3)

图 3. Linux SCSI 子系统的分层架构

SCSI 较高层

SCSI 子系统的较高层代表的是内核(设备级)最高级别的接口。它由一组驱动器组成,比如块设备(SCSI 磁盘和 SCSI CD-ROM)和字符设备(SCSI 磁带和 SCSI generic)。较高层接受来自上层(比如 VFS)的请求并将其转换成 SCSI 请求。较高层负责完成 SCSI 命令并将状态信息通知上层。

SCSI 磁盘驱动器在 ./linux/drivers/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/drivers/scsi/st.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 中间层

SCSI 中间层是 SCSI 较高层和较低层的公共服务层(可以在 ./linux/drivers/scsi/scsi.c 内部分地实现)。它提供了很多可供较高层和较低层驱动器使用的函数,因而可以充当这两层间的连接层。中间层很重要,原因是它抽象化了较低层驱动器 (LLD)的实现,可以在 ./linux/drivers/scsi/hosts.c 中部分地实现。这意味着可以以同样的方式使用带不同接口的 Fibre Channel 主机总线适配器(HBA)。

低层驱动器注册和错误处理都由 SCSI 中间层提供。中间层还提供了较高层和较低层间的 SCSI 命令排队。SCSI 中间层的一个重要功能是将来自较高层的命令请求转换成 SCSI 请求。它也负责管理特定于 SCSI 的错误恢复。

中间层可以连接 SCSI 子系统的较高层和较低层。它接受对 SCSI 事务的请求并对这些请求进行排队以便处理 (如 ./linux/drivers/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 也包括在内。

 

SCSI 客户机/服务器模型

在主机和存储介质进行通信期间,主机通常充当 SCSI 启动程序。在计算机存储中,SCSI 启动程序是启动 SCSI 会话的端点,这意味着它会发送 SCSI 命令。存储介质通常充当 SCSI 目标,它接收和处理 SCSI 命令。SCSI 目标等待启动程序的命令,然后提供请求的输入/输出数据转换。

SCSI 目标通常为启动程序提供一个或多个逻辑单元号(LUN)。在计算机存储介质上,LUN 仅是分配给逻辑单元的号码。逻辑单元是一个 SCSI 协议实体,实际的 I/O 操作只处理这种实体。每个 SCSI 目标可以提供一个或多个逻辑单元;它本身不执行 I/O,但代替特定的逻辑单元执行。

在存储区域中,LUN 通常表示一个主机能够执行读写操作的 SCSI 磁盘。图 1 显示 SCSI 客户机/服务器模型是如何工作的。

启动程序首先向目标发送命令,然后目标解码命令并向启动程序请求数据,或将数据发送给启动程序。在这之后,目标将状态发送给启动程序。如果状态损坏,启动程序将向目标发送一个请求检测(sense)指令。目标将返回检测数据,告知启动程序哪里出错。

现在我们研究与存储相关的 SCSI 命令。

Linux 通用 SCSI 驱动器

Linux 中的 SCSI 设备的命名方式能够帮助用户识别设备。例如,第一个 SCSI CD-ROM 是 /dev/scd0。SCSI 磁盘的标签为 /dev/sda、/dev/sdb 和 /dev/sdc 等。当设备初始化完成时,Linux SCSI 磁盘驱动器接口仅发送 SCSI READ 和 WRITE 命令。

这些 SCSI 设备可能具有通用的名称和接口,比如 /dev/sg0、/dev/sg1 或 /dev/sga、/dev/sgb 等。通过这些通用的 驱动器接口,您就可以将 SCSI 命令直接发送到 SCSI 设备,而不需要经过在 SCSI 磁盘上创建(并装载到某个目录)的文件系统。在图 2 中,您可以看到不同的应用程序如何与 SCSI 设备通信。 

图 2. 与 SCSI 设备通信的各种方式

通过 Linux 通用驱动器接口,您可以构建能够向 SCSI 设备发送更多 SCSI 命令的应用程序。也就是说您又多了一种选择。要确定哪个 SCSI 设备表示某个 sg 接口,您可以使用 sg_map 命令列出所有映射

[root@taomaoy ~]# sg_map -i /dev/sg0 /dev/sda ATA ST3160812AS 3.AA 

/dev/sg1 /dev/scd0 HL-DT-ST RW/DVD GCC-4244N 1.02

如何使用 Red Hat 或 Fedora,则要安装 sg3_utils。现在我们看看如何执行典型的 SCSI 系统调用命令。

典型的 SCSI 通用驱动器命令

对于字符设备,SCSI 通用驱动器支持许多典型的系统调用,比如 open()、close()、read()、write、poll() 和 ioctl()。向特定的 SCSI 设备发送 SCSI 命令的步骤也非常简单:

  1. 打开 SCSI 通用设备文件(比如 sg1)获取 SCSI 设备的文件描述符。
  2. 准备好 SCSI 命令。
  3. 设置相关的内存缓冲区。
  4. 调用 ioctl() 函数执行 SCSI 命令。
  5. 关闭设备文件。

典型的 ioctl() 函数类似于:ioctl(fd,SG_IO,p_io_hdr);。

这里的 ioctl() 函数必须具有 3 个参数:

  1. fd 是设备文件的文件描述符。通过调用 open() 成功打开设备文件之后,将需要获取这个参数。
  2. SG_IO 表明将 sg_io_hdr 对象作为 ioctl() 函数的第三个参数提交,并且在 SCSI 命令结束时返回。
  3. p_io_hdr 是指向 sg_io_hdr 对象的指针,该对象包含 SCSI 命令和其他设置。

SCSI 通用驱动器的最重要数据结构是 struct sg_io_hdr,它在 scsi/sg.h 中定义,并且包含如何使用 SCSI 命令的信息。清单 1 给出了这个结构的定义。


清单 1. sg_io_hdr 结构的定义
 

  1. typedef struct sg_io_hdr
  2. {
  3.     int interface_id; /* [i] 'S' (required) */
  4.     int dxfer_direction; /* [i] */
  5.     unsigned char cmd_len; /* [i] */
  6.     unsigned char mx_sb_len; /* [i] */
  7.     unsigned short iovec_count; /* [i] */
  8.     unsigned int dxfer_len; /* [i] */
  9.     void * dxferp; /* [i], [*io] */
  10.     unsigned char * cmdp; /* [i], [*i] */
  11.     unsigned char * sbp; /* [i], [*o] */
  12.     unsigned int timeout; /* [i] unit: millisecs */
  13.     unsigned int flags; /* [i] */
  14.     int pack_id; /* [i->o] */
  15.     void * usr_ptr; /* [i->o] */
  16.     unsigned char status; /* [o] */
  17.     unsigned char masked_status; /* [o] */
  18.     unsigned char msg_status; /* [o] */
  19.     unsigned char sb_len_wr; /* [o] */
  20.     unsigned short host_status; /* [o] */
  21.     unsigned short driver_status; /* [o] */
  22.     int resid; /* [o] */
  23.     unsigned int duration; /* [o] */
  24.     unsigned int info; /* [o] */
  25. } sg_io_hdr_t; /* 64 bytes long (on i386) */

不需要用到这个结构中的所有字段,因此这?仅列出最常用的字段:

  • interface_id:一般应该设置为 S。
  • dxfer_direction:用于确定数据传输的方向;常常使用以下值之一:
    • SG_DXFER_NONE:不需要传输数据。比如 SCSI Test Unit Ready 命令。
    • SG_DXFER_TO_DEV:将数据传输到设备。使用 SCSI WRITE 命令。
    • SG_DXFER_FROM_DEV:从设备输出数据。使用 SCSI READ 命令。
    • SG_DXFER_TO_FROM_DEV:双向传输数据。
    • SG_DXFER_UNKNOWN:数据的传输方向未知。
  • cmd_len:指向 SCSI 命令的 cmdp 的字节长度。
  • mx_sb_len:当 sense_buffer 为输出时,可以写回到 sbp 的最大大小。
  • dxfer_len:数据传输的用户内存的长度。
  • dxferp:指向数据传输时长度至少为 dxfer_len 字节的用户内存的指针。
  • cmdp:指向将要执行的 SCSI 命令的指针。
  • sbp:缓冲检测指针。
  • timeout:用于使特定命令超时。
  • status:由 SCSI 标准定义的 SCSI 状态字节。

总而言之,当用这种方法传输数据时,cmdp 必须指向其长度存储在 cmd_len 中的 SCSI CDB;sbp 指向最大长度为 mx_sb_len 的用户内存。如果出现错误,将把检测数据写回到这个位置。dxferp 指向内存;数据将根据 dxfer_direction 传输到 SCSI 设备或从中传输出来。

最后,我们看看 inquiry 命令,以及如何使用通用驱动器执行它。

例子:执行一个 inquiry 命令

inquiry 命令是所有 SCSI 设备实现的最常用的 SCSI 命令。这个命令用于请求 SCSI 设备的基本信息,并且常常用作 ping 操作,以测试 SCSI 设备是否在线。表 2 显示如何定义 SCSI 标准。

表 2. inquiry 命令格式定义

  位 7 位 6 位 5 位 4 位 3 位 2 位 1 位 0
字节 0 Operation code = 12h
字节 1 LUN Reserved EVPD
字节 2 Page code
字节 3 Reserved
字节 4 Allocation length
字节 5 Control

如果 EVPD 参数位(用于启用关键产品数据)为 0 并且 Page Code 参数字节为 0,那么目标将返回标准 inquiry 数据。如果 EVPD 参数为 1,那么目标将返回对应 page code 字段的特定于供应商的数据。

清单 2 显示了使用 SCSI 通用 API 的源代码片段。我们先看看设置 sg_io_hdr 的示例。


清单 2. 设置 sg_io_hdr

  1. struct sg_io_hdr * init_io_hdr() {
  2.   struct sg_io_hdr * p_scsi_hdr = (struct sg_io_hdr *)malloc(sizeof(struct sg_io_hdr));
  3.   memset(p_scsi_hdr, 0, sizeof(struct sg_io_hdr));
  4.   if (p_scsi_hdr) {
  5.    p_scsi_hdr->interface_id = 'S'; /* this is the only choice we */
  6.     /* this would put the LUN to 2nd byte of cdb*/
  7.     p_scsi_hdr->flags = SG_FLAG_LUN_INHIBIT;
  8.   }
  9.   return p_scsi_hdr;
  10. }
  11.  
  12. void destroy_io_hdr(struct sg_io_hdr * p_hdr) {
  13.     if (p_hdr) {
  14.         free(p_hdr);
  15.     }
  16. }
  17.  
  18. void set_xfer_data(struct sg_io_hdr * p_hdr, void * data, unsigned int length) {
  19.     if (p_hdr) {
  20.         p_hdr->dxferp = data;
  21.         p_hdr->dxfer_len = length;
  22.     }
  23. }
  24.  
  25. void set_sense_data(struct sg_io_hdr * p_hdr, unsigned char * data,
  26.         unsigned int length) {
  27.     if (p_hdr) {
  28.         p_hdr->sbp = data;
  29.         p_hdr->mx_sb_len = length;
  30.     }
  31. }

这些函数还用于设置 sg_io_hdr 对象。其中的一些字段指向用户空间内存;当执行完毕时,来自 SCSI 命令的 inquiry 输出数据将复制到 dxferp 指向的内存。如果出现错误并且需要检测数据,检测数据将复制到 sbp 指向的位置。清单 3 显示了一个向 SCSI 目标发送 inquiry 命令的示例。


清单 3. 向 SCSI 目标发送 inquiry 命令

  1. int execute_Inquiry(int fd, int page_code, int evpd, struct sg_io_hdr * p_hdr) {
  2.     unsigned char cdb[6];
  3.     /* set the cdb format */
  4.     cdb[0] = 0x12; /*This is for Inquery*/
  5.     cdb[1] = evpd & 1;
  6.     cdb[2] = page_code & 0xff;
  7.     cdb[3] = 0;
  8.     cdb[4] = 0xff;
  9.     cdb[5] = 0; /*For control filed, just use 0 */
  10.     
  11.     p_hdr->dxfer_direction = SG_DXFER_FROM_DEV;
  12.     p_hdr->cmdp = cdb;
  13.     p_hdr->cmd_len = 6;
  14.  
  15.     int ret = ioctl(fd, SG_IO, p_hdr);
  16.     if (ret<0) {
  17.         printf("Sending SCSI Command failed.\n");
  18.         close(fd);
  19.         exit(1);
  20.     }
  21.     return p_hdr->status;
  22. }

因此,这个函数首先根据 inquiry 标准格式准备 CDB,然后调用 ioctl() 函数,提交文件描述符 SG_IO 和 sg_io_hdr 对象;返回的状态存储在 sg_io_hdr 对象的 status 字段中。

现在我们看看应用程序如何使用这个函数执行 inquiry 命令,如清单 4 所示:


清单 4. 应用程序执行 inquiry 命令

  1. unsigned char sense_buffer[SENSE_LEN];
  2. unsigned char data_buffer[BLOCK_LEN*256];
  3. void test_execute_Inquiry(char * path, int evpd, int page_code) {
  4.     struct sg_io_hdr * p_hdr = init_io_hdr();
  5.     set_xfer_data(p_hdr, data_buffer, BLOCK_LEN*256);
  6.     set_sense_data(p_hdr, sense_buffer, SENSE_LEN);
  7.     int status = 0;
  8.     int fd = open(path, O_RDWR);
  9.     if (fd>0) {
  10.         status = execute_Inquiry(fd, page_code, evpd, p_hdr);
  11.         printf("the return status is %d\n", status);
  12.         if (status!=0) {
  13.             show_sense_buffer(p_hdr);
  14.         } else{
  15.             show_vendor(p_hdr);
  16.             show_product(p_hdr);
  17.             show_product_rev(p_hdr);
  18.         }
  19.     } else {
  20.         printf("failed to open sg file %s\n", path);
  21.     }
  22.     close(fd);
  23.     destroy_io_hdr(p_hdr);
  24. }

发送 SCSI 命令的步骤非常简单。首先必须分配用户空间数据缓冲区和检测缓冲区,并将它们指向 sg_io_hdr 对象。然后打开设备驱动器并获取文件描述符。有了这些参数之后,就可以将 SCSI 命令发送到目标设备。当这个命令完成时,SCSI 目标的输出将被复制到用户空间缓冲区。


清单 5. 使用参数将 SCSI 命令发送到目标设备

  1. void show_vendor(struct sg_io_hdr * hdr) {
  2.     unsigned char * buffer = hdr->dxferp;
  3.     int i;
  4.     printf("vendor id:");
  5.     for (i=8; i<16; ++i) {
  6.         putchar(buffer[i]);
  7.     }
  8.     putchar('\n');
  9. }
  10.  
  11. void show_product(struct sg_io_hdr * hdr) {
  12.     unsigned char * buffer = hdr->dxferp;
  13.     int i;
  14.     printf("product id:");
  15.     for (i=16; i<32; ++i) {
  16.         putchar(buffer[i]);
  17.     }
  18.     putchar('\n');
  19. }
  20.  
  21. void show_product_rev(struct sg_io_hdr * hdr) {
  22.     unsigned char * buffer = hdr->dxferp;
  23.     int i;
  24.     printf("product ver:");
  25.     for (i=32; i<36; ++i) {
  26.         putchar(buffer[i]);
  27.     }
  28.     putchar('\n');
  29. }
  30. int main(int argc, char * argv[]) {
  31.     test_execute_Inquiry(argv[1], 0, 0);
  32.     return EXIT_SUCCESS;
  33. }

SCSI Inquiry Command(Page Code 和 EVPD 字段皆设置为 0)的标准响应很复杂。根据标准,供应商 ID 从第 8 字节扩展到第 15 字节,产品 ID 从第 16 字节扩展到第 31 字节,产品版本从第 32 字节扩展到第 35 字节。必须获取这些信息,以检查命令是否成功执行。

 

SCSI command 的所有指令   指令含义

文章内容来自
Linux SCSI 子系统剖析 
探索 Linux 通用 SCSI 驱动器

发布了107 篇原创文章 · 获赞 272 · 访问量 58万+

猜你喜欢

转载自blog.csdn.net/wxh0000mm/article/details/105580640