The Linux generic SCSI driver

The Linux generic SCSI driver

http://www.tldp.org/HOWTO/SCSI-Generic-HOWTO/index.html
The driver’s purpose is to allow SCSI commands to be sent directly to SCSI devices.
用户通过Linux中的通用SCSI驱动(sg),可以直接向SCSI设备发送SCSI命令。

Linux实现了一个通用的SCSI设备驱动,如果一个设备支持SCSI协议,那么当它插入后,SCSI设备驱动将自动识别它,并创建一个相关联的设备文件,通常为/dev/sg0/dev/sg1等(一切设备皆文件)。你通过这设备文件便可以相设备发送SCSI命令。

这里写图片描述

上图展示了与一个SCSI设备交互的方式,通过SCSI Generic(SCSI通用驱动,以下简称sg)可以发送更多SCSI命令。

sg3 utils

utilities for devices using the SCSI command set
https://github.com/hreinecke/sg3_utils

sg3 utils是一个工具包,提供了使用SCSI命令的一些示例程序,ubuntu环境下安装命令为:

$ sudo apt-get install sg3-utils

sg_map

Show mapping from sg devices to other scsi device names

使用sg_map命令可以列出sg设备到其他scsi设备名称的映射。一个示例输出如下所示:

$ sudo sg_map -i 
/dev/sg0  /dev/sda  ATA       ST1000DM003-1CH1  CC57
/dev/sg1  /dev/sr0  PLDS      DVD-RW DS8A9SH    EL3A

可以看到,第一个为硬盘,它不仅识别为sda,还被识别为了sg0。因此不经可以通过文件系统操作它,还可以通过sg驱动发送scsi命令来与它交互。

向SCSI设备发送SCSI命令

The procedure for sending SCSI commands to a specific SCSI device is also very simple:
Open the SCSI generic device file (such as sg1) to get the file descriptor of SCSI device.
Prepare the SCSI command.
Set related memory buffers.
Call the ioctl() function to execute the SCSI command.
Close the device file.

  1. 打开设备文件得到文件描述符fd(比如:/dev/sg0) open(“/dev/sg0”, O_RDWR)
  2. 准备SCSI命令,并设置响应的buffer
  3. 调用ioctl发送scsi命令
    a. 第一个参数fd
    b. 第二个参数SG_IO (表示第三个参数为pointer to sg_io_hdr)
    c. 第三个参数pointer to sg_io_hdr
  4. 关闭设备文件 close(fd)

其中第二步的sg_io_hdr需要重点分析

struct sg_io_hdr

typedef struct sg_io_hdr
{
    int interface_id;               /* [i] 'S' (required) */
    int dxfer_direction;            /* [i] */
    unsigned char cmd_len;          /* [i] */
    unsigned char mx_sb_len;        /* [i] */
    unsigned short iovec_count;     /* [i] */
    unsigned int dxfer_len;         /* [i] */
    void * dxferp;                  /* [i], [*io] */
    unsigned char * cmdp;           /* [i], [*i]  */
    unsigned char * sbp;            /* [i], [*o]  */
    unsigned int timeout;           /* [i] unit: millisecs */
    unsigned int flags;             /* [i] */
    int pack_id;                    /* [i->o] */
    void * usr_ptr;                 /* [i->o] */
    unsigned char status;           /* [o] */
    unsigned char masked_status;    /* [o] */
    unsigned char msg_status;       /* [o] */
    unsigned char sb_len_wr;        /* [o] */
    unsigned short host_status;     /* [o] */
    unsigned short driver_status;   /* [o] */
    int resid;                      /* [o] */
    unsigned int duration;          /* [o] */
    unsigned int info;              /* [o] */
} sg_io_hdr_t;  /* 64 bytes long (on i386) */

