文章目录
1 概述
1.1 scsi协议
小型计算机系统接口(SCSI,Small Computer System Interface)是一种用于计算机及其周边设备之间(硬盘、软驱、光驱、打印机、扫描仪等)系统级接口的独立处理器标准。
虽然名字带个接口,但其实是一个协议,用于规定系统与硬件进行数据交互。通过scsi协议,系统就不用适配各种各样的通信总线协议。
1.2 术语
- Initiator:启动器,可以发送SCSI命令并接收响应的设备;
- Target:目标器,充当SCSI命令接收器的设备;
- LUN:逻辑单元,是SCSI目标器中充当数据源或目的地的名字空间资源,一个目标器可以包含多个LUN,每个LUN可以具有不同的属性;
- NEXUS:NEXUS用于描述启动器端口和目标器端口之间的连接。
- CDB:(指令/命令)描述块,一个完整的SCSI请求由CDB、数据、命令属性信息构成,其中,CDB描述了该SCSI命令的具体细节,包括命令操作类型(read/write/inquiry等)、数据的处理方式等信息。最早的SCSI-1支持的是6个字节的指令,后来慢慢演变成了10字节,12字节,16字节,后续还有应用扩展为32字节的CDB。关于其它长度的CDB格式定义,可以参考《SCSI Primary Commands》(SPC)手册。
- Status code:当目标器设备完成命令处理后,会返回状态响应给启动器,用于指示命令的完成状态:完成或者异常。SCSI协议约定使用特定的状态码(Status code)和状态限定符(Status qualifier)来告知应用命令的处理结果。
- Status qualifier:状态限定符,Status qualifier与配合Status code,提供更多返回该状态的原因的信息。
- Sense Data:当命令以CHECK_CONDITION状态终止时,目标器设备会返回Sense Data,用于告知上层应用命令终止的原因,应用会根据Sense data确定如何进一步处理。Sense data主要通过Sense key、Asc(additional sense code)以及Ascq(additional sense code qualifier)来描述命令执行的异常信息。
- Sense key描述了主要错误信息的类别;
- Asc和Ascq在Sense key的基础上进一步说明了具体的错误原因。
- LUN:逻辑单元号,比较难理解,可以先放放,不求甚解。
- BUS:这是计算机机构里面的东西,我觉得只要记住scsi设备中一个Target可以连结多个Initiator就行。
- SCSI ID:每个连接在SCSI总线上的设备(包括主机和外设)都有一个唯一的ID,称为SCSI ID,用于识别不同设备。SCSI ID的数量与总线的宽度有关,通常有8位(允许8个设备)或16位(允许16个设备)配置。
1.3 iSCSI
在SCSI的演进过程中,iSCSI(Internet Small Computer System Interface)是一种使用TCP/IP协议通过网络来传输SCSI命令的技术,通常用于网络存储(如SAN,存储区域网络)中。在iSCSI中,iSCSI ID是指用来唯一标识iSCSI设备的标识符,用于主机(initiator)和存储设备(target)之间的连接和识别。
2 应用
2.1 CDB
scsi协议包含的范围太广,我们可以暂时放放,专注于如何调用它。CDB(Command Descripotr Block)就是向scsi设备发送的指令。1
上图说明了指令的格式。可以看到对于CDB只有一条强制规定,就是首个字节必须为OPERATION CODE(Opcode),并且CDB长度有6、10、12、16等字节长度的类型。
拿上图CDB(6)举例:
- 第 0 字节: 操作码,在2.2节有各种操作码,合在一起叫指令集。
- 第 1-3 字节: 逻辑块地址(LBA,3 字节表示)。
- 第 4 字节: 传输块的数量(最多 256 个块,即传输 0 代表 256 块)。
- 第 5 字节: 控制字节。
在看看CDB(10):
- 第 0 字节: 操作码。
- 第 1 字节: 保留或控制位。
- 第 2-5 字节: 逻辑块地址 (LBA)。
- 第 6 字节: 保留或控制位。
- 第 7-8 字节: 传输长度(块数量)。
- 第 9 字节: 控制字节。
2.2 scsi指令集(Opcode set)
2.1说了scsi的指令,最重要的是Opcode,不同的硬件设备支持不同的指令,具体有哪些指令呢,这里看一种Mass Storage设备所使用的SCSI命令集:
指令代码
指令名称 | 指令名称 | 指令说明 |
---|---|---|
0x00 | Test Unit Ready | 查询设备是否ready |
0x03 | Request Sense | 主机请求设备返回执行结果,及获取状态信息 |
0x12 | Inquiry | 获取设备信息 |
0x1A | Mode Sense(6) | 向host传输参数 |
0x5A | Mode Sense(10) | 向host传输参数 |
0x25 | Read Capacity(10) | 读取设备容量 |
0x28 | Read(10) | Host从设备读取数据 |
0x2A | Write(10) | Host写数据到存储设备 |
0x23 | Read Format Capacity | 查询当前容量及可用空间 |
0x15 | Mode Select(6) | 允许Host对外部设备设置参数 |
0x55 | Mode Select(10) | 允许Host对外部设备设置参数 |
0x1E | Prevent/Allow Medium Removal | 禁止/允许存储介质移动 |
0x1B | Start/Stop Uint | 启动/停止存储单元电源(写保护) |
0xA0 | Report LUNs | 索取设备的LUN数和LUN清单 |
0x2F | Verify | 在存储中验证数据 |
2.3 指令集文档
SCSI命令集文档由SPC(SCSI Primary Commands)、SBC(SCSI Block Commands)、ZBC(Zoned Block Commands)、SES(Enclosure Services Commands)等构成,其中,最常使用的是SPC和SBC文档:
- SPC文档定义了适用于所有SCSI设备的通用命令集,如INQUERY、TUR等命令以及Sense信息格式;
- SBC文档中定义了适用于块设备的命令集,如READ、WRITE、VERIFY等。
- SSC文档定义了流设备,如磁带,的命令集等
通常SCSI文档推荐阅读的顺序为SAM、SPC以及特定于设备类型的命令集文档。
发送指令
我们用2.3的命令来试试查询一个scsi设备的信息。查询信息用的指令码是0x12。
#include <iostream>
#include <fcntl.h> // For open()
#include <unistd.h> // For close()
#include <cstring> // For memset()
#include "sg.h" // For sg_io_hdr_t (SCSI generic (sg) ioctl interface)
#include <sys/ioctl.h> // For ioctl()
#define INQUIRY_CMDLEN 6
#define INQUIRY_REPLY_LEN 96
#define SCSI_TIMEOUT 20000 // Timeout in milliseconds
void parse_inquiry_response(const unsigned char* inquiry_response) {
for (int i = 0; i < INQUIRY_REPLY_LEN; ++i) {
printf("%02x ", inquiry_response[i]);
if ((i + 1) % 16 == 0)
std::cout << std::endl;
}
// 解析设备类型
int device_type = inquiry_response[0] & 0x1F;
std::cout << "Device Type: " << device_type << std::endl;
// 解析制造商
char vendor[9];
memset(vendor, 0, sizeof(vendor));
memcpy(vendor, &inquiry_response[8], 8);
std::cout << "Vendor: " << vendor << std::endl;
// 解析产品标识
char product[17];
memset(product, 0, sizeof(product));
memcpy(product, &inquiry_response[16], 16);
std::cout << "Product: " << product << std::endl;
// 解析产品修订号
char revision[5];
memset(revision, 0, sizeof(revision));
memcpy(revision, &inquiry_response[32], 4);
std::cout << "Revision: " << revision << std::endl;
}
// 发送 SCSI INQUIRY 命令
int send_scsi_inquiry(const char* device_path) {
int sg_fd = open(device_path, O_RDWR); // 打开 SCSI 设备
if (sg_fd < 0) {
std::cerr << "Failed to open device: " << device_path << std::endl;
return -1;
}
// SCSI INQUIRY 命令描述符块 (CDB)
unsigned char inquiry_cdb[INQUIRY_CMDLEN] = {0x12, 0, 0, 0, INQUIRY_REPLY_LEN, 0};
// 缓存 SCSI 设备返回的数据
unsigned char inquiry_response[INQUIRY_REPLY_LEN];
// Sense 缓冲区,用于捕获错误信息
unsigned char sense_buffer[32];
// 初始化 sg_io_hdr 结构
sg_io_hdr_t io_hdr;
memset(&io_hdr, 0, sizeof(sg_io_hdr_t));
io_hdr.interface_id = 'S'; // 固定为 'S'
io_hdr.cmdp = inquiry_cdb; // 指向 CDB 的指针
io_hdr.cmd_len = sizeof(inquiry_cdb);
io_hdr.dxferp = inquiry_response; // 数据缓冲区的指针
io_hdr.dxfer_len = sizeof(inquiry_response);
io_hdr.dxfer_direction = SG_DXFER_FROM_DEV; // 数据从设备传输到主机
io_hdr.sbp = sense_buffer; // Sense 数据的指针
io_hdr.mx_sb_len = sizeof(sense_buffer);
io_hdr.timeout = SCSI_TIMEOUT; // 超时时间
io_hdr.flags = 0;
// 通过 ioctl() 发送 SCSI 命令
if (ioctl(sg_fd, SG_IO, &io_hdr) < 0) {
std::cerr << "SG_IO ioctl failed" << std::endl;
close(sg_fd);
return -1;
}
// 打印解析后的 INQUIRY 数据
parse_inquiry_response(inquiry_response);
close(sg_fd);
return 0;
}
int main() {
const char* device_path = "/dev/sg0"; // 指定你的 SCSI 设备路径
send_scsi_inquiry(device_path);
return 0;
}
输出:
00 00 05 02 1f 00 00 02 4d 73 66 74 20 20 20 20
56 69 72 74 75 61 6c 20 44 69 73 6b 20 20 20 20
31 2e 30 20 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
Device Type: 0
Vendor: Msft
Product: Virtual Disk
Revision: 1.0
reponse 查询的返回信息
实际查询到的返回信息inquiry_response是那一个字符矩阵,要从里面解析出可读的设备信息,首先需要了解响应数据的格式。标准的 SCSI INQUIRY 响应包含设备的类型、制造商、产品信息等。
SCSI INQUIRY 响应的前36字节是标准的,格式如下:
Byte 位置 | 含义 |
---|---|
0 | 设备类型 (Peripheral Device Type) |
1 | 设备类型修饰符 |
2 | ISO/ECMA/ANSI 版本 |
3 | 响应数据格式 (Response Data Format) |
4-7 | 额外数据长度 (Additional Length) |
8-15 | 制造商 (Vendor Identification) |
16-31 | 产品标识 (Product Identification) |
32-35 | 产品修订号 (Product Revision Level) |
SCSI INQUIRY 响应还可以包含其他扩展信息,但标准的36字节已经包含了制造商、产品标识和修订号等关键信息。如果你需要更多信息,可以进一步解析后面的字节,例如 SCSI 标准版本号、其他功能支持等。
Inquiry CDB Format
Field | Description |
---|---|
Command Support Data (CMDDT) | This field is not supported and must be set to 0. |
Enable Vital Product Data (EVPD) | An EVPD value of 1 indicates that the vital product data specified by the Page Code should be returned. A value of 0 indicates that standard inquiry data should be returned. |
Page Code | This field specifies which vital product data page to return if the EVPD bit is set to 1. If the EVPD bit is set to 0, the Page Code must be 00h. The library supports the following page codes: 00h - Supported Vital Product Data pages (this list) 80h - Unit Serial Number page 83h – Device Identification page 85h - Management Network Addresses page C8h - Vendor Specific Device Capabilities page |
2.6 sg_io_hdr 结构的定义
对于字符设备,linux驱动器支持许多典型的系统调用,比如open()、close()、read()、write、poll() 和 ioctl()。向特定的 SCSI 设备发送 SCSI 命令的步骤也非常简单:
- 打开 SCSI 通用设备文件(比如 sg1)获取 SCSI 设备的文件描述符。
- 准备好 SCSI 命令。
- 设置相关的内存缓冲区。
- 调用 ioctl() 函数执行 SCSI 命令。
- 关闭设备文件。
典型的 ioctl() 函数类似于:ioctl(fd,SG_IO,p_io_hdr);
这里的 ioctl() 函数必须具有 3 个参数:
- fd 是设备文件的文件描述符。通过调用 open() 成功打开设备文件之后,将需要获取这个参数。
- SG_IO 表明将 sg_io_hdr 对象作为 ioctl() 函数的第三个参数提交,并且在 SCSI 命令结束时返回。
- p_io_hdr 是指向 sg_io_hdr 对象的指针,该对象包含 SCSI 命令和其他设置。
SCSI 通用驱动器的最重要数据结构是 struct sg_io_hdr,它在 scsi/sg.h 中定义,并且包含如何使用 SCSI 命令的信息。下面给出了这个结构的定义:
typedef struct sg_io_hdr
{
int interface_id; /* [i] 'S' for SCSI generic (required) */
int dxfer_direction; /* [i] data transfer direction */
unsigned char cmd_len; /* [i] SCSI command length ( <= 16 bytes) */
unsigned char mx_sb_len; /* [i] max length to write to sbp */
unsigned short int iovec_count; /* [i] 0 implies no scatter gather */
unsigned int dxfer_len; /* [i] byte count of data transfer */
void * dxferp; /* [i], [*io] points to data transfer memory
or scatter gather list */
unsigned char * cmdp; /* [i], [*i] points to command to perform */
unsigned char * sbp; /* [i], [*o] points to sense_buffer memory */
unsigned int timeout; /* [i] MAX_UINT->no timeout (unit: millisec) */
unsigned int flags; /* [i] 0 -> default, see SG_FLAG... */
int pack_id; /* [i->o] unused internally (normally) */
void * usr_ptr; /* [i->o] unused internally */
unsigned char status; /* [o] scsi status */
unsigned char masked_status;/* [o] shifted, masked scsi status */
unsigned char msg_status; /* [o] messaging level data (optional) */
unsigned char sb_len_wr; /* [o] byte count actually written to sbp */
unsigned short int host_status; /* [o] errors from host adapter */
unsigned short int driver_status;/* [o] errors from software driver */
int resid; /* [o] dxfer_len - actual_transferred */
unsigned int duration; /* [o] time taken by cmd (unit: millisec) */
unsigned int info; /* [o] auxiliary information */
} sg_io_hdr_t;
这里列出一些常用字段:2
字段 | 应用 |
---|---|
interface_id | 通常位 ‘S’, 表示通用SCSI协议 |
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 | 可以写回到 sbp 的最大大小。 |
dxfer_len | 数据传输的用户内存的长度。 |
dxferp | 指向数据传输时长度至少为 dxfer_len 字节的用户内存的指针。 |
cmdp | 指向将要执行的 SCSI 命令的指针。 |
sbp | 指针,如果出现错误,将把检测数据写回到这个位置 |
timeout | 用于使特定命令超时。 |
status | 由 SCSI 标准定义的 SCSI 状态字节。 |
sbp | 指针,如果出现错误,将把检测数据写回到这个位置 |
2.7 Sense Data 获取和处理
定义一个sense buffer来接收错误信息。在sg_io_hdr_t
中,sg_io_hdr_t .sbp指向sense buffer,sg_io_hdr_t .mx_sb_len用于指明该缓冲区的大小。如果发生错误且SCSI命令返回了Sense Data,sg_io_hdr_t .sb_len_wr,将会返回Sense Data的实际长度。所以sb_len_wr>0,则代表存在Sense Data。
Sense Data 是一个包含详细错误信息的缓冲区。通常,它会包含以下信息:
- 响应码: Sense Data 的第一个字节,表示错误的类型。
- 错误代码: 表示具体的 SCSI 错误原因。
- 附加信息: 可能包含与错误相关的逻辑块地址或其他详细信息。
为了进一步解读 Sense Data 中的内容,您可以参考 SCSI 标准或设备的 SCSI 错误码文档。Linux 提供的 sg3_utils 包含工具 sg_decode_sense,可以帮助解释这些 Sense Data。举个栗子:
sense data: 70 00 05 00 00 00 00 0A 00 00 00 00 24 00 00 00
sg_decode_sense 70 00 05 00 00 00 00 0A 00 00 00 00 24 00 00 00
输出:
Fixed format, current; Sense key: Illegal Request
Additional sense: Invalid field in cdb
3 SCSI 指南
3.1 强制SCSI指令
SCSI规范说明书为所有设备定义了强制命令和可选命令,每个设备都有自己的一套命令,其中有一些是强制的,其他一些是可选的,首先来看一下哪些命令适用于所有设备。
有4个所有设备都支持的命令:
代码 | 指令 |
---|---|
00H | Test unit Ready |
03H | Request sense |
12H | Inquery |
1DH | Send Diagnostic |
这些命令都要处理设备标识符、状态和错误报告。
3.2 SCSI 可选指令
代码 | 指令 |
---|---|
18H | Copy |
1CH | Receive Diagnostic Result |
39H | Compare |
3AH | Copy and Verify |
3BH | Write Buffer |
3CH | Read Buffer |
40H | Change Definition |
4CH | Log Select |
4DH | Log Sense |
说明:略…(很多我也不知道T_T)
3.3 SCSI 模式页机制
有4个命令是为所有设备定义的,他们仅对某些设备上强制要求支持的:
代码 | 命令 |
---|---|
15H | Mode Select(6) |
1AH | Mode Sense(6) |
55H | Mode Select(10) |
5AH | Mode Sense(10) |
其实就是两个命令,Mode Sense(模式感知)和Mode Select(模式选择),有6字节和10字节两个版本。
这两个命令可以读取或设置许多设备参数。参数以页的方式组织,用页代码标识出来,很多页代码是设备特定的,但是有一些是和SCSI参数相关的,如断开连接和命令排队算法。3
上面一段读完是不是不明所以?
计算机总有一堆读起来让人费解的术语,我来翻译一下上面的话:
你可以通过上面两个命令来获取和配置设备运行的参数。
举个栗子:通过Mode Sense来获取当前硬盘是否开启写缓存,如果要改变缓存策略,可以通过Mode Select修改设备参数实现。
SCSI 模式页是设备行为配置的重要机制,允许主机系统通过特定的页码来读取或设置存储设备的各类操作参数。通过使用模式页,系统管理员或驱动程序可以灵活调整设备的性能、可靠性和电源管理等行为。
当然,可以想见,这样的操作要求程序员对硬件设备足够熟悉才能把握修改哪些参数是合理且有效的。
4 读写 SCSI(磁带) 设备数据
从这里开始只讨论磁带。磁带与磁盘是完全不同的存储设备,与前面几节讲述的通用SCSI设备不同,磁带独特的顺序读写和大容量存储特性将带来怎样的体验?
4.1 写磁带的基本流程
Load/Unload (0x1B) //装载磁带
Test Unit Ready (0x00) //检查状态
[MODE SELECT (0x15)] //设置块大小
Write (0x0A) //写数据
Synchronize Buffer (0x10)//刷新缓冲区
[Write Filemarks (0x10)] //设置文件标志
Load/Unload (0x1B) //卸载磁带
Request Sense (0x03) //检查操作结果
4.2 LBA 逻辑块地址
LBA,Logical Block Address,是磁带的最小可寻址单位。磁带的数据是以逻辑块为单位来读写的,而不是按字节进行操作。每个逻辑块的大小通常是固定的,如512字节或4096字节,具体取决于设备的配置。当主机向SCSI设备发出读或写命令时,它不会直接指定某个字节地址,而是指定一个逻辑块地址(LBA)和一个逻辑块数(transfer length)
4.3 设置块的大小
#include <iostream>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <scsi/sg.h>
#include <cstring>
#define TAPE_DEVICE "/dev/sg3" // SCSI 磁带设备
#define MODE_SELECT_CMD 0x15 // SCSI MODE SELECT 命令操作码
#define BLOCK_SIZE 65536 // 要设置的块大小 64KB
print_sense_buffer(unsigned char* sense_buffer, int len)
{
std::cout << "Sense Buffer: sg_decode_sense ";
for (int i = 0; i < len; ++i) {
std::cout << std::hex << (int)sense_buffer[i] << " ";
}
std::cout << std::dec << std::endl;
}
set_scsi_block_size(unsigned int block_size,const char *TAPE_DEVICE)
{
int fd = open(TAPE_DEVICE, O_RDWR);
if (fd < 0) {
perror("Failed to open tape device");
return -1;
}
struct sg_io_hdr io_hdr;
unsigned char cdb[6]; // MODE SELECT(6) 命令格式
unsigned char sense_buffer[32];
unsigned char data_out[12]; // MODE SELECT 数据包,取决于设备文档和支持
// 填充 MODE SELECT 数据
memset(&io_hdr, 0, sizeof(struct sg_io_hdr));
memset(cdb, 0, sizeof(cdb));
memset(sense_buffer, 0, sizeof(sense_buffer));
memset(data_out, 0, sizeof(data_out));
// 设置块大小到数据包中
data_out[2] = 0x10;
data_out[9] = (block_size >> 16) & 0xFF;
data_out[10] = (block_size >> 8) & 0xFF;
data_out[11] = block_size & 0xFF;
// 填充 MODE SELECT 命令
cdb[0] = MODE_SELECT_CMD; // MODE SELECT 操作码
cdb[1] = 0x10; // PF位设为1,表示可更改模式参数
cdb[4] = sizeof(data_out); // 数据包长度
// 填充 sg_io_hdr
io_hdr.interface_id = 'S';
io_hdr.cmd_len = sizeof(cdb);
io_hdr.mx_sb_len = sizeof(sense_buffer);
io_hdr.dxfer_direction = SG_DXFER_TO_DEV; // 方向:写入设备
io_hdr.dxfer_len = sizeof(data_out);
io_hdr.dxferp = data_out;
io_hdr.cmdp = cdb;
io_hdr.sbp = sense_buffer;
io_hdr.timeout = 5000; // 5秒超时
// 发送命令
int result = ioctl(fd, SG_IO, &io_hdr);
if (result < 0) {
perror("SCSI MODE SELECT failed");
return -1;
}
if ((io_hdr.info & SG_INFO_OK_MASK) != SG_INFO_OK) {
std::cerr << "SCSI 命令执行错误" << std::endl;
if (io_hdr.sb_len_wr > 0) {
std::cerr << "Sense Data 错误码: ";
print_sense_buffer(sense_buffer, io_hdr.sb_len_wr); // 输出 Sense Buffer
}
close(fd);
return 1;
}
std::cout << "SCSI block size set to " << block_size << " bytes." << std::endl;
return 0;
}
int main() {
set_scsi_block_size(BLOCK_SIZE,TAPE_DEVICE);
return 0;
}
4.4 CDB(6) Mode Select - 15h
设置设备参数需要查看相应设备的文档,当前讨论上面6字节CDB格式。
StorageTek SL4000 Modular Library System SCSI Reference Guide
这是oracle带库的参考文档
15h命令CDB(6)的组成:
Command Definitions
PF (Page Format)
设置为1,don’t ask anything
SP (Save Parameters)
设置为0,don’t ask anything
Parameter List Length
置为传输数据的长度,若没有数据置0。
Any other value is an error and is not supported.
Scalar Intelligent Libraries SCSI Reference Guide
昆腾带库的CDB略有不同:
可以看到1字节的高三位需填写Logical Unit Number,其他与oracle基本一致。就不再赘述了。
4.5 Mode Select 命令的 data_out 格式
data_out并不是协议中的某个术语,本文专指向设备发送的内容,也就是dxferp指针所指向的缓冲区。
在 SCSI Mode Select 命令中,data_out 是要发送的数据缓冲区,它必须遵循 SCSI 协议的特定结构和格式要求。具体来说,data_out 中的内容取决于你使用的磁带设备以及你要设置的参数。以下是 data_out 中常见的结构和要求:
- Mode Parameter Header(模式参数头部,描述模式数据的总长度、块描述符的数量等)
- Block Descriptor(块描述符,可选字段,用 于描述设备块的大小、块数量等)
- Mode Page(s)(模式页面,包含具体的设备配置参数,如缓存策略、块大小、传输速率等)
Mode Parameter Header长度为4个字节:
- Byte 0: Mode Data Length(为0,通常不填)
- Byte 1: Medium Type (磁带为0x10)
- Byte 2:Device-Specific Parameter (如只读标志等)
- Byte 3: Block Descriptor Length(指定接下来块描述符的长度)
Block Descriptor 长度为8字节,字段定义如下4:
从这个表很容易看出Block Descriptor这几个字节应该填什么。在上面代码也有展示,其他的细节如Logical block length限定范围,number of blocks 的范围,需要去查看指定文档。
4.6 关于块
小块:
- 优点:适合随机访问场景,适合存储小文件。
- 缺点:性能较差,因为每次写入的数据量较小,设备头的寻道和转移开销较大。
大块:
- 优点:适合顺序写入或大文件存储,性能较好。
- 缺点:占用更多缓存空间,可能导致小文件处理效率降低。
如果没有提前设置块,可能会出现的情况:
- 数据块大小匹配: 如果你写入的数据正好符合设备的默认或先前设置的块大小,写入可能会成功,数据会按照预期的方式写入磁带。
- 数据块大小不匹配: 如果写入的数据大小与设备的块大小不一致,磁带设备可能会返回错误(例如 SCSI Check Condition),通常会报错“Invalid Block Size”或类似的错误。设备可能会拒绝写入,或者抛出错误并停止写操作。
可能返回的错误:
- Medium Error
- Invalid Block Size
- Aborted Command
4.7 写数据
#include <iostream>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <scsi/sg.h>
#include <cstring>
using namespace std;
#define BLOCK_SIZE 65536 // 要设置的块大小 64KB
#define MODE_SELECT_CMD 0x15 // SCSI MODE SELECT 命令操作码
#define WRITE_BUFFER_SIZE 65536 // 定义写缓冲区大小
#define SCSI_WRITE_6_COMMAND 0x0A // SCSI 6字节WRITE命令的操作码
#define SENSE_BUFFER_LEN 32 // Sense Buffer 长度
#define READ_BUFFER_SIZE 65536 // 定义读缓冲区大小
#define SCSI_READ_6_COMMAND 0x08 // SCSI 6字节READ命令的操作码
#define TAPE_DEVICE "/dev/sg19" // 磁带设备路径
write_block(char *buffer,int blocks)
{
// 打开设备
int fd = open(TAPE_DEVICE, O_RDWR);
if (fd < 0) {
std::cerr << "无法打开设备: " << TAPE_DEVICE << " 错误: " << strerror(errno) << std::endl;
return 1;
}
// 要写入的数据
unsigned char write_buffer[WRITE_BUFFER_SIZE];
for(int i = 0;i<WRITE_BUFFER_SIZE;i++)
write_buffer[i] = i%'a' + 'a';
//memset(write_buffer, 'A', sizeof(write_buffer)); // 填充数据'A'
// SCSI CDB (6字节写命令)
unsigned char cdb[6];
memset(cdb, 0, sizeof(cdb));
cdb[0] = SCSI_WRITE_6_COMMAND; // 操作码 (WRITE 6)
// LBA字段(逻辑块地址),在这个简单示例中使用0地址
cdb[1] = 0x01;
cdb[2] = blocks >> 16; // LBA的高字节
cdb[3] = blocks >> 8; // LBA的中间字节
cdb[4] = blocks; // LBA的字节
cdb[5] = 0x00;
// Sense Buffer(请求检测数据缓冲区)
unsigned char sense_buffer[SENSE_BUFFER_LEN];
memset(sense_buffer, 0, sizeof(sense_buffer));
// 创建 SG_IO 结构
sg_io_hdr_t io_hdr;
memset(&io_hdr, 0, sizeof(sg_io_hdr_t));
io_hdr.interface_id = 'S'; // 使用SCSI通用接口
io_hdr.cmd_len = sizeof(cdb); // CDB的长度
io_hdr.mx_sb_len = sizeof(sense_buffer); // 没有错误返回信息缓冲区
io_hdr.dxfer_direction = SG_DXFER_TO_DEV; // 数据传输方向:主机到设备
io_hdr.dxfer_len = sizeof(write_buffer); // 数据缓冲区长度
io_hdr.dxferp = write_buffer; // 指向数据缓冲区
io_hdr.cmdp = cdb; // 指向CDB
io_hdr.sbp = sense_buffer; // 指向Sense Buffer
io_hdr.timeout = 5000; // 超时时间:5秒
// 使用 ioctl 发送 SCSI 命令
int status = ioctl(fd, SG_IO, &io_hdr);
if (status < 0) {
std::cerr << "SCSI WRITE 命令失败: " << strerror(errno) << std::endl;
close(fd);
return 1;
}
// 检查传输状态
if ((io_hdr.info & SG_INFO_OK_MASK) != SG_INFO_OK) {
std::cerr << "SCSI 命令执行错误" << std::endl;
if (io_hdr.sb_len_wr > 0) {
std::cerr << "Sense Data 错误码: ";
print_sense_buffer(sense_buffer, io_hdr.sb_len_wr); // 输出 Sense Buffer
}
close(fd);
return 1;
}
std::cout << "成功写入 " << WRITE_BUFFER_SIZE << " 字节到磁带设备" << std::endl;
// 关闭设备
close(fd);
return 0;
}
int main() {
write_block(NULL,1);
return 0;
}
4.8 WRITE command (0x0a)
写命令就是客户端向服务器的逻辑地址传输数据的过程。先来看看cdb 6 的结构:
TRANSFER LENGTH如何设置需要查阅文档。
由于没有查到中文版的,我目前会翻译一点ssc的文档,如果翻译不对还请指正:
上图的FIXED比特位决定了传入的是变长的块还是固定块。关于变成和固定模式,需要查阅更多文档。
如果FIXED置为1,TRANSFER LENGTHE的值就是要传输的块的数量。如果FIEXED为0。。。请查阅文档,因为我们不需要讨论所有情况,SSC的文档有几百页,本文很难完全说完,所有这里我们就固定为1.
上面我们讨论了FIXED的含义,TRANSFER LENGTHE就很好设置了,将实际写入的buffer长度除以之前设置的块长65536向上取整,就是需要设置的块数,按照三位bit位存在cdb里面就行。
4.9 读数据
#include <iostream>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <scsi/sg.h>
#include <cstring>
using namespace std;
#define BLOCK_SIZE 65536 // 要设置的块大小 64KB
#define MODE_SELECT_CMD 0x15 // SCSI MODE SELECT 命令操作码
#define WRITE_BUFFER_SIZE 65536 // 定义写缓冲区大小
#define SCSI_WRITE_6_COMMAND 0x0A // SCSI 6字节WRITE命令的操作码
#define SENSE_BUFFER_LEN 32 // Sense Buffer 长度
#define READ_BUFFER_SIZE 65536 // 定义读缓冲区大小
#define SCSI_READ_6_COMMAND 0x08 // SCSI 6字节READ命令的操作码
#define TAPE_DEVICE "/dev/sg19" // 磁带设备路径
read_block(char *buffer, int blocks)
{
// 打开设备
int fd = open(TAPE_DEVICE, O_RDWR);
if (fd < 0) {
std::cerr << "无法打开设备: " << TAPE_DEVICE << " 错误: " << strerror(errno) << std::endl;
return 1;
}
// 准备读取的数据缓冲区
unsigned char read_buffer[READ_BUFFER_SIZE];
memset(read_buffer, 0, sizeof(read_buffer)); // 将缓冲区初始化为0
// SCSI CDB (6字节读命令)
unsigned char cdb[6];
memset(cdb, 0, sizeof(cdb));
cdb[0] = SCSI_READ_6_COMMAND; // 操作码 (READ 6)
// LBA字段(逻辑块地址),在这个简单示例中使用0地址
cdb[1] = 0x01; // LBA的高字节
cdb[2] = blocks >> 16; // LBA的高字节
cdb[3] = blocks >> 8; // LBA的中间字节
cdb[4] = blocks; // LBA的字节
cdb[5] = 0x00;
// Sense Buffer(请求检测数据缓冲区)
unsigned char sense_buffer[SENSE_BUFFER_LEN];
memset(sense_buffer, 0, sizeof(sense_buffer));
// 创建 SG_IO 结构
sg_io_hdr_t io_hdr;
memset(&io_hdr, 0, sizeof(sg_io_hdr_t));
io_hdr.interface_id = 'S'; // 使用SCSI通用接口
io_hdr.cmd_len = sizeof(cdb); // CDB的长度
io_hdr.mx_sb_len = sizeof(sense_buffer); // 没有错误返回信息缓冲区
io_hdr.dxfer_direction = SG_DXFER_FROM_DEV; // 数据传输方向:设备到主机
io_hdr.dxfer_len = sizeof(read_buffer); // 数据缓冲区长度
io_hdr.dxferp = read_buffer; // 指向数据缓冲区
io_hdr.cmdp = cdb; // 指向CDB
io_hdr.sbp = sense_buffer; // 指向Sense Buffer
io_hdr.timeout = 5000; // 超时时间:5秒
// 使用 ioctl 发送 SCSI 命令
int status = ioctl(fd, SG_IO, &io_hdr);
if (status < 0) {
std::cerr << "SCSI READ 命令失败: " << strerror(errno) << std::endl;
close(fd);
return 1;
}
// 检查传输状态
if ((io_hdr.info & SG_INFO_OK_MASK) != SG_INFO_OK) {
std::cerr << "SCSI 命令执行错误" << std::endl;
if (io_hdr.sb_len_wr > 0) {
std::cerr << "Sense Data 错误码: ";
print_sense_buffer(sense_buffer, io_hdr.sb_len_wr); // 输出 Sense Buffer
}
close(fd);
return 1;
}
// 输出读取到的数据
std::cout << "成功读取 " << READ_BUFFER_SIZE << " 字节的数据:" << std::endl;
std::cout.write(reinterpret_cast<char*>(read_buffer), READ_BUFFER_SIZE);
std::cout << std::endl;
// 关闭设备
close(fd);
return 0;
}
4.10 错误case
遇到一种错误:
// 检查传输状态
if ((io_hdr.info & SG_INFO_OK_MASK) != SG_INFO_OK) {
std::cerr << "SCSI 命令执行错误" << std::endl;
if (io_hdr.sb_len_wr > 0) {
std::cerr << "Sense Data 错误码: ";
print_sense_buffer(sense_buffer, io_hdr.sb_len_wr); // 输出 Sense Buffer
}
close(fd);
return 1;
}
info返回了错误,但是sb_len_wr没有返回详细的错误信息,在网上搜了一遍后发现错误的原因可以在 /var/log/message 找到些蛛丝马迹。
Sep 20 03:31:13 localhost iscsid[957]: iscsid: Kernel reported iSCSI connection 77:0 error (1002 - ISCSI_ERR_DATA_OFFSET: Seeking offset beyond the size of the iSCSI segment) state (3)
Sep 20 03:31:13 localhost kernel: connection77:0: detected conn error (1002)
Sep 20 03:31:14 localhost kernel: hv_balloon: Balloon request will be partially fulfilled. Balloon floor reached.
Sep 20 03:31:15 localhost iscsid[957]: iscsid: connection77:0 is operational after recovery (1 attempts)
这就是/var/log/message
打印的出错日志。
可能的原因(由chatGpt回答),这里强烈建议多用用人工滞胀,搜索功能还是蛮好用的说:
翻译下:首先上面解释了错误码啥意思,像1002说是越界了,State(3)说是虽然发生了错误,但ISCSI连接还是还是活跃的。
该错误是由 iSCSI 连接中的数据偏移错误引起的,系统尝试访问超出 iSCSI 段可用大小的数据。这可能是由于配置错误、网络问题、数据损坏或软件错误造成的。通过遵循概述的故障排除步骤,可以缩小并解决问题的根本原因。
5 SCSI重要命令
5.1 Move Medium - A5h 移动磁带
在第四节读写磁带数据前有一个必不可少的动作,就是上带。对于磁带库来说有机械臂专门做这个工作,而对于独立驱动器来说就是人工操作。这个命令以后我可能调整下排版,将它放在第四节前面说。
这个命令将会在物理上将磁带从源位置移动到目的位置。磁带库会在合理的范围内重试这个操作,如果失败,会将磁带移回到源位置。移动磁带的设备叫做picker,本文将它称作机械臂。
当源位置和目的位置是相同的,磁带库也会做一次移动操作,Get and Put。
执行这个命令时磁带库会检测源位置是否有磁带和目的位置是否是空,还会检测源和目的元素的兼容性。检测结果失败会返回 Check Condition
。
Move Medium - A5h 是磁带库主要的命令,如果由于一些原因移动失败(如源为空,目的为满,介质不兼容等),磁带状态应该重新被初始化并且同步,这适用于硬件错误和非法请求( hardware errors
and illegal requests
.)。
Move Medium CDB Format
又到了最重要的CDB格式环节,照例放图:
字段 | 描述 |
---|---|
Logical Unit Number | 逻辑单元号 |
Medium Transport Element Address | 机械臂地址 |
Source Element Address | 源地址 |
Destination Element Address | 目的地址 |
其他所有默认置为0,不要问为什么。这个cdb格式很明确,主要问题是如何获取这些地址信息。
5.2 Read Element Status - B8h 获取磁带库地址信息
带库将会返回当前设备元素的状态和信息。这里的元素指的是磁带库的组成部分,如机械臂,槽位,驱动器等。包括未安装的驱动器合磁带盒等,所以对于控制可访问性和异常条件的字段的处理非常重要。如果状态信息可疑,应当用 INITIALIZE ELEMENT STATUS WITH RANGE command 刷新它。
Read Element Status CDB Format
照例老环节,CDB的格式:
字段 | 描述 |
---|---|
Volume Tag (VolTag) | 条形码是否会返回,1返回,0不返回。 |
Element Type Code | 这个表明返回的元素类型,具体看下表。 |
Starting Element Address | 要查询的起始元素地址(2字节,高字节在前) |
Number of Elements | 需要报告的元素的数量(2字节,高字节在前) |
Current Data (CurData) | |
Device ID (DVCID) | 查询的起始元素地址(高字节在前) |
Allocation Length | 分配的缓冲区长度,用于保存返回的数据(高字节在前) |
Element Type Code:
字段 | 描述 |
---|---|
0000b (0) | All element types reported |
0001b (1) | Medium transport element (accessor) |
0010b (2) | Storage element |
0011b (3) | Import/Export element |
0100b (4) | Data transfer element (drives) |
Read Element Status Response
读元素的响应数据。响应数据包含一个8字节响应头,接下来是多个响应页,每个响应页都包含一个页头,然后每个页头对应多个元素描述符。格式大概是:
最多只有4个状态页,每种元素类型对应一个。
Element Status Header format
field | description |
---|---|
First Element Address Reported | This field indicates the lowest element address found that meets the CDB request |
Number of Elements Available | This field indicates the number of elements found that meet the CDB request |
Byte Count of Report Available | This field indicates the number of available element status bytes that meet the CDB requirements. The value does not include the eight-byte element status header, and is not adjusted to match the value specified in the Allocation Length field of the CDB. This facilitates first issuing a READ ELEMENT STATUS command with an allocation length of eight bytes in order to determine the allocation length required to transfer all the element status data specified by the command. |
Byte Count of Report Available : 这个字段返回响应数据的长度,这段话描述的scsi协议的一个机制,它的核心意思是解释如何通过分配长度来获取设备的状态信息。在执行Read Element Status命令时,会返回设备的状态信息,这些信息由若干字节组成,该字段表示有多少字节的状态信息可供读取。在这些状态信息前,有一个8字节的头部,它存储了一些元数据,例如返回信息的总长度,这里明确指出,字段中给出的长度不包含这个8字节头部。cdb中有一个allocation length字段,它告诉应用程序准备了多大的缓冲区来接收数据。这里的描述表明,设备不会自动调整返回的数据大小以适应应用程序设置的缓冲区大小,而是告诉你有多少数据可用。为了确保你知道设备将返回多少字节的状态数据,通常你可以先发送一个分配长度为 8 字节的 READ ELEMENT STATUS 命令。通过这种方式,你可以得到返回数据的头部信息,其中包括状态数据的总长度。这样,你可以根据该信息来分配足够的内存来接收完整的状态数据。
Element Status Page Header
5.2 READ POSITION command
updating…
5.3 Mode Sense (6) - 1Ah
这个命令主要是获取磁带库的相关信息,接下来我会通过这个命令来获取磁带库的驱动器、磁带槽的地址,数量等信息。通过获取的这些信息我们可以使用mode select 命令来设置磁带库的行为。文档中说:强烈建议使用这个命令来初始化磁带库参数用于获取和配置最灵活的磁带库软件。
MODE SENSE CDB format
照样先来看看CDB 的格式:
field | description |
---|---|
Disable Block Descriptors (DBD) | A value of 0 or 1 is supported, although block descriptors are not returned. 文档说的不是很清楚,没太理解,就置为0 了。 |
page control (PC) | 这里固定设置为0,需要其他用法可查看文档。 |
Page Code | 页码,很重要的一个字段,例子中使用0x1d页码。 |
Allocation Length | 给响应数据分配的buffer长度。 |
Mode Parameter Header format for Mode Sense (6)
mode sense的响应数据首先是一个响应头,接下来是响应页。响应头占四个字节:
mode data length的含义是响应数据的长度,这个长度不包含它本身但是包含剩下三字节的保留字段。
Supported Mode Pages
磁带库支持的模式页如下:
我们用0x1d来获取信息。
Element Address Assignment Page (1Dh)
上面是响应数据(模式页)的结构体,元素的数量和地址都用两字节来存储,低位地址存数据高位,高位地址存数据低位,也就是大端存储,而linux系统中数据存储的方式是小端存储,所以这里需要自己转换一下。将每个地址和数量的存放的两个字节交换。
参考文章
浅析SCSI协议 https://blog.csdn.net/anyegongjuezjd/article/details/128999903 ↩︎
探索Linux通用SCSI驱动器 https://www.cnblogs.com/kernel-style/p/3340250.html ↩︎
《SCSI Commands Reference Manual》Fibre Channel (FC)Serial Attached SCSI (SAS) 100293068, Rev. J ↩︎