新闻  |   论坛  |   博客  |   在线研讨会
关于linux SCSI 子系统
电子禅石 | 2019-12-27 15:58:10    阅读:9203   发布文章

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 readyInquiryRequest senseRead capacityReadWriteMode senseMode 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

字节 0Operation code = 12h
字节 1LUNReservedEVPD
字节 2Page code
字节 3Reserved
字节 4Allocation length
字节 5Control

如果 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. void destroy_io_hdr(struct sg_io_hdr * p_hdr) {

  12.     if (p_hdr) {

  13.         free(p_hdr);

  14.     }

  15. }


  16. void set_xfer_data(struct sg_io_hdr * p_hdr, void * data, unsigned int length) {

  17.     if (p_hdr) {

  18.         p_hdr->dxferp = data;

  19.         p_hdr->dxfer_len = length;

  20.     }

  21. }


  22. void set_sense_data(struct sg_io_hdr * p_hdr, unsigned char * data,

  23.         unsigned int length) {

  24.     if (p_hdr) {

  25.         p_hdr->sbp = data;

  26.         p_hdr->mx_sb_len = length;

  27.     }

  28. }


这些函数还用于设置 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.     int ret = ioctl(fd, SG_IO, p_hdr);

  15.     if (ret<0) {

  16.         printf("Sending SCSI Command failed.\n");

  17.         close(fd);

  18.         exit(1);

  19.     }

  20.     return p_hdr->status;

  21. }


因此,这个函数首先根据 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. void show_product(struct sg_io_hdr * hdr) {

  11.     unsigned char * buffer = hdr->dxferp;

  12.     int i;

  13.     printf("product id:");

  14.     for (i=16; i<32; ++i) {

  15.         putchar(buffer[i]);

  16.     }

  17.     putchar('\n');

  18. }


  19. void show_product_rev(struct sg_io_hdr * hdr) {

  20.     unsigned char * buffer = hdr->dxferp;

  21.     int i;

  22.     printf("product ver:");

  23.     for (i=32; i<36; ++i) {

  24.         putchar(buffer[i]);

  25.     }

  26.     putchar('\n');

  27. }

  28. int main(int argc, char * argv[]) {

  29.     test_execute_Inquiry(argv[1], 0, 0);

  30.     return EXIT_SUCCESS;

  31. }


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


SCSI command 的所有指令   指令含义

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


*博客内容为网友个人发布,仅代表博主个人观点,如有侵权请联系工作人员删除。

参与讨论
登录后参与讨论
属于自己的技术积累分享,成为嵌入式系统研发高手。
推荐文章
最近访客