注释中[i]表示输入参数,[o]表示输出参数

    interface_id: 固定为'S'.
    dxfer_direction: 数据传输方向,可以为以下值:
        SG_DXFER_NONE: 无数据传输. 例如: a SCSI Test Unit Ready command.
        SG_DXFER_TO_DEV: 数据传输到设备. 
        SG_DXFER_FROM_DEV: 数据从设备传输到主机.
        SG_DXFER_TO_FROM_DEV: 双向数据传输
        SG_DXFER_UNKNOWN: 数据传输方向未知.
    cmd_len: cmdp指向的SCSI命令的长度.
    mx_sb_len: sense数据缓冲区的长度.(设备向主机传回)
    dxfer_len: 数据传输长度The length of the user memory for data transfer.
    dxferp: 指向数据传输空间的起始地址.大小最小为dxfer_len. dxfer_direction为SG_DXFER_TO_DEV时,表示主机向设备传送数据的空间起始地址; 为SG_DXFER_FROM_DEV时,表示主机从设备接收数据的缓冲区起始地址
    cmdp: 指向scsi命令(又称为SCSI CDB)
    sbp: 指向 sense buffer,发生错误时,设备会向这里写入一些信息.
    timeout: 命令超时时间.
    status: SCSI状态,由SCSI标准定义.

inquery命令

inquery命令是一个最常见的scsi命令,所有scsi设备都实现了这个命令.此命令用于请求scsi设备的基本信息,类似于ping一个设备是否online.

inquery命令长度为6字节,cmd_len=6; 命令格式为(cmdp):

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

执行一个inquery命令的代码

int execute_Inquiry(int fd, int page_code, int evpd, struct sg_io_hdr * p_hdr) {
    unsigned char cdb[6];
    /* set the cdb format */
    cdb[0] = 0x12; /*This is for Inquery*/
    cdb[1] = evpd & 1;
    cdb[2] = page_code & 0xff;
    cdb[3] = 0;
    cdb[4] = 0xff;
    cdb[5] = 0; /*For control filed, just use 0 */

    p_hdr->dxfer_direction = SG_DXFER_FROM_DEV;
    p_hdr->cmdp = cdb;
    p_hdr->cmd_len = 6;

    int ret = ioctl(fd, SG_IO, p_hdr);
    if (ret<0) {
        printf("Sending SCSI Command failed.\n");
        close(fd);
        exit(1);
    }
    return p_hdr->status;
}

scsi opcode

https://en.wikipedia.org/wiki/SCSI_command

struct sg_io_hdr结构体中的cmdp指向具体的scsi命令, 即cdb. 第一个字节cdb[0]表示命令码, 头文件/usr/include/scsi/scsi.h中定义了一些标准规定的命令码(opcodes). 可以看到这些命令的前三比特为 000 001 100 101, 高三比特为110 111没有占用可用于厂商自定义的命令(大于0xC0).

SG_FLAG_LUN_INHIBIT
http://linux-scsi.vger.kernel.narkive.com/pe7RhO0E/sg-flag-lun-inhibit-broken
本意是用来禁止cdb[1]的高三比特被修改. 但是内核根本没有使用这个参数. 所以谨慎使用cdb[1]的高三比特.

注意,对于kernel 3.10,如果设备的scsi level值小于3, cdb[1]的高三比特会被设置为lun. kernel 4.4则不会有这个行为. 具体原因见下面内核代码.因此,对于厂商自定义的opcode, 确定字节设备的scsi level(cat /proc/scsi/scsi),以确定cdb的第二字节高三位是否会被修改, 这是我遇到的一个坑>_<

// https://elixir.bootlin.com/linux/v3.10/source/include/scsi/scsi.h#L538
#define SCSI_2          3

