基于 Linux Kernel 源码深度分析 路径:
drivers/scsi/|include/scsi/|drivers/ata/
- SCSI 子系统概述
- 核心数据结构
- SCSI 命令生命周期
- SCSI 中间层
- 错误处理与恢复
- libata(SATA/PATA)
- SCSI 设备类型驱动
- 多路径(DM-Multipath)
- SAS/iSCSI/NVMe-oF
- drivers/scsi/ 目录主要文件一览
- 调试工具
- SCSI 架构层次深度解析:ULD、midlayer、HBA 驱动
- scsi_host 与 scsi_device 数据结构详解
- SCSI 命令生命周期深度解析
- 错误恢复机制(EH)深度解析
- SCSI 队列深度管理与 Tagged Command Queuing
- libsas 框架深度解析
- NVMe over Fabrics:nvme-tcp 与 nvme-rdma
- SCSI Multipath 与 scsi_dh 深度解析
- UFS(Universal Flash Storage)子系统深度解析
- SCSI 压力测试与故障注入
- 附录:关键代码定位速查
Linux 存储栈是一个层次分明的软件架构,SCSI 子系统位于块层(Block Layer)与底层硬件驱动之间,承担着协议翻译、队列管理和错误恢复的核心职责。
+------------------------------------------------------------------+
| 用户空间 (User Space) |
| open() / read() / write() / ioctl() |
+------------------------------------------------------------------+
|
+------------------------------------------------------------------+
| VFS / Page Cache / Direct I/O |
+------------------------------------------------------------------+
|
+------------------------------------------------------------------+
| 块设备层 (Block Layer / blk-mq) |
| bio --> request_queue --> blk_mq_hw_ctx --> scsi_mq_ops |
| 标签队列、IO 调度(mq-deadline、kyber、none) |
+------------------------------------------------------------------+
|
+------------------------------------------------------------------+
| SCSI 中间层 (SCSI Mid-Layer) |
| scsi_queue_rq() --> scsi_dispatch_cmd() --> queuecommand() |
| 错误处理线程 (scsi_error_handler)、设备扫描、命令超时 |
+------------------------------------------------------------------+
/ \
+------------------+ +----------------------------+
| SCSI LLD 驱动 | | libata (ATA 转 SCSI) |
| (MPT、Megasas 等) | | ata_scsi_queuecmd() |
+------------------+ +----------------------------+
| |
+------------------+ +----------------------------+
| SAS HBA / FC | | AHCI/SATA 控制器 |
+------------------+ +----------------------------+
\ /
+------------------------------------------------------------------+
| 物理存储介质 |
| HDD / SSD / SAS / SATA / ATAPI / NVMe |
+------------------------------------------------------------------+
SCSI 子系统可以分为三个层次:
| 层次 | 代码位置 | 主要职责 |
|---|---|---|
| 上层驱动(ULD) | drivers/scsi/sd.c、sr.c、st.c、sg.c |
设备类型驱动,与块层/字符设备接口 |
| SCSI 中间层 | drivers/scsi/scsi_lib.c、scsi_error.c |
队列管理、错误恢复、blk-mq 集成 |
| 低层驱动(LLD / HBA) | drivers/scsi/hpsa.c 等;drivers/ata/ |
控制器硬件操作 |
SCSI 标准(Small Computer System Interface)起源于 1986 年,最初为并行总线设计。Linux SCSI 子系统从早期的单队列模式,历经 blk-mq 改造(Linux 3.13+),全面拥抱多队列,并通过 libata 将 SATA/PATA 设备统一纳入 SCSI 框架管理,形成当前的架构。
文件:include/scsi/scsi_host.h,第 42 行起
scsi_host_template 是 HBA 驱动向 SCSI 中间层注册的函数表(ops),类似于面向对象的"类定义":
struct scsi_host_template {
/* IO 热路径字段(同一 cacheline)*/
unsigned int cmd_size; /* 驱动私有数据大小(跟随 scsi_cmnd) */
/* 必选:命令入队回调 */
enum scsi_qc_status (*queuecommand)(struct Scsi_Host *, struct scsi_cmnd *);
/* 可选:预留命令队列 */
enum scsi_qc_status (*queue_reserved_command)(...);
/* 可选:批量提交通知(硬件 doorbell) */
void (*commit_rqs)(struct Scsi_Host *, u16);
/* 错误恢复回调(至少需要一个)*/
int (*eh_abort_handler)(struct scsi_cmnd *);
int (*eh_device_reset_handler)(struct scsi_cmnd *);
int (*eh_target_reset_handler)(struct scsi_cmnd *);
int (*eh_bus_reset_handler)(struct scsi_cmnd *);
int (*eh_host_reset_handler)(struct scsi_cmnd *);
/* 队列深度与限制 */
int can_queue; /* 单个 HW 队列最大并发命令数 */
int nr_reserved_cmds; /* 保留命令数量 */
short cmd_per_lun; /* 每个 LUN 的命令数 */
/* 散列表(SG)限制 */
unsigned short sg_tablesize;
unsigned int max_sectors;
/* blk-mq 相关 */
unsigned host_tagset:1; /* 全 host 共享 tagspace */
unsigned queuecommand_may_block:1; /* queuecommand 可能睡眠 */
...
};
关键字段说明:
queuecommand:必须实现,SCSI 中间层通过它把scsi_cmnd推送给 HBA 驱动(include/scsi/scsi_host.h:87)eh_*_handler:错误恢复回调,代表不同粒度的 reset 操作,至少实现其中一个host_tagset:置 1 时,所有硬件队列共享同一套 tag,用于 NVMe 风格控制器
文件:include/scsi/scsi_host.h,第 558 行起
每个物理或虚拟 HBA 对应一个 Scsi_Host 实例:
struct Scsi_Host {
struct list_head __devices; /* 该 host 下所有 scsi_device 链表 */
struct list_head __targets; /* scsi_target 链表 */
struct list_head starved_list; /* 因资源不足而被饿死的设备列表 */
spinlock_t *host_lock; /* 主锁 */
struct mutex scan_mutex; /* 扫描互斥 */
/* 错误恢复相关 */
struct list_head eh_abort_list; /* 待 abort 的命令 */
struct list_head eh_cmd_q; /* EH 命令队列 */
struct task_struct *ehandler; /* EH 内核线程(scsi_error_handler) */
struct completion *eh_action;
const struct scsi_host_template *hostt;
struct scsi_transport_template *transportt;
/* blk-mq tag set(scsi_mq_setup_tags 初始化)*/
struct blk_mq_tag_set tag_set;
/* 队列参数 */
int can_queue; /* 每队列最大并发数 */
unsigned int nr_hw_queues; /* 硬件队列数量 */
unsigned int host_no; /* 唯一编号,对应 /proc/scsi/scsi 中的序号 */
/* 状态机 */
enum scsi_host_state shost_state; /* CREATED/RUNNING/RECOVERY/DEL 等 */
unsigned long hostdata[]; /* LLD 私有数据(对齐到 unsigned long) */
};
状态机(include/scsi/scsi_host.h:548):
SHOST_CREATED --> SHOST_RUNNING --> SHOST_CANCEL --> SHOST_DEL
|
+--> SHOST_RECOVERY --> SHOST_RUNNING
|
+--> SHOST_CANCEL_RECOVERY --> SHOST_DEL_RECOVERY
文件:include/scsi/scsi_device.h,第 103 行起
每个 LUN(Logical Unit Number)对应一个 scsi_device:
struct scsi_device {
struct Scsi_Host *host; /* 归属 host */
struct request_queue *request_queue; /* 关联的块层队列 */
/* 识别信息 */
unsigned int id, channel;
u64 lun;
unsigned char type; /* TYPE_DISK / TYPE_ROM / TYPE_TAPE 等 */
char scsi_level; /* SCSI 版本(SCSI_2 / SCSI_SPC_2 等)*/
/* INQUIRY 数据 */
unsigned char *inquiry;
const char *vendor, *model, *rev;
struct scsi_vpd __rcu *vpd_pg83; /* Device ID VPD 页(用于多路径) */
/* 队列管理 */
struct sbitmap budget_map; /* 队列深度预算位图 */
unsigned short queue_depth; /* 当前队列深度 */
unsigned short max_queue_depth; /* 最大允许队列深度 */
atomic_t device_blocked; /* QUEUE_FULL 时的 backoff 计数 */
/* 设备标志位(大量 bit field,来自 INQUIRY 结果或黑名单) */
unsigned tagged_supported:1; /* 是否支持 tagged queuing */
unsigned simple_tags:1; /* 是否启用 simple queue tag */
unsigned is_ata:1; /* 是否为 ATA 设备(通过 libata) */
unsigned removable:1; /* 可移除介质 */
unsigned use_10_for_rw:1; /* 优先使用 10 字节读写命令 */
/* 状态机 */
enum scsi_device_state sdev_state; /* CREATED/RUNNING/QUIESCE/OFFLINE 等 */
/* 统计计数器 */
atomic_t iorequest_cnt;
atomic_t iodone_cnt;
atomic_t ioerr_cnt;
atomic_t iotmo_cnt; /* 超时计数 */
unsigned long sdev_data[]; /* 供传输层/驱动存储私有数据 */
};
设备状态机(include/scsi/scsi_device.h:38):
SDEV_CREATED --> SDEV_RUNNING --> SDEV_QUIESCE --> SDEV_OFFLINE
|
+--> SDEV_BLOCK --> SDEV_RUNNING(解块后)
|
+--> SDEV_CANCEL --> SDEV_DEL
文件:include/scsi/scsi_cmnd.h,第 74 行起
scsi_cmnd 是 SCSI 命令的核心载体,嵌入在 blk-mq 的 request 的 PDU 区域(通过 blk_mq_rq_to_pdu / blk_mq_rq_from_pdu 互相转换):
struct scsi_cmnd {
struct scsi_device *device; /* 目标设备 */
struct list_head eh_entry; /* EH 队列链接 */
struct delayed_work abort_work; /* 延迟 abort 工作队列 */
int budget_token; /* 队列预算 token(对应 sbitmap 槽位)*/
int retries; /* 已重试次数 */
int allowed; /* 最大允许重试次数 */
unsigned short cmd_len; /* CDB 长度 */
enum dma_data_direction sc_data_direction; /* DMA 方向 */
unsigned char cmnd[32]; /* SCSI CDB(命令描述符块)*/
/* 数据传输 */
struct scsi_data_buffer sdb; /* 主数据 SG 表 */
struct scsi_data_buffer *prot_sdb; /* DIF/DIX 保护信息 SG 表 */
unsigned resid_len; /* 残余长度(未传输) */
/* sense 数据 */
unsigned char *sense_buffer; /* 96 字节 sense 缓冲区 */
unsigned sense_len;
int flags; /* SCMD_TAGGED / SCMD_INITIALIZED / SCMD_LAST 等 */
unsigned long state; /* SCMD_STATE_COMPLETE / SCMD_STATE_INFLIGHT */
/* 完成结果(高 16 位 host_byte,低 8 位 status_byte)*/
int result;
unsigned char *host_scribble; /* LLD 私有指针(可选) */
};
result 字段编码:
bit[31:24] bit[23:16] bit[15:8] bit[7:0]
(unused) host_byte (unused) status_byte
host_byte: DID_OK / DID_ERROR / DID_ABORT / DID_RESET / DID_NO_CONNECT ...
status_byte: SAM_STAT_GOOD / SAM_STAT_CHECK_CONDITION / SAM_STAT_BUSY ...
Scsi_Host (shost)
|-- hostt --> scsi_host_template (驱动 ops 函数表)
|-- tag_set --> blk_mq_tag_set (blk-mq 集成点)
|-- ehandler --> task_struct (EH 内核线程)
|-- __devices --> scsi_device --> scsi_device --> ...
|
|-- request_queue (块层队列)
|-- sdev_target --> scsi_target
| |-- id, channel
| `-- siblings --> scsi_device ...
`-- sdev_data[] (传输层私有)
scsi_cmnd (嵌入在 request PDU 中)
|-- device --> scsi_device
|-- cmnd[32] (SCSI CDB)
|-- sdb.table.sgl --> scatterlist[] (DMA 传输)
`-- sense_buffer[96]
用户空间 write()/read()
|
v
VFS / Page Cache
|
v
bio 构建(bio_alloc / submit_bio)
|
v
blk-mq 层
request_queue --> blk_mq_hw_ctx
[IO 调度器:mq-deadline / kyber / none]
|
v scsi_mq_ops.get_budget() -- 从 budget_map 获取 token
v scsi_mq_ops.queue_rq() -- 入口:scsi_queue_rq()
|
+-- scsi_mq_get_budget() 检查设备队列深度预算
+-- scsi_device_state_check() 检查设备状态
+-- scsi_target_queue_ready() 检查 target 是否就绪
+-- scsi_host_queue_ready() 检查 host 是否就绪
+-- scsi_prepare_cmd() 由 ULD 填写 CDB(sd_init_command)
|
v
scsi_dispatch_cmd()
[drivers/scsi/scsi_lib.c:1589]
|
+-- 检查设备状态(SDEV_DEL / SDEV_BLOCK)
+-- 检查 CDB 长度 vs max_cmd_len
+-- trace_scsi_dispatch_cmd_start()
|
v hostt->queuecommand(host, cmd)
| [LLD 接收命令,提交给 HBA 硬件]
|
v (异步:硬件中断/MSI)
|
LLD 调用 scsi_done(cmd) 或 scsi_done_direct(cmd)
[include/scsi/scsi_cmnd.h:161]
|
v scsi_done_internal()
| --> blk_mq_complete_request() [软中断回调]
|
v scsi_complete()
[drivers/scsi/scsi_lib.c:1541]
|
+-- scsi_decide_disposition() 判断结果
| |-- SUCCESS --> scsi_finish_command()
| |-- NEEDS_RETRY --> scsi_queue_insert() 重新入队
| |-- ADD_TO_MLQUEUE --> 暂时退回设备队列
| `-- 其他(失败) --> scsi_eh_scmd_add() 交给 EH 线程
|
v scsi_finish_command()
| --> ULD 的 done() 回调(sd_done)
| --> blk_mq_end_request() 通知块层完成
|
v
bio complete / page writeback done
blk-mq 的 queue_rq 回调,是 SCSI 中间层的命令入口:
- 调用
scsi_mq_get_budget从sdev->budget_map申请队列预算 token(基于sbitmap,无锁实现) - 检查设备/target/host 三层就绪状态,任一不满足则返回
BLK_STS_RESOURCE - 调用
scsi_prepare_cmd,进而调用 ULD 的init_command(如sd_init_command)填充 CDB - 设置
SCMD_TAGGED标志(若设备支持 tagged queueing) - 调用
scsi_dispatch_cmd将命令推送至 LLD
static enum scsi_qc_status scsi_dispatch_cmd(struct scsi_cmnd *cmd)
{
// 检查设备是否已被删除
if (unlikely(cmd->device->sdev_state == SDEV_DEL)) { ... }
// 将 LUN 写入 CDB[1](旧协议兼容)
if (cmd->device->lun_in_cdb)
cmd->cmnd[1] = ...;
// 调用 LLD 的 queuecommand
rtn = host->hostt->queuecommand(host, cmd);
...
}LLD 完成命令后调用 scsi_done(cmd),经由软中断进入 scsi_complete:
static void scsi_complete(struct request *rq)
{
disposition = scsi_decide_disposition(cmd);
switch (disposition) {
case SUCCESS: scsi_finish_command(cmd); break;
case NEEDS_RETRY: scsi_queue_insert(cmd, SCSI_MLQUEUE_EH_RETRY); break;
case ADD_TO_MLQUEUE: scsi_queue_insert(cmd, SCSI_MLQUEUE_DEVICE_BUSY); break;
default: scsi_eh_scmd_add(cmd); /* 送入 EH */ break;
}
}SCSI 中间层通过 scsi_mq_ops 结构与 blk-mq 对接(drivers/scsi/scsi_lib.c:2054):
static const struct blk_mq_ops scsi_mq_ops_no_commit = {
.get_budget = scsi_mq_get_budget, /* 申请队列预算 */
.put_budget = scsi_mq_put_budget, /* 释放队列预算 */
.queue_rq = scsi_queue_rq, /* 命令入队(热路径)*/
.complete = scsi_complete, /* 命令完成回调 */
.timeout = scsi_timeout, /* 超时处理 */
.init_request = scsi_mq_init_request, /* 初始化 scsi_cmnd */
.exit_request = scsi_mq_exit_request,
.busy = scsi_mq_lld_busy, /* 查询 LLD 是否忙碌 */
.map_queues = scsi_map_queues, /* CPU 到 HW queue 映射 */
.poll = scsi_mq_poll, /* 轮询完成(for HIPRI) */
};
/* 若 LLD 实现了 commit_rqs,使用带 commit 的版本 */
static const struct blk_mq_ops scsi_mq_ops = {
...
.commit_rqs = scsi_commit_rqs, /* 批量提交通知(硬件 doorbell)*/
};tag set 初始化(scsi_mq_setup_tags,drivers/scsi/scsi_lib.c:2103):
cmd_size=sizeof(scsi_cmnd) + hostt->cmd_size + SG 内联空间- blk-mq 为每个
request分配固定大小的 PDU 区域,scsi_cmnd嵌入其中
Tagged Queueing 允许 SCSI 设备同时处理多条命令,每条命令附带一个 tag 号:
scsi_device.tagged_supported = 1 (INQUIRY 数据中 bit 确认)
scsi_device.simple_tags = 1 (启用 simple queue tag)
scsi_queue_rq() 中:
if (sdev->simple_tags)
cmd->flags |= SCMD_TAGGED; /* 标记命令使用 tagged mode */
队列深度管理:
scsi_device.queue_depth:当前实际队列深度scsi_device.max_queue_depth:上限scsi_device.budget_map:用sbitmap实现的无锁 token 分配QUEUE_FULL响应会触发scsi_track_queue_full,动态降低队列深度,之后通过queue_ramp_up_period逐步恢复
三层队列控制:
host 级别: Scsi_Host.can_queue + host_blocked
target 级别:scsi_target.can_queue + target_blocked
device 级别:scsi_device.queue_depth + device_blocked (via budget_map)
scsi_set_blocked(drivers/scsi/scsi_lib.c:79)根据 LLD 返回的拒绝原因设置退避计数:
switch (reason) {
case SCSI_MLQUEUE_HOST_BUSY:
atomic_set(&host->host_blocked, host->max_host_blocked);
break;
case SCSI_MLQUEUE_DEVICE_BUSY:
atomic_set(&device->device_blocked, device->max_device_blocked);
break;
case SCSI_MLQUEUE_TARGET_BUSY:
atomic_set(&starget->target_blocked, starget->max_target_blocked);
break;
}退避计数不为 0 时,blk-mq 的 busy 回调(scsi_mq_lld_busy)阻止新命令入队,直至计数归零。
SCSI 错误处理(Error Handling,EH)是整个子系统中最复杂的部分,采用专属内核线程 + 多级 reset 层次的设计。
每个 Scsi_Host 对应一个 EH 内核线程(scsi_error_handler),在 host 注册时由 scsi_add_host_with_dma 创建:
/* drivers/scsi/scsi_error.c:2342 */
int scsi_error_handler(void *data)
{
struct Scsi_Host *shost = data;
while (true) {
set_current_state(TASK_INTERRUPTIBLE);
if (kthread_should_stop()) break;
/* 等待条件:所有 busy 命令都失败了 */
if (shost->host_failed != scsi_host_busy(shost)) {
schedule();
continue;
}
/* 执行恢复 */
if (shost->transportt->eh_strategy_handler)
shost->transportt->eh_strategy_handler(shost);
else
scsi_unjam_host(shost); /* 默认恢复策略 */
shost->host_failed = 0;
scsi_restart_operations(shost); /* 重启 IO */
}
}命令超时(blk-mq timeout)
--> scsi_timeout()
--> scsi_abort_command() [调度延迟 abort work]
--> scmd_eh_abort_handler() [工作队列中执行]
|
+-- scsi_try_to_abort_cmd() 成功 --> 重试或完成
|
`-- 失败 --> scsi_eh_scmd_add()
--> wake_up_process(shost->ehandler)
命令失败(非超时,如 CHECK_CONDITION):
scsi_complete()
--> scsi_decide_disposition() 返回非 SUCCESS/RETRY
--> scsi_eh_scmd_add()
--> scsi_eh_wakeup() [drivers/scsi/scsi_error.c:64]
--> wake_up_process(shost->ehandler)
scsi_unjam_host 调用 scsi_eh_ready_devs,按升级顺序尝试恢复(drivers/scsi/scsi_error.c:1048):
Level 1: 命令 Abort(eh_abort_handler)
| 失败
v
Level 2: 设备(LUN)Reset(eh_device_reset_handler)
| 失败
v
Level 3: Target Reset(eh_target_reset_handler)
| 失败
v
Level 4: 总线 Reset(eh_bus_reset_handler) 等待 BUS_RESET_SETTLE_TIME=10s
| 失败
v
Level 5: Host Reset(eh_host_reset_handler) 等待 HOST_RESET_SETTLE_TIME=10s
| 失败
v
Level 6: 设备下线(offline),停止向该设备发送 IO
对应代码(drivers/scsi/scsi_error.c:1048):
static void scsi_abort_eh_cmnd(struct scsi_cmnd *scmd)
{
if (scsi_try_to_abort_cmd(...) != SUCCESS)
if (scsi_try_bus_device_reset(scmd) != SUCCESS)
if (scsi_try_target_reset(scmd) != SUCCESS)
if (scsi_try_bus_reset(scmd) != SUCCESS)
scsi_try_host_reset(scmd);
}CHECK_CONDITION 状态下,EH 通过 REQUEST SENSE 命令获取 sense data(scsi_eh_prep_cmnd,drivers/scsi/scsi_error.c:1071)。Sense key 决定处置方式:
| Sense Key | 含义 | 典型处置 |
|---|---|---|
NO_SENSE (0x0) |
无错误 | 成功 |
RECOVERED_ERROR (0x1) |
设备自行恢复 | 成功 + 警告 |
NOT_READY (0x2) |
设备未就绪 | 等待后重试 |
MEDIUM_ERROR (0x3) |
介质错误 | 有限重试 |
HARDWARE_ERROR (0x4) |
硬件错误 | 触发 EH |
UNIT_ATTENTION (0x6) |
设备重置/介质变更 | 重试 |
ILLEGAL_REQUEST (0x5) |
非法命令 | 失败,不重试 |
ABORTED_COMMAND (0xb) |
命令被 abort | 重试 |
Scsi_Host.eh_deadline(默认 -1 无限制)可设置 EH 最大允许时间。超出 deadline 后,scsi_host_eh_past_deadline 返回 true,EH 跳过耗时操作(如总线 reset),直接使设备下线。
libata 是将 ATA/ATAPI 设备伪装成 SCSI 设备的兼容层,使 ATA 磁盘可以通过标准 SCSI 接口被 sd、sr 等驱动管理。
sd.c (SCSI disk ULD)
|
| (SCSI 命令,如 READ(10))
v
ata_scsi_queuecmd() [drivers/ata/libata-scsi.c:4502]
|
+-- ata_scsi_find_dev() 根据 scsi_device 找到 ata_device
+-- __ata_scsi_queuecmd() [libata-scsi.c:4427]
|
+-- ATA 设备:ata_get_xlat_func() 选择翻译函数
| READ(10/16) --> ata_read_translate()
| WRITE(10/16) --> ata_write_translate()
| INQUIRY --> ata_scsi_inquiry_response()(模拟)
|
+-- ATAPI 设备:直接透传 SCSI CDB
|
v ata_scsi_translate(dev, scmd, xlat_func)
|
+-- 构建 ata_queued_cmd (qc)
+-- xlat_func() 将 SCSI CDB 转为 ATA taskfile
+-- ata_qc_issue(qc) 提交给 AHCI/SFF 控制器
struct ata_port {
struct ata_port_operations *ops; /* 控制器操作函数表 */
struct ata_link link; /* 主链路(SATA 1 设备)*/
struct ata_link *pmp_link; /* Port Multiplier 的子链路 */
struct ata_link *slave_link; /* 次链路(PATA 从设备)*/
struct scsi_host_template *scsi_host_template;
struct Scsi_Host *scsi_host; /* 关联的 SCSI host */
...
};
struct ata_device {
struct ata_link *link; /* 归属链路 */
unsigned int devno; /* 0 或 1(PATA master/slave)*/
struct scsi_device *sdev; /* 对应的 SCSI 设备 */
u64 n_sectors; /* 设备容量(扇区数)*/
unsigned int class; /* ATA_DEV_ATA / ATA_DEV_ATAPI / ATA_DEV_NONE */
u8 pio_mode, dma_mode; /* 传输模式 */
u16 id[ATA_ID_WORDS]; /* IDENTIFY DEVICE 数据(256 个 u16)*/
/* NCQ(Native Command Queuing)支持 */
u8 ncq_send_recv_cmds[];
...
};
struct ata_link {
struct ata_port *ap;
struct ata_device device[ATA_MAX_DEVICES]; /* PATA: 2, SATA: 1 */
unsigned int active_tag; /* 当前活跃命令 tag(非 NCQ 时)*/
u32 sactive; /* NCQ 活跃命令位图 */
...
};
READ(10) → ATA READ DMA EXT 的翻译流程:
SCSI CDB: [0x28, 0, LBA_high, LBA, LBA, LBA, 0, Transfer_len_high, Transfer_len, 0]
ata_read_translate():
qc->tf.command = ATA_CMD_READ_DMA_EXT (0x25)
qc->tf.lbah = (LBA >> 24) & 0xff
qc->tf.lbam = (LBA >> 16) & 0xff
qc->tf.lbal = (LBA >> 8) & 0xff
qc->tf.nsect = transfer_length
qc->protocol = ATA_PROT_DMA
对于 ATA 设备上 SCSI 无直接对应的命令(如 MODE SENSE、READ CAPACITY),libata 通过 ata_scsi_simulate(drivers/ata/libata-scsi.c)模拟返回适当响应,而不是真正发送给设备。
NCQ 是 SATA 的标签队列支持,对应 SCSI 的 Tagged Queueing:
- 最多 32 个并发命令(tag 0-31)
- ATA 命令:
READ FPDMA QUEUED(0x60) /WRITE FPDMA QUEUED(0x61) - 通过
link.sactive位图跟踪活跃命令 - 完成时由 AHCI 控制器产生中断,通过
qc_ncq_fill_rtf回调报告完成的命令集合
上层驱动(Upper Layer Driver,ULD)为不同类型的 SCSI 设备提供操作接口。
ULD 通过 scsi_driver 结构注册(include/scsi/scsi_driver.h:12):
struct scsi_driver {
struct device_driver gendrv; /* 父类,包含 probe/remove */
int (*rescan)(struct device *);
blk_status_t (*init_command)(struct scsi_cmnd *); /* 构造 CDB */
void (*uninit_command)(struct scsi_cmnd *);
int (*done)(struct scsi_cmnd *); /* 命令完成处理 */
int (*eh_action)(struct scsi_cmnd *, int); /* EH 钩子 */
void (*resume)(struct scsi_cmnd *);
};文件:drivers/scsi/sd.c
最核心的 ULD,处理 TYPE_DISK、TYPE_MOD、TYPE_ZAC(ZBC/ZAC 分区块设备):
/* sd.c:4360 */
static struct scsi_driver sd_template = {
.gendrv = {
.name = "sd",
.probe = sd_probe,
.remove = sd_remove,
...
},
.init_command = sd_init_command, /* 构造 READ/WRITE CDB */
.done = sd_done, /* 处理完成状态和 sense 数据 */
};sd_init_command(drivers/scsi/sd.c:1470):
根据请求类型(读/写/FLUSH/DISCARD/WRITE SAME)选择合适的 SCSI 命令:
- 普通读写:
READ(10)/READ(16)/WRITE(10)/WRITE(16) - 大 LBA(>0xffffffff):强制使用 16 字节命令
- FLUSH:
SYNCHRONIZE CACHE(10)或(16) - DISCARD:
UNMAP或WRITE SAME WITH UNMAP - T10 PI(DIF):设置
prot_op字段
sd_done(drivers/scsi/sd.c:2310):
处理完成结果,包括 RECOVERED_ERROR(记录日志后成功)、介质错误(上报 EIO)等。
支持的高级特性:
sd_dif.c:T10 DIF/DIX 数据完整性(Data Integrity Field)sd_zbc.c:ZBC(Zoned Block Command)分区块设备支持
文件:drivers/scsi/sr.c
处理 TYPE_ROM、TYPE_WORM,提供 /dev/sr0 等光驱设备:
- 实现
CDROMREADTOCHDR、CDROMREADTOCENTRY等 CD 特有 ioctl - 使用
READ(10)读取数据,READ CAPACITY检测介质 - 通过
sdev->changed标志处理介质更换事件
文件:drivers/scsi/st.c
处理 TYPE_TAPE,提供 /dev/st0(倒带)和 /dev/nst0(非倒带):
- 字符设备,不使用块层
- 使用
SPACE、REWIND、WRITE_FILEMARKS、READ、WRITE等磁带专用命令 - 复杂的缓冲管理(固定块模式 vs 可变块模式)
文件:drivers/scsi/sg.c
提供 /dev/sg* 通用 SCSI pass-through 接口:
- 用户空间可直接构造任意 SCSI CDB 并发送
smartctl、sg_utils、cdparanoia等工具均通过 sg 接口工作- 通过
SG_IOioctl 发送命令,结构体sg_io_hdr_t描述 CDB + 数据缓冲区
| 驱动文件 | 设备类型 | TYPE 值 |
|---|---|---|
ch.c |
SCSI 磁带自动换带机(Changer) | TYPE_MEDIUM_CHANGER (0x08) |
ses.c |
SCSI 存储机箱服务(SES/SESZ) | TYPE_ENCLOSURE (0x0D) |
DM-Multipath(Device Mapper Multipath)是 Linux 的存储多路径解决方案,通过将多条物理路径(path)聚合为一个逻辑设备,提供冗余和负载均衡。
用户空间 /dev/mapper/mpathX
|
Device Mapper (dm-core)
|
dm-multipath target
|
Path Selector(RR / MQ / QUEUE_LENGTH 等)
|
+----+----+----+
| | | |
path0 path1 path2 path3 (每条 path = scsi_device)
| | | |
HBA0 HBA0 HBA1 HBA1 (多块 HBA 或同一 HBA 的多端口)
| | | |
SAN 存储阵列
SCSI 设备处理器(Device Handler)是 SCSI 层与 DM-Multipath 之间的接口层,处理存储阵列特定的 path 切换逻辑。
文件:drivers/scsi/scsi_dh.c、drivers/scsi/device_handler/
/* 内置的设备处理器 */
drivers/scsi/device_handler/scsi_dh_alua.c /* ALUA(TPGS)标准 */
drivers/scsi/device_handler/scsi_dh_emc.c /* EMC CLARiiON / Symmetrix */
drivers/scsi/device_handler/scsi_dh_rdac.c /* NetApp / LSI RDAC */
drivers/scsi/device_handler/scsi_dh_hp_sw.c /* HP MSA Active-Standby */ALUA(Asymmetric Logical Unit Access):
ALUA 是 SPC-3 标准中定义的路径状态管理机制(scsi_dh_alua.c):
Target Port Group States:
Active/Optimized (AO) --> 最优路径,直接 IO
Active/Non-Optimized (ANO) --> 次优路径,可用但性能差
Standby --> 待机,需 failover 激活
Unavailable --> 不可用
Transitioning --> 切换中
SCSI 命令:REPORT TARGET PORT GROUPS (0x9e/0x0a)
SET TARGET PORT GROUPS (0x9e/0x0b)
IO 失败(如 CHECK_CONDITION + NOT_READY)
|
v dm-multipath 捕获错误
|
v scsi_dh->check_sense() 判断是否需要 failover
|
v dm_pg_init() --> 切换 path selector 选择不同 path
|
v ALUA: 发送 SET TARGET PORT GROUPS 命令
| 切换 target port group 状态
|
v 重新提交 IO 到新 path
SAS 是 SCSI 并行接口的串行化版本,是数据中心 HDD/SSD 的主流接口之一。
架构:
sd.c / sg.c (ULD)
|
SCSI 中间层
|
libsas (include/scsi/libsas.h, drivers/scsi/libsas/)
|
SAS HBA 驱动(如 mpt3sas、pm8001、hisi_sas)
|
SAS 交换机 / Expander
|
SAS/SATA 设备
核心结构(include/scsi/libsas.h):
struct sas_ha_struct { /* SAS 主机适配器 */
struct asd_sas_phy **sas_phy; /* PHY 数组(物理层) */
struct asd_sas_port **sas_port; /* PORT 数组(逻辑端口)*/
struct sas_domain_function_template *lldd_ha; /* LLD 回调 */
...
};
struct asd_sas_port { /* SAS 端口(可含多个 PHY) */
struct domain_device *port_dev; /* 连接的设备 */
struct list_head dev_list;
...
};iSCSI 通过以太网传输 SCSI 命令,支持软件实现和硬件 TOE(TCP Offload Engine)。
软件栈(drivers/scsi/iscsi_tcp.c,基于 libiscsi):
iscsi_tcp 驱动
|
libiscsi (drivers/scsi/libiscsi.c)
|
iscsi_transport 层 (include/scsi/scsi_transport_iscsi.h)
|
SCSI 中间层 (Scsi_Host)
核心结构(include/scsi/libiscsi.h):
struct iscsi_session { /* iSCSI 会话 */
struct iscsi_transport *tt;
struct iscsi_conn *leadconn; /* 主连接 */
u32 cmdsn; /* 命令序列号 */
u32 exp_cmdsn; /* 期望的下一个序列号 */
...
};
struct iscsi_conn { /* TCP 连接 */
struct iscsi_session *session;
struct socket *sock; /* 底层 TCP socket */
...
};
struct iscsi_host { /* iSCSI 主机(Scsi_Host 的私有数据)*/
char local_address[]; /* Initiator IP 地址 */
...
};iSCSI 命令封装:SCSI CDB 被封装在 iSCSI PDU(Protocol Data Unit)中,通过 TCP 发送,目标端解封装后执行,结果再封装返回。
NVMe-oF 是 NVMe 协议在网络传输上的扩展,支持 RDMA(RoCE/iWARP)、FC 和 TCP 传输。
与 SCSI 的关系:
NVMe-oF 不使用 SCSI 中间层,有独立的 drivers/nvme/ 栈,但:
nvme-rdma / nvme-tcp / nvme-fc
|
nvme host core (drivers/nvme/host/core.c)
|
NVMe 队列(sq/cq,基于 blk-mq)
|
RDMA 传输层 / TCP 传输层 / FC 传输层
NVMe 设备的 /dev/nvme0n1 直接通过 NVMe 驱动暴露,绕过了 SCSI 层。但某些 NVMe 设备会通过 nvme-scsi 翻译层(用于兼容 SES/EnclosureServices 等)接受 SCSI 命令。
| 属性 | SAS | iSCSI | NVMe-oF |
|---|---|---|---|
| 传输介质 | SAS 线缆(3/6/12/24 Gbps) | 以太网 | RDMA/TC/FC |
| 协议层 | SCSI | SCSI over TCP | NVMe(非 SCSI) |
| 延迟 | 极低(直连) | 中(网络 RTT) | 极低(RDMA) |
| 主机接口 | SCSI 中间层 | SCSI 中间层 | NVMe 核心 |
| 队列深度 | 多(SAS 支持 256 tag) | 中 | 极深(65535) |
| 文件 | 功能 |
|---|---|
scsi.c |
SCSI 初始化、模块注册 |
scsi_lib.c |
命令构建、队列管理、blk-mq 集成(热路径核心) |
scsi_error.c |
错误处理线程、abort/reset、sense 解析 |
scsi_scan.c |
总线扫描、INQUIRY、设备发现 |
hosts.c |
Scsi_Host 分配/释放(scsi_host_alloc、scsi_remove_host) |
scsi_sysfs.c |
sysfs 接口(/sys/class/scsi_device/) |
scsi_devinfo.c |
设备黑/白名单(quirks)数据库 |
scsi_pm.c |
电源管理(suspend/resume) |
scsi_dh.c |
设备处理器框架(DM-Multipath 接口) |
scsi_proc.c |
/proc/scsi/ 接口 |
scsi_ioctl.c |
SCSI ioctl 处理 |
scsi_bsg.c |
BSG(Block SCSI Generic)接口 |
scsi_transport_*.c |
FC/SAS/SPI/iSCSI/SRP 传输层 |
| 文件 | 设备类型 |
|---|---|
sd.c |
SCSI 磁盘(/dev/sd*) |
sd_dif.c |
T10 DIF/DIX 数据完整性扩展 |
sd_zbc.c |
ZBC 分区块命令(SMR 硬盘) |
sr.c |
SCSI 光驱(/dev/sr*) |
st.c |
SCSI 磁带(/dev/st*、/dev/nst*) |
sg.c |
通用 SCSI pass-through(/dev/sg*) |
ch.c |
磁带库自动换带机 |
ses.c |
SES/SESZ 存储机箱服务 |
| 文件 | 适用阵列 |
|---|---|
device_handler/scsi_dh_alua.c |
符合 ALUA(TPGS)标准的阵列(通用) |
device_handler/scsi_dh_emc.c |
EMC CLARiiON / VNX |
device_handler/scsi_dh_rdac.c |
LSI/NetApp RDAC 阵列 |
device_handler/scsi_dh_hp_sw.c |
HP MSA Active-Standby |
| 文件/目录 | 硬件 |
|---|---|
hpsa.c |
HP Smart Array RAID |
megaraid*.c |
LSI MegaRAID |
mpt3sas/ |
LSI/Broadcom SAS 3.0 HBA |
ipr.c |
IBM Power RAID(pSeries) |
iscsi_tcp.c + libiscsi.c |
软件 iSCSI |
scsi_debug.c |
虚拟 SCSI 设备(调试/测试用) |
| 文件 | 功能 |
|---|---|
libata-core.c |
ATA 核心(port 初始化、设备枚举、命令提交) |
libata-scsi.c |
ATA ↔ SCSI 命令翻译层 |
libata-eh.c |
ATA 错误处理 |
libata-sata.c |
SATA 专用功能(PHY、OOB、LPM) |
libata-sff.c |
SFF(Standard Form Factor,老式并行 ATA)支持 |
libahci.c |
AHCI 核心实现(SATA 标准控制器) |
ahci.c |
PCI-based AHCI 驱动 |
ahci_platform.c |
Platform 设备 AHCI(嵌入式 SoC) |
# 查看所有 SCSI 设备
cat /proc/scsi/scsi
# 手动扫描新设备(旧接口)
echo "scsi add-single-device H C T L" > /proc/scsi/scsi
# 手动移除设备
echo "scsi remove-single-device H C T L" > /proc/scsi/scsi# 查看设备状态(SDEV 状态机)
cat /sys/class/scsi_device/0:0:0:0/device/state
# 查看队列深度
cat /sys/block/sda/device/queue_depth
# 动态修改队列深度
echo 64 > /sys/block/sda/device/queue_depth
# 查看设备 INQUIRY 信息
cat /sys/block/sda/device/vendor
cat /sys/block/sda/device/model
# 触发设备重新扫描
echo 1 > /sys/class/scsi_device/0:0:0:0/device/rescan
# 查看 SCSI host 信息
ls /sys/class/scsi_host/
cat /sys/class/scsi_host/host0/can_queue
cat /sys/class/scsi_host/host0/nr_hw_queues# 查询设备信息(sg_inq)
sg_inq /dev/sda
sg_inq --vpd --page=0x83 /dev/sda # Device ID VPD 页(用于多路径 WWID)
# 读取 SCSI 日志页
sg_logs /dev/sda # 所有日志页
sg_logs --page=0x02 /dev/sda # Write Error Counter 日志
# 发送测试命令
sg_turs /dev/sda # TEST UNIT READY
sg_readcap /dev/sda # READ CAPACITY
# 读取 Sense 数据
sg_requests /dev/sda
# 查看 ZBC 信息(SMR 硬盘)
sg_rep_zones /dev/sda# 通过 SCSI INQUIRY + ATA PASSTHROUGH 读取 SMART 数据
smartctl -a /dev/sda # 所有 SMART 信息
smartctl -H /dev/sda # 健康状态
smartctl -t short /dev/sda # 短测试
smartctl -t long /dev/sda # 长测试
# 直接操作 SAS 设备
smartctl -a --device=scsi /dev/sda通过 sysctl 启用详细日志(drivers/scsi/scsi_sysctl.c):
# 查看当前日志级别
cat /proc/sys/dev/scsi/logging_level
# 启用详细 IO 日志(级别 3)
echo 3 > /proc/sys/dev/scsi/logging_level
# 日志级别含义(按 bit 位控制不同子系统):
# bit 0-3: HLQUEUE 高层队列进出
# bit 4-7: HLCOMPLETE 高层完成
# bit 8-11: LLQUEUE 低层队列进出
# bit 12-15: LLCOMPLETE 低层完成
# bit 16-19: SCAN 设备扫描
# bit 20-23: MLQUEUE 中间层队列
# bit 24-27: ERROR 错误处理
# bit 28-31: TIMEOUT 超时处理# 跟踪 SCSI 设备的块层 IO(含队列事件)
blktrace -d /dev/sda -o trace
blkparse trace.blktrace.0 | head -50
# 关键事件类型:
# Q: 命令入队
# G: 获取 request
# I: 命令进入 IO 调度器
# D: 分发给驱动(dispatch)
# C: 命令完成# 查看 SCSI 命令调试信息(需 CONFIG_SCSI_CONSTANTS)
ls /sys/kernel/debug/block/
# 通过 ftrace 跟踪 SCSI 事件
echo 1 > /sys/kernel/debug/tracing/events/scsi/enable
cat /sys/kernel/debug/tracing/trace | grep scsi# 查看多路径状态(dm-multipath)
multipath -ll
# 查看路径健康状态
multipathd show paths
# 手动切换路径
multipathd switchpathgroup multipath_name 1
# 查看 ALUA 状态
cat /sys/block/sda/device/access_stateSCSI 子系统的三层设计是 Linux 内核模块化思想的典型体现,每层有明确的职责边界:
+---------------------------------------------------------------+
| ULD(Upper Layer Driver,上层驱动) |
| sd.c / sr.c / st.c / sg.c / ch.c / ses.c |
| 职责:设备类型语义、CDB 构造、用户接口(块设备/字符设备) |
| 不感知硬件细节,仅面向 scsi_device 发命令 |
+---------------------------------------------------------------+
|
scsi_driver ops
(init_command / done / eh_action)
|
+---------------------------------------------------------------+
| SCSI Mid-Layer(中间层) |
| scsi_lib.c / scsi_error.c / scsi_scan.c / hosts.c |
| 职责:blk-mq 集成、队列深度管理、错误恢复线程、设备扫描 |
| 提供统一接口给 ULD,向下通过 scsi_host_template 调用 HBA |
+---------------------------------------------------------------+
|
scsi_host_template ops
(queuecommand / eh_*)
|
+---------------------------------------------------------------+
| LLD(Low Level Driver,低层驱动 / HBA 驱动) |
| hpsa.c / mpt3sas/ / megaraid/ / iscsi_tcp.c / libata |
| 职责:硬件寄存器操作、DMA 映射、中断处理、命令描述符构造 |
| 不感知 SCSI 协议语义,仅负责把 scsi_cmnd 送到硬件并取回结果 |
+---------------------------------------------------------------+
|
物理硬件
(SAS HBA / SATA 控制器 / FC HBA)
层间通信机制:
ULD --> 中间层:
- scsi_execute_cmd() [同步,用于内部命令,如 INQUIRY]
- sd_init_command() [异步,构造 IO 命令的 CDB]
中间层 --> LLD:
- hostt->queuecommand(host, cmd) [命令下发]
- hostt->eh_abort_handler(cmd) [EH 回调]
LLD --> 中间层:
- scsi_done(cmd) [命令完成通知]
- scsi_report_device_reset(...) [设备 reset 通知]
HBA 驱动通过以下方式向内核注册:
/* 驱动定义 scsi_host_template */
static struct scsi_host_template hpsa_driver_template = {
.module = THIS_MODULE,
.name = "hpsa",
.proc_name = "hpsa",
.queuecommand = hpsa_scsi_queue_command,
.scan_start = hpsa_scan_start,
.scan_finished = hpsa_scan_finished,
.change_queue_depth = hpsa_change_queue_depth,
.eh_abort_handler = hpsa_eh_abort_handler,
.eh_device_reset_handler = hpsa_eh_device_reset_handler,
.eh_host_reset_handler = hpsa_eh_host_reset_handler,
.can_queue = 1024,
.cmd_per_lun = 128,
.max_sectors = 8192,
.sg_tablesize = SG_TABLESIZE,
.host_tagset = 1,
...
};
/* 驱动 probe 时分配并注册 Scsi_Host */
shost = scsi_host_alloc(&hpsa_driver_template, sizeof(struct ctlr_info));
scsi_add_host(shost, &pdev->dev);
scsi_scan_host(shost); /* 触发总线扫描,发现设备 */scsi_host_alloc(drivers/scsi/hosts.c)分配 Scsi_Host 结构,其中 hostdata[] 弹性数组存放 LLD 私有数据(大小由 sizeof(struct ctlr_info) 决定)。
scsi_scan_host(drivers/scsi/scsi_scan.c)触发异步扫描:
scsi_scan_host()
--> async_schedule(do_scsi_scan_host, shost)
|
v 对每个 channel 和 id 调用
v scsi_scan_channel()
|
v scsi_probe_and_add_lun(shost, channel, id, 0, ...)
|
v 发送 INQUIRY 命令(scsi_execute_cmd)
v 解析 INQUIRY 响应(厂商/型号/设备类型)
v scsi_alloc_sdev() 创建 scsi_device
v SCSI-3 设备:发送 REPORT LUNS 获取所有 LUN
v 为每个 LUN 创建对应的 scsi_device
v scsi_add_lun() --> device_add() --> 触发 ULD probe
INQUIRY 命令的解析(drivers/scsi/scsi_scan.c):
- 字节 0:设备类型(0x00=磁盘,0x01=磁带,0x05=CD-ROM)
- 字节 1 bit7:可移除介质标志(RMB)
- 字节 2:SCSI 版本
- 字节 16-31:厂商(8 字节)
- 字节 32-47:型号(16 字节)
- 字节 48-51:固件版本(4 字节)
传输层位于 SCSI 中间层与 LLD 之间,封装特定传输协议的发现和管理功能:
/* 传输层模板(以 SAS 为例)*/
struct scsi_transport_template *sas_attach_transport(
struct sas_function_template *ft);
/* 传输层通过 sysfs 暴露传输特定属性 */
/sys/class/sas_host/host0/
/sys/class/sas_phy/phy-0:0/
/sys/class/sas_device/end_device-0:0/各传输层文件:
| 文件 | 传输协议 |
|---|---|
scsi_transport_sas.c |
SAS(SMP/SSP/STP) |
scsi_transport_fc.c |
Fibre Channel |
scsi_transport_iscsi.c |
iSCSI |
scsi_transport_spi.c |
SCSI 并行接口(老式) |
scsi_transport_srp.c |
SCSI RDMA Protocol |
Scsi_Host(include/scsi/scsi_host.h:558)是内核中最复杂的存储相关结构体之一,其关键字段按功能分组如下:
Scsi_Host 字段分组
|
+-- 设备管理
| __devices (scsi_device 链表头)
| __targets (scsi_target 链表头)
| scan_mutex (扫描互斥锁)
|
+-- 错误恢复(EH)
| ehandler (EH 内核线程 task_struct)
| eh_cmd_q (等待 EH 处理的命令链表)
| eh_abort_list (待 abort 的命令链表)
| eh_action (EH 同步 completion)
| host_failed (失败命令计数)
| eh_deadline (EH 超时时限)
| last_reset (上次 reset 时间戳)
|
+-- blk-mq 集成
| tag_set (blk_mq_tag_set)
| nr_hw_queues (硬件队列数量)
|
+-- 队列控制
| can_queue (每 HW 队列最大命令数)
| host_blocked (退避计数,原子变量)
| max_host_blocked (最大退避计数,默认 5)
| nr_reserved_cmds (保留命令槽数量)
|
+-- 驱动 ops
| hostt (scsi_host_template 指针)
| transportt (传输层模板指针)
|
+-- 标识
| host_no (全局唯一编号)
| shost_state (状态机状态)
|
+-- 功能标志
| use_blk_mq (always 1 in modern kernel)
| host_tagset (全 host 共享 tag space)
|
`-- LLD 私有
hostdata[] (弹性数组,大小在 alloc 时指定)
Scsi_Host 的生命周期管理:
scsi_host_alloc() --> 引用计数初始化为 1
|
scsi_add_host() --> 创建 EH 线程,注册到 sysfs
|
scsi_scan_host() --> 异步扫描总线,发现设备
|
(运行期间)
|
scsi_remove_host() --> 停止接受新命令,清除设备列表
|
scsi_host_put() --> 引用计数减 1,为 0 时释放
scsi_device(include/scsi/scsi_device.h:103)代表一个逻辑单元(LUN),与块层 request_queue 一一对应:
scsi_device 字段分组
|
+-- 寻址
| host (归属 Scsi_Host)
| id (target ID)
| channel (通道号,通常为 0)
| lun (LUN 号,64 位)
|
+-- 设备识别
| type (设备类型,来自 INQUIRY)
| vendor/model/rev (厂商型号固件字符串)
| inquiry (原始 INQUIRY 数据)
| vpd_pg0/pg83/pg80 (VPD 页,用于 WWID/SN 等)
|
+-- 块层集成
| request_queue (对应的块层队列)
| budget_map (sbitmap,无锁队列深度控制)
| queue_depth (当前队列深度)
| max_queue_depth (最大允许队列深度)
|
+-- 状态机
| sdev_state (SDEV_CREATED/RUNNING/BLOCK/QUIESCE/OFFLINE/DEL)
| device_blocked (退避计数)
|
+-- 特性标志
| tagged_supported (支持 tagged queuing)
| simple_tags (启用 simple tag)
| ordered_tags (支持 ordered tag)
| wce_default_on (写缓存默认开启)
| no_start_on_add (不在添加时发送 START UNIT)
| allow_restart (允许在 RESTART 后重新扫描)
| removable (可移除介质)
| is_ata (通过 libata 管理的 ATA 设备)
|
+-- 统计
| iorequest_cnt (发出的 IO 请求总数)
| iodone_cnt (完成的 IO 请求总数)
| ioerr_cnt (错误的 IO 请求总数)
| iotmo_cnt (超时的 IO 请求总数)
|
`-- LLD/传输层私有
sdev_data[] (弹性数组)
scsi_target(include/scsi/scsi_device.h)代表一个 SCSI target(即一个设备端点),可以有多个 LUN:
scsi_target
|-- id, channel (target 寻址)
|-- can_queue (target 级别队列深度)
|-- target_blocked (target 级别退避计数)
|-- starget_data[] (传输层私有数据)
`-- siblings --> scsi_device (该 target 下的所有 LUN)
三层队列深度关系图:
Scsi_Host.can_queue = 1024 (host 级别,所有命令总数上限)
|
+-- scsi_target.can_queue = 128 (单个 target 上限)
|
+-- scsi_device.queue_depth = 64 (单个 LUN 上限)
|
+-- budget_map (sbitmap,实际计数)
SCSI 热路径结构体对 cacheline 对齐非常敏感。scsi_cmnd 中最频繁访问的字段被刻意排列在前几个 cacheline 内:
scsi_cmnd 内存布局(64 字节 cacheline)
cacheline 0 (0-63 字节):
device (8 字节) - 热路径必需
eh_entry (16 字节)
abort_work (32 字节)
budget_token (4 字节)
retries/allowed (4 字节)
cacheline 1 (64-127 字节):
cmd_len (2 字节)
sc_data_direction (4 字节)
cmnd[32] (32 字节) - CDB,热路径必需
...
cacheline 2+ (128 字节+):
sdb (scatter-gather table)
sense_buffer (96 字节,另分配)
result (4 字节)
scsi_execute_cmd(drivers/scsi/scsi_lib.c:295)是 SCSI 子系统内部发送管理命令的统一接口,用于 INQUIRY、MODE SENSE、READ CAPACITY 等需要同步结果的场景:
int scsi_execute_cmd(struct scsi_device *sdev,
const unsigned char *cmd, /* SCSI CDB */
blk_opf_t opf, /* REQ_OP_DRV_IN / REQ_OP_DRV_OUT */
void *buffer, /* 数据缓冲区 */
unsigned int bufflen, /* 缓冲区长度 */
int timeout, /* 超时(jiffies)*/
int ml_retries, /* 中间层重试次数 */
const struct scsi_exec_args *args)执行流程:
scsi_execute_cmd()
|
v scsi_alloc_request() 分配 request(从 blk-mq 池)
|
v blk_rq_map_kern() 映射数据缓冲区到 request SG 表
|
v scmd->cmnd = cmd 填写 CDB
v scmd->allowed = ml_retries 设置重试次数
v req->timeout = timeout 设置超时
|
v blk_execute_rq(req, true) 同步提交(head injection,插队到队列头)
| [阻塞等待完成]
|
v 检查 resid_len(剩余未传输长度)
v 拷贝 sense_buffer 给调用者
v 返回 scmd->result
|
v blk_mq_free_request() 释放 request
使用示例(INQUIRY 命令,drivers/scsi/scsi_scan.c):
unsigned char cmd[6] = {
INQUIRY, 0, 0, 0,
(unsigned char) SCSI_DEFAULT_INQLEN, 0
};
result = scsi_execute_cmd(sdev, cmd, REQ_OP_DRV_IN,
inq_result, SCSI_DEFAULT_INQLEN,
HZ/2, /* 500ms 超时 */
3, /* 3 次重试 */
NULL);不同操作对应的 SCSI CDB 构造(以 sd_init_command 为核心):
读写操作(sd_setup_read_write_cmnd):
LBA <= 0xffffffff AND 传输长度 <= 0xffff:
使用 READ(10)/WRITE(10):6 字节命令 + 4 字节 LBA + 2 字节长度
CDB = [0x28/0x2a, flags, LBA(4B), 0, len(2B), 0]
LBA > 0xffffffff OR 传输长度 > 0xffff:
使用 READ(16)/WRITE(16):16 字节命令
CDB = [0x88/0x8a, flags, LBA(8B), len(4B), 0, 0]
FLUSH 操作(sd_setup_flush_cmnd):
CDB = [0x35, 0, 0, 0, 0, 0, 0, 0, 0, 0] (SYNCHRONIZE CACHE 10)
或 CDB = [0x91, ...] (SYNCHRONIZE CACHE 16)
DISCARD 操作(sd_setup_discard_cmnd):
UNMAP: CDB = [0x42, 0, ...]
WRITE SAME: CDB = [0x41/0x93, 0x08, ...] (bit3 = UNMAP)
scsi_queue_rq(drivers/scsi/scsi_lib.c:1829)的完整处理步骤:
static blk_status_t scsi_queue_rq(struct blk_mq_hw_ctx *hctx,
const struct blk_mq_queue_data *bd)
{
struct request *req = bd->rq;
struct scsi_device *sdev = req->q->queuedata;
struct Scsi_Host *shost = sdev->host;
struct scsi_cmnd *cmd = blk_mq_rq_to_pdu(req);
/* Step 1: 检查 reserved request(保留命令通道,用于 EH 等管理命令)*/
if (!blk_mq_is_reserved_rq(req)) {
/* Step 2: 检查设备状态 */
if (unlikely(sdev->sdev_state != SDEV_RUNNING)) {
ret = scsi_device_state_check(sdev, req);
if (ret != BLK_STS_OK) goto out_put_budget;
}
/* Step 3: 三层队列就绪检查 */
if (!scsi_target_queue_ready(shost, sdev)) goto out_put_budget;
if (scsi_host_in_recovery(shost)) goto out_dec_target_busy;
if (!scsi_host_queue_ready(q, shost, sdev, cmd)) goto out_dec_target_busy;
}
/* Step 4: 初始化 cmd 私有数据(LLD 私有字段清零)*/
if (shost->hostt->cmd_size && !shost->hostt->init_cmd_priv)
memset(scsi_cmd_priv(cmd), 0, shost->hostt->cmd_size);
/* Step 5: 调用 ULD 的 init_command 构造 CDB */
if (!(req->rq_flags & RQF_DONTPREP)) {
ret = scsi_prepare_cmd(req); /* --> ULD->init_command() */
req->rq_flags |= RQF_DONTPREP;
}
/* Step 6: 设置 TAGGED 和 LAST 标志 */
if (sdev->simple_tags) cmd->flags |= SCMD_TAGGED;
if (bd->last) cmd->flags |= SCMD_LAST;
/* Step 7: 启动计时器,提交给 LLD */
blk_mq_start_request(req);
reason = shost->hostt->queuecommand(shost, cmd);
/* Step 8: 处理 LLD 拒绝(SCSI_MLQUEUE_*) */
if (reason) {
scsi_set_blocked(cmd, reason);
ret = BLK_STS_RESOURCE;
goto out_dec_host_busy;
}
return BLK_STS_OK;
}scsi_complete(drivers/scsi/scsi_lib.c:1541)是命令完成的决策中心:
scsi_complete(rq)
|
v cmd = blk_mq_rq_to_pdu(rq)
|
v iodone_cnt++ (统计计数)
v 如果 cmd->result != 0:ioerr_cnt++
|
v disposition = scsi_decide_disposition(cmd)
|
+-- [disposition == SUCCESS]
| --> scsi_finish_command(cmd)
| --> ULD->done(cmd) [如 sd_done]
| --> blk_mq_end_request(rq, BLK_STS_OK)
|
+-- [disposition == NEEDS_RETRY]
| --> scsi_queue_insert(cmd, SCSI_MLQUEUE_EH_RETRY)
| --> blk_mq_requeue_request() [回到队列头重新尝试]
|
+-- [disposition == ADD_TO_MLQUEUE]
| --> scsi_queue_insert(cmd, SCSI_MLQUEUE_DEVICE_BUSY)
| --> 设置 device_blocked,延迟重入队
|
`-- [其他,如 FAILED]
--> scsi_eh_scmd_add(cmd)
--> 加入 shost->eh_cmd_q
--> scsi_eh_wakeup() 唤醒 EH 线程
scsi_decide_disposition 的决策逻辑(drivers/scsi/scsi_error.c):
cmd->result 中的 host_byte 和 status_byte:
DID_NO_CONNECT / DID_ABORT / DID_RESET --> FAILED
DID_OK + SAM_STAT_GOOD --> SUCCESS
DID_OK + SAM_STAT_CHECK_CONDITION --> 解析 sense data
|-- sense key == RECOVERED_ERROR --> SUCCESS (has_warning)
|-- sense key == NOT_READY --> NEEDS_RETRY(有限次数)
|-- sense key == UNIT_ATTENTION --> NEEDS_RETRY(有限次数)
`-- 其他 --> FAILED(交给 EH)
DID_OK + SAM_STAT_BUSY --> ADD_TO_MLQUEUE
DID_OK + SAM_STAT_TASK_SET_FULL (QUEUE_FULL) --> ADD_TO_MLQUEUE
+ scsi_track_queue_full()
blk-mq 超时机制通过 scsi_timeout(drivers/scsi/scsi_lib.c)回调触发:
[硬件没有在 req->timeout 时间内完成命令]
|
v blk_mq 超时定时器触发
v scsi_timeout(req)
|
v 发起延迟工作(abort_work,延迟 SCSI_ABORT_DELAY=0ms)
v scmd_eh_abort_handler()
|
+-- 尝试快速 abort(不经过 EH 线程)
| hostt->eh_abort_handler(cmd)
| 如果成功 --> 重新提交命令
|
`-- 如果 abort 失败
scsi_eh_scmd_add(cmd) --> 交给 EH 线程
EH 线程(scsi_error_handler,drivers/scsi/scsi_error.c)的唤醒条件是精心设计的:
/* drivers/scsi/scsi_error.c:64 */
void scsi_eh_wakeup(struct Scsi_Host *shost, unsigned int busy)
{
lockdep_assert_held(shost->host_lock);
/* 只有当所有 in-flight 命令都已失败时才唤醒 EH 线程 */
/* 这避免了在正常 IO 进行时误触发 EH */
if (busy == shost->host_failed) {
trace_scsi_eh_wakeup(shost);
wake_up_process(shost->ehandler);
}
}这个设计很重要:只有当 host_failed(已失败命令数)等于 busy(当前活跃命令数)时,EH 线程才会被唤醒。这确保了 EH 在所有正常 IO 都已处理完毕后才介入,避免干扰正常路径。
scsi_unjam_host(drivers/scsi/scsi_error.c)是 EH 的核心恢复函数:
scsi_unjam_host(shost)
|
v scsi_eh_get_sense()
| 对每个失败命令发送 REQUEST SENSE 获取 sense data
| (某些命令可能通过 AUTO SENSE 已经带回了 sense 数据)
|
v scsi_eh_ready_devs()
| 按层次尝试恢复(见下面的详细流程)
|
v scsi_eh_flush_done_q()
| 对恢复成功的命令重新提交
| 对无法恢复的命令返回 EIO 给上层
|
v scsi_restart_operations()
| 解除 host/target/device 的 blocked 状态
| 重启 IO 队列
/* drivers/scsi/scsi_error.c */
static void scsi_eh_ready_devs(struct Scsi_Host *shost,
struct list_head *work_q,
struct list_head *done_q)
{
/* Phase 1: 尝试命令级 abort */
if (!scsi_eh_abort_cmds(work_q, done_q))
return; /* 全部恢复成功,直接返回 */
/* Phase 2: 尝试 START UNIT(设备可能只是 spindown 状态)*/
scsi_eh_stu(shost, work_q, done_q);
/* Phase 3: 尝试设备(LUN)级 reset */
if (!scsi_eh_bus_device_reset(shost, work_q, done_q)) return;
/* Phase 4: 尝试 target reset */
scsi_eh_target_reset(shost, work_q, done_q);
/* Phase 5: 检查 EH deadline,超时则直接下线 */
if (scsi_host_eh_past_deadline(shost)) {
scsi_eh_offline_sdevs(work_q, done_q);
return;
}
/* Phase 6: 总线 reset + 等待 BUS_RESET_SETTLE_TIME 秒 */
scsi_eh_bus_reset(shost, work_q, done_q);
/* Phase 7: host reset + 等待 HOST_RESET_SETTLE_TIME 秒 */
scsi_eh_host_reset(shost, work_q, done_q);
/* Phase 8: 对所有剩余命令设置设备下线 */
scsi_eh_offline_sdevs(work_q, done_q);
}BUS_RESET_SETTLE_TIME 和 HOST_RESET_SETTLE_TIME 在 drivers/scsi/scsi_error.c:57-58 均定义为 10 秒,这是为了等待设备在 reset 后稳定就绪。
SCSI sense data 格式(固定格式,最常见):
Byte 0: Response Code (0x70 = current error, 0x71 = deferred error)
Byte 1: Obsolete
Byte 2: [bit7=FILEMARK] [bit6=EOM] [bit5=ILI] [bit4=SKSV] [bit3:0=Sense Key]
Byte 3-6: Information (LBA of error, if valid)
Byte 7: Additional Sense Length
Byte 8-11: Command-Specific Information
Byte 12: Additional Sense Code (ASC)
Byte 13: Additional Sense Code Qualifier (ASCQ)
Byte 14: Field Replaceable Unit Code
Byte 15-17: Sense Key Specific
典型 ASC/ASCQ 组合:
0x04/0x01 "Logical Unit Is in Process of Becoming Ready"
0x04/0x02 "Logical Unit Not Ready, Initializing Command Required"
0x08/0x00 "Logical Unit Communication Failure"
0x11/0x00 "Unrecovered Read Error"
0x21/0x00 "Logical Block Address Out of Range"
0x25/0x00 "Logical Unit Not Supported"
0x27/0x00 "Write Protected"
0x28/0x00 "Not Ready to Ready Change, Medium May Have Changed"
0x29/0x00 "Power On, Reset, or Bus Reset Occurred"
0x3f/0x0e "Reported LUNs Data Has Changed"
scsi_normalize_sense(drivers/scsi/scsi_common.c)将原始 sense 数据解析为 scsi_sense_hdr 结构:
struct scsi_sense_hdr {
u8 response_code;
u8 sense_key;
u8 asc;
u8 ascq;
u8 byte4;
u8 byte5;
u8 byte6;
u8 additional_length;
};对于 SAS 和 FC 等传输层,EH 会委托传输层的 eh_strategy_handler 处理:
/* drivers/scsi/scsi_error.c */
if (shost->transportt->eh_strategy_handler)
shost->transportt->eh_strategy_handler(shost);
else
scsi_unjam_host(shost);libsas 的 sas_scsi_recover_host(drivers/scsi/libsas/sas_scsi_host.c)实现了 SAS 特有的恢复策略,包括:
- SAS abort task(
SMP管理帧) - SAS target reset(TMF: Task Management Function)
- SAS 端口 reset
- 重新发现 SAS 域(sas_rediscover_dev)
SCSI 中间层使用 sbitmap(include/linux/sbitmap.h)实现无锁的队列预算控制:
sbitmap 结构:
多个 sbitmap_word,每个 word 包含一个 unsigned long 位图
通过 percpu 的 alloc_hint 分散分配,减少竞争
scsi_device.budget_map:
- sbitmap_get() 申请一个 token(找到并设置一个 0 位)
- sbitmap_put() 释放一个 token(清除对应位)
优势:
- 无锁(仅依赖原子操作 test_and_set_bit)
- 多队列友好(percpu hint 减少 cacheline 竞争)
- O(1) 时间复杂度
与旧式 atomic_t 计数器相比,sbitmap 在高并发场景下减少了 cacheline 竞争,是 blk-mq 重构时引入的关键优化。
QUEUE_FULL 处理(drivers/scsi/scsi.c:259):
int scsi_track_queue_full(struct scsi_device *sdev, int depth)
{
/* 记录 QUEUE_FULL 事件时间,防止频繁调整 */
if ((jiffies >> 8) == (sdev->last_queue_full_time >> 8))
return 0;
sdev->last_queue_full_time = jiffies;
if (sdev->last_queue_full_depth != depth) {
/* 队列深度发生了变化,重置计数 */
sdev->last_queue_full_count = 1;
sdev->last_queue_full_depth = depth;
return 0;
}
sdev->last_queue_full_count++;
/* 连续 3 次相同深度的 QUEUE_FULL,才真正降低队列深度 */
if (sdev->last_queue_full_count <= 10)
return 0;
return scsi_change_queue_depth(sdev, depth);
}队列深度恢复(ramp-up 机制):
SCSI 子系统会在 sdev->queue_ramp_up_period(默认 120 秒)后尝试逐步增加队列深度,直至恢复到 max_queue_depth。这是一个渐进式的自适应机制。
SCSI 标准定义了三种命令排队方式(SAM-2 协议):
Simple Queue Tag (0x20):
设备可以按任意顺序执行,适合大多数 IO
通过 cmd->flags |= SCMD_TAGGED 启用
Ordered Queue Tag (0x21):
必须按顺序执行,用于需要顺序保证的操作
(如写后读验证)
通过 REQ_OP_ORDERED 请求触发
Head of Queue Tag (0x22):
插入到队列头部,优先执行
用于高优先级命令(如 EH 的 REQUEST SENSE)
Linux SCSI 主要使用 Simple Tag,Ordered Tag 和 Head Tag 仅在特定场景下通过 scsi_execute_cmd 等接口触发。
在支持多个硬件队列的 HBA 上(如 NVMe 风格的 SAS HBA),SCSI 中间层通过 scsi_map_queues 将 CPU 映射到硬件队列:
CPU 0, 1, 2, 3 --> HW Queue 0
CPU 4, 5, 6, 7 --> HW Queue 1
CPU 8, 9, 10, 11 --> HW Queue 2
...
映射策略(blk_mq_map_queues):
- 基于 CPU 的 NUMA 节点亲和性
- 相同 NUMA 节点的 CPU 优先映射到同一个 HW Queue
- 减少跨 NUMA 的 DMA 和内存访问
host_tagset = 1 时(NVMe 风格,所有 HW Queue 共享 tag),blk-mq 使用全局 tag set;否则每个 HW Queue 有独立的 tag set。
libsas(drivers/scsi/libsas/)是 SAS HBA 驱动的公共框架,提供 SAS 域发现、任务管理、错误恢复等通用功能,使 HBA 驱动只需实现底层硬件操作:
+--------------------------------------------------+
| SCSI 中间层 (scsi_lib.c) |
+--------------------------------------------------+
|
+--------------------------------------------------+
| libsas 公共框架 (drivers/scsi/libsas/) |
| sas_discover.c - 域发现(Expander 遍历) |
| sas_scsi_host.c - SCSI host 集成 |
| sas_task.c - 任务(SCSI 命令)提交 |
| sas_event.c - 事件(PHY up/down)处理 |
| sas_expander.c - SAS Expander 管理 |
| sas_ata.c - SAS 连接的 SATA 设备支持 |
| sas_port.c - SAS 端口管理 |
| sas_phy.c - SAS PHY 管理 |
+--------------------------------------------------+
|
+--------------------------------------------------+
| SAS HBA 低层驱动(LLD) |
| mpt3sas/ - Broadcom/LSI SAS 3.0 |
| pm8001/ - Agilent/PMC SAS HBA |
| hisi_sas/ - HiSilicon SAS |
+--------------------------------------------------+
|
+--------------------------------------------------+
| SAS 物理层:PHY / Expander / End Device |
+--------------------------------------------------+
SAS 域发现(drivers/scsi/libsas/sas_discover.c)从 PHY 的 OOB(Out-Of-Band)信号开始:
PHY 连接(OOB 完成)
|
v sas_notify_port_event(PORTE_BYTES_DMAED)
| [LLD 通知 libsas 有新连接]
|
v sas_get_port_device() [libsas/sas_discover.c:49]
| 分析 IDENTIFY frame 判断连接设备类型:
| - SAS end device (SSP/SMP)
| - SATA device
| - SAS expander (edge/fanout)
|
v [End Device]
v sas_discover_end_dev()
| --> 发送 SMP REPORT GENERAL 获取设备信息
| --> 创建 domain_device
| --> 注册 SCSI target/device
|
v [Expander]
v sas_discover_expander()
| --> 发送 SMP DISCOVER 枚举所有 PHY
| --> 对每个连接的设备递归发现
| --> 建立拓扑树(domain_device 树)
|
v [SATA Device]
v sas_discover_sata()
| --> 通过 SATA 帧发送 IDENTIFY DEVICE
| --> 创建 ATA port,交给 libata 管理
domain_device(include/scsi/libsas.h):SAS 域中每个设备的抽象:
struct domain_device {
struct sas_ha_struct *port->ha; /* 所属 SAS HBA */
enum sas_device_type dev_type; /* SAS_END_DEVICE / SAS_EDGE_EXPANDER / ... */
u8 sas_addr[SAS_ADDR_SIZE]; /* 8 字节 SAS 地址(WWN)*/
u8 hashed_sas_addr[HASHED_SAS_ADDR_SIZE];
struct asd_sas_port *port; /* 连接的 SAS 端口 */
struct list_head siblings; /* 同端口的兄弟设备 */
struct domain_device *parent; /* 父设备(Expander)*/
struct list_head children; /* 子设备(仅 Expander)*/
/* 设备特定数据(union)*/
union {
struct dev_to_host_fis identify_device_data; /* SATA 设备 */
struct sas_end_device end_dev; /* SSP 设备 */
struct expander_device ex_dev; /* Expander */
};
struct scsi_device *sdev; /* 对应的 SCSI 设备(SSP end device)*/
};libsas LLD 回调表(struct sas_domain_function_template):
struct sas_domain_function_template {
/* 任务提交 */
int (*lldd_execute_task)(struct sas_task *, gfp_t);
int (*lldd_abort_task)(struct sas_task *);
int (*lldd_abort_task_set)(struct domain_device *, u8 *lun);
int (*lldd_clear_aca)(struct domain_device *, u8 *lun);
int (*lldd_clear_task_set)(struct domain_device *, u8 *lun);
int (*lldd_I_T_nexus_reset)(struct domain_device *);
int (*lldd_lu_reset)(struct domain_device *, u8 *lun);
int (*lldd_query_task)(struct sas_task *);
/* 端口管理 */
void (*lldd_port_formed)(struct asd_sas_phy *);
void (*lldd_port_deformed)(struct asd_sas_phy *);
/* 设备管理 */
int (*lldd_dev_found)(struct domain_device *);
void (*lldd_dev_gone)(struct domain_device *);
...
};当 SAS Expander 后面连接 SATA 设备时,libsas 通过 sas_ata.c 与 libata 集成:
SAS Expander --> SATA PHY --> SATA 磁盘
|
libsas 发现 SATA 设备后:
sas_ata_device_link_error() / sas_ata_schedule_reset()
--> 创建 ata_port(假设的 ATA 控制器)
--> libata 发送 IDENTIFY DEVICE
--> SATA 设备注册为 SCSI 设备(通过 libata-scsi 翻译层)
--> sd.c 识别为磁盘
这套机制使得 SAS-attached SATA(SAS 扩展器后面的 SATA 磁盘)和直连 SATA 磁盘对用户完全透明,都呈现为 /dev/sdX。
NVMe over Fabrics(NVMeOF)将 NVMe 协议扩展到网络传输,当前支持三种传输:
+---------------------------------------+
| NVMe 主机核心 |
| drivers/nvme/host/core.c |
| - 命名空间管理 (/dev/nvme0n1) |
| - 队列管理 (sq/cq 对) |
| - blk-mq 集成 |
+---------------------------------------+
|
+---------+---------+
| |
+----------+ +----------+ +----------+
| nvme-tcp | | nvme-rdma| | nvme-fc |
| (tcp.c) | | (rdma.c) | | (fc.c) |
+----------+ +----------+ +----------+
| | |
TCP/IP RDMA FC
(Linux net) (ib_verbs) (FC 传输)
NVMeOF 不使用 SCSI 中间层,与 SCSI 子系统并行存在,通过 blk-mq 直接与块层交互。
drivers/nvme/host/tcp.c 实现了 NVMe/TCP 传输(NVM Express over Fabrics:TCP Transport Specification)。
关键数据结构:
/* nvme_tcp_queue:每个 TCP 连接对应一个队列 */
struct nvme_tcp_queue {
struct socket *sock; /* TCP socket */
struct work_struct io_work; /* IO 工作队列(发送和接收)*/
struct list_head send_list; /* 待发送的请求列表 */
spinlock_t lock;
struct nvme_tcp_ctrl *ctrl; /* 归属的控制器 */
struct ib_cq *ib_cq; /* N/A for TCP */
int queue_size; /* 队列深度 */
size_t cmnd_capsule_len; /* 命令胶囊大小 */
...
};
/* nvme_tcp_request:单个 NVMe 命令的 TCP 传输状态 */
struct nvme_tcp_request {
struct nvme_request req; /* NVMe 请求(通用头)*/
__le16 status; /* 完成状态 */
struct nvme_tcp_queue *queue; /* 所属队列 */
struct list_head entry; /* 挂入 send_list */
u32 offset; /* 当前发送/接收偏移 */
size_t pdu_len; /* PDU 长度 */
size_t pdu_sent; /* 已发送字节数 */
...
};PDU 发送状态机(drivers/nvme/host/tcp.c:97):
enum nvme_tcp_send_state {
NVME_TCP_SEND_CMD_PDU = 0, /* 发送命令 PDU(NVMe 命令封装)*/
NVME_TCP_SEND_H2C_PDU, /* 发送 H2C Data PDU(写数据)*/
NVME_TCP_SEND_DATA, /* 发送数据负载 */
NVME_TCP_SEND_DDGST, /* 发送数据摘要(可选)*/
};NVMe/TCP PDU 格式:
NVMe/TCP Command PDU:
+----+----+----+----+----+----+----+----+
| PDU Type (0x00) | Flags | Hlen | Pdo |
+----+----+----+----+----+----+----+----+
| PDU Length (4B) |
+----+----+----+----+----+----+----+----+
| NVMe SQE (64B) |
| (Opcode, NSID, CID, LBA, etc.) |
+----+----+----+----+----+----+----+----+
| 可选:数据摘要 / SGL 段 |
+----+----+----+----+----+----+----+----+
NVMe/TCP Completion PDU:
+----+----+----+----+----+----+----+----+
| PDU Type (0x04) | Flags | ... |
+----+----+----+----+----+----+----+----+
| NVMe CQE (16B) |
| (Status, SQHD, CID) |
+----+----+----+----+----+----+----+----+
TLS 支持(Linux 5.20+):
tcp.c 支持 TLS 1.3 加密传输(CONFIG_NVME_TCP_TLS),握手超时默认 10 秒:
/* drivers/nvme/host/tcp.c:50 */
static int tls_handshake_timeout = 10;
module_param(tls_handshake_timeout, int, 0644);drivers/nvme/host/rdma.c 实现了 NVMe/RDMA 传输,基于 InfiniBand verbs(rdma/ib_verbs.h)。
关键数据结构(drivers/nvme/host/rdma.c:42):
struct nvme_rdma_device {
struct ib_device *dev; /* RDMA 设备(HCA)*/
struct ib_pd *pd; /* Protection Domain */
struct kref ref;
unsigned int num_inline_segments; /* 内联数据段数 */
};
struct nvme_rdma_queue {
struct nvme_rdma_qe *rsp_ring; /* 预分配的响应 WR 环 */
int queue_size;
struct nvme_rdma_ctrl *ctrl;
struct ib_cq *ib_cq; /* 完成队列 */
struct ib_qp *qp; /* 队列对 */
struct rdma_cm_id *cm_id; /* RDMA CM 连接 ID */
bool pi_support; /* 数据完整性支持 */
};
struct nvme_rdma_request {
struct nvme_request req;
struct ib_mr *mr; /* 内存区域(RDMA 注册)*/
struct nvme_rdma_qe sqe; /* 发送 WR */
struct ib_sge sge[1 + NVME_RDMA_MAX_INLINE_SEGMENTS];
struct ib_reg_wr reg_wr; /* MR 注册 WR */
struct nvme_rdma_sgl data_sgl; /* 数据 SGL */
bool use_sig_mr; /* 签名 MR(数据完整性)*/
};RDMA 传输流程:
NVMe 命令提交
|
v nvme_rdma_queue_rq()
| 构建 ib_send_wr(发送工作请求)
| 注册内存区域(ib_reg_mr 或内联数据)
|
v ib_post_send() --> RDMA 发送队列
| [硬件通过 RDMA 发送 NVMe SQE]
|
v [Target 完成命令,发送 CQE]
|
v RDMA 完成队列(CQ)中断
v nvme_rdma_recv_done()
| 解析 NVMe CQE
|
v nvme_complete_rq() --> blk_mq_end_request()
RDMA 的核心优势是零拷贝:数据直接在应用程序缓冲区和目标端内存之间传输,不经过内核协议栈,延迟极低(1-5 微秒量级)。
drivers/nvme/host/fabrics.c 提供 nvme-tcp、nvme-rdma、nvme-fc 共用的抽象层:
/* nvmf_host:代表本端 NVMe 主机标识 */
struct nvmf_host {
struct kref ref;
struct list_head list;
char nqn[NVMF_NQN_SIZE]; /* 主机 NQN(qualified name)*/
uuid_t id; /* 主机 UUID */
};
/* nvmf_ctrl_options:连接参数(通过 /dev/nvme-fabrics 配置)*/
struct nvmf_ctrl_options {
unsigned mask;
int transport; /* tcp / rdma / fc */
char *subsysnqn; /* 目标 NQN */
char *traddr; /* 目标地址(IP 或 WWNN)*/
char *trsvcid; /* 服务 ID(端口号,如 "4420")*/
u32 queue_size; /* 每个队列的深度 */
unsigned int nr_io_queues; /* IO 队列数量 */
bool discovery_nqn; /* 是否为 Discovery 连接 */
...
};连接通过 /dev/nvme-fabrics 字符设备发起:
# 连接 NVMe/TCP 目标
echo "transport=tcp,traddr=192.168.1.100,trsvcid=4420,nqn=nqn.2021-01.io.target" \
> /dev/nvme-fabrics
# 连接 NVMe/RDMA 目标
echo "transport=rdma,traddr=192.168.1.100,trsvcid=4791,nqn=..." \
> /dev/nvme-fabricsDM-Multipath 是 Device Mapper 框架的一个 target 实现,其内核代码位于 drivers/md/dm-mpath.c,与 SCSI 层的接口点是 scsi_dh:
用户空间
multipathd / multipath
|
| (ioctl: DM_TABLE_LOAD)
v
/dev/mapper/control (Device Mapper 控制设备)
|
v
dm-core (drivers/md/dm.c)
|
v
dm-mpath target (drivers/md/dm-mpath.c)
|
+-- path_selector (选择路径算法)
| round-robin: 轮询(drivers/md/dm-round-robin.c)
| queue-length: 最短队列(drivers/md/dm-queue-length.c)
| service-time: 最短服务时间(drivers/md/dm-service-time.c)
|
+-- scsi_dh (设备处理器,处理 failover 逻辑)
| alua / emc / rdac / hp_sw
|
`-- paths (path 列表,每个 path = scsi_device)
ALUA 设备处理器(drivers/scsi/device_handler/scsi_dh_alua.c)维护 Target Port Group 状态:
/* drivers/scsi/device_handler/scsi_dh_alua.c:61 */
struct alua_port_group {
struct kref kref;
struct list_head node; /* 挂入全局 port_group_list */
unsigned char device_id_str[256]; /* 用于唯一标识 TPG */
int group_id; /* Target Port Group ID */
int tpgs; /* TPGS 模式:implicit/explicit/both */
int state; /* 当前 TPG 状态 */
int valid_states; /* 设备支持的状态位图 */
unsigned long expiry; /* 状态缓存过期时间 */
struct delayed_work rtpg_work; /* 定期轮询 RTPG 的工作队列 */
};ALUA 状态轮询机制:
scsi_dh_alua 附加到 scsi_device 时:
1. 读取 INQUIRY 数据中的 TPGS 字段
bit[5:4] == 01: implicit ALUA(目标端自动切换)
bit[5:4] == 10: explicit ALUA(主机端主动切换)
bit[5:4] == 11: both
2. 发送 REPORT TARGET PORT GROUPS(RTPG)命令
获取所有 TPG 的当前状态
3. 将结果缓存在 alua_port_group 中
4. 如果是 "Transitioning" 状态,启动 delayed_work
周期性重新查询(间隔 ALUA_RTPG_RETRY_DELAY = 2 秒)
explicit ALUA 切换流程:
IO 路径发生 NOT_READY(ASC 0x04)
|
v alua_check_sense()
| 判断是否需要 STPG(SET TARGET PORT GROUPS)
|
v alua_rtpg_queue()
| 将 STPG 工作加入 kaluad_wq 工作队列
|
v alua_rtpg() [工作队列中执行]
| 发送 SET TARGET PORT GROUPS 命令
| 请求将 Standby/ANO 路径切换为 Active/Optimized
|
v 等待切换完成(轮询 RTPG,最多 ALUA_FAILOVER_TIMEOUT = 60 秒)
|
v 通知 dm-multipath 路径状态已改变
v dm_pg_init_complete()
|
v 重新提交失败的 IO
scsi_dh(drivers/scsi/scsi_dh.c)是 SCSI 设备处理器框架,定义了设备处理器必须实现的接口:
struct scsi_device_handler {
struct list_head list;
struct module *module;
const char *name; /* 处理器名称,如 "alua" */
/* 附加到设备(发现设备时调用)*/
int (*attach)(struct scsi_device *);
void (*detach)(struct scsi_device *);
/* 激活一条路径(切换前准备)*/
int (*activate)(struct scsi_device *, activate_complete, void *);
/* 判断路径是否可用 */
blk_status_t (*prep_fn)(struct scsi_device *, struct request *);
/* 分析 sense data,判断是否需要 failover */
enum scsi_disposition (*check_sense)(struct scsi_device *,
struct scsi_sense_hdr *);
/* 请求路径切换 */
int (*set_params)(struct scsi_device *, const char *);
};设备处理器通过 scsi_register_device_handler() 注册,在 scsi_dh_attach(drivers/scsi/scsi_dh.c)被调用时与 scsi_device 关联。
dm-mpath 的 IO 提交路径(简化):
/* drivers/md/dm-mpath.c */
static int multipath_map_bio(struct dm_target *ti, struct bio *bio)
{
struct multipath *m = ti->private;
struct pgpath *pgpath;
/* 1. 通过 path selector 选择路径 */
pgpath = choose_pgpath(m, bio);
if (!pgpath) {
/* 无可用路径,根据配置决定排队还是报错 */
return queue_or_error(m, bio);
}
/* 2. 通过 scsi_dh 检查路径是否真正可用 */
if (pgpath->pg->ps.type->prep_fn) {
r = pgpath->pg->ps.type->prep_fn(pgpath->path.dev->bdev, bio);
if (r != BLK_STS_OK)
return queue_or_error(m, bio);
}
/* 3. 映射 bio 到选中的路径(scsi_device)*/
bio_set_dev(bio, pgpath->path.dev->bdev);
return DM_MAPIO_REMAPPED;
}UFS(Universal Flash Storage)是移动设备(手机、平板)主流的存储接口标准,由 JEDEC 制定,基于 UniPro 传输层和 MIPI M-PHY 物理层。
+--------------------------------------------------+
| ULD 层(SCSI sd.c) |
| /dev/sdX 或 /dev/sda |
+--------------------------------------------------+
|
+--------------------------------------------------+
| SCSI 中间层 |
| (scsi_lib.c / scsi_error.c) |
+--------------------------------------------------+
|
+--------------------------------------------------+
| ufshcd(UFS HCD,主机控制器驱动) |
| drivers/ufs/core/ufshcd.c |
| - UTP Transfer Request(SCSI 命令通道) |
| - UTP Task Management(任务管理) |
| - UFS Query(设备属性/描述符/标志读写) |
| - UniPro 链路管理(Hibernate/Active 状态切换) |
+--------------------------------------------------+
|
+--------------------------------------------------+
| 平台相关 HCI 实现 |
| drivers/ufs/host/ufs-qcom.c (Qualcomm) |
| drivers/ufs/host/ufs-exynos.c (Samsung) |
| drivers/ufs/host/ufs-mediatek.c (MediaTek) |
| drivers/ufs/host/ufs-hisi.c (HiSilicon) |
+--------------------------------------------------+
|
+--------------------------------------------------+
| UniPro / M-PHY 物理层 |
| HS-G1~G4(半速/全速 档位 1-4) |
| 最高 HS-G4 Lane2 = 23.2 Gbps |
+--------------------------------------------------+
ufs_hba(include/ufs/ufshcd.h:948)是 UFS 子系统的核心结构,等价于 SCSI 的 Scsi_Host:
struct ufs_hba {
void __iomem *mmio_base; /* HCI 寄存器基地址 */
/* DMA 描述符区域 */
struct utp_transfer_cmd_desc *ucdl_base_addr; /* 命令描述符列表 */
struct utp_transfer_req_desc *utrdl_base_addr; /* 传输请求描述符列表 */
struct utp_task_req_desc *utmrdl_base_addr;/* 任务管理请求描述符列表 */
struct Scsi_Host *host; /* 关联的 SCSI host(UFS 通过 SCSI 暴露) */
struct device *dev; /* 平台设备 */
/* UFS 设备标识 */
struct scsi_device *ufs_device_wlun; /* W-LUN 0xC1(设备整体控制)*/
struct scsi_device *ufs_rpmb_wlun; /* W-LUN 0xC4(RPMB 分区)*/
/* 链路状态 */
enum ufs_dev_pwr_mode curr_dev_pwr_mode; /* Active/Sleep/Powerdown */
enum uic_link_state uic_link_state; /* Off/Active/Hibern8/Broken */
/* 请求跟踪 */
unsigned long outstanding_reqs; /* 位图,每位代表一个 slot 是否在用 */
unsigned long outstanding_tasks; /* 位图,任务管理请求跟踪 */
int nutrs; /* 传输请求队列深度(最大 32 或 256 for MCQ)*/
int nutmrs; /* 任务管理请求队列深度(最大 8)*/
/* 功能标志 */
u32 capabilities; /* HCI capabilities 寄存器 */
u32 ufs_version; /* 支持的 UFS 版本(2.1/3.1/4.0)*/
u32 quirks; /* 硬件 quirk 位图 */
/* 变体操作(平台特定回调)*/
const struct ufs_hba_variant_ops *vops;
/* 时钟管理 */
struct ufs_clk_gating clk_gating; /* 时钟门控 */
struct ufs_clk_scaling clk_scaling; /* 时钟调频(devfreq)*/
/* 错误恢复 */
struct work_struct eh_work; /* EH 工作队列 */
u32 errors; /* 错误寄存器状态 */
u32 uic_error; /* UIC 层错误 */
enum ufshcd_state ufshcd_state; /* host 状态机 */
/* MCQ(Multi-Circular Queue,UFS 4.0+)*/
bool mcq_sup;
bool mcq_enabled;
int nr_hw_queues; /* MCQ 模式下的硬件队列数量 */
};UFS 使用 UPIU(UFS Protocol Information Unit)作为命令和数据传输的容器:
UPIU 类型:
COMMAND UPIU (0x01) - SCSI 命令(包含 CDB)
DATA OUT UPIU (0x02) - 写数据
DATA IN UPIU (0x22) - 读数据
RESPONSE UPIU (0x21) - 命令完成响应
TASK MANAGEMENT UPIU (0x04) - 任务管理(abort/reset)
QUERY REQUEST UPIU (0x16) - 属性/描述符查询
QUERY RESPONSE UPIU (0x36) - 查询响应
UPIU 基本格式(32 字节基础头 + 可变扩展):
+----+----+----+----+
| Trans. | Flags | 字节 0-3
+----+----+----+----+
| LUN | Task Tag| 字节 4-7
+----+----+----+----+
| Cmd Set |Reserved | 字节 8-11
+----+----+----+----+
| Data Seg Length | 字节 12-15
+----+----+----+----+
| ...CDB 或数据... | 字节 16+
+----+----+----+----+
ufshcd_lrb(Local Reference Block,include/ufs/ufshcd.h:177)是 UFS 控制器跟踪每条命令的数据结构,类似于 SCSI 的 scsi_cmnd:
struct ufshcd_lrb {
/* DMA 描述符地址 */
struct utp_transfer_req_desc *utr_descriptor_ptr; /* UTRD(传输请求描述符)*/
struct utp_upiu_req *ucd_req_ptr; /* 命令 UPIU */
struct utp_upiu_rsp *ucd_rsp_ptr; /* 响应 UPIU */
struct ufshcd_sg_entry *ucd_prdt_ptr; /* PRDT(物理区域描述符表)*/
/* DMA 物理地址(调试用)*/
dma_addr_t utrd_dma_addr;
dma_addr_t ucd_req_dma_addr;
dma_addr_t ucd_rsp_dma_addr;
dma_addr_t ucd_prdt_dma_addr;
int scsi_status; /* SCSI 状态码(来自响应 UPIU)*/
int command_type; /* SCSI / UFS Query / NOP */
u8 lun; /* LUN(8 位,W-LUN 映射特殊值)*/
bool intr_cmd; /* 是否为中断命令(不参与中断聚合)*/
/* 时间戳(用于性能分析和调试)*/
ktime_t issue_time_stamp;
ktime_t compl_time_stamp;
#ifdef CONFIG_SCSI_UFS_CRYPTO
int crypto_key_slot; /* 内联加密密钥槽(-1 表示不使用)*/
u64 data_unit_num; /* 加密数据单元编号 */
#endif
};sd_init_command() 构造 CDB
|
v SCSI 中间层 scsi_queue_rq()
| --> hostt->queuecommand() --> ufshcd_queuecommand()
|
v ufshcd_queuecommand() [drivers/ufs/core/ufshcd.c]
| 1. 选择一个空闲的 lrb(通过 outstanding_reqs 位图)
| 2. 构建 UPIU:ufshcd_compose_upiu()
| - 填写 Transaction Type = COMMAND UPIU
| - 将 SCSI CDB 复制到 UPIU 的 CDB 字段
| 3. 构建 PRDT:ufshcd_map_sg()
| - 将 scsi_cmnd 的 SG 表转换为 UFS PRDT 格式
| 4. 构建 UTRD:ufshcd_prepare_req_desc_hdr()
| - 设置传输方向、UPIU 地址、PRDT 地址
| 5. 发出 doorbell:ufshcd_send_command()
| - 写 UTP Transfer Request Doorbell 寄存器
|
v [硬件处理:UFS 设备通过 UniPro 接收命令并执行]
|
v 完成中断(UTP_TRANSFER_REQ_COMPL)
v ufshcd_intr() --> ufshcd_transfer_req_compl()
| 1. 读取 UTP Transfer Request Completion Notification 寄存器
| 2. 遍历完成的 lrb
| 3. 解析响应 UPIU,获取 SCSI 状态
| 4. 调用 scsi_done(cmd) 通知 SCSI 中间层
Hibernate8(链路低功耗):
UniPro 链路在空闲时可进入 Hibernate8 状态,节省功耗。ufshcd 通过 ufshcd_hibern8_enter/exit 管理链路状态转换,并维护 uic_link_state 状态机:
UIC_LINK_ACTIVE_STATE <--> UIC_LINK_HIBERN8_STATE
| |
| ufshcd_hibern8_enter() |
+----------------------------->/
|<-----------------------------+
| ufshcd_hibern8_exit() |
|
v 如果 exit 失败:
UIC_LINK_BROKEN_STATE --> 触发 EH(ufshcd_eh_work)
时钟门控(Clock Gating):
/* include/ufs/ufshcd.h:413 */
struct ufs_clk_gating {
struct delayed_work gate_work; /* 延迟门控工作 */
struct work_struct ungate_work; /* 解除门控工作 */
int active_reqs; /* 活跃请求计数 */
bool is_enabled; /* 是否启用门控 */
unsigned long delay_ms; /* 门控延迟(毫秒)*/
};时钟调频(Clock Scaling):
UFS 支持通过 devfreq 框架动态调整 HS-Gear(传输速度档位),在 IO 负载低时降低频率节省功耗:
I/O 繁忙 --> 提升到 HS-G4 Lane2(最高速)
I/O 空闲 --> 降到 HS-G1 Lane1(最低速)
通过 devfreq simple-ondemand 调度器控制
sysfs 接口:
/sys/bus/platform/.../clkscale_enable
/sys/bus/platform/.../clkscale_min_gear
Write Booster(UFS 3.1+):
Write Booster 是 UFS 设备内部的 SLC 写加速缓存,可显著提升短突发写性能。ufshcd 通过 UFS Query 命令管理 Write Booster 的开关。
MCQ(Multi-Circular Queue,UFS 4.0):
UFS 4.0 引入 MCQ 支持多个硬件 IO 队列(类似 NVMe 的 SQ/CQ),对应代码在 drivers/ufs/core/ufs-mcq.c。MCQ 支持的队列数量通过 hba->nr_hw_queues 控制。
UFS 的 EH 通过工作队列(而非独立 kthread)实现:
/* include/ufs/ufshcd.h:512 */
enum ufshcd_state {
UFSHCD_STATE_RESET, /* 链路未建立 */
UFSHCD_STATE_OPERATIONAL, /* 正常工作 */
UFSHCD_STATE_EH_SCHEDULED_NON_FATAL, /* EH 已调度,非致命错误 */
UFSHCD_STATE_EH_SCHEDULED_FATAL, /* EH 已调度,致命错误 */
UFSHCD_STATE_ERROR, /* 不可恢复错误 */
};UFS EH 工作队列(hba->eh_work)处理步骤:
- 收集错误信息(
hba->errors、hba->uic_error) - 尝试 UniPro 链路重置
- 尝试设备重启(通过 WLUN 发送 START STOP UNIT)
- 如果仍然失败,尝试完整的 host 重置
- 最终失败时将 state 设为
UFSHCD_STATE_ERROR,所有命令返回 DID_ERROR
UFS 设备存在大量厂商特定的兼容性问题,通过 hba->quirks 位图处理(include/ufs/ufshcd.h:531):
| Quirk 标志 | 含义 |
|---|---|
UFSHCD_QUIRK_BROKEN_INTR_AGGR |
中断聚合功能不正常,禁用之 |
UFSHCD_QUIRK_BROKEN_LCC |
设备 LCC 命令处理有问题 |
UFSHCD_QUIRK_BROKEN_AUTO_HIBERN8 |
自动 Hibernate8 功能不工作 |
UFSHCD_QUIRK_SKIP_RESET_INTR_AGGR |
不允许软件重置中断聚合计数器 |
UFSHCD_QUIRK_KEYS_IN_PRDT |
加密密钥写入 PRDT,需在请求完成后清零 |
UFSHCD_QUIRK_MCQ_BROKEN_INTR |
MCQ 中断处理有问题,使用替代方式 |
scsi_debug(drivers/scsi/scsi_debug.c)是 Linux 内核内置的 SCSI 测试驱动,可在没有任何物理硬件的情况下创建虚拟 SCSI 设备:
# 加载 scsi_debug 模块,创建一个 1GB 的虚拟 SCSI 磁盘
modprobe scsi_debug dev_size_mb=1024 num_tgts=1
# 创建多个虚拟设备(用于多路径测试)
modprobe scsi_debug dev_size_mb=1024 num_tgts=2 add_host=2
# 验证设备已创建
ls /dev/sd*
cat /proc/scsi/scsi
# 卸载
rmmod scsi_debugscsi_debug 支持的主要参数:
| 参数 | 含义 | 默认值 |
|---|---|---|
dev_size_mb |
虚拟磁盘大小(MB) | 8 |
num_tgts |
每个 host 的 target 数 | 1 |
add_host |
创建的 host 数量 | 1 |
num_parts |
每个 target 的 LUN 数 | 0 |
delay |
命令完成延迟(纳秒) | 1 |
every_nth |
每 N 条命令注入一次错误 | 0 (禁用) |
inq_product_id |
INQUIRY 返回的产品 ID | "scsi_debug" |
dif |
数据完整性字段类型(0-3) | 0 |
dix |
主机 DIF 扩展 | 0 |
lbpu |
启用 UNMAP/DISCARD | 0 |
lbpws |
启用 WRITE SAME | 0 |
max_queue |
队列深度 | 1 |
ndelay |
纳秒级延迟(精细控制) | 0 |
fake_rw |
不实际写入数据(性能测试) | 0 |
queue_type |
队列类型(0=simple, 1=ordered, 2=no) | 0 |
使用 scsi_debug 测试 T10 DIF:
# 创建支持 DIF Type 1 的虚拟设备
modprobe scsi_debug dev_size_mb=256 dif=1 dix=1
# 格式化并测试数据完整性
mkfs.ext4 -b 4096 /dev/sda
mount /dev/sda /mnt/test
# 测试完整性:写入数据后,任何位翻转都应该被检测到scsi_debug 支持通过 sysfs 动态注入各种错误:
# 每 100 条命令注入一次 CHECK_CONDITION(NOT_READY)
echo 100 > /sys/bus/pseudo/drivers/scsi_debug/every_nth
# 注入特定 sense 数据
echo "0x70 0x00 0x04 0x00 0x00 0x00 0x00 0x0a 0x00 0x00 0x00 0x00 0x04 0x01" \
> /sys/bus/pseudo/drivers/scsi_debug/add_host
# 注入超时(通过 delay 参数设置超大延迟)
echo 100000000 > /sys/bus/pseudo/drivers/scsi_debug/delayLinux 内核提供了通用的故障注入框架(CONFIG_FAULT_INJECTION),SCSI 相关的故障注入可通过:
# UFS 故障注入(drivers/ufs/core/ufs-fault-injection.c)
# 需要 CONFIG_SCSI_UFS_FAULT_INJECTION=y
# 查看 UFS 故障注入接口
ls /sys/kernel/debug/ufshcd/
# 注入 UFS command abort
echo 1 > /sys/kernel/debug/ufshcd/0/inject_error_uas
# NVMe 故障注入
# drivers/nvme/host/fault_inject.c
ls /sys/kernel/debug/nvme0/
echo 1 > /sys/kernel/debug/nvme0/fault_inject/times
echo 100 > /sys/kernel/debug/nvme0/fault_inject/probabilityblktests(https://github.com/osandov/blktests)是专为块层和 SCSI 子系统设计的功能测试框架:
# 安装 blktests
git clone https://github.com/osandov/blktests.git
cd blktests && make
# 运行 SCSI 相关测试
./check scsi
# 针对特定设备运行测试
TEST_DEVS=/dev/sda ./check block
# 重要测试用例:
# scsi/001: scsi_debug 基础功能测试
# scsi/002: 队列深度压力测试
# scsi/003: 多路径故障切换测试
# block/001: 基础读写功能
# block/007: 队列深度调整测试fio(Flexible I/O Tester)是最常用的存储压力测试工具,支持各种 SCSI 设备的性能测试:
# SCSI 磁盘顺序读测试 (fio job file: seq_read.fio)
[global]
ioengine=libaio
direct=1
numjobs=1
runtime=60
group_reporting=1
[seq-read]
rw=read
bs=128k
iodepth=32
filename=/dev/sda
# 运行
fio seq_read.fio# 随机 4K 读写混合(模拟数据库负载)
fio --name=randmix --ioengine=libaio --direct=1 \
--rw=randrw --rwmixread=70 --bs=4k \
--iodepth=128 --numjobs=4 --runtime=300 \
--filename=/dev/sda --group_reporting
# 测试 SCSI tagged queuing 效果(对比不同队列深度)
for depth in 1 4 8 16 32 64 128; do
fio --name=test --ioengine=libaio --direct=1 \
--rw=randread --bs=4k --iodepth=$depth \
--numjobs=1 --runtime=10 \
--filename=/dev/sda --output-format=terse \
| awk -F';' '{print "depth='$depth' IOPS="$8}'
done# 发送 TEST UNIT READY,检查设备是否就绪
sg_turs -v /dev/sda
# 读取错误计数器日志页
sg_logs -p 0x03 /dev/sda # Read Error Counter
sg_logs -p 0x05 /dev/sda # Verify Error Counter
sg_logs -p 0x06 /dev/sda # Non-Medium Error
# 查看自检结果
sg_logs -p 0x10 /dev/sda # Self-test Results
# 执行后台自检
sg_senddiag --foreground /dev/sda
# 重置错误计数器
sg_logs --reset /dev/sda
# 发送 SCSI 命令并查看详细 sense data
sg_raw -v /dev/sda 00 00 00 00 00 00 # TEST UNIT READY(详细模式)Linux 内核的 tracepoint 系统提供了丰富的 SCSI 事件追踪:
# 查看所有 SCSI 相关 tracepoint
ls /sys/kernel/debug/tracing/events/scsi/
# 典型事件:
# scsi_dispatch_cmd_start 命令开始下发
# scsi_dispatch_cmd_done 命令下发完成
# scsi_eh_wakeup EH 线程唤醒
# scsi_dispatch_cmd_timeout 命令超时
# 启用所有 SCSI tracepoint
echo 1 > /sys/kernel/debug/tracing/events/scsi/enable
# 只追踪 EH 事件
echo 1 > /sys/kernel/debug/tracing/events/scsi/scsi_eh_wakeup/enable
# 查看追踪结果
cat /sys/kernel/debug/tracing/trace
# 追踪特定命令(过滤 READ 命令,opcode=0x28)
echo 'opcode==0x28' > \
/sys/kernel/debug/tracing/events/scsi/scsi_dispatch_cmd_start/filter
echo 1 > /sys/kernel/debug/tracing/events/scsi/scsi_dispatch_cmd_start/enable# 采集 SCSI 命令提交路径的 CPU 性能数据
perf record -g -e cycles \
-a sleep 10 -- \
fio --name=test --ioengine=libaio --direct=1 \
--rw=randread --bs=4k --iodepth=128 \
--numjobs=4 --runtime=10 --filename=/dev/sda
# 分析热点函数
perf report --sort=dso,symbol | head -50
# 典型热路径(随机读场景):
# scsi_queue_rq() [scsi_lib.c]
# scsi_mq_get_budget() [scsi_lib.c]
# sbitmap_get() [sbitmap.c]
# blk_mq_get_driver_tag() [blk-mq.c]
# hostt->queuecommand() [LLD]当系统崩溃时,可以通过 crash 工具分析 SCSI 状态:
# crash 命令中分析 SCSI 状态
# 查看所有 Scsi_Host
crash> sym scsi_host_list
crash> list -H <scsi_host_list_addr> -o Scsi_Host.sh_list Scsi_Host
# 查看 EH 命令队列
crash> struct Scsi_Host.eh_cmd_q <shost_addr>
crash> list -H <eh_cmd_q_addr> -o scsi_cmnd.eh_entry scsi_cmnd
# 查看 scsi_cmnd 详细信息
crash> struct scsi_cmnd <cmd_addr>
crash> p ((struct scsi_cmnd *)<addr>)->cmnd # 查看 CDB
# 查看当前所有 SCSI 设备
crash> foreach task bt | grep -A5 scsi_error_handler
# 使用 scsi_debug 模拟多路径环境
modprobe scsi_debug dev_size_mb=512 add_host=2 num_tgts=1
# 查看创建的设备
multipath -ll
# 模拟一条路径故障(写入错误触发 scsi_debug 注入 errors)
echo 1 > /sys/bus/pseudo/drivers/scsi_debug/every_nth
# 验证 dm-multipath 自动切换
# IO 应该继续到另一条路径,无中断
# 恢复路径
echo 0 > /sys/bus/pseudo/drivers/scsi_debug/every_nth
# 查看路径状态变化
multipathd show paths| 关键点 | 文件位置 |
|---|---|
scsi_host_template 结构定义 |
include/scsi/scsi_host.h:42 |
Scsi_Host 结构定义 |
include/scsi/scsi_host.h:558 |
scsi_device 结构定义 |
include/scsi/scsi_device.h:103 |
scsi_cmnd 结构定义 |
include/scsi/scsi_cmnd.h:74 |
| blk-mq ops 注册 | drivers/scsi/scsi_lib.c:2054 |
| 命令入队(热路径) | drivers/scsi/scsi_lib.c:1829 |
| 命令分发至 LLD | drivers/scsi/scsi_lib.c:1589 |
| 命令完成处理 | drivers/scsi/scsi_lib.c:1541 |
scsi_execute_cmd 同步接口 |
drivers/scsi/scsi_lib.c:295 |
scsi_set_blocked 退避 |
drivers/scsi/scsi_lib.c:79 |
| EH 线程主循环 | drivers/scsi/scsi_error.c:2342 |
| EH 唤醒函数 | drivers/scsi/scsi_error.c:64 |
| EH 多级 reset | drivers/scsi/scsi_error.c:1048 |
BUS/HOST_RESET_SETTLE_TIME |
drivers/scsi/scsi_error.c:57-58 |
| SCSI 设备状态机 | include/scsi/scsi_device.h:38 |
scsi_track_queue_full |
drivers/scsi/scsi.c:259 |
| ATA→SCSI 命令入口 | drivers/ata/libata-scsi.c:4502 |
| ATA→SCSI 命令翻译 | drivers/ata/libata-scsi.c:4427 |
ata_device 结构 |
include/linux/libata.h:719 |
ata_port 结构 |
include/linux/libata.h:869 |
sd_init_command |
drivers/scsi/sd.c:1470 |
sd_done |
drivers/scsi/sd.c:2310 |
| ALUA 设备处理器 | drivers/scsi/device_handler/scsi_dh_alua.c |
| ALUA port group 结构 | drivers/scsi/device_handler/scsi_dh_alua.c:61 |
| libsas 设备发现 | drivers/scsi/libsas/sas_discover.c:49 |
| libsas LLD 回调表 | include/scsi/libsas.h |
ufs_hba 结构定义 |
include/ufs/ufshcd.h:948 |
ufshcd_lrb 结构定义 |
include/ufs/ufshcd.h:177 |
| UFS 状态机 | include/ufs/ufshcd.h:512 |
| UFS 时钟门控 | include/ufs/ufshcd.h:413 |
| UFS quirk 定义 | include/ufs/ufshcd.h:531 |
| nvme-tcp 发送状态机 | drivers/nvme/host/tcp.c:97 |
| nvme-rdma 队列结构 | drivers/nvme/host/rdma.c:85 |
| nvme-fabrics 主机管理 | drivers/nvme/host/fabrics.c:25 |
| scsi_debug 模块参数 | drivers/scsi/scsi_debug.c:64 |
| UFS 故障注入 | drivers/ufs/core/ufs-fault-injection.c |
由 Claude Code 分析生成