Skip to content

Latest commit

 

History

History
3089 lines (2465 loc) · 103 KB

File metadata and controls

3089 lines (2465 loc) · 103 KB

Linux SCSI 子系统深度解析

基于 Linux Kernel 源码深度分析 路径:drivers/scsi/ | include/scsi/ | drivers/ata/


目录

  1. SCSI 子系统概述
  2. 核心数据结构
  3. SCSI 命令生命周期
  4. SCSI 中间层
  5. 错误处理与恢复
  6. libata(SATA/PATA)
  7. SCSI 设备类型驱动
  8. 多路径(DM-Multipath)
  9. SAS/iSCSI/NVMe-oF
  10. drivers/scsi/ 目录主要文件一览
  11. 调试工具
  12. SCSI 架构层次深度解析:ULD、midlayer、HBA 驱动
  13. scsi_host 与 scsi_device 数据结构详解
  14. SCSI 命令生命周期深度解析
  15. 错误恢复机制(EH)深度解析
  16. SCSI 队列深度管理与 Tagged Command Queuing
  17. libsas 框架深度解析
  18. NVMe over Fabrics:nvme-tcp 与 nvme-rdma
  19. SCSI Multipath 与 scsi_dh 深度解析
  20. UFS(Universal Flash Storage)子系统深度解析
  21. SCSI 压力测试与故障注入
  22. 附录:关键代码定位速查

1. SCSI 子系统概述

1.1 在存储栈中的位置

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                |
+------------------------------------------------------------------+

1.2 SCSI 子系统组成

SCSI 子系统可以分为三个层次:

层次 代码位置 主要职责
上层驱动(ULD) drivers/scsi/sd.csr.cst.csg.c 设备类型驱动,与块层/字符设备接口
SCSI 中间层 drivers/scsi/scsi_lib.cscsi_error.c 队列管理、错误恢复、blk-mq 集成
低层驱动(LLD / HBA) drivers/scsi/hpsa.c 等;drivers/ata/ 控制器硬件操作

1.3 历史演进

SCSI 标准(Small Computer System Interface)起源于 1986 年,最初为并行总线设计。Linux SCSI 子系统从早期的单队列模式,历经 blk-mq 改造(Linux 3.13+),全面拥抱多队列,并通过 libata 将 SATA/PATA 设备统一纳入 SCSI 框架管理,形成当前的架构。


2. 核心数据结构

2.1 scsi_host_template — 驱动模板

文件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 风格控制器

2.2 Scsi_Host — 主机适配器实例

文件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

2.3 scsi_device — SCSI 逻辑单元

文件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

2.4 scsi_cmnd — SCSI 命令

文件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 ...

2.5 数据结构关系图

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]

3. SCSI 命令生命周期

3.1 完整流程图

用户空间 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

3.2 关键函数分析

scsi_queue_rqdrivers/scsi/scsi_lib.c:1829

blk-mq 的 queue_rq 回调,是 SCSI 中间层的命令入口

  1. 调用 scsi_mq_get_budgetsdev->budget_map 申请队列预算 token(基于 sbitmap,无锁实现)
  2. 检查设备/target/host 三层就绪状态,任一不满足则返回 BLK_STS_RESOURCE
  3. 调用 scsi_prepare_cmd,进而调用 ULD 的 init_command(如 sd_init_command)填充 CDB
  4. 设置 SCMD_TAGGED 标志(若设备支持 tagged queueing)
  5. 调用 scsi_dispatch_cmd 将命令推送至 LLD

scsi_dispatch_cmddrivers/scsi/scsi_lib.c:1589

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);
    ...
}

scsi_done / scsi_completedrivers/scsi/scsi_lib.c:1541

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;
    }
}

4. SCSI 中间层

4.1 blk-mq 集成

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_tagsdrivers/scsi/scsi_lib.c:2103):

  • cmd_size = sizeof(scsi_cmnd) + hostt->cmd_size + SG 内联空间
  • blk-mq 为每个 request 分配固定大小的 PDU 区域,scsi_cmnd 嵌入其中

4.2 标签队列(Tagged Queueing)

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)

4.3 命令重试与退避

scsi_set_blockeddrivers/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)阻止新命令入队,直至计数归零。