// https://elixir.bootlin.com/linux/v3.10/source/drivers/scsi/scsi.c#L729
int scsi_dispatch_cmd(struct scsi_cmnd *cmd)
{
...
    /* 
     * If SCSI-2 or lower, store the LUN value in cmnd.
     */
    if (cmd->device->scsi_level <= SCSI_2 &&
        cmd->device->scsi_level != SCSI_UNKNOWN) {
        cmd->cmnd[1] = (cmd->cmnd[1] & 0x1f) |
                   (cmd->device->lun << 5 & 0xe0);
    }
...

proc

/proc/scsi
http://www.tldp.org/HOWTO/SCSI-Generic-HOWTO/proc.html

scsi logging

If you need scsi logging, use /proc/sys/dev/scsi/logging_level kernel variable. Set it as follows:
-1 – Enable scsi events to syslog.
0 – Disable scsi events to syslog.

通过将/proc/sys/dev/scsi/logging_level设置为-1可以打开scsi events的syslog(通过dmesg查看,或者/var/log/messages)

驱动分析

https://elixir.bootlin.com/linux/v3.10/source/drivers/scsi/sg.c

分析的内核版本为Linux kernel 3.10, 上面为驱动的源码.
具体分析见另一篇文章.

在2.6.18版本内核上遇到的问题

我们的scsi命令为私有命令,长度为16字节。相同的代码在kernel3.x 4.x上没有问题,但是跑在2.6内核上就会有错误。

int r = ioctl(fd, SG_IO, &hdr);

执行上述语句后,hdr的status为0,一开始以为成功了,但是发现host_status=5. 下面是host_status取值含义:

http://www.tldp.org/HOWTO/SCSI-Generic-HOWTO/x291.html
SG_ERR_DID_ABORT [0x05] Told to abort for some other reason. From lk 2.4.15 the SCSI subsystem supports 16 byte commands however few adapter drivers do. Those HBA drivers that don’t support 16 byte commands will yield this error code if a 16 byte command is passed to a SCSI device they control.

host_status为5表示:由于一些原因操作失败。(跟没说一样)从kernel2.4.15开始,SCSI子系统支持16字节的scsi命令,然而仅有很少的adapter driver支持这一特性。那些不支持16字节的HBA driver在收到16字节的命令时会返回此错误码。

那么问题来了:什么是HBA驱动呢?

https://docs.oracle.com/cd/E19455-01/805-7378/6j6un037j/index.html
这里写图片描述
The HBA transport layer is a software and hardware layer responsible for transporting a SCSI command to a SCSI target device. The HBA driver provides resource allocation, DMA management, and transport services in response to requests made by SCSI target drivers through SCSA. The host adapter driver also manages the host adapter hardware and the SCSI protocols necessary to perform the commands. When a command has been completed, the HBA driver calls the target driver’s SCSI pkt command completion routine.
Figure 15-2 illustrates this flow, with emphasis placed on the transfer of information from target drivers to SCSA to HBA drivers. Typical transport entry points and function calls are included.

rhel 5.1 设置本地源: https://blog.csdn.net/qq_36784975/article/details/80923653

使用sg3_util的工具sg_raw可以发送任意的scsi命令:

# sg_raw -r 0 /dev/sda1 c0 0 87 0 0 0 0 0 0 0 0 0 
SCSI Status: Good 

No data received

经过测试,rhel5.1 kernel 2.6.18, scsi命令必须在12字节以内(包含12字节),但是我们的私有scsi命令为16字节,因此会出错。

Mmap-ed IO

http://www.tldp.org/HOWTO/SCSI-Generic-HOWTO/mmapio.html
https://github.com/hreinecke/sg3_utils/blob/master/src/sgm_dd.c
以从设备读取数据为例子:

修改内核参数,开启dio

# echo 1 > /proc/scsi/sg/allow_dio

打开设备,进行mmap挂在共享内存到mmapbuf_

    fd_ = open(path.c_str(), O_RDWR);
    if (fd_ != -1) {
        /* Just to be safe, check we have a new sg device by trying an ioctl */
        int k;
        if ((ioctl(fd_, SG_GET_VERSION_NUM, &k) < 0) || (k < 30122)) {
            Log::Error("needs sg driver version >= 3.1.22, current version is:%d\n", k);
        CloseDevice();
            return false;
        }

    /* This version uses memory-mapped transfers (i.e. mmap() call from the user
   space) to speed transfers. */
        mmapbuf_ = (unsigned char *)mmap(NULL, 8000, PROT_READ | PROT_WRITE,
MAP_SHARED, fd_, 0);
        if (MAP_FAILED == mmapbuf_) {
            Log::Error("error using mmap() on file: %s", path.c_str());
        CloseDevice();
            return false;
    }

设置sg flags的SG_FLAG_MMAP_IO 标志位

hdr_.flags |= SG_FLAG_MMAP_IO; 

调用ioctl

ioctl(fd_, SG_IO, &hdr_);

ioctl成功返回后,结果存放在共享内存mmapbuf_

其他参考

https://www.ibm.com/developerworks/library/l-scsi-api/index.html
https://blog.csdn.net/hamin123/article/details/42499825

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值