资讯详情

SCSI数据结构详解:从CDB到Sense Data的存储故障排查手册

📅 2026/10/24 6:02:34 | 华诺云谱 👁 阅读
SCSI数据结构详解:从CDB到Sense Data的存储故障排查手册
如果你调过存储设备报错应该对下面这类日志不陌生sd 2:0:0:0: [sdb] Sense Key : 0x3 (medium error) sd 2:0:0:0: [sdb] ASC0x11 ASCQ0x00很多人的第一反应是“盘坏了换盘”。但 0x03 后面还跟着 0x11 / 0x00这个组合其实是在告诉你读取逻辑块时遇到了未校正的介质错误而不是简单的坏道更不是断电、未就绪或者命令参数填错。要做这种区分就得看懂SCSI协议里最常用的那批数据结构CDB命令块、Sense Data、INQUIRY数据、Mode参数和Log参数。这篇文章就是把常用SCSI数据结构逐字节拆开再配上 Linux 环境下的解析命令和代码片段方便你以后遇到类似问题直接照方抓药。只要你做存储驱动、固件开发、系统底层运维或者只是想把sg3_utils的输出真正看明白这篇都适用。1. 从报错到定位SCSI命令链路里流转的数据结构1.1 命令响应模型的“三张纸”一次 SCSI IO 从发出到结束最核心的交互可以理解成办事窗口。发起端initiator通常是 HBA 驱动或者内核里的 SCSI 中间层它把一个 IO 请求翻译成一条命令描述块也就是 CDB。CDB 里面写的是“做什么、从哪个 LBA 开始、要传多少数据”。目标设备target执行完之后正常情况就返回数据和状态异常情况则回一个 Sense Data告诉你为什么没做成。设备本身长什么样、支持哪些能力则通过 INQUIRY 数据汇报。这三类数据加上用来读写设备参数的 Mode Parameter、用来获取设备自监测信息的 Log Parameter基本构成了日常排查 SCSI 问题时最常用到的协议数据结构。你不需要把整本协议卷标背下来先把手头高频出现的这几类彻底搞懂就已经能解决绝大多数问题。1.2 高频数据结构清单数据结构数据方向典型用途CDB 命令描述块initiator - target描述要执行的操作、LBA、传输长度Sense Datatarget - initiator返回错误原因/异常状态INQUIRY Data / VPDtarget - initiator设备类型、厂商、产品号、稳定标识Mode Parameter双向缓存策略、超时、电源管理等参数读/改Log Parametertarget - initiator自监测日志、温度、重试次数等统计Read Capacity Datatarget - initiator容量、逻辑块大小等信息这里要注意/dev/sda这类块设备节点和/dev/sg0这类通用SCSI节点看到的东西不一样。块设备层会帮你完成命令封装和错误分类而用SG_IOioctl 直接操作的是最原始的命令交互。想理解数据结构最好从后者入手。1.3 传输层变了结构没变SCSI 是一种命令传输协议底层可以走 SAS、FC、iSCSI、USB UAS甚至现在很多 NVMe 设备内部也会做协议翻译对外暴露一套兼容的 SCSI 视图。传输层无论怎么换CDB 的字节布局、Sense Data 的语义、INQUIRY 的字段含义都保持不变。这也是为什么你掌握好这一套结构换到任何存储栈上都还能用。真正变化的是封装的帧、链路层的寻址方式和超时语义这些属于传输层问题不在本文讨论范围内。2. CDB命令块的三代演化与字节级拆解2.1 CDB(6)、CDB(10)、CDB(16) 怎么选CDB 不是只有一个固定长度。常见的有 6 字节、10 字节、16 字节甚至还有 12 字节、32 字节的变种。长度越长能表达的 LBA 位数和字段就越多。CDB(6)历史最久LBA 只有 21 位所以容量很小的命令才用它。多见于 TEST UNIT READY、MODE SENSE(6)、REQUEST SENSE。CDB(10)目前最常见。LBA 是 32 位覆盖绝大多数块设备的寻址需求比如 READ(10)、WRITE(10)、READ CAPACITY(10)。CDB(16)LBA 是 64 位主要面向超大容量的设备和大块 IO比如 READ(16)、WRITE(16)、READ CAPACITY(16)。选哪个长度不是随手定的每个操作码都规定了固定的 CDB 格式。你组装 CDB 时只能严格按照该命令的格式填字段否则目标设备直接返回 Illegal Request也就是 Sense Key 0x05。2.2 READ CAPACITY(10) 的逐字节注释拿最常用的 READ CAPACITY(10) 举例它的操作码是 0x25。CDB 一共 10 字节布局如下字节位/字段说明Byte 0Operation Code 0x25命令码Byte 1Bit4 PMI部分介质指示通常置 0Byte 1Bit7-5 LUN旧式设备可能用它新协议一般由设备自行处理Byte 2-5LBA起始逻辑块地址大端序Byte 6-9保留/控制一般填 0大部分 SCSI 多字节字段都用大端序也就是网络字节序。很多刚上手的人容易在这里栽跟头在 x86 小端环境里直接把0x1000填进缓冲区结果发出去的命令全都乱套。READ CAPACITY(10) 成功后会返回 8 字节数据字段长度说明LBA of last block4 字节最后一个逻辑块地址所以实际块数一般是这个值 1Block length4 字节每个逻辑块的字节长度如果你看到返回值是0xFFFFFFFF说明设备能力已经超过 2^32 - 1 个块你再用这个命令去换算容量就已经失真必须改用 READ CAPACITY(16)通过 SERVICE ACTION IN操作码 0x9E服务动作 0x10读取 64 位容量信息。2.3 用 C 语言组装并发送一条 CDB在用户态直接和 SCSI 设备交互最常用的入口是SG_IOioctl。下面是一段很基础但足够完整的示例向设备发送 TEST UNIT READY命令码 0x00CDB 长度 6 字节。#include stdio.h #include string.h #include fcntl.h #include sys/ioctl.h #include linux/types.h #include scsi/sg.h int main(void) { int fd; unsigned char cdb[6] {0x00, 0, 0, 0, 0, 0}; /* TEST UNIT READY */ unsigned char sense[32]; unsigned char data[36]; struct sg_io_hdr io; fd open(/dev/sg0, O_RDWR); if (fd 0) { perror(open); return 1; } memset(io, 0, sizeof(io)); memset(sense, 0, sizeof(sense)); io.interface_id S; io.dxfer_direction SG_DXFER_FROM_DEV; /* 数据从设备到主机 */ io.cmd_len sizeof(cdb); io.cmdp cdb; io.sbp sense; io.mx_sb_len sizeof(sense); io.dxferp data; io.dxfer_len sizeof(data); io.timeout 5000; /* 毫秒 */ if (ioctl(fd, SG_IO, io) 0) { perror(SG_IO); close(fd); return 1; } /* io.status 为 0 表示 GOOD2 表示 CHECK CONDITION */ printf(host_status%d driver_status%d status%d\n, io.host_status, io.driver_status, io.status); if (io.status 2) { /* 这里就拿到了 Sense Data后文会讲怎么解析 */ printf(sense data received\n); } close(fd); return 0; }struct sg_io_hdr里的几个字段要特别留意dxfer_direction决定 DMA 方向dxferp是数据缓冲区指针dxfer_len必须和 CDB 里写的分配长度一致不然设备返回的数据可能超出你的缓冲区。mx_sb_len是 Sense 缓冲区的最大长度内核会把设备返回的 Sense Data 拷贝到这个 buffer 里。2.4 常用操作码速查表操作码命令名CDB长度用途0x00TEST UNIT READY6查询设备是否就绪0x03REQUEST SENSE6主动读取上次错误状态0x12INQUIRY6读取设备信息和 VPD0x1AMODE SENSE(6)6读取模式参数短格式0x25READ CAPACITY(10)10读取容量、块大小0x28READ(10)10读数据0x2AWRITE(10)10写数据0x35SYNCHRONIZE CACHE(10)10刷缓存到介质0x4DLOG SENSE10读取日志页0x5AMODE SENSE(10)10读取模式参数长格式0x88READ(16)1664 位 LBA 读数据0x8AWRITE(16)1664 位 LBA 写数据0x9ESERVICE ACTION IN16通过服务动作读扩展数据如 READ CAPACITY(16)这张表覆盖了我平时排查问题时的九成使用场景。真正写驱动时你还会遇到 UNMAP、WRITE SAME、COMPARE AND WRITE 这类命令但结构思路完全一致。3. 两种Sense格式与解码固定格式和描述符格式不能混读3.1 第一个字节是总钥匙Sense Data 最迷惑人的地方在于它有两种格式固定格式Fixed Format和描述符格式Descriptor Format。判断方法就是看第一个字节的低 7 位响应码格式含义0x70固定格式当前错误0x71固定格式延迟错误Deferred Error0x72描述符格式当前错误0x73描述符格式延迟错误固定格式里Sense Key 在 Byte 2ASC 在 Byte 12ASCQ 在 Byte 13。描述符格式完全不同Sense Key 在 Byte 1ASC/ASCQ 不固定而是分散在后面的不同描述符里最常见的 0x80 信息描述符会包含 LBA 信息0x00 描述符则包含 ASC 和 ASCQ。有些人用固定格式的偏移去解析描述符格式结果取出来的 Key 和 ASC 全是乱的。所以拿到 Sense Data 的第一步永远是读 Byte 0先确认格式再往下解析。3.2 Sense Key 和 ASC/ASCQ 组合起来才有意义单独看 Sense Key 只能知道错误大类真正定位要靠 ASC/ASCQ 的组合。下面是我工作中经常遇到的组合Sense KeyASCASCQ常见含义0x02 Not Ready0x040x01逻辑单元正在准备中稍后再试0x02 Not Ready0x040x02需要初始化命令才能就绪0x03 Medium Error0x110x00未校正的读取错误0x05 Illegal Request0x200x00非法命令操作码0x05 Illegal Request0x210x00逻辑块地址越界0x06 Unit Attention0x290x00设备刚刚被复位0x06 Unit Attention0x3F0x00目标设备运行条件发生变化0x0B Aborted Command0x470x00SCSI 总线奇偶校验错误打个比方Sense Key 相当于“文件打不开”这个大分类ASC 相当于“原因是权限不足”ASCQ 则再细分为“是明确拒绝还是没有这个权限”。三个字段合起来才是完整的事故原因。3.3 用内核工具和 sg3_utils 解码如果你在写内核驱动scsi_sense_hdr是你最好的朋友。内核提供了现成的解析函数不需要自己手工搬字节#include scsi/scsi_common.h struct scsi_sense_hdr ssh; unsigned char sense_buffer[SCSI_SENSE_BUFFERSIZE]; if (scsi_normalize_sense(sense_buffer, SCSI_SENSE_BUFFERSIZE, ssh)) { printk(key0x%x asc0x%x ascq0x%x\n, ssh.sense_key, ssh.asc, ssh.ascq); }用户态排查时sg3_utils工具包里的sg_decode_sense可以直接把原始 Sense 字节翻译成人话sg_decode_sense 70 00 03 00 00 00 00 0a 00 00 00 00 11 00 00 00它会把这一串字节解析成类似Fixed format, current; Sense key: Medium Error Additional sense: Read error - unrecovered这种方式比对着协议表格吃力地翻快得多。建议排查时把 dmesg 里打印的原始 Sense 字节完整保留下来再丢给sg_decode_sense比看内核已经格式化好的文案信息量更大因为它保留了 ASCQ、命令特定信息甚至 LBA 信息。4. 读透INQUIRY与VPD设备“身份证”和稳定标识4.1 标准INQUIRY数据的关键字段INQUIRY 命令操作码是 0x12。不带 VPD 时它会返回标准 INQUIRY 数据默认 36 字节不上不下的字段特别多但实际用到的就那么几个字节名称说明Byte 0Peripheral Qualifier / Device Type高 3 位是设备限定符低 5 位是设备类型Byte 1RMB / VersionBit7 表示是否可移动介质Byte 6-7Version Descriptor支持的标准版本描述Byte 7 实际是 Additional Length附加长度通常为 31表示后面还有 31 字节Byte 8-15Vendor Identification厂商标识 ASCIIByte 16-31Product Identification产品型号 ASCIIByte 32-35Product Revision Level固件版本 ASCII设备类型中最常见的是 0x00 直接块设备磁盘、0x01 顺序访问设备磁带、0x05 CD/DVD 设备。看到0x1F表示未知或设备不支持该类型。在 Linux 下最简单的查看方式就是sg_inq /dev/sda lsscsilsscsi输出里那串ATA XXX就是 INQUIRY 数据里的 Vendor 和 Product ID 拼接出来的虽然经过 SAT 翻译但底层依然来自 INQUIRY。4.2 VPD 0x83拿到真正稳定的设备标识标准 INQUIRY 只能告诉你设备“叫什么”不能告诉你设备“是谁”。同一个型号的多块盘光靠 Product ID 根本分不清。这时候要让 INQUIRY 带 EVPD 位并指定页代码。VPD 页里面最重要的几个页代码名称用途0x00Supported VPD Pages查看设备支持哪些 VPD 页0x80Unit Serial Number序列号0x83Device Identification设备标识符含 WWN、NAA、EUI-640x86Block Limits最大/最小 IO 限制0x89ATA InformationATA 命令日志现代 SATA SSD 很关键0xB1Block Device Characteristics块设备特性0x83 页是最重要的。它里面包含一个或多个 Designator每个 Designator 有 Association 和 Designator Type常见类型包括Type 2EUI-64Type 3NAA其中 NAA 5 和 NAA 6 就是常说的 WWNType 8SCSI 名称字符串多路径环境里盘符一直漂移但 VPD 0x83 的 WWN 永远固定。用lsscsi -w就能看到类似0x5000c500xxxx的输出这就是从 VPD 0x83 里读出来的。做存储绑定、故障替换、容灾切换时认准这个标识永远比认/dev/sdX可靠。4.3 自己解析INQUIRY的一段简单代码上面的 C 示例主体不用改只需要改 CDB 部分。下面这个片段演示怎样发一条 EVPD 的 INQUIRY分别页代码 0x83unsigned char cdb[6] {0x12, 1, 0x83, 0, 0, 0}; /* Byte0: opcode * Byte1: bit0EVPD * Byte2: page code 0x83 * Byte3-4: allocation length * Byte5: control */这里的 allocation length 要注意。INQUIRY CDB 的分配长度是 16 位但 Byte3 是低字节、Byte4 是高字节。分配长度不够时设备就只返回前面能放得下的部分。解析的时候要先读 Byte 0 确认 Peripheral 类型再跳过固定长度的头部接着按 Designator 的长度逐个遍历。5. Mode参数与Log页控制缓存、温度与自监测数据5.1 Mode Parameter 的三层包裹结构Mode Sense 返回的数据不是简单的键值对而是一个三层结构Mode Parameter Header、Block Descriptor、Mode Page。Header 的作用是告诉你整个返回体的长度和后面 Block Descriptor 有多长解析时必须先把这段长度读完然后老老实实跳过 Block Descriptor才能找到真正的 Mode Page。MODE SENSE(6) 命令码 0x1A 是短格式MODE SENSE(10) 命令码 0x5A 是长格式。两者的 CDB 结构类似字段MODE SENSE(6)MODE SENSE(10)操作码0x1A0x5AByte1DBD 位控制是否关闭块描述符DBD 位Byte2PC 页控制 页代码PC 页代码Byte3子页代码子页代码Byte4分配长度分配长度低字节Byte5控制分配长度高字节DBD 位很有用。块描述符在大多数场景下派不上用场设置 DBD1 后返回数据里不包含 Block Descriptor解析模式页可以少跨一层。5.2 缓存模式页 0x08写缓存和读缓存的开关对块设备来说最常被人翻出来改的 Mode Page 是 0x08也就是缓存页。它包含写缓存使能WCE、读缓存禁用RCD等一堆布尔位。sg_modes -p 8 /dev/sda这条命令会把缓存页内容列出来。如果设备支持写缓存你会看到 WCE 位是 1如果某些环境下要保证掉电安全性你觉得链路层已经足够稳定可以考虑关掉 WCE。但我一般只建议在彻底理解了设备掉电行为之后再去动它。很多盘在关闭写缓存后性能会断崖式下降而且一些虚拟化环境根本不给你透传这个能力改了等于白改。MODE SELECT 写参数时更要谨慎。参数页里的字段并不是每次都能成功改变设备可能因为介质保护、密钥校验这些原因拒绝。所以每次 MODE SELECT 之后都应该立刻用 MODE SENSE 读回来核对再基于读回结果继续后续判断。5.3 LOG SENSE温度、坏块计数和自监测数据LOG SENSE 命令码 0x4D用来读取设备的日志页。和 Mode 参数不同Log 参数主要是设备自己记录下来的运行统计和自监测数据。Linux 下直接看最常用的几个页sg_logs -p 0x0d /dev/sda # 温度页 sg_logs -p 0x2f /dev/sda # 容量页温度页 0x0D 会返回当前温度、参考温度、最高工作温度等。在磁盘阵列巡检里这个信息比 SMART 的某些原始值更直接。很多 SSD 还会通过 Log 页返回磨损均衡计数、重映射事件这类数据。解析 Log 页的套路和 Mode 类似先看 CDB 里的页代码和分配长度返回体头部是页代码和页长度后面是参数描述符。每个参数描述符有自己的参数代码、参数长度和格式解析时务必用参数描述符里的长度字段去跳转不要假设所有描述符等长。6. 串起来的一套调试组合拳SG_IO、内核结构与避坑清单6.1 内核里对应的两个核心结构如果你在做内核驱动或底层存储模块除了协议结构还要熟悉内核里两个核心的 C 结构体。struct scsi_cmnd是 SCSI 命令在 Linux 内核中的活体载体。它里面有cmnd数组存的就是上面说的 CDBsense_buffer存的就是 Sense Data还有SCp用来记录当前数据传输进度。块层提交下来的请求经过调度后最终会被封装成scsi_cmnd所以驱动里一般不手工组装 CDB而是处理scsi_cmnd里已经填好的字段。struct scsi_device对应一个物理 LUN里面有sdev_target、scsi_level、queue_depth这些与设备能力相关的信息。INQUIRY 返回的数据会被解析后塞进这个结构驱动读取sdev-type、sdev-vendor、sdev-model就能知道设备类型。6.2 一次排查的标准动作我在环境里排查 SCSI 问题时通常按下面的顺序走一遍基本上能把问题范围缩到很小先用lsscsi -g确认设备在 SCSI 层的映射关系记下 sg 节点号和盘符。用sg_inq /dev/sgX拿标准 INQUIRY 和 VPD 0x83确认设备的真实身份。用sg_readcap -l /dev/sgX看容量和块大小同时确认是否需要走 16 字节 CDB。如果设备报 IO 错误把 dmesg 里完整的 Sense 字节抓下来丢给sg_decode_sense翻译。根据翻译结果去查 ASC/ASCQ 组合而不是只看 Sense Key。这一套动作花不到两分钟但能避免很多无意义的换盘、重启、拔插。我见过不少所谓“坏盘”最后查出来是中间链路信号质量导致的ABORTED COMMAND或者多路径配置漂移导致的UNIT ATTENTION根本不用动硬件。6.3 绕开这些坑能省一个星期的排查时间第一个坑是字节序。SCSI 所有多字节字段都是大端序和网络字节序一致。构造 CDB 时别直接把小端值塞进去除了数据缓冲区本身所有 LBA、长度字段都必须是大端。第二个坑是分配长度和实际数据长度。INQUIRY、MODE SENSE、LOG SENSE 的 CDB 里都写了分配长度但设备返回的数据可能比分配长度短也可能刚好等于分配长度。always 只解析返回数据里的实际长度字段不要想当然。比如标准 INQUIRY 数据不是每次都 36 字节某些老设备返回的附加长度可能比 31 小。第三个坑是 Sense 格式混读。前面反复强调过看到 Byte 0 是 0x72 或 0x73 就必须走描述符解析逻辑别再用固定格式的偏移直接读 ASC、ASCQ。内核里的scsi_normalize_sense已经帮你处理了这些差异别自己手搓一个只适配固定格式的解析函数。第四个坑是残余长度。SG_IO返回结构里有resid字段表示设备实际传输的数据量和dxfer_len之间差多少。比如分配了 64 字节设备只回了 40 字节resid就是 24。解析数据时要用dxfer_len - resid得到真正的数据长度。第五个坑是不要把块设备节点和 sg 节点搞混。用块设备节点发出的 CDB 会经过内核块层、设备映射、IO 调度器你看到的时序和命令未必是设备侧真正收到的。想抓最原始的协议交互直接用/dev/sgX和SG_IO或者开启 ULP 层的 trace才能看到完整命令。最后分享一个我自己养成的小习惯遇到 SCSI 上报错第一件事不是去看内核把 Sense 翻译成了什么煽情文案而是先把原始 Sense 字节完整打印出来响应码扫一眼再对应 Key 和 ASC/ASCQ。这个习惯帮我少走了很多弯路也希望你在摸熟这些结构之后能形成自己的一套排查节奏。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