5. 错误处理与恢复

SCSI 错误处理(Error Handling,EH)是整个子系统中最复杂的部分,采用专属内核线程 + 多级 reset 层次的设计。

5.1 EH 线程

每个 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 */
    }
}

5.2 触发路径

命令超时(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)

5.3 多级 Reset 层次

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);
}

5.4 Sense Data 处理

CHECK_CONDITION 状态下,EH 通过 REQUEST SENSE 命令获取 sense data(scsi_eh_prep_cmnddrivers/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 重试

5.5 EH deadline 机制

Scsi_Host.eh_deadline(默认 -1 无限制)可设置 EH 最大允许时间。超出 deadline 后,scsi_host_eh_past_deadline 返回 true,EH 跳过耗时操作(如总线 reset),直接使设备下线。


6. libata(SATA/PATA)

libata 是将 ATA/ATAPI 设备伪装成 SCSI 设备的兼容层,使 ATA 磁盘可以通过标准 SCSI 接口被 sd、sr 等驱动管理。

6.1 架构

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 控制器

6.2 核心数据结构

ata_portinclude/linux/libata.h:869

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 */
    ...
};

ata_deviceinclude/linux/libata.h:719

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[];
    ...
};

ata_linkinclude/linux/libata.h:841

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 活跃命令位图 */
    ...
};

6.3 SCSI 命令翻译示例

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 SENSEREAD CAPACITY),libata 通过 ata_scsi_simulatedrivers/ata/libata-scsi.c模拟返回适当响应,而不是真正发送给设备。

6.4 NCQ(Native Command Queuing)

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 回调报告完成的命令集合

7. SCSI 设备类型驱动

上层驱动(Upper Layer Driver,ULD)为不同类型的 SCSI 设备提供操作接口。

7.1 ULD 注册机制

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 *);
};

7.2 sd(SCSI Disk Driver)

文件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_commanddrivers/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:UNMAPWRITE SAME WITH UNMAP
  • T10 PI(DIF):设置 prot_op 字段

sd_donedrivers/scsi/sd.c:2310):

处理完成结果,包括 RECOVERED_ERROR(记录日志后成功)、介质错误(上报 EIO)等。

支持的高级特性

  • sd_dif.c:T10 DIF/DIX 数据完整性(Data Integrity Field)
  • sd_zbc.c:ZBC(Zoned Block Command)分区块设备支持

7.3 sr(SCSI CD-ROM/DVD Driver)

文件drivers/scsi/sr.c

处理 TYPE_ROM、TYPE_WORM,提供 /dev/sr0 等光驱设备:

  • 实现 CDROMREADTOCHDRCDROMREADTOCENTRY 等 CD 特有 ioctl
  • 使用 READ(10) 读取数据,READ CAPACITY 检测介质
  • 通过 sdev->changed 标志处理介质更换事件

7.4 st(SCSI Tape Driver)

文件drivers/scsi/st.c

处理 TYPE_TAPE,提供 /dev/st0(倒带)和 /dev/nst0(非倒带):

  • 字符设备,不使用块层
  • 使用 SPACEREWINDWRITE_FILEMARKSREADWRITE 等磁带专用命令
  • 复杂的缓冲管理(固定块模式 vs 可变块模式)

7.5 sg(SCSI Generic Driver)

文件drivers/scsi/sg.c

提供 /dev/sg* 通用 SCSI pass-through 接口:

  • 用户空间可直接构造任意 SCSI CDB 并发送
  • smartctlsg_utilscdparanoia 等工具均通过 sg 接口工作
  • 通过 SG_IO ioctl 发送命令,结构体 sg_io_hdr_t 描述 CDB + 数据缓冲区

7.6 其他类型驱动

驱动文件 设备类型 TYPE 值
ch.c SCSI 磁带自动换带机(Changer) TYPE_MEDIUM_CHANGER (0x08)
ses.c SCSI 存储机箱服务(SES/SESZ) TYPE_ENCLOSURE (0x0D)

8. 多路径(DM-Multipath)

8.1 概述

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 存储阵列

8.2 SCSI Device Handler(scsi_dh)

SCSI 设备处理器(Device Handler)是 SCSI 层与 DM-Multipath 之间的接口层,处理存储阵列特定的 path 切换逻辑。

文件drivers/scsi/scsi_dh.cdrivers/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)

8.3 path 切换流程

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

9. SAS/iSCSI/NVMe-oF

9.1 SAS(Serial Attached SCSI)

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;
    ...
};

9.2 iSCSI(SCSI over TCP/IP)

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 发送,目标端解封装后执行,结果再封装返回。

9.3 NVMe-oF(NVMe over Fabrics)

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 命令。

9.4 协议对比

属性 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)

10. drivers/scsi/ 目录主要文件一览

10.1 SCSI 中间层(核心)

文件 功能
scsi.c SCSI 初始化、模块注册
scsi_lib.c 命令构建、队列管理、blk-mq 集成(热路径核心)
scsi_error.c 错误处理线程、abort/reset、sense 解析
scsi_scan.c 总线扫描、INQUIRY、设备发现
hosts.c Scsi_Host 分配/释放(scsi_host_allocscsi_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 传输层

10.2 上层驱动(ULD)

文件 设备类型
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 存储机箱服务

10.3 设备处理器(DM-Multipath)

文件 适用阵列
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

10.4 代表性 LLD(低层驱动)

文件/目录 硬件
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 设备(调试/测试用)

10.5 drivers/ata/ 关键文件

文件 功能
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)

11. 调试工具

11.1 /proc/scsi/ 接口

# 查看所有 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

11.2 /sys/class/scsi_device//sys/block/

# 查看设备状态(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

11.3 sg_utils(SCSI 通用工具集)

# 查询设备信息(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

11.4 smartctl(S.M.A.R.T. 工具)

# 通过 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

11.5 内核 SCSI 日志(SCSI_LOG)

通过 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 超时处理

11.6 blktrace / blkparse

# 跟踪 SCSI 设备的块层 IO(含队列事件)
blktrace -d /dev/sda -o trace
blkparse trace.blktrace.0 | head -50

# 关键事件类型:
# Q: 命令入队
# G: 获取 request
# I: 命令进入 IO 调度器
# D: 分发给驱动(dispatch)
# C: 命令完成

11.7 debugfs SCSI 接口

# 查看 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

11.8 多路径工具

# 查看多路径状态(dm-multipath)
multipath -ll

# 查看路径健康状态
multipathd show paths

# 手动切换路径
multipathd switchpathgroup multipath_name 1

# 查看 ALUA 状态
cat /sys/block/sda/device/access_state

12. SCSI 架构层次深度解析:ULD、midlayer、HBA 驱动

12.1 三层架构的职责边界

SCSI 子系统的三层设计是 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 通知]

12.2 scsi_host_template 注册流程

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_allocdrivers/scsi/hosts.c)分配 Scsi_Host 结构,其中 hostdata[] 弹性数组存放 LLD 私有数据(大小由 sizeof(struct ctlr_info) 决定)。

12.3 设备扫描机制

scsi_scan_hostdrivers/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 字节)

12.4 传输层(Transport Layer)

传输层位于 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

13. scsi_host 与 scsi_device 数据结构详解

13.1 Scsi_Host 完整字段解析

Scsi_Hostinclude/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 时释放

13.2 scsi_device 完整字段解析

scsi_deviceinclude/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[]       (弹性数组)

13.3 scsi_target 结构

scsi_targetinclude/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,实际计数)

13.4 内存布局与 cacheline 优化

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 字节)

14. SCSI 命令生命周期深度解析

14.1 scsi_execute_cmd:同步命令接口

scsi_execute_cmddrivers/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);

14.2 CDB 构造:ULD 的核心工作

不同操作对应的 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)

14.3 命令分发的详细过程

scsi_queue_rqdrivers/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;
}

14.4 命令完成路径的详细分析

scsi_completedrivers/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()

14.5 命令超时处理

blk-mq 超时机制通过 scsi_timeoutdrivers/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 线程

15. 错误恢复机制(EH)深度解析

15.1 EH 线程的等待条件

EH 线程(scsi_error_handlerdrivers/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 都已处理完毕后才介入,避免干扰正常路径。

15.2 scsi_unjam_host 完整流程

scsi_unjam_hostdrivers/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 队列

15.3 EH 多级恢复的 scsi_eh_ready_devs 细节

/* 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_TIMEHOST_RESET_SETTLE_TIMEdrivers/scsi/scsi_error.c:57-58 均定义为 10 秒,这是为了等待设备在 reset 后稳定就绪。

15.4 sense data 详细解析

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_sensedrivers/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;
};

15.5 EH 与传输层的交互

对于 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_hostdrivers/scsi/libsas/sas_scsi_host.c)实现了 SAS 特有的恢复策略,包括:

  • SAS abort task(SMP 管理帧)
  • SAS target reset(TMF: Task Management Function)
  • SAS 端口 reset
  • 重新发现 SAS 域(sas_rediscover_dev)

16. SCSI 队列深度管理与 Tagged Command Queuing

16.1 sbitmap:高效无锁队列深度控制

SCSI 中间层使用 sbitmapinclude/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 重构时引入的关键优化。

16.2 队列深度动态调整

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。这是一个渐进式的自适应机制。

16.3 Tagged Command Queuing 的三种 tag 类型

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 等接口触发。

16.4 多队列(Multi-Queue)架构下的队列映射

在支持多个硬件队列的 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。


17. libsas 框架深度解析

17.1 libsas 架构概述

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          |
+--------------------------------------------------+

17.2 SAS 域发现流程

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 管理

17.3 libsas 关键数据结构

domain_deviceinclude/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 *);
    ...
};

17.4 libsas 与 libata 的集成

当 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


18. NVMe over Fabrics:nvme-tcp 与 nvme-rdma

18.1 NVMeOF 整体架构

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 直接与块层交互。

18.2 nvme-tcp 深度解析

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);

18.3 nvme-rdma 深度解析

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 微秒量级)。

18.4 nvme-fabrics 公共框架

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-fabrics

19. SCSI Multipath 与 scsi_dh 深度解析

19.1 dm-multipath 内核架构

DM-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)

19.2 alua_port_group:ALUA 状态管理

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

19.3 scsi_dh 接口

scsi_dhdrivers/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_attachdrivers/scsi/scsi_dh.c)被调用时与 scsi_device 关联。

19.4 多路径 IO 路径选择

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;
}

20. UFS(Universal Flash Storage)子系统深度解析

20.1 UFS 简介与架构位置

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            |
+--------------------------------------------------+

20.2 ufs_hba:UFS 主机控制器结构

ufs_hbainclude/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 模式下的硬件队列数量 */
};

20.3 UFS 协议层次:UPIU

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+
  +----+----+----+----+

20.4 ufshcd_lrb:本地引用块

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
};

20.5 UFS 命令提交流程

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 中间层

20.6 UFS 特有功能

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 控制。

20.7 UFS 错误处理

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)处理步骤:

  1. 收集错误信息(hba->errorshba->uic_error
  2. 尝试 UniPro 链路重置
  3. 尝试设备重启(通过 WLUN 发送 START STOP UNIT)
  4. 如果仍然失败,尝试完整的 host 重置
  5. 最终失败时将 state 设为 UFSHCD_STATE_ERROR,所有命令返回 DID_ERROR

20.8 UFS Quirk 机制

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 中断处理有问题,使用替代方式

21. SCSI 压力测试与故障注入

21.1 scsi_debug:虚拟 SCSI 设备

scsi_debugdrivers/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_debug

scsi_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
# 测试完整性:写入数据后,任何位翻转都应该被检测到

21.2 故障注入:scsi_debug 的错误模拟

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/delay

21.3 内核故障注入框架

Linux 内核提供了通用的故障注入框架(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/probability

21.4 blktests:块层功能测试套件

blktestshttps://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: 队列深度调整测试

21.5 fio:IO 压力测试

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

21.6 sg_utils 故障诊断工具集

# 发送 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(详细模式)

21.7 内核 ftrace 追踪 SCSI 事件

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

21.8 perf 分析 SCSI 热路径

# 采集 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]

21.9 crash dump 中的 SCSI 调试

当系统崩溃时,可以通过 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

21.10 多路径故障切换测试

# 使用 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 分析生成