7. Linux内核SCSI子系统¶
在现代Linux存储架构中,尽管NVMe和SATA等协议在消费级市场占据主导,但SCSI(Small Computer System Interface,小型计算机系统接口)协议及其子系统架构依然是整个Linux存储栈的基石。 无论是企业级的SAS、光纤通道(Fibre Channel),还是网络存储(iSCSI),甚至是常见的USB大容量存储设备(USB Mass Storage),在Linux内核中最终都会被抽象并映射到SCSI子系统中进行处理。
7.1. SCSI基础知识¶
7.1.1. SCSI的作用¶
SCSI子系统提供统一的设备发现和I/O调度框架。它屏蔽了底层物理总线(SATA, USB, 网络)的差异,让上层的块设备层只需面对标准的SCSI命令,极大地简化了存储驱动的开发。
7.1.2. SCSI的发展与演进¶
SCSI最初是一种并行硬件总线标准,用于连接硬盘、磁带机等外设。随着技术发展,物理总线逐渐被串行技术取代(如SAS、Fibre Channel),但SCSI命令集协议(SCSI Command Set)被保留并标准化下来。 如今,SCSI更多是指一套逻辑协议标准(如SPC-4主命令集、SBC-3块命令集),它定义了主机与存储设备之间如何进行通信。
初代并行SCSI:早期8/16位并行总线协议,传输速率低、抗干扰能力弱,仅用于传统机械硬盘、光驱设备,现已基本淘汰。
SCSI-2/SCSI-3标准化:完善协议命令集、设备模型、错误处理机制,奠定了现代SCSI协议的基础架构,支持多设备挂载、并行I/O处理。
串行SCSI体系:衍生出SAS、SATA、FC等主流串行存储协议,均兼容SCSI命令集,保留SCSI协议核心逻辑,大幅提升传输速率和稳定性,成为当前服务器、PC存储的主流协议。
SPC-4及最新规范:标准化缓存管理、数据校验、异步通知等高级功能,Linux内核SCSI子系统默认遵循SPC-4规范,本章节实验驱动也基于该规范实现设备交互。
7.1.3. SCSI的工作原理¶
SCSI协议采用客户端-服务器(发起者-目标设备)模型,工作原理为:主机适配器(发起者)向SCSI设备(目标设备)发送标准化SCSI命令,设备解析命令并执行对应操作(读写数据、查询信息、设备校验等),完成后返回执行结果与状态信息,内核根据状态处理I/O请求或异常错误。
其核心概念包括:
Initiator(发起者/主机):发起I/O请求的一端,即服务器/PC的SCSI控制器(HBA)。
Target(目标设备):接收并执行命令的存储设备控制器。
LUN(Logical Unit Number,逻辑单元号):Target内部的具体逻辑存储实体(如一块具体的硬盘)。一个Target可以包含多个LUN。
CDB(Command Descriptor Block,命令描述块):Initiator发送给Target的命令包,包含操作码、LBA地址、扇区数量、传输长度等关键信息。例如6字节、10字节或16字节的READ/WRITE命令。
Sense Data(感知数据):当命令执行失败或出现异常时,Target返回给Initiator的详细错误信息(包含Sense Key, ASC, ASCQ),内核据此完成错误处理、重试、告警等操作。
SG(散列聚合):Linux内核I/O数据传输机制,支持将非连续内存缓冲区的数据聚合传输,适配大块存储I/O,SCSI子系统原生支持SG机制。
7.2. SCSI子系统整体架构¶
Linux内核SCSI子系统采用分层架构设计,从上至下分为SCSI较高层、SCSI中间层、SCSI较低层,各层各司其职、相互协作,既保证了上层接口的统一性,又保留了底层硬件的适配灵活性。
7.2.1. SCSI较高层¶
SCSI较高层对接Linux通用块设备层、文件系统层,负责设备抽象、请求封装、用户态交互,与硬件完全无关。 主要功能包括:SCSI设备枚举、设备节点创建、块设备请求转换为SCSI标准命令、设备属性管理、用户态ioctl接口适配、设备状态上报等。
较高层将文件系统的读写请求、用户态设备查询请求,统一封装为标准SCSI命令结构体,下发至中间层处理,屏蔽底层硬件差异。
7.2.2. SCSI中间层¶
SCSI中间层是整个SCSI子系统的调度枢纽,承上启下,主要功能包括:SCSI主机适配器管理、SCSI命令队列调度、命令预处理/后处理、Sense错误数据解析、通用错误处理、SG散列聚合数据拷贝。
中间层屏蔽了底层硬件的私有交互逻辑,为上层提供统一的命令处理入口,同时为底层驱动提供标准化的回调接口、工具函数。
7.2.3. SCSI较低层¶
SCSI较低层为硬件适配层,由厂商私有驱动或虚拟驱动组成,对接具体硬件设备。主要功能包括:实现中间层定义的回调函数、硬件命令下发、硬件数据读写、硬件错误处理、私有资源管理、异步命令调度等。
较低层驱动无需关心SCSI协议通用逻辑,仅需适配自身硬件特性,完成命令的硬件执行。
7.3. SCSI子系统核心结构体¶
7.3.1. SCSI驱动管理结构体¶
SCSI驱动管理结构体(struct scsi_driver)是SCSI较高层核心驱动管理结构体,基于内核标准设备驱动模型封装,用于抽象SCSI类型驱动的通用行为接口。 该结构体承接设备生命周期管理、命令初始化/销毁、命令完成回调、错误恢复处理等逻辑,是所有SCSI设备驱动注册、绑定、业务调度的载体。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | struct scsi_driver {
struct device_driver gendrv; // 内核通用设备驱动基类,绑定驱动模型
/* 设备休眠恢复回调:设备从休眠唤醒时触发 */
int (*resume)(struct device *);
/* 设备重扫描回调:总线重扫描、热插拔重枚举时触发 */
void (*rescan)(struct device *);
/* 命令初始化回调:SCSI命令创建后、下发前初始化 */
blk_status_t (*init_command)(struct scsi_cmnd *);
/* 命令销毁回调:SCSI命令完成后、释放前资源清理 */
void (*uninit_command)(struct scsi_cmnd *);
/* 命令完成回调:命令处理收尾、结果上报 */
int (*done)(struct scsi_cmnd *);
/* 错误处理回调:EH机制下执行命令重试、终止等操作 */
int (*eh_action)(struct scsi_cmnd *, int);
/* 错误重置回调:SCSI设备/主机异常重置时触发 */
void (*eh_reset)(struct scsi_cmnd *);
};
|
7.3.2. SCSI主机实例结构体¶
SCSI主机实例结构体(struct Scsi_Host)用于描述、管理SCSI主机适配器的顶层核心结构体,是整个SCSI子系统的管理载体。该结构体统一维护主机硬件能力、挂载设备链表、I/O队列参数、错误恢复资源、私有数据空间,所有SCSI设备、命令调度、异常处理、主机重置操作均依托该结构体完成,是SCSI底层驱动与内核中间层交互的载体。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | struct Scsi_Host {
struct list_head __devices; // 本机SCSI设备链表头,管理所有LUN设备
struct list_head __targets; // 本机Target目标设备链表头,管理所有设备节点
spinlock_t *host_lock; // 主机全局自旋锁,保护命令/I/O并发操作
struct mutex scan_mutex; // 设备扫描互斥锁,防止并发扫描冲突
struct list_head eh_abort_list; // 错误处理:待中止命令链表
struct list_head eh_cmd_q; // 错误处理:异常待处理命令队列
struct task_struct *ehandler; // 错误处理内核线程,专门处理SCSI异常恢复
struct scsi_host_template *hostt; // 绑定本机SCSI主机模板,回调函数/能力参数载体
struct scsi_transport_template *transportt; // SCSI传输层模板,管理传输协议属性
struct blk_mq_tag_set tag_set; // 块多队列标签集合,管理I/O队列深度与请求标签
atomic_t host_blocked; // 主机阻塞计数,控制主机启停调度状态
unsigned int host_no; // 系统全局SCSI主机唯一编号,用于标识与proc展示
unsigned int max_channel; // 支持的最大通道号,限制设备通道寻址范围
unsigned int max_id; // 支持的最大Target ID,限制设备ID寻址范围
u64 max_lun; // 支持的最大LUN编号,限制逻辑单元寻址范围
unsigned short max_cmd_len; // 主机支持的最大SCSI CDB命令长度
int this_id; // 本机在SCSI总线上的适配器ID
int can_queue; // 主机硬件最大并发排队命令数量
short cmd_per_lun; // 单个LUN设备允许的最大并发命令数
unsigned short sg_tablesize; // 单次I/O支持的最大SG散列聚合分片数量
unsigned int max_sectors; // 单条SCSI命令允许处理的最大扇区数
unsigned nr_hw_queues; // SCSI主机硬件队列数量,决定并发调度粒度
unsigned host_self_blocked:1; // 位域标记:主机是否主动阻塞,暂停接收新请求
unsigned host_tagset:1; // 位域标记:是否启用全局主机级标签队列
struct device shost_dev; // SCSI主机设备模型对象,用于sysfs设备展示
struct device *dma_dev; // DMA绑定设备指针,用于虚拟主机DMA寻址适配
unsigned long hostdata[] __attribute__ ((aligned (sizeof(unsigned long)))); // 柔性数组,驱动私有数据存储空间
/* 其他成员省略 */
};
|
7.3.3. SCSI主机模板结构体¶
SCSI主机模板结构体(struct scsi_host_template)用于定义SCSI主机适配器的能力参数、回调函数接口,是底层驱动与SCSI中间层的绑定载体。内核通过该模板识别驱动的命令处理方式、错误处理能力、硬件参数等特性。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | struct scsi_host_template {
int (*queuecommand)(struct Scsi_Host *, struct scsi_cmnd *); // SCSI命令下发入口,底层驱动核心实现
int (*eh_abort_handler)(struct scsi_cmnd *); // 命令中止处理
int (*eh_host_reset_handler)(struct scsi_cmnd *); // 主机重置处理
struct module *module; // 绑定当前内核模块
const char *name; // SCSI主机适配器名称
int can_queue; // 主机最大并发排队命令数
int this_id; // SCSI总线主机适配器ID
short cmd_per_lun; // 单个LUN最大并发命令数
unsigned int max_sectors; // 单条命令最大处理扇区数
unsigned short sg_tablesize; // 最大SG散列聚合分片数
unsigned emulated:1; // 标记是否为虚拟模拟设备
/* 其他成员省略 */
};
|
7.3.4. SCSI命令结构体¶
SCSI命令结构体(struct scsi_cmnd)用于封装单条SCSI命令的所有信息,包含命令描述块、传输方向、设备信息、执行结果、Sense错误缓冲区、剩余传输长度等,是SCSI命令处理的数据载体。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | struct scsi_cmnd {
struct scsi_device *device; // 关联的SCSI设备对象
unsigned long jiffies_at_alloc; // 命令分配时间戳,用于超时统计
int retries; // 命令重试次数
unsigned short cmd_len; // SCSI命令CDB有效长度
enum dma_data_direction sc_data_direction; // 数据传输方向
unsigned char cmnd[32]; // SCSI命令描述块(CDB),存储操作码+参数
struct scsi_data_buffer sdb; // 命令数据传输缓冲区
unsigned resid_len; // 未完成传输的剩余数据长度
unsigned sense_len; // Sense错误数据有效长度
unsigned char *sense_buffer; // 错误感知数据缓冲区,存储异常信息
int flags; // 命令状态标志位
unsigned long state; // 命令当前执行状态
int result; // 底层驱动返回的命令执行结果码
/* 其他成员省略 */
};
|
7.3.5. SCSI逻辑设备结构体¶
SCSI逻辑设备结构体(struct scsi_device)用于抽象一个SCSI逻辑设备(LUN)。内核每扫描到一个可用SCSI磁盘/设备,都会生成一个该结构体实例, 统一管理设备属性、状态、队列参数、超时重试策略,是上层块设备与底层SCSI主机的中间映射载体。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | struct scsi_device {
struct Scsi_Host *host; // 所属SCSI主机适配器指针
struct scsi_target *sdev_target; // 所属Target目标设备指针
struct request_queue *request_queue; // 设备块请求队列,管理I/O请求调度
struct list_head siblings; // 同一Host下所有SCSI设备链表节点
struct list_head same_target_siblings; // 同一Target下的设备链表节点
atomic_t device_blocked; // 设备阻塞标记,处理QUEUE_FULL状态
unsigned short queue_depth; // 当前设备I/O队列深度
unsigned short max_queue_depth; // 设备支持的最大队列深度
unsigned int id; // 设备Target ID
unsigned int channel; // 设备总线通道号
u64 lun; // 设备逻辑单元LUN编号
unsigned sector_size; // 设备单扇区字节大小
unsigned char type; // SCSI设备类型(磁盘/光驱/RAID等)
char scsi_level; // 设备支持的SCSI协议版本
char inq_periph_qual; // INQUIRY外设设备有效状态标识
const char *vendor; // 设备厂商名称
const char *model; // 设备型号信息
const char *rev; // 设备固件版本
unsigned int eh_timeout; // SCSI错误处理超时时间
unsigned removable:1; // 可移动设备标记
unsigned offline:1; // 设备离线状态标记
unsigned ready:1; // 设备就绪状态标记
unsigned tagged_supported:1; // 设备是否支持SCSI标签队列
atomic_t iorequest_cnt; // 累计下发I/O请求计数
atomic_t iodone_cnt; // 累计完成I/O请求计数
atomic_t ioerr_cnt; // 累计I/O错误计数
struct device sdev_dev; // 内核设备模型对象,用于sysfs管理
enum scsi_device_state sdev_state; // 设备当前运行状态枚举
void *hostdata; // 底层驱动私有数据指针
unsigned long sdev_data[] __attribute__((aligned(sizeof(unsigned long)))); // 设备私有柔性数组
/* 其他成员省略 */
};
|
7.3.6. SCSI目标设备结构体¶
SCSI目标设备结构体(struct scsi_target)用于管理SCSI总线上的一个目标设备节点(Target),一个Target下可挂载多个LUN(scsi_device)。 该结构体负责统一维护Target级别的设备状态、扫描信息、资源引用,实现多LUN设备的统一管理。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | struct scsi_target {
struct list_head siblings; // 同一主机下所有Target设备链表节点
struct list_head devices; // 该Target下属所有LUN设备链表头
struct device dev; // Target内核设备模型对象,用于sysfs管理
struct kref reap_ref; // Target设备引用计数,防止内存泄漏
unsigned int channel; // 设备所属总线通道号
unsigned int id; // Target设备唯一ID
atomic_t target_busy; // 统计底层驱动活跃命令数
atomic_t target_blocked; // Target阻塞标记,处理设备繁忙状态
unsigned int can_queue; // Target级最大并发命令数限制
char scsi_level; // 设备支持的SCSI协议版本
enum scsi_target_state state; // Target当前运行状态枚举
void *hostdata; // 底层驱动私有数据指针
unsigned long starget_data[] __attribute__((aligned(sizeof(unsigned long)))); // 传输层私有柔性数组
/* 其他成员省略 */
};
|
7.3.7. SCSI数据缓冲区结构体¶
SCSI数据缓冲区结构体(struct scsi_data_buffer)内嵌于scsi_cmnd,所有SCSI读写、校验、拷贝的数据传输均通过该结构体完成,统一管理线性内存与SG散列聚合内存。
1 2 3 4 | struct scsi_data_buffer {
struct sg_table table; // SG散列聚合内存表,用于大块数据传输
unsigned length; // 当前数据缓冲区有效长度
};
|
7.3.8. SCSI磁盘结构体¶
SCSI磁盘结构体(struct scsi_disk)用于描述SCSI类型磁盘设备,承接上层scsi_device设备,绑定内核标准块设备gendisk,完成SCSI协议设备到系统通用磁盘设备的映射。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | struct scsi_disk {
struct scsi_device *device; // 关联的SCSI逻辑设备
struct device disk_dev; // SCSI磁盘sysfs设备节点
struct gendisk *disk; // 内核通用块设备结构体
atomic_t openers; // 设备打开引用计数
sector_t capacity; // 磁盘有效容量
/* 数据传输参数约束 */
int max_retries; // I/O最大重试次数
u32 min_xfer_blocks; // 最小传输块数
u32 max_xfer_blocks; // 最大传输块数
u32 opt_xfer_blocks; // 最优传输块数
/* 磁盘硬件与块参数 */
unsigned int physical_block_size;// 物理块大小
u8 media_present; // 介质是否存在
u8 write_prot; // 磁盘写保护状态
/* 其他成员省略 */
};
|
7.4. SCSI子系统核心函数¶
7.4.1. 主机分配¶
7.4.1.1. scsi_host_alloc函数¶
scsi_host_alloc函数用于分配SCSI主机适配器结构体内存,并预留驱动私有数据空间,是SCSI主机注册的第一步,所有SCSI驱动初始化必须调用该函数。
函数原型:
1 | struct Scsi_Host *scsi_host_alloc(const struct scsi_host_template *sht, int privsize);
|
参数说明:
sht:SCSI主机模板结构体指针,定义主机能力与回调函数。
privsize:驱动私有数据结构体大小,内核会自动预留对应内存空间。
返回值:成功返回Scsi_Host主机结构体指针,失败返回NULL。
7.4.2. 主机释放¶
7.4.2.1. scsi_host_put函数¶
scsi_host_put函数用于释放SCSI主机适配器的引用,是scsi_host_alloc的逆操作,当驱动模块卸载、设备热拔插或主机注册失败需要回滚时调用。 它会递减主机的底层设备模型引用计数,当引用计数降为0时,内核将彻底释放Scsi_Host结构体及其在scsi_host_alloc时预留的私有数据内存,防止内存泄漏。
函数原型:
1 | void scsi_host_put(struct Scsi_Host *shost);
|
参数说明:
shost:需要释放引用的SCSI主机结构体指针。
7.4.3. 主机注册¶
7.4.3.1. scsi_add_host函数¶
scsi_add_host函数用于将分配完成的SCSI主机适配器注册到内核SCSI子系统,完成设备模型初始化、资源绑定,使主机正式被内核识别。
函数原型:
1 | int scsi_add_host(struct Scsi_Host *host, struct device *dev);
|
参数说明:
host:已分配的SCSI主机结构体指针。
dev:父设备指针,虚拟设备可传入NULL。
返回值:成功返回0,失败返回负错误码。
7.4.4. 设备扫描¶
7.4.4.1. scsi_scan_host函数¶
scsi_scan_host函数用于扫描指定SCSI主机下挂载的所有Target/LUN设备,枚举设备、初始化设备参数、创建设备节点,是驱动加载后识别SCSI设备的核心函数。
函数原型:
1 | void scsi_scan_host(struct Scsi_Host *host);
|
参数说明:
host:待扫描的SCSI主机结构体指针。
7.4.5. 命令收尾回调¶
7.4.5.1. scsi_done函数¶
scsi_done函数是SCSI命令生命周期的终点,底层驱动完成命令硬件处理、数据拷贝、异常填充后,必须调用该函数通知SCSI中间层,标记命令执行完成,触发内核后续的I/O收尾、请求释放、上层回调等流程。
函数原型:
1 | void scsi_done(struct scsi_cmnd *cmd);
|
参数说明:
cmd:已完成处理的SCSI命令结构体指针。
7.4.6. SG数据拷贝¶
7.4.6.1. scsi_sg_copy_from_buffer函数¶
scsi_sg_copy_from_buffer函数用于线性内存数据拷贝至SG散列缓冲区,主要适配SCSI 写操作 场景,将内核线性连续缓冲区的数据,拆分拷贝到SCSI命令的SG离散缓冲区中,自动适配散列内存分片规则,屏蔽SG内存碎片化细节,简化底层驱动数据拷贝。
函数原型:
1 2 | int scsi_sg_copy_from_buffer(struct scsi_cmnd *cmd,
const void *buf, int buflen);
|
参数说明:
cmd:当前待处理的SCSI命令结构体,函数自动提取内部SG散列缓冲区信息。
buf:源线性连续数据缓冲区指针。
buflen:需要拷贝的数据字节长度。
返回值:成功返回实际拷贝字节数,失败返回0。
7.4.6.2. scsi_sg_copy_to_buffer函数¶
scsi_sg_copy_to_buffer函数用于SG散列缓冲区数据拷贝至线性内存,主要适配SCSI 读操作 场景,遍历SCSI命令的SG离散内存分片,将聚合数据完整拷贝到内核线性连续缓冲区,统一离散数据输出格式。
函数原型:
1 2 | int scsi_sg_copy_to_buffer(struct scsi_cmnd *cmd,
void *buf, int buflen);
|
参数说明:
cmd:当前待处理的SCSI命令结构体,函数自动提取内部SG散列缓冲区信息。
buf:目标线性连续数据缓冲区指针。
buflen:需要拷贝的数据字节长度。
返回值:成功返回实际拷贝字节数,失败返回0。
7.4.7. 数据残余量设置¶
7.4.7.1. scsi_set_resid函数¶
在SCSI命令执行完成后,如果设备实际传输的数据量小于主机请求的数据量,底层驱动需要记录这个“差额”,即残余数据长度。 scsi_set_resid函数用于设置该残余量,防止上层误以为数据已全部传输完成。
函数原型:
1 | void scsi_set_resid(struct scsi_cmnd *cmd, unsigned int resid);
|
参数说明:
cmd:指向当前处理的SCSI命令结构体的指针。
resid:残余数据长度,单位字节,其计算逻辑通常为:主机请求的总传输字节数 - 设备实际成功传输的字节数。
7.5. SCSI标准命令¶
7.5.1. SCSI命令基本原理与CDB结构¶
SCSI命令是主机与SCSI设备交互的唯一指令,所有读写、查询、配置、校验操作均依赖标准化的SCSI命令完成。 SCSI命令以CDB(Command Description Block,命令描述块)为载体,存储于scsi_cmnd->cmnd[]数组中,由主机下发、设备解析执行。
SCSI命令根据长度分为6字节短命令与10字节长命令,还有16字节扩展命令,现代Linux内核与存储设备默认使用10字节命令,以支持更大LBA寻址空间,适配大容量磁盘。 所有CDB命令通用基础字段如下:
Opcode(操作码):CDB第0字节,标识SCSI命令类型,内核与驱动通过该字段分发处理逻辑。
LBA地址:逻辑块寻址地址,标识磁盘读写起始扇区位置。
Transfer Length:传输长度,标识本次操作的扇区数量/数据长度。
Control控制位:命令结束标记,控制命令同步、链接等属性。
7.5.2. SCSI核心命令¶
Linux内核所有SCSI设备(机械硬盘、SSD、光驱、U盘、RAID阵列、SAN存储等)均遵循SPC(SCSI Primary Commands)国际标准协议。 内核SCSI子系统对标准SCSI命令做了统一适配,所有合规的硬件/虚拟SCSI驱动都必须实现基础命令集,以完成内核设备枚举、I/O调度、设备管理等通用功能。
7.5.2.1. 6字节基础短命令¶
6字节SCSI命令为早期SCSI-2协议规范,CDB固定6字节,LBA寻址空间小、传输长度有限,不支持大容量磁盘, 仅用于设备状态查询、简单参数配置等非大数据传输场景。
TEST_UNIT_READY 设备就绪检测(0x00)
命令作用:基础设备状态探测命令,内核高频轮询调用,用于检测SCSI设备当前是否处于就绪可用状态。该命令无数据读写、无参数配置,仅获取设备健康状态。
使用场景:设备上电初始化、总线扫描、热插拔恢复、IO超时重试后状态校验。设备正常就绪返回GOOD状态,未就绪、故障、离线则返回CHECK_CONDITION及对应错误码。
INQUIRY 设备查询命令(0x12)
命令作用:标准6字节查询命令。用于读取设备固有静态信息,完成设备类型识别、协议版本匹配、厂商型号校验。
使用场景:主机总线扫描阶段使用,内核通过返回数据区分磁盘、光驱、RAID等设备类型,完成设备注册与绑定,是所有SCSI设备接入系统的前置。
MODE_SENSE_6 模式读取(6字节,0x1A)
命令作用:6字节版本设备参数读取命令,用于获取SCSI设备的工作模式参数,包含缓存策略、读写超时、介质类型、硬件特性配置等。
使用场景:老旧存储设备参数查询,适配低版本SCSI协议设备,仅支持有限长度的模式页数据读取。
MODE_SELECT_6 模式配置(6字节,0x15)
命令作用:6字节版本设备参数配置命令,与MODE_SENSE_6配对使用,用于修改设备默认工作参数,配置缓存开关、读写优先级、纠错策略等硬件特性。
使用场景:初始化阶段设备参数初始化、老旧设备模式适配,现代大容量设备基本废弃,改用10字节版本。
READ_6 短读命令(0x08)
命令作用:标准6字节数据读取命令,CDB长度短,LBA与传输长度字段位数受限,仅支持小容量磁盘、小范围数据读取。
使用场景:兼容早期小容量SCSI设备,仅老旧设备使用,现代Linux内核默认优先使用10字节读命令。
WRITE_6 短写命令(0x0A)
命令作用:6字节数据写入命令,与READ_6配对,用于小容量、小数据量磁盘写入操作,寻址与传输能力有限。
使用场景:老旧SCSI设备兼容写入,现代Linux内核默认优先使用10字节写命令。
7.5.2.2. 10字节标准长命令¶
10字节SCSI命令为SCSI-3及SPC-4主流规范,扩展32位LBA寻址,支持TB级大容量磁盘,传输长度灵活可控,是当前Linux系统SSD、SAS、FC等存储的默认命令。
MODE_SENSE_10 高级模式读取(0x5A)
命令作用:10字节增强版模式查询命令,支持读取完整的设备模式页、缓存参数、PI校验参数、电源管理参数等高级配置。
使用场景:现代存储设备初始化参数读取、硬件特性探测、存储阵列参数校验,是主流设备的标准查询接口。
MODE_SELECT_10 高级模式配置(0x55)
命令作用:10字节增强版模式配置命令,配套MODE_SENSE_10,支持配置完整的设备高级工作模式,适配现代存储丰富的硬件特性。
使用场景:系统初始化设备参数固化、缓存策略配置、硬件功能开关设置。
READ_CAPACITY_10 容量查询(0x25)
命令作用:10字节磁盘容量查询标准命令,用于获取设备有效最大LBA地址与单扇区字节大小,内核据此计算磁盘总容量,初始化块设备容量参数。
使用场景:绝大多数常规容量磁盘的容量探测,是Linux内核默认的容量读取方式。
READ_10 标准读命令(0x28)
命令作用:现代存储核心读命令,32位LBA寻址,支持大容量磁盘任意扇区读取,支持大范围连续扇区批量读取。
使用场景:系统日常文件读取、程序加载、磁盘数据读取,适配所有主流SCSI存储设备。
WRITE_10 标准写命令(0x2A)
命令作用:现代存储核心写命令,与READ_10配对,支持大容量磁盘任意LBA扇区批量数据写入。
使用场景:文件写入、数据保存、缓存刷新写入等写IO场景。
VERIFY_10 数据校验命令(0x2F)
命令作用:10字节磁盘数据完整性校验命令,无需主机与设备数据拷贝,仅校验指定LBA区间的扇区合法性、数据完整性、介质有效性。
使用场景:系统磁盘自检、坏块扫描、数据一致性校验等,保障存储数据可靠性。
SYNCHRONIZE_CACHE 缓存同步命令(0x35)
命令作用:设备缓存刷新命令,强制将磁盘板载缓存中的脏数据刷入物理存储介质,防止断电数据丢失。
使用场景:系统关机、重启、文件系统同步、umount卸载、手动缓存刷盘,是数据安全持久化的关键命令。
7.5.2.3. 16字节扩展命令¶
16字节SCSI命令为扩展协议,支持64位超宽LBA寻址,专门适配PB级超大容量磁盘、分布式存储、SAN阵列设备。
READ_CAPACITY_16 扩展容量查询(0x9E)
命令作用:16字节超大容量磁盘容量查询命令,提供64位LBA寻址能力,支持远超32位寻址上限的超大存储设备容量上报。
使用场景:超大容量SSD、企业级SAN存储、磁盘阵列、分布式存储设备容量探测,内核优先尝试16字节查询,失败后降级使用10字节命令。
7.6. 内核SCSI子系统源码解析¶
内核SCSI子系统相关源码都位于内核源码/drivers/scsi/目录下,存在众多驱动文件,各层代表驱动文件如下:
7.6.1. 较高层驱动¶
较高层驱动处理特定类型的SCSI设备,向用户空间提供标准的块设备接口(如/dev/sda)或字符设备接口(如/dev/sg0),负责设备特有的命令处理和数据转换。
以下选取 瑞芯微6.1.99内核版本 的sd.c进行说明。
7.6.1.1. sd.c驱动解析¶
sd.c专门用于处理SCSI磁盘(TYPE_DISK),它在系统中最终呈现为我们熟悉的/dev/sda、/dev/sdb等块设备。
核心数据结构
sd.c通过定义一个scsi_driver结构体,向SCSI中间层注册自己的回调函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 | static struct scsi_driver sd_template = {
.gendrv = {
.name = "sd",
.probe = sd_probe, // 设备发现时调用
.remove = sd_remove, // 设备移除时调用
.pm = &sd_pm_ops, // 电源管理回调
},
.init_command = sd_init_command, // 将request转换为SCSI CDB
.done = sd_done, // 处理SCSI命令完成后的结果
.eh_action = sd_eh_action, // 错误恢复动作
/* 省略部分成员 */
};
|
设备探测
当底层驱动调用scsi_scan_host后,中间层发送INQUIRY命令发现这是一个磁盘(TYPE_DISK),就会触发sd.c的Probe流程。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | // 以下函数省略源码部分内容
static int sd_probe(struct device *dev) {
// 分配scsi_disk和gendisk
sdkp = kzalloc(sizeof(*sdkp), GFP_KERNEL);
gd = blk_mq_alloc_disk_for_queue(...);
// 生成设备名,如sda、sdb
sd_format_disk_name("sd", index, gd->disk_name, DISK_NAME_LEN);
// 设置块设备操作集
gd->fops = &sd_fops;
// 重新验证磁盘,读取容量、缓存等
sd_revalidate_disk(gd);
// 向块设备层注册设备,生成/dev/sdX节点
device_add_disk(dev, gd, NULL);
}
|
重新验证磁盘
sd_revalidate_disk函数通过发送一系列SCSI命令来摸清设备的底细,可以概括为“一问容量,二问特性,三问状态,四定参数”。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 | // 以下函数省略源码部分内容
static int sd_revalidate_disk(struct gendisk *disk)
{
struct scsi_disk *sdkp = scsi_disk(disk);
struct scsi_device *sdp = sdkp->device;
// ... 变量初始化 ...
// 基础检查,如果设备离线则直接退出
if (!scsi_device_online(sdp))
goto out;
// 分配临时缓冲区用于接收SCSI命令的响应数据
buffer = kmalloc(SD_BUF_SIZE, GFP_KERNEL);
// 磁盘起转(spin up),发送TEST_UNIT_READY命令,如果磁盘未就绪,则让磁盘电机起转,等待其准备就绪
sd_spinup_disk(sdkp);
// 1.读取容量,发送READ_CAPACITY_10或READ_CAPACITY_16命令,获取磁盘的总扇区数,以及逻辑/物理扇区大小
if (sdkp->media_present) {
sd_read_capacity(sdkp, buffer);
// 2.获取高级特性,如果设备支持Vital Product Data (VPD),则读取其高级能力声明
if (scsi_device_supports_vpd(sdp)) {
sd_read_block_provisioning(sdkp); // 是否支持精简配置
sd_read_block_limits(sdkp); // I/O限制,最大传输块数、UNMAP粒度、最优传输大小等
sd_read_block_characteristics(sdkp);// 设备特性,是否为SSD/非旋转设备、SMR盘、ZBC磁盘
sd_zbc_read_zones(sdkp, buffer); // 如果是ZBC磁盘,读取Zone(区域)的布局信息
sd_read_cpr(sdkp); // 定位多磁头/多致动器磁盘特性
}
// 如果与上次扫描不同,打印容量变更日志
sd_print_capacity(sdkp, old_capacity);
// 3.获取设备状态与缓存策略,通过发送MODE_SENSE命令读取设备的模式页面(Mode Pages),了解其运行状态
sd_read_write_protect_flag(sdkp, buffer); // 检查磁盘是否被写保护 ,如SD卡的物理锁、磁带写保护
sd_read_cache_type(sdkp, buffer); // 检查读写缓存策略及是否支持FUA
sd_read_app_tag_own(sdkp, buffer); // 检查DIF/DIX(端到端数据保护)的ATO位
sd_read_write_same(sdkp, buffer); // 探测是否支持WRITE SAME命令,用于快速清零或配合UNMAP
sd_read_security(sdkp, buffer); // 检查是否支持TCG Opal等硬件加密安全协议
sd_config_protection(sdkp); // 根据检查结果启用或禁用DIF/DIX数据完整性校验
}
// 配置Flush策略,如果开启了写缓存(WCE),则告诉块层需要发送SYNCHRONIZE_CACHE命令来保证数据缓存同步
sd_set_flush_flag(sdkp);
// 4.计算并设置I/O尺寸限制,告诉块层:最小I/O 尺寸(io_min)、最优I/O尺寸 (io_opt)、单次最大传输扇区数(max_sectors)
// 这些参数直接决定了文件系统(如ext4)如何对齐I/O以获得最佳性能
dev_max = sdp->use_16_for_rw ? SD_MAX_XFER_BLOCKS : SD_DEF_XFER_BLOCKS;
// 省略中间计算逻辑...
q->limits.max_sectors = rw_max;
// 标记首次扫描结束,并向内核和用户态广播最新的磁盘容量,这会触发udev重新扫描分区表,如生成sda1、sda2
sdkp->first_scan = 0;
set_capacity_and_notify(disk, logical_to_sectors(sdp, sdkp->capacity));
// 配置WRITE ZEROES(快速写零)的降级策略
sd_config_write_same(sdkp);
kfree(buffer);
// 特殊设备后置处理,对于ZBC分区磁盘,必须在容量设置完成后,才能重新验证Zone的状态
if (sd_zbc_revalidate_zones(sdkp))
set_capacity_and_notify(disk, 0);
out:
return 0;
}
|
构建SCSI命令
当文件系统产生I/O,块层将其打包为request,SCSI中间层将其转为scsi_cmnd,然后调用sd_init_command。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | // 以下函数省略源码部分内容
static blk_status_t sd_init_command(struct scsi_cmnd *cmd)
{
struct request *rq = scsi_cmd_to_rq(cmd);
switch (req_op(rq)) {
case REQ_OP_DISCARD:
switch (scsi_disk(rq->q->disk)->provisioning_mode) {
case SD_LBP_UNMAP:
return sd_setup_unmap_cmnd(cmd); // 构建UNMAP
// ...
case REQ_OP_WRITE_ZEROES:
return sd_setup_write_zeroes_cmnd(cmd);// 构建WRITE SAME
case REQ_OP_FLUSH:
return sd_setup_flush_cmnd(cmd); // 构建SYNCHRONIZE_CACHE
case REQ_OP_READ:
case REQ_OP_WRITE:
return sd_setup_read_write_cmnd(cmd); // 构建READ/WRITE(6/10/16/32)
// ... 处理ZBC分区磁盘的特殊命令
}
}
|
重点看sd_setup_read_write_cmnd函数,负责将块设备层下发的通用I/O请求,翻译并封装成底层硬件能看懂的SCSI读/写命令(CDB)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 | // 以下函数省略源码部分内容
static blk_status_t sd_setup_read_write_cmnd(struct scsi_cmnd *cmd)
{
// 提取请求核心参数
struct request *rq = scsi_cmd_to_rq(cmd);
struct scsi_device *sdp = cmd->device;
struct scsi_disk *sdkp = scsi_disk(rq->q->disk);
// 将块层的扇区号转换为SCSI逻辑块地址(LBA),并计算需要传输的块数
sector_t lba = sectors_to_logical(sdp, blk_rq_pos(rq));
unsigned int nr_blocks = sectors_to_logical(sdp, blk_rq_sectors(rq));
bool write = rq_data_dir(rq) == WRITE; // 判断是读还是写
// 2. 为本次I/O分配并映射SG表,这是DMA传输的基础
blk_status_t ret = scsi_alloc_sgtables(cmd);
if (ret != BLK_STS_OK)
return ret;
ret = BLK_STS_IOERR; // 默认错误状态
// 3. 前置合法性检查
if (!scsi_device_online(sdp) || sdp->changed) {
scmd_printk(KERN_ERR, cmd, "device offline or changed\n");
goto fail;
}
// 检查是否越界访问,如请求的结束位置超出了磁盘总容量
if (blk_rq_pos(rq) + blk_rq_sectors(rq) > get_capacity(rq->q->disk)) {
scmd_printk(KERN_ERR, cmd, "access beyond end of device\n");
goto fail;
}
// 检查I/O请求是否按照设备的逻辑块大小对齐
unsigned int mask = logical_to_sectors(sdp, 1) - 1;
if ((blk_rq_pos(rq) & mask) || (blk_rq_sectors(rq) & mask)) {
scmd_printk(KERN_ERR, cmd, "request not aligned to the logical block size\n");
goto fail;
}
// 4. 处理已知的硬件Bug
// 某些劣质SD卡读卡器在访问最后1~2个逻辑块时会挂起
// 如果触发了该bug标记,则主动截断请求,避免触碰危险区域
threshold = sdkp->capacity - SD_LAST_BUGGY_SECTORS;
if (unlikely(sdp->last_sector_bug && lba + nr_blocks > threshold)) {
if (lba < threshold)
nr_blocks = threshold - lba; // 截断到安全边界
else
nr_blocks = 1; // 只访问单个安全块
}
// 5. 处理ZBC分区磁盘的特殊追加写操作
if (req_op(rq) == REQ_OP_ZONE_APPEND) {
ret = sd_zbc_prepare_zone_append(cmd, &lba, nr_blocks);
if (ret) goto fail;
}
// 6. 配置数据完整性保护,如果设备或主机支持端到端的数据校验,防止静默数据损坏,则配置保护标志
fua = rq->cmd_flags & REQ_FUA ? 0x8 : 0;
dix = scsi_prot_sg_count(cmd);
dif = scsi_host_dif_capable(cmd->device->host, sdkp->protection_type);
if (dif || dix)
protect = sd_setup_protect_cmnd(cmd, dix, dif);
// 7. 选择CDB命令描述块的长度
// 根据LBA大小、传输块数以及设备能力,动态选择最合适的SCSI命令格式
if (protect && sdkp->protection_type == T10_PI_TYPE2_PROTECTION) {
// 特殊情况:Type 2保护必须使用32字节扩展CDB
ret = sd_setup_rw32_cmnd(cmd, write, lba, nr_blocks, protect | fua);
} else if (sdp->use_16_for_rw || (nr_blocks > 0xffff)) {
// 大容量/大数据量:LBA > 2TB 或单次传输 > 65535 块,使用16字节CDB
ret = sd_setup_rw16_cmnd(cmd, write, lba, nr_blocks, protect | fua);
} else if ((nr_blocks > 0xff) || (lba > 0x1fffff) || sdp->use_10_for_rw || protect) {
// 中等容量:LBA > 2TB(21位限制) 或单次传输 > 255 块,使用10字节CDB
ret = sd_setup_rw10_cmnd(cmd, write, lba, nr_blocks, protect | fua);
} else {
// 小容量/小数据量:使用古老的6字节CDB
ret = sd_setup_rw6_cmnd(cmd, write, lba, nr_blocks, protect | fua);
}
if (unlikely(ret != BLK_STS_OK))
goto fail;
// 8. 配置命令执行元数据
cmd->transfersize = sdp->sector_size; // 每次传输的基本单位大小
cmd->underflow = nr_blocks << 9; // 预期最小传输字节数
cmd->allowed = sdkp->max_retries; // 允许的最大硬件重试次数
cmd->sdb.length = nr_blocks * sdp->sector_size; // 本次命令的总数据载荷长度
// 9. 调试日志与成功返回
SCSI_LOG_HLQUEUE(1,
scmd_printk(KERN_INFO, cmd,
"%s: block=%llu, count=%d\n", __func__,
(unsigned long long)blk_rq_pos(rq),
blk_rq_sectors(rq)));
SCSI_LOG_HLQUEUE(2,
scmd_printk(KERN_INFO, cmd,
"%s %d/%u 512 byte blocks.\n",
write ? "writing" : "reading", nr_blocks,
blk_rq_sectors(rq)));
/* 返回 BLK_STS_OK,表示命令已完全准备好,可以交给SCSI中间层下发给底层驱动了 */
return BLK_STS_OK;
fail:
// 失败清理:释放之前分配的SG表,防止内存泄漏
scsi_free_sgtables(cmd);
return ret;
}
|
处理命令完成
底层驱动执行完命令后,调用scsi_done,中间层最终回调sd_done。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | // 以下函数省略源码部分内容
static int sd_done(struct scsi_cmnd *SCpnt)
{
int result = SCpnt->result;
struct scsi_sense_hdr sshdr;
//...
// 1. 检查是否有错误
if (result) {
sense_valid = scsi_command_normalize_sense(SCpnt, &sshdr);
}
// 2. 根据Sense Key处理错误
switch (sshdr.sense_key) {
case HARDWARE_ERROR:
case MEDIUM_ERROR:
// 计算实际成功传输的字节数,处理部分完成的情况
good_bytes = sd_completed_bytes(SCpnt);
break;
// ...
case ILLEGAL_REQUEST:
// 如果设备不支持UNMAP或WRITE SAME,动态降级策略
case 0x20: /* INVALID COMMAND OPCODE */
case 0x24: /* INVALID FIELD IN CDB */
switch (SCpnt->cmnd[0]) {
case UNMAP:
sd_config_discard(sdkp, SD_LBP_DISABLE);
break;
}
// 3. 返回成功传输的字节数给中间层/块层
return good_bytes;
}
|
电源管理
在系统休眠(suspend)或关机(shutdown)时,sd.c会负责保护数据:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | // 以下函数省略源码部分内容
static void sd_shutdown(struct device *dev)
{
// 1. 如果开启了写缓存,必须发送 SYNCHRONIZE_CACHE 将数据刷入磁盘
if (sdkp->WCE && sdkp->media_present) {
sd_sync_cache(sdkp, NULL);
}
// 2. 发送START STOP UNIT命令让磁盘电机停转,休眠磁头
if (sdkp->device->manage_shutdown) {
sd_start_stop_device(sdkp, 0); // 0表示STOP
}
}
|
错误恢复
当磁盘出现坏道或超时,SCSI中间层会触发EH,sd.c提供了sd_eh_action回调:
1 2 3 4 5 6 7 8 9 10 | // 以下函数省略源码部分内容
static int sd_eh_action(struct scsi_cmnd *scmd, int eh_disp)
{
// 如果设备不断在读写命令上超时,但TEST UNIT READY却成功
// sd.c会认为磁盘介质已损坏,主动将设备置为OFFLINE,防止系统卡死
if (sdkp->medium_access_timed_out >= sdkp->max_medium_access_timeouts) {
scsi_device_set_state(sdev, SDEV_OFFLINE);
}
}
|
7.6.2. 中间层驱动¶
中间层驱动有设备扫描,管理SCSI主机控制器、设备,维护命令队列,统一处理SCSI错误等职责。
7.6.2.1. scsi.c驱动解析¶
以下选取 瑞芯微6.1.99内核版本 的scsi.c进行说明。
子系统初始化
init_scsi是整个SCSI中间层启动的入口,通过subsys_initcall在系统早期执行。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | static int __init init_scsi(void)
{
int error;
// 初始化Procfs接口,如/proc/scsi/scsi
error = scsi_init_procfs();
if (error)
goto cleanup_queue;
// 初始化设备黑名单/quirk数据库
error = scsi_init_devinfo();
if (error)
goto cleanup_procfs;
// 初始化主机管理数据结构
error = scsi_init_hosts();
if (error)
goto cleanup_devlist;
// 初始化Sysctl接口
error = scsi_init_sysctl();
if (error)
goto cleanup_hosts;
// 注册Sysfs类和设备模型支持
error = scsi_sysfs_register();
if (error)
goto cleanup_sysctl;
// 初始化netlink通信
scsi_netlink_init();
printk(KERN_NOTICE "SCSI subsystem initialized\n");
return 0;
// 错误清理标签...
}
|
命令完成的步骤
scsi_finish_command函数是中间层最重要的函数之一。当底层驱动处理完命令并调用scsi_done后,最终会汇聚到这里。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | // 以下函数省略源码部分内容
void scsi_finish_command(struct scsi_cmnd *cmd)
{
struct scsi_device *sdev = cmd->device;
struct scsi_target *starget = scsi_target(sdev);
struct Scsi_Host *shost = sdev->host;
struct scsi_driver *drv;
unsigned int good_bytes;
// 解除设备忙碌状态:告诉中间层,该设备/目标/主机现在可以接收新命令了
scsi_device_unbusy(sdev, cmd);
if (atomic_read(&shost->host_blocked)) atomic_set(&shost->host_blocked, 0);
if (atomic_read(&starget->target_blocked)) atomic_set(&starget->target_blocked, 0);
if (atomic_read(&sdev->device_blocked)) atomic_set(&sdev->device_blocked, 0);
// 默认假设所有请求的数据都成功传输了
good_bytes = scsi_bufflen(cmd);
// 如果不是透传命令,则调用较高层驱动(如sd.c)的done回调
if (!blk_rq_is_passthrough(scsi_cmd_to_rq(cmd))) {
int old_good_bytes = good_bytes;
drv = scsi_cmd_to_driver(cmd); // 获取sd_template
if (drv->done)
// sd_done会根据Sense数据,修正实际成功传输的字节数(good_bytes)
good_bytes = drv->done(cmd);
// 兼容处理,如果较高层ULD没有修改good_bytes,但底层返回了残余量(resid),则扣除残余量
if (good_bytes == old_good_bytes)
good_bytes -= scsi_get_resid(cmd);
}
// 最终交还给块设备层,唤醒等待的进程
scsi_io_completion(cmd, good_bytes);
}
|
动态队列管理
真实的存储设备(尤其是低端磁盘)处理能力有限。如果主机发送命令过快,设备会返回QUEUE_FULL状态,中间层必须处理这种情况,而不是直接报错。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | // 改变设备的队列深度
int scsi_change_queue_depth(struct scsi_device *sdev, int depth)
{
// 限制深度不能超过主机控制器的最大能力,且不超过4096的安全上限
depth = min_t(int, depth, scsi_device_max_queue_depth(sdev));
if (depth > 0) {
sdev->queue_depth = depth;
wmb();
}
// 同步更新块设备层的队列深度
if (sdev->request_queue)
blk_set_queue_depth(sdev->request_queue, depth);
sbitmap_resize(&sdev->budget_map, sdev->queue_depth);
return sdev->queue_depth;
}
// 跟踪QUEUE_FULL事件,调整队列深度
int scsi_track_queue_full(struct scsi_device *sdev, int depth)
{
// 防止同一jiffies内的多次QUEUE_FULL被重复计数
if ((jiffies >> 4) == (sdev->last_queue_full_time >> 4))
return 0;
sdev->last_queue_full_time = jiffies;
// 统计在特定深度下连续发生QUEUE_FULL的次数
if (sdev->last_queue_full_depth != depth) {
sdev->last_queue_full_count = 1;
sdev->last_queue_full_depth = depth;
} else {
sdev->last_queue_full_count++;
}
// 如果连续10次在该深度下都报告QUEUE_FULL,说明设备真的吃不消了
if (sdev->last_queue_full_count <= 10)
return 0;
// 触发降速,主动降低队列深度,减轻设备压力
return scsi_change_queue_depth(sdev, depth);
}
|
设备VPD查询
VPD(Vital Product Data)是SCSI设备提供的一组标准页面,包含设备的序列号、支持的命令、块限制等关键信息,scsi.c提供了统一的获取框架。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 | // 以下函数省略源码部分内容
// 内部辅助函数:执行INQUIRY命令获取特定VPD页面
static int scsi_vpd_inquiry(struct scsi_device *sdev, unsigned char *buffer, u8 page, unsigned len)
{
int result;
unsigned char cmd[16];
if (len < 4)
return -EINVAL;
cmd[0] = INQUIRY;
cmd[1] = 1; // EVPD=1,表示请求VPD页面而非标准INQUIRY
cmd[2] = page; // 请求的页面号(如0x80序列号, 0x83设备标识, 0xB0块限制)
cmd[3] = len >> 8;
cmd[4] = len & 0xff;
cmd[5] = 0;
// 执行命令,超时设为30秒,最多重试3次
result = scsi_execute_req(sdev, cmd, DMA_FROM_DEVICE, buffer,
len, NULL, 30 * HZ, 3, NULL);
}
// 对外接口:安全地获取VPD页面
int scsi_get_vpd_page(struct scsi_device *sdev, u8 page, unsigned char *buf, int buf_len)
{
int result, vpd_len;
// 1. 先询问设备该VPD页面有多大,防止老旧设备对过大缓冲区报错
vpd_len = scsi_get_vpd_size(sdev, page);
if (vpd_len <= 0)
return -EINVAL;
vpd_len = min(vpd_len, buf_len);
memset(buf, 0, buf_len);
// 2. 使用确认的大小再次请求,获取完整数据
result = scsi_vpd_inquiry(sdev, buf, page, vpd_len);
if (result < 0)
return -EINVAL;
else if (result > vpd_len)
dev_warn_once(&sdev->sdev_gendev,
"%s: VPD page 0x%02x result %d > %d bytes\n",
__func__, page, result, vpd_len);
return 0;
}
// 在设备发现时,自动挂载常用的 VPD 页面到 scsi_device 结构中
void scsi_attach_vpd(struct scsi_device *sdev)
{
int i;
struct scsi_vpd *vpd_buf;
// 先获取Page 0,看看设备支持哪些页面
vpd_buf = scsi_get_vpd_buf(sdev, 0);
if (!vpd_buf)
return;
// 遍历支持的页面列表,自动拉取并缓存关键页面
for (i = 4; i < vpd_buf->len; i++) {
if (vpd_buf->data[i] == 0x80) scsi_update_vpd_page(sdev, 0x80, &sdev->vpd_pg80); // 序列号
if (vpd_buf->data[i] == 0x83) scsi_update_vpd_page(sdev, 0x83, &sdev->vpd_pg83); // 设备标识
if (vpd_buf->data[i] == 0xb0) scsi_update_vpd_page(sdev, 0xb0, &sdev->vpd_pgb0); // 块限制(最大传输大小等)
// ... 其他页面
}
kfree(vpd_buf);
}
|
设备查找与引用管理
在内核中,设备可能随时被热拔插,如果驱动正在查找设备时设备被移除,会导致空指针解引用(Kernel Panic),scsi.c提供了带引用计数保护的查找机制。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | // 带引用计数保护的设备查找
struct scsi_device *scsi_device_lookup(struct Scsi_Host *shost,
uint channel, uint id, u64 lun)
{
struct scsi_device *sdev;
unsigned long flags;
// 1. 加锁保护链表遍历
spin_lock_irqsave(shost->host_lock, flags);
sdev = __scsi_device_lookup(shost, channel, id, lun);
// 2. 如果找到了,尝试增加引用计数(scsi_device_get)
// 如果设备正在被删除,get会失败,返回NULL,确保安全
if (sdev && scsi_device_get(sdev))
sdev = NULL;
spin_unlock_irqrestore(shost->host_lock, flags);
return sdev; // 调用者使用完毕后,必须调用scsi_device_put(sdev)释放引用
}
|
7.6.2.2. scsi_lib.c驱动解析¶
scsi_lib.c的主要职责是对接Linux块层多队列(blk-mq)架构,负责将块层的I/O请求(request)转化为SCSI命令(scsi_cmnd),管理SG列表,处理命令的完成与错误恢复,并提供同步发送SCSI命令功能。
以下选取 瑞芯微6.1.99内核版本 的scsi_lib.c进行说明。
块层入口
当块层准备好一个request时,会调用scsi_queue_rq函数,它是SCSI命令生命周期的起点。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 | // 以下函数省略源码部分内容
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_cmnd *cmd = blk_mq_rq_to_pdu(req);
int reason;
// 1. 检查设备状态,如果设备离线、被阻塞或处于恢复中,直接拒绝或重试
if (unlikely(sdev->sdev_state != SDEV_RUNNING)) {
ret = scsi_device_state_check(sdev, req);
if (ret != BLK_STS_OK)
goto out_put_budget;
}
ret = BLK_STS_RESOURCE;
// 2. 检查Target和Host的并发限制,防止压垮后端存储
if (!scsi_target_queue_ready(shost, sdev))
goto out_put_budget;
if (!scsi_host_queue_ready(q, shost, sdev, cmd))
goto out_dec_target_busy;
// 3. 初始化scsi_cmnd,分配SG 表,调用较高层 ULD(如sd.c的init_command)构建CDB
if (!(req->rq_flags & RQF_DONTPREP)) {
ret = scsi_prepare_cmd(req);
if (ret != BLK_STS_OK)
goto out_dec_host_busy;
req->rq_flags |= RQF_DONTPREP;
} else {
clear_bit(SCMD_STATE_COMPLETE, &cmd->state);
}
// 4. 标记命令开始执行,并分发给底层驱动
blk_mq_start_request(req);
reason = scsi_dispatch_cmd(cmd); // 调用底层驱动的queuecommand
// 5. 如果底层驱动拒绝,如队列满,则设置阻塞状态并让块层重试
if (reason) {
scsi_set_blocked(cmd, reason);
ret = BLK_STS_RESOURCE;
goto out_dec_host_busy;
}
return BLK_STS_OK;
// 错误处理标签...
}
|
其中scsi_dispatch_cmd负责将打包好的命令交给底层硬件驱动。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 | // 以下函数省略源码部分内容
static int scsi_dispatch_cmd(struct scsi_cmnd *cmd)
{
struct Scsi_Host *host = cmd->device->host;
// 检查设备是否已被删除
if (unlikely(cmd->device->sdev_state == SDEV_DEL)) {
cmd->result = DID_NO_CONNECT << 16;
goto done;
}
// ...
// 检查CDB长度是否超出主机控制器的限制
if (cmd->cmd_len > cmd->device->host->max_cmd_len) {
SCSI_LOG_MLQUEUE(3, scmd_printk(KERN_INFO, cmd,
"queuecommand : command too long. "
"cdb_size=%d host->max_cmd_len=%d\n",
cmd->cmd_len, cmd->device->host->max_cmd_len));
cmd->result = (DID_ABORT << 16);
goto done;
}
// ...
// 调用底层驱动(LLD)注册的queuecommand回调
rtn = host->hostt->queuecommand(host, cmd);
// 如果LLD返回BUSY,中间层会将其放入重试队列
if (rtn) {
atomic_dec(&cmd->device->iorequest_cnt);
trace_scsi_dispatch_cmd_error(cmd, rtn);
if (rtn != SCSI_MLQUEUE_DEVICE_BUSY &&
rtn != SCSI_MLQUEUE_TARGET_BUSY)
rtn = SCSI_MLQUEUE_HOST_BUSY;
SCSI_LOG_MLQUEUE(3, scmd_printk(KERN_INFO, cmd,
"queuecommand : request rejected\n"));
}
return rtn;
done:
scsi_done(cmd); // 直接完成并报错
return 0;
}
|
块层完成回调
当底层驱动(LLD)完成命令并调用scsi_done后,流程回到scsi_lib.c,这里需要解析硬件返回的状态,决定是成功、重试还是报错。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | // 以下函数省略源码部分内容
static void scsi_complete(struct request *rq)
{
struct scsi_cmnd *cmd = blk_mq_rq_to_pdu(rq);
enum scsi_disposition disposition;
// ...
// 1. 决定命令的最终处置方式 (SUCCESS, NEEDS_RETRY, ADD_TO_MLQUEUE, 或进 EH)
disposition = scsi_decide_disposition(cmd);
// 2. 根据处置方式分流
switch (disposition) {
case SUCCESS:
scsi_finish_command(cmd); // 成功:交还给较高层ULD (如sd.c的sd_done)
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); // 严重错误:交给SCSI错误恢复(EH)线程
break;
}
}
|
I/O完成与Sense数据解析
如果命令返回了错误,中间层需要解析Sense Data(感知数据)来决定下一步动作,降级处理、延迟重试和错误隔离等。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 | // 以下函数省略源码部分内容
static void scsi_io_completion_action(struct scsi_cmnd *cmd, int result)
{
struct request *req = scsi_cmd_to_rq(cmd);
int level = 0;
enum {ACTION_FAIL, ACTION_REPREP, ACTION_DELAYED_REPREP,
ACTION_RETRY, ACTION_DELAYED_RETRY} action;
struct scsi_sense_hdr sshdr;
bool sense_valid;
bool sense_current = true;
blk_status_t blk_stat;
sense_valid = scsi_command_normalize_sense(cmd, &sshdr);
if (sense_valid)
sense_current = !scsi_sense_is_deferred(&sshdr);
blk_stat = scsi_result_to_blk_status(result);
if (host_byte(result) == DID_RESET) {
action = ACTION_RETRY;
} else if (sense_valid && sense_current) {
// 根据sense_key决定动作
switch (sshdr.sense_key) {
case UNIT_ATTENTION:
// 设备状态改变:如换盘、总线重置,通常直接重试
if (cmd->device->removable) {
cmd->device->changed = 1;
action = ACTION_FAIL;
} else {
action = ACTION_RETRY;
}
case ILLEGAL_REQUEST:
// ...
// 非法请求:可能是发了设备不支持的命令,如用READ(10)访问只支持READ(6)的老设备
} else if (sshdr.asc == 0x20 || sshdr.asc == 0x24) {
cmd->device->use_10_for_rw = 0; // 降级标记
action = ACTION_REPREP; // 重新构建CDB并重试
} else {
action = ACTION_FAIL;
}
break;
case NOT_READY:
// 设备未就绪:如磁盘正在起转、格式化中
if (sshdr.asc == 0x04) {
switch (sshdr.ascq) {
case 0x01: /* becoming ready */
case 0x04: /* format in progress */
case 0x05: /* rebuild in progress */
case 0x06: /* recalculation in progress */
case 0x07: /* operation in progress */
case 0x08: /* Long write in progress */
case 0x09: /* self test in progress */
case 0x11: /* notify (enable spinup) required */
case 0x14: /* space allocation in progress */
case 0x1a: /* start stop unit in progress */
case 0x1b: /* sanitize in progress */
case 0x1d: /* configuration in progress */
case 0x24: /* depopulation in progress */
action = ACTION_DELAYED_RETRY; // 延迟重试
break;
// ... 其他 Sense Key 处理 ...
}
}
// 执行决定的动作:失败则截断I/O,重试则重新入队
switch (action) {
case ACTION_FAIL: scsi_end_request(req, blk_stat, ...); break;
case ACTION_REPREP: scsi_mq_requeue_cmd(cmd, 0); break;
case ACTION_RETRY: __scsi_queue_insert(cmd, SCSI_MLQUEUE_EH_RETRY, false); break;
}
}
|
SG表与数据完整性管理
SCSI命令的数据通常不是连续内存,scsi_lib.c负责为这些非连续内存构建Scatter-Gather列表,并处理端到端的数据保护。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | // 以下函数省略源码部分内容
blk_status_t scsi_alloc_sgtables(struct scsi_cmnd *cmd)
{
struct request *rq = scsi_cmd_to_rq(cmd);
unsigned short nr_segs = blk_rq_nr_phys_segments(rq);
// 1. 分配数据SG表
if (unlikely(sg_alloc_table_chained(&cmd->sdb.table, nr_segs,
cmd->sdb.table.sgl, SCSI_INLINE_SG_CNT)))
return BLK_STS_RESOURCE;
// 2. 将块层的bio映射到SG表中
count = __blk_rq_map_sg(rq->q, rq, cmd->sdb.table.sgl, &last_sg);
// 3. 处理DMA边界对齐和Drain缓冲
if (blk_rq_bytes(rq) & rq->q->dma_pad_mask) { ... }
if (need_drain) { ... }
// 4. 如果启用了数据完整性保护(DIF/DIX),分配额外的保护信息SG表
if (blk_integrity_rq(rq)) {
struct scsi_data_buffer *prot_sdb = cmd->prot_sdb;
ivecs = blk_rq_count_integrity_sg(rq->q, rq->bio);
sg_alloc_table_chained(&prot_sdb->table, ivecs, ...);
count = blk_rq_map_integrity_sg(rq->q, rq->bio, prot_sdb->table.sgl);
}
return BLK_STS_OK;
}
|
命令执行同步
在设备探测(如sd_revalidate_disk)或错误恢复时,内核需要同步发送SCSI命令并等待结果,scsi_lib.c提供了支持。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | // 以下函数省略源码部分内容
int __scsi_execute(struct scsi_device *sdev, const unsigned char *cmd,
int data_direction, void *buffer, unsigned bufflen,
unsigned char *sense, struct scsi_sense_hdr *sshdr,
int timeout, int retries, blk_opf_t flags,
req_flags_t rq_flags, int *resid)
{
struct request *req;
struct scsi_cmnd *scmd;
// 1. 分配一个特殊的内部Request
req = scsi_alloc_request(sdev->request_queue, ...);
// 2. 将内核缓冲区映射到Request
if (bufflen) {
ret = blk_rq_map_kern(sdev->request_queue, req, buffer, bufflen, GFP_NOIO);
}
// 3. 填充CDB和超时/重试参数
scmd = blk_mq_rq_to_pdu(req);
scmd->cmd_len = COMMAND_SIZE(cmd[0]);
memcpy(scmd->cmnd, cmd, scmd->cmd_len);
scmd->allowed = retries;
req->timeout = timeout;
req->cmd_flags |= flags;
req->rq_flags |= rq_flags | RQF_QUIET;
// 4. 同步阻塞执行,直到命令完成
blk_execute_rq(req, true);
// 5. 提取Sense数据和结果
if (sense && scmd->sense_len)
memcpy(sense, scmd->sense_buffer, SCSI_SENSE_BUFFERSIZE);
if (sshdr)
scsi_normalize_sense(scmd->sense_buffer, scmd->sense_len, sshdr);
ret = scmd->result;
out:
blk_mq_free_request(req);
return ret;
}
|
7.7. 虚拟SCSI主机驱动实验¶
本实验驱动根据内核SCSI子系统框架设计,处于SCSI子系统的 较低层 ,对上承接SCSI中间层协议调度、IO管理,对下模拟物理SCSI硬件设备行为。 本实验驱动默认创建16MB容量的虚拟SCSI磁盘,模拟SCSI设备枚举、命令交互、数据读写、异常处理, 支持SPC-4标准协议的SCSI命令,实现异步命令处理、Sense错误上报、EH错误恢复、IO数据统计等功能。
本章的示例代码目录为: linux_driver/41_scsi_subsystem
7.7.1. 驱动代码详解¶
7.7.1.1. 核心定义与数据结构¶
完成驱动基础信息、虚拟磁盘硬件参数、标准SCSI协议命令码的宏封装,同时定义驱动核心私有数据结构体与工具宏。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | /* 虚拟磁盘配置 */
#define VIRTUAL_DISK_SIZE_MB 16 /* 虚拟磁盘大小(MB) */
#define VIRTUAL_SECTOR_SIZE 512 /* 扇区大小(字节) */
#define VIRTUAL_DISK_SECTORS ((VIRTUAL_DISK_SIZE_MB * 1024 * 1024) / VIRTUAL_SECTOR_SIZE) /* 总扇区数 */
/* 虚拟主机私有数据结构体 */
struct vhost_data {
unsigned char *disk_buffer; /* 内存中的虚拟磁盘缓冲区 */
spinlock_t lock; /* 磁盘访问自旋锁 */
unsigned int sectors_read; /* 统计:已读取扇区数 */
unsigned int sectors_written; /* 统计:已写入扇区数 */
struct workqueue_struct *wq; /* 异步命令处理工作队列 */
};
/* 异步命令处理工作项结构体 */
struct vhost_work {
struct work_struct work; /* 工作结构体 */
struct scsi_cmnd *cmd; /* 关联的SCSI命令 */
};
/* 从scsi_host获取vhost_data的宏 */
#define host_to_vhost(host) ((struct vhost_data *)shost_priv(host))
/* 支持的SCSI命令操作码 */
#define OPCODE_TEST_UNIT_READY 0x00 /* 设备就绪检测(6字节) */
#define OPCODE_INQUIRY 0x12 /* 查询设备信息(6字节) */
#define OPCODE_MODE_SENSE_6 0x1A /* 模式读取(6字节) */
#define OPCODE_MODE_SENSE_10 0x5A /* 模式读取(10字节) */
#define OPCODE_MODE_SELECT_6 0x15 /* 模式配置(6字节) */
#define OPCODE_MODE_SELECT_10 0x55 /* 模式配置(10字节) */
#define OPCODE_READ_CAPACITY_10 0x25 /* 容量查询(10字节) */
#define OPCODE_READ_CAPACITY_16 0x9E /* 容量查询(16字节) */
#define OPCODE_READ_6 0x08 /* 读命令(6字节) */
#define OPCODE_READ_10 0x28 /* 读命令(10字节) */
#define OPCODE_WRITE_6 0x0A /* 写命令(6字节) */
#define OPCODE_WRITE_10 0x2A /* 写命令(10字节) */
#define OPCODE_VERIFY_10 0x2F /* 数据校验(10字节) */
#define OPCODE_SYNCHRONIZE_CACHE 0x35 /* 缓存同步(10字节) */
|
关键说明:
第2-4行:宏定义虚拟磁盘容量、扇区大小,计算得总扇区数。
第7-13行:vhost_data私有结构体,包含虚拟磁盘内存缓冲区、并发保护锁、IO统计、异步工作队列,是虚拟磁盘硬件的载体。
第16-19行:vhost_work结构体,适配内核工作队列机制,用于封装SCSI命令与异步工作项,实现底层非阻塞IO处理。
第22行:自定义工具宏,快速从SCSI主机结构体获取底层私有数据。
第25-38行:参照SPC-4协议标准,定义主流SCSI命令操作码,规范驱动可响应的协议指令。
7.7.1.2. 内核版本兼容¶
由于Linux内核5.15版本前后SCSI命令完成回调接口发生架构变更,底层驱动直接编译会出现接口报错,该函数通过内核版本宏判断,适配新旧内核接口。
1 2 3 4 5 6 7 8 | static inline void vhost_scsi_done(struct scsi_cmnd *cmd)
{
#if LINUX_VERSION_CODE >= KERNEL_VERSION(5, 15, 136)
scsi_done(cmd);
#else
cmd->scsi_done(cmd);
#endif
}
|
7.7.1.3. SCSI主机模板配置¶
定义SCSI主机模板结构体,模板集中定义虚拟SCSI主机的硬件参数、并发能力、底层回调函数入口,内核通过该模板识别设备属性、调度命令、触发异常处理。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | /*
* SCSI主机模板 - 定义主机适配器能力
*/
static struct scsi_host_template vhost_template = {
.name = DRV_NAME, /* 主机名称 */
.module = THIS_MODULE, /* 所属模块 */
.queuecommand = vhost_queuecommand, /* 队列命令处理函数 */
.eh_abort_handler = vhost_eh_abort_handler, /* 中止处理函数 */
.eh_host_reset_handler = vhost_eh_host_reset_handler, /* 主机重置处理函数 */
/* 主机能力 */
.can_queue = 16, /* 最大可队列命令数 */
.this_id = 7, /* 主机适配器的SCSI ID */
.cmd_per_lun = 1, /* 每个LUN的命令数 */
.max_sectors = 256, /* 每命令最大扇区数 */
.sg_tablesize = 64, /* 最大scatter-gather元素数 */
.emulated = 1, /* 这是一个模拟设备 */
};
|
关键说明:
第5-7行:绑定驱动名称、模块所有者、队列命令处理回调。
第8-9行:绑定EH异常处理回调,注册底层故障恢复接口。
第12-14行:配置主机并发能力,定义最大队列命令数、单LUN并行命令数。
第15-16行:限制单命令最大传输扇区、SG散列段数。
第17行:标记为模拟设备,告知内核该设备为软件虚拟硬件。
7.7.1.4. 模块初始化与卸载¶
模块初始化依次完成主机资源分配、私有数据初始化、虚拟磁盘内存创建、工作队列初始化、设备注册与扫描枚举。 模块卸载依次完成设备注销、资源回收、状态打印等收尾工作。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 | /* 模块初始化函数 */
static int __init virtual_scsi_host_init(void)
{
struct Scsi_Host *host; /* SCSI主机指针 */
struct vhost_data *vhost; /* 虚拟主机私有数据指针 */
int ret = 0; /* 返回值 */
pr_info("%s: Virtual SCSI Host Driver v%s initializing...\n", DRV_NAME, DRV_VERSION);
/* 分配SCSI主机,附带私有数据空间 */
host = scsi_host_alloc(&vhost_template, sizeof(struct vhost_data));
if (!host) {
pr_err("%s: Failed to allocate SCSI host\n", DRV_NAME);
return -ENOMEM; /* 返回内存不足错误 */
}
/* 获取私有数据区域指针 */
vhost = shost_priv(host);
/* 初始化自旋锁 */
spin_lock_init(&vhost->lock);
/* 使用vzalloc分配虚拟磁盘缓冲区,用于大内存分配 */
vhost->disk_buffer = vzalloc(VIRTUAL_DISK_SIZE_MB * 1024 * 1024);
if (!vhost->disk_buffer) {
pr_err("%s: Failed to allocate virtual disk buffer (%d MB)\n",
DRV_NAME, VIRTUAL_DISK_SIZE_MB);
ret = -ENOMEM; /* 返回内存不足错误 */
goto free_host; /* 跳转到清理 */
}
/* 向虚拟磁盘写入一些初始数据 */
{
unsigned int i;
for (i = 0; i < VIRTUAL_DISK_SECTORS; i++) {
unsigned char *sector = vhost->disk_buffer + i * VIRTUAL_SECTOR_SIZE; /* 扇区指针 */
memset(sector, (i & 0xFF), VIRTUAL_SECTOR_SIZE); /* 用扇区号的低8位填充 */
}
}
pr_info("%s: Allocated virtual disk: %d MB (%u sectors, %d bytes/sector)\n",
DRV_NAME, VIRTUAL_DISK_SIZE_MB, VIRTUAL_DISK_SECTORS, VIRTUAL_SECTOR_SIZE);
/* 创建工作队列用于异步命令处理 */
vhost->wq = create_singlethread_workqueue(DRV_NAME);
if (!vhost->wq) {
pr_err("%s: Failed to create workqueue\n", DRV_NAME);
ret = -ENOMEM; /* 返回内存不足错误 */
goto free_buffer; /* 跳转到清理 */
}
/* 设置DMA掩码 */
dma_set_mask(&host->shost_gendev, DMA_BIT_MASK(32));
/* 将主机添加到系统 */
ret = scsi_add_host(host, NULL);
if (ret) {
pr_err("%s: Failed to add SCSI host (ret=%d)\n", DRV_NAME, ret);
goto free_wq; /* 跳转到清理 */
}
/* 扫描主机查找设备 */
scsi_scan_host(host);
/* 保存指针用于清理 */
g_host = host;
pr_info("%s: Virtual SCSI Host initialized successfully!\n", DRV_NAME);
pr_info("%s: Statistics: sectors_read=%u, sectors_written=%u\n",
DRV_NAME, vhost->sectors_read, vhost->sectors_written);
return 0;
free_wq:
destroy_workqueue(vhost->wq); /* 销毁工作队列 */
free_buffer:
vfree(vhost->disk_buffer); /* 释放虚拟磁盘缓冲区 */
free_host:
scsi_host_put(host); /* 释放SCSI主机 */
return ret;
}
/* 模块卸载函数 */
static void __exit virtual_scsi_host_exit(void)
{
struct Scsi_Host *host = g_host; /* 获取SCSI主机指针 */
struct vhost_data *vhost; /* 虚拟主机数据指针 */
if (!host) {
pr_err("%s: No SCSI host found\n", DRV_NAME);
return; /* 无主机,直接返回 */
}
vhost = shost_priv(host); /* 获取私有数据 */
pr_info("%s: Unloading Virtual SCSI Host Driver...\n", DRV_NAME);
pr_info("%s: Final statistics:\n", DRV_NAME);
pr_info("%s: Sectors read: %u\n", DRV_NAME, vhost->sectors_read);
pr_info("%s: Sectors written: %u\n", DRV_NAME, vhost->sectors_written);
/* 移除主机 */
scsi_remove_host(host);
/* 等待所有待处理命令完成 */
flush_workqueue(vhost->wq);
/* 销毁工作队列 */
destroy_workqueue(vhost->wq);
/* 释放虚拟磁盘缓冲区 */
vfree(vhost->disk_buffer);
/* 释放主机引用 */
scsi_host_put(host);
/* 清空全局指针 */
g_host = NULL;
pr_info("%s: Virtual SCSI Host Driver unloaded successfully!\n", DRV_NAME);
}
module_init(virtual_scsi_host_init);
module_exit(virtual_scsi_host_exit);
|
关键说明:
第11-15行:分配SCSI主机结构体与底层私有数据空间,完成设备载体创建。
第21行:初始化自旋锁,为底层并发IO操作提供保护。
第24-30行:申请16MB内核内存,作为虚拟磁盘底层存储介质。
第33-39行:按扇区编号填充数据,初始化磁盘数据,方便读写功能测试校验。
第45-50行:创建内核工作队列,为异步命令处理提供任务调度载体。
第53行:配置32位DMA掩码,虚拟驱动不需要,但SCSI子系统要求。
第56-60行:向SCSI子系统注册主机设备,完成底层硬件接入内核。
第63行:扫描枚举虚拟LUN设备,触发内核设备枚举流程。
第74-80行:异常回退,初始化失败逐级释放底层资源。
第89-92行:校验主机设备有效性,规避空指针异常。
第96-100行:打印底层IO运行统计信息,展示设备运行状态。
第103行:移除SCSI主机设备,注销底层硬件设备节点。
第106行:刷新工作队列,等待所有底层异步IO任务执行完毕。
第109-112行:销毁工作队列、释放虚拟磁盘底层内存,回收所有内核资源。
第115行:释放主机引用计数,注销底层设备载体。
第118行:清空全局指针,避免野指针问题。
7.7.1.5. 错误Sense数据生成¶
该部分实现SCSI标准Sense错误数据生成,当底层设备出现非法命令、LBA越界、参数错误等异常时,该函数按照SPC-4协议标准,生成内核可识别的标准化错误信息,上报给SCSI中间层。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | /*
* 为错误条件生成Sense数据
* @param cmd: SCSI命令结构体
* @param sense_key: 感知键
* @param asc: 附加感知码
* @param ascq: 附加感知码限定符
*/
static void set_sense_data(struct scsi_cmnd *cmd, int sense_key, int asc, int ascq)
{
unsigned char *sense = cmd->sense_buffer; /* 获取Sense缓冲区指针 */
memset(sense, 0, SCSI_SENSE_BUFFERSIZE); /* 清零Sense缓冲区 */
/* 固定格式Sense数据 (70h或71h) */
sense[0] = 0x70; /* 固定格式,当前错误 */
sense[2] = sense_key; /* 设置感知键 */
sense[7] = 10; /* 附加Sense长度 */
sense[12] = asc; /* 设置附加感知码 */
sense[13] = ascq; /* 设置附加感知码限定符 */
}
|
关键说明:
第10行:获取内核预置的SCSI命令Sense缓冲区,使用内核标准内存空间存储异常信息。
第12行:清零缓冲区,避免残留数据导致底层错误信息上报异常。
第15行:固定协议标识位,声明为标准SCSI当前错误格式,保证内核正常解析。
第16行:写入Sense错误大类,区分非法请求、设备未就绪等核心异常类型。
第17行:定义附加错误数据长度,遵循协议数据帧格式。
第18-19行:写入细分错误码与限定符,定位底层硬件的异常原因。
7.7.1.6. 异步命令调度¶
命令队列处理函数
vhost_queuecommand函数是SCSI子系统中间层与底层驱动的唯一交互入口,内核所有下发的SCSI命令都会优先进入该函数。 函数功能为接收原始命令、创建异步工作项、绑定命令上下文、推入内核工作队列,将同步上下文的命令请求转换为异步任务。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | /*
* 队列命令处理函数 - 所有SCSI命令在这里接收
* @param host: SCSI主机
* @param cmd: SCSI命令
* @return: 返回0表示成功
*/
static int vhost_queuecommand(struct Scsi_Host *host, struct scsi_cmnd *cmd)
{
struct vhost_data *vhost = host_to_vhost(host); /* 获取虚拟主机数据 */
struct vhost_work *vwork; /* 工作项 */
dev_dbg(&host->shost_gendev, "Queued SCSI command: opcode 0x%02x, cmd_len=%d\n",
cmd->cmnd[0], cmd->cmd_len);
/* 为异步处理分配工作项 */
vwork = kmalloc(sizeof(struct vhost_work), GFP_ATOMIC);
if (!vwork) {
dev_err(&host->shost_gendev, "Failed to allocate work item\n");
cmd->result = DID_ERROR << 16; /* 设置错误结果 */
vhost_scsi_done(cmd); /* 完成命令 */
return -ENOMEM; /* 返回内存不足错误 */
}
/* 初始化工作结构体 */
vwork->cmd = cmd; /* 关联命令 */
INIT_WORK(&vwork->work, vhost_process_command); /* 初始化异步任务的执行回调函数 */
/* 将工作项加入工作队列 */
queue_work(vhost->wq, &vwork->work);
return 0; /* 返回成功 */
}
|
关键说明:
第9-13行:获取私有驱动数据与主机设备句柄,打印当前入队命令码与命令长度,用于底层调试。
第16-22行:以原子内存分配方式创建异步工作项,适配中断上下文运行环境。
第25-26行:将接收的SCSI命令结构体与工作项绑定,初始化工作队列回调函数,完成同步指令封装。
第29行:将封装完成的异步任务推入自定义工作队列,交由内核调度线程异步执行。
命令异步分发处理
vhost_process_command函数是异步任务的执行回调函数,工作队列调度触发该函数后,完成任务解析、设备合法性校验、命令码分发、异常错误封装、命令收尾与内存资源释放等工作。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 | /*
* 处理SCSI命令 - 从工作队列调用
* @param work: 工作结构体指针
*/
static void vhost_process_command(struct work_struct *work)
{
struct vhost_work *vwork = container_of(work, struct vhost_work, work); /* 获取工作项 */
struct scsi_cmnd *cmd = vwork->cmd; /* 获取SCSI命令 */
struct Scsi_Host *host = cmd->device->host; /* 获取SCSI主机 */
unsigned char opcode = cmd->cmnd[0]; /* 获取命令操作码 */
int ret = 0; /* 返回值 */
/* 只处理目标ID为0和LUN为0的命令 */
if (cmd->device->id != 0 || cmd->device->lun != 0) {
set_sense_data(cmd, NO_SENSE, 0x00, 0x00); /* 设置无错误 */
cmd->result = (DID_NO_CONNECT << 16) | SAM_STAT_CHECK_CONDITION; /* 无连接 */
vhost_scsi_done(cmd); /* 完成命令 */
kfree(vwork); /* 释放工作项 */
return;
}
/* 根据操作码分发处理 */
switch (opcode) {
case OPCODE_TEST_UNIT_READY: /* 0x00 - 设备就绪检测(6字节):检查设备是否可访问 */
ret = do_test_unit_ready(cmd);
break;
case OPCODE_INQUIRY: /* 0x12 - 查询设备信息(6字节):返回厂商名、产品名等 */
ret = do_inquiry(cmd);
break;
case OPCODE_MODE_SENSE_6: /* 0x1A - 模式读取(6字节):返回设备模式页面(缓存、错误恢复等) */
ret = do_mode_sense_6(cmd);
break;
case OPCODE_MODE_SENSE_10: /* 0x5A - 模式读取(10字节):同上,支持更大的数据长度 */
ret = do_mode_sense_10(cmd);
break;
case OPCODE_MODE_SELECT_6: /* 0x15 - 模式配置(6字节):设置设备模式参数 */
ret = do_mode_select_6(cmd);
break;
case OPCODE_MODE_SELECT_10: /* 0x55 - 模式配置(10字节):同上,支持更大的数据长度 */
ret = do_mode_select_10(cmd);
break;
case OPCODE_READ_CAPACITY_10: /* 0x25 - 读取容量(10字节):返回最大LBA和扇区大小(32位) */
ret = do_read_capacity_10(cmd);
break;
case OPCODE_READ_CAPACITY_16: /* 0x9E - 读取容量(16字节):返回最大LBA和扇区大小(64位) */
ret = do_read_capacity_16(cmd);
break;
case OPCODE_READ_6: /* 0x08 - 读命令(6字节):从磁盘读取数据(LBA < 2^24) */
case OPCODE_READ_10: /* 0x28 - 读命令(10字节):从磁盘读取数据(LBA < 2^32) */
ret = do_read(cmd);
break;
case OPCODE_WRITE_6: /* 0x0A - 写命令(6字节):向磁盘写入数据(LBA < 2^24) */
case OPCODE_WRITE_10: /* 0x2A - 写命令(10字节):向磁盘写入数据(LBA < 2^32) */
ret = do_write(cmd);
break;
case OPCODE_VERIFY_10: /* 0x2F - 数据校验(10字节):验证指定扇区的数据完整性 */
ret = do_verify(cmd);
break;
case OPCODE_SYNCHRONIZE_CACHE: /* 0x35 - 同步缓存(10字节):将缓存数据刷新到磁盘 */
ret = do_synchronize_cache(cmd);
break;
default: /* 未知命令:返回非法请求错误 */
dev_dbg(&host->shost_gendev, "Unsupported SCSI command: 0x%02x\n", opcode);
set_sense_data(cmd, ILLEGAL_REQUEST, 0x20, 0x00); /* 无效命令操作码 */
cmd->result = (DID_OK << 16) | SAM_STAT_CHECK_CONDITION;
ret = 0;
break;
}
/* 异步完成命令 */
vhost_scsi_done(cmd);
/* 释放工作项 */
kfree(vwork);
}
|
关键说明:
第7-10行:通过工作结构体反向解析出绑定的SCSI命令与工作项,获取当前待处理指令信息。
第14-20行:校验设备LUN与设备编号,仅允许合法0号虚拟设备访问,拦截非法设备请求并上报标准SCSI异常信息。
第23-61行:通过Switch分支匹配所有支持的SCSI命令操作码,分流至对应的读写、查询、校验、模式配置处理函数,实现模块化命令处理。
第62-67行:捕获未知不支持的命令码,自动填充非法请求Sense错误,避免驱动崩溃。
第71行:调用统一版本兼容收尾接口,标记底层所有命令执行完毕,通知内核完成IO资源回收。
第71行:主动释放异步工作项堆内存,避免驱动内存泄漏。
7.7.1.7. 基础设备状态查询¶
该部分实现TEST_UNIT_READY设备就绪检测命令处理,通过该函数模拟硬件设备就绪状态,向上层中间层反馈设备可用性。
1 2 3 4 5 6 7 8 9 10 11 | /*
* 执行SCSI TEST UNIT READY命令
* @param cmd: SCSI命令结构体
* @return: 返回处理状态
*/
static int do_test_unit_ready(struct scsi_cmnd *cmd)
{
/* 虚拟磁盘总是就绪的 */
cmd->result = DID_OK << 16; /* 设置命令结果为成功 */
return 0;
}
|
关键说明:
第9行:固定赋值SCSI标准成功状态码DID_OK,标识底层设备传输无异常、设备就绪。
7.7.1.8. 设备信息查询¶
该部分实现INQUIRY查询命令处理,通过该函数向内核上报虚拟SCSI设备的厂商信息、产品型号、协议版本、固件版本等硬件身份信息,让Linux内核识别当前设备类型、完成驱动匹配与设备注册。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 | /*
* INQUIRY命令响应数据 (SPC-4标准)
* 总长度36字节,其中前4字节为标准头部,后接31字节厂商/产品信息
*/
static unsigned char inquiry_data[] = {
/* 标准INQUIRY头部 (字节0~7) */
0x00, /* 外设限定符(bit 7-5)=0 设备存在, 设备类型(bit 4-0)=0 直接访问块设备,即磁盘 */
0x00, /* RMB(bit 7)=0 介质不可移除,其余位保留 */
0x06, /* 版本: 0x06 表示SPC-4 (ANSI版本) */
0x02, /* 响应数据格式: 0x02 标准格式,附加数据从字节4开始 */
0x1F, /* 附加长度 = 31 (0x1F) 表示后续31字节有效,总长度=4+1+31=36 */
0x00, 0x00, 0x00, /* 保留字节,填0 */
/* 厂商名:"LubanCat" (8字节) */
0x4C, 0x75, 0x62, 0x61, /* L u b a */
0x6E, 0x43, 0x61, 0x74, /* n C a t */
/* 产品ID:"VIRTUAL DISK" (16字节) */
0x56, 0x49, 0x52, 0x54, /* V I R T */
0x55, 0x41, 0x4C, 0x20, /* U A L (空格) */
0x44, 0x49, 0x53, 0x4B, /* D I S K */
0x20, 0x20, 0x20, 0x20, /* 补空格至16字节 */
/* 产品修订级别:"1.0 " (4字节) */
0x31, 0x2E, 0x30, 0x20, /* 1 . 0 (空格) */
};
/*
* 执行SCSI INQUIRY命令
* @param cmd: SCSI命令结构体
* @return: 返回实际传输的数据长度
*/
static int do_inquiry(struct scsi_cmnd *cmd)
{
unsigned char *cdb = cmd->cmnd; /* 获取命令描述块指针 */
u32 alloc_len = get_unaligned_be16(cdb + 3); /* 主机请求的分配长度 (CDB[3:4]大端序) */
u32 data_len = sizeof(inquiry_data); /* 本地INQUIRY数据总长度 (u32避免min类型警告) */
u32 copy_len = min(alloc_len, data_len); /* 实际复制长度为请求长度与本地数据的最小值 */
/* 设置数据传输方向: 数据从设备流向主机 */
cmd->sc_data_direction = DMA_FROM_DEVICE;
/* 将INQUIRY数据复制到sg列表,只复制允许的字节数 */
scsi_sg_copy_from_buffer(cmd, inquiry_data, copy_len);
/* 设置剩余未传输字节数:请求长度 - 实际传输长度 */
scsi_set_resid(cmd, alloc_len - copy_len);
cmd->result = DID_OK << 16; /* 命令成功完成 */
return copy_len; /* 返回实际传输字节数 */
}
|
关键说明:
第1行:设置附加长度为31(0x1F),确保INQUIRY总长度为36字节。
第36行:从命令描述块的第3-4字节以大端序提取主机请求的分配长度。
第38行:使用min将实际传输长度限制为主机请求长度与本地数据总长的较小值。
第41行:指定数据传输方向为从设备到主机,与INQUIRY命令的“读取”性质一致。
第44行:通过内核标准接口将inquiry_data中copy_len字节的数据拷贝至SCSI中间层分配的scatter-gather缓冲区,完成数据向上层上报。
第47行:正确设置残留长度,向内核报告“请求了alloc_len字节,实际仅传输copy_len字节”。
第49行:标记命令成功完成,状态码置于SCSI结果的host byte字段,通知内核此INQUIRY无异常。
第50行:返回实际传输字节数,上层可据此校验或做后续处理。
7.7.1.9. 设备容量查询¶
该部分实现READ_CAPACITY 10字节/16字节容量查询命令处理,负责向上层内核上报虚拟磁盘的最大有效LBA地址与单扇区大小,是系统识别磁盘容量、完成分区与IO寻址的依据。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 | /*
* 执行SCSI READ CAPACITY(10)命令
* @param cmd: SCSI命令结构体
* @return: 返回响应数据长度
*/
static int do_read_capacity_10(struct scsi_cmnd *cmd)
{
u32 last_lba = VIRTUAL_DISK_SECTORS - 1; /* 最后一个逻辑块地址(32位) */
u32 sector_size = VIRTUAL_SECTOR_SIZE; /* 扇区大小 */
unsigned char buf[8]; /* 8字节响应缓冲区 */
/* READ CAPACITY(10)响应格式 - 大端序 */
buf[0] = (last_lba >> 24) & 0xFF; /* LBA高字节 */
buf[1] = (last_lba >> 16) & 0xFF;
buf[2] = (last_lba >> 8) & 0xFF;
buf[3] = last_lba & 0xFF; /* LBA低字节 */
buf[4] = (sector_size >> 24) & 0xFF; /* 扇区大小高字节 */
buf[5] = (sector_size >> 16) & 0xFF;
buf[6] = (sector_size >> 8) & 0xFF;
buf[7] = sector_size & 0xFF; /* 扇区大小低字节 */
cmd->sc_data_direction = DMA_FROM_DEVICE; /* 设置数据传输方向 */
/* 将数据复制到scatter-gather列表(DMA_FROM_DEVICE: 从buf到sg) */
scsi_sg_copy_from_buffer(cmd, buf, 8);
cmd->result = DID_OK << 16; /* 设置命令结果为成功 */
scsi_set_resid(cmd, 0); /* 没有剩余数据 */
return 8; /* 返回8字节 */
}
/*
* 执行SCSI READ CAPACITY(16)命令
* @param cmd: SCSI命令结构体
* @return: 返回响应数据长度
*/
static int do_read_capacity_16(struct scsi_cmnd *cmd)
{
u64 last_lba = VIRTUAL_DISK_SECTORS - 1; /* 最后一个逻辑块地址(64位) */
u32 sector_size = VIRTUAL_SECTOR_SIZE; /* 扇区大小 */
unsigned char buf[32]; /* 32字节响应缓冲区 */
memset(buf, 0, 32); /* 清零缓冲区 */
/* READ CAPACITY(16)响应格式 - 大端序 */
/* 字节0-7: 最后一个可访问块的逻辑块地址 */
buf[0] = (last_lba >> 56) & 0xFF; /* LBA最高字节 */
buf[1] = (last_lba >> 48) & 0xFF;
buf[2] = (last_lba >> 40) & 0xFF;
buf[3] = (last_lba >> 32) & 0xFF;
buf[4] = (last_lba >> 24) & 0xFF;
buf[5] = (last_lba >> 16) & 0xFF;
buf[6] = (last_lba >> 8) & 0xFF;
buf[7] = last_lba & 0xFF; /* LBA最低字节 */
/* 字节8-11: 逻辑块长度(字节) */
buf[8] = (sector_size >> 24) & 0xFF;
buf[9] = (sector_size >> 16) & 0xFF;
buf[10] = (sector_size >> 8) & 0xFF;
buf[11] = sector_size & 0xFF;
cmd->sc_data_direction = DMA_FROM_DEVICE; /* 设置数据传输方向 */
/* 将数据复制到scatter-gather列表(DMA_FROM_DEVICE: 从buf到sg) */
scsi_sg_copy_from_buffer(cmd, buf, 32);
cmd->result = DID_OK << 16; /* 设置命令结果为成功 */
scsi_set_resid(cmd, 0); /* 没有剩余数据 */
return 32; /* 返回32字节 */
}
|
关键说明:
第8-9行(10字节函数):基于全局宏参数计算出磁盘最后一个有效扇区编号,作为磁盘最大寻址边界。
第10行:按照SPC-4协议标准,READ_CAPACITY_10指令固定返回8字节应答数据,因此定义长度为8的字符数组作为协议帧缓冲区。
第13-16行:SCSI协议与Linux内核设备交互使用网络大端序(大字节优先、高地址存低字节),而x86、ARM等主流CPU为小端序,对32位最大LBA地址进行逐字节移位拆分,将高位数据存入低地址、低位数据存入高地址,完成主机字节序向网络字节序的转换。
第18-21行:对512字节标准扇区大小进行四字节大端序封装,遵循READ_CAPACITY_10协议帧第4~7字节存储扇区尺寸的要求。
第23行:配置数据传输方向,明确本次交互为设备向内核主机上报数据。
第26行:调用内核标准接口,将封装完成的8字节标准容量协议帧,拷贝至SCSI命令SG散列缓冲区。
第28行:赋值DID_OK成功状态码,标记当前命令执行无误。
第29行:调用scsi_set_resid设置剩余数据为0,告知内核本次应答数据完整。
第31行:返回8字节固定协议长度,匹配READ_CAPACITY_10指令标准应答帧大小。
第41-42行(16字节函数):采用u64 64位无符号长整型存储最大LBA地址。
第43-45行:按照SPC-4协议标准,READ_CAPACITY_16指令固定返回32字节应答数据,因此定义长度为32的字符数组作为协议帧缓冲区,并且清零缓冲区。
第49-56行:对64位超长LBA地址进行8字节逐位拆分填充,覆盖32位高位寻址空间。
第59-62行:在扩展协议指定偏移位置封装扇区大小参数,兼容新型大容量磁盘的拓展属性预留字段。
第64-72行:进行设备读数据方向配置、SG散列拷贝、成功状态赋值、剩余数据清零,与10字节函数处理方式一致。
7.7.1.10. 设备模式配置与读取¶
该部分实现MODE_SENSE模式读取、MODE_SELECT模式配置命令的处理,用于读取设备的缓存参数、错误恢复策略、硬件配置信息,支持系统对设备参数进行配置。
6字节的MODE_SENSE/MODE_SELECT
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 | /*
* 执行SCSI MODE SENSE(6)命令
* 返回模式页面数据,包括缓存信息
* @param cmd: SCSI命令结构体
* @return: 返回响应数据长度
*/
static int do_mode_sense_6(struct scsi_cmnd *cmd)
{
unsigned char *cdb = cmd->cmnd; /* 获取命令描述块指针 */
u32 alloc_len = cdb[4]; /* MODE SENSE(6)的alloc_len在cdb[4] */
int dbd = (cdb[1] & 0x08) != 0; /* DBD位:禁止块描述符 */
int pcode = cdb[2] & 0x3F; /* 页面代码 */
int pcontrol = (cdb[2] >> 6) & 0x03; /* 页面控制字段 */
unsigned char buf[64]; /* 64字节响应缓冲区 */
int offset = 0; /* 当前写入位置偏移 */
int len; /* 实际响应长度 */
memset(buf, 0, 64); /* 清零缓冲区 */
/* 页面控制:0=当前值, 1=可更改值, 2=默认值, 3=保存值(不支持) */
if (pcontrol == 3) {
set_sense_data(cmd, ILLEGAL_REQUEST, 0x20, 0x00);
cmd->result = (DID_OK << 16) | SAM_STAT_CHECK_CONDITION;
return 0;
}
/* MODE SENSE(6)响应头 */
buf[0] = 0; /* 模式数据长度(后面设置) */
buf[1] = 0; /* 介质类型(0=无介质) */
buf[2] = 0x10; /* 设备特定:DPOFUA=1 */
buf[3] = dbd ? 0 : 8; /* 块描述符长度 */
offset = 4; /* 头4字节已填充 */
/* 块描述符(8字节) */
if (!dbd) {
/* 块数量(大端序) */
buf[offset++] = (VIRTUAL_DISK_SECTORS >> 24) & 0xFF;
buf[offset++] = (VIRTUAL_DISK_SECTORS >> 16) & 0xFF;
buf[offset++] = (VIRTUAL_DISK_SECTORS >> 8) & 0xFF;
buf[offset++] = VIRTUAL_DISK_SECTORS & 0xFF;
/* 保留 */
buf[offset++] = 0;
buf[offset++] = 0;
/* 块长度(大端序) */
buf[offset++] = (VIRTUAL_SECTOR_SIZE >> 8) & 0xFF;
buf[offset++] = VIRTUAL_SECTOR_SIZE & 0xFF;
}
/* 处理特定页面代码 */
switch (pcode) {
case 0x00: /* 所有页面 */
case 0x08: /* 缓存页面 */
/* 缓存页面:页面代码=0x08, 页面长度=0x12(18字节) */
buf[offset++] = 0x08; /* 页面代码 */
buf[offset++] = 0x12; /* 页面长度(18) */
buf[offset++] = 0x14; /* 缓存控制:WCE=1, RCD=1 */
buf[offset++] = 0x00; /* 缓存大小(MSB) - 未使用 */
buf[offset++] = 0xff; /* 缓存大小 */
buf[offset++] = 0xff; /* 缓存大小 */
buf[offset++] = 0x00; /* 缓存大小(LSB) */
buf[offset++] = 0x00; /* 阈值 */
buf[offset++] = 0xff; /* 预取大小 */
buf[offset++] = 0xff; /* 预取大小 */
buf[offset++] = 0xff; /* 预取上限 */
buf[offset++] = 0xff; /* 预取上限 */
buf[offset++] = 0x80; /* 缓存禁用(CD)=0, FUA=1 */
buf[offset++] = 0x14; /* 缓存控制(与字节2相同) */
buf[offset++] = 0x00; /* 保留 */
buf[offset++] = 0x00; /* 保留 */
buf[offset++] = 0x00; /* 保留 */
buf[offset++] = 0x00; /* 保留 */
buf[offset++] = 0x00; /* 保留 */
break;
case 0x01: /* 读写错误恢复页面 */
/* 简单的错误恢复页面 */
buf[offset++] = 0x01; /* 页面代码 */
buf[offset++] = 0x0a; /* 页面长度(10) */
buf[offset++] = 0x00; /* 错误恢复控制 */
buf[offset++] = 0x00; /* 读重试次数 */
buf[offset++] = 0x00; /* 校正跨度 */
buf[offset++] = 0x00; /* 磁头偏移次数 */
buf[offset++] = 0x00; /* 数据选通偏移次数 */
buf[offset++] = 0x00; /* 写重试次数 */
buf[offset++] = 0x00; /* 恢复时间限制 */
buf[offset++] = 0x00; /* 恢复时间限制 */
buf[offset++] = 0x00; /* 保留 */
break;
default:
/* 未知页面 - 只返回头 */
break;
}
/* 设置总长度 */
len = min(offset, (int)alloc_len);
buf[0] = len - 1; /* 模式数据长度 = 总长度 - 1 */
cmd->sc_data_direction = DMA_FROM_DEVICE; /* 设置数据传输方向 */
scsi_sg_copy_from_buffer(cmd, buf, len); /* 将数据复制到sg列表 */
cmd->result = DID_OK << 16; /* 设置命令结果为成功 */
scsi_set_resid(cmd, alloc_len - len); /* 设置剩余长度 */
return len; /* 返回实际传输长度 */
}
/*
* 执行SCSI MODE SELECT(6)命令
* @param cmd: SCSI命令结构体
* @return: 返回0表示成功
*/
static int do_mode_select_6(struct scsi_cmnd *cmd)
{
unsigned char *cdb = cmd->cmnd; /* 获取命令描述块指针 */
u32 param_len = cdb[4]; /* MODE SELECT(6)的参数长度在cdb[4] */
unsigned char *buf; /* 临时缓冲区指针 */
/* 对于虚拟磁盘,接受但忽略模式选择 */
cmd->sc_data_direction = DMA_TO_DEVICE; /* 设置数据传输方向 */
/* 读取并丢弃数据 */
buf = kmalloc(param_len, GFP_KERNEL); /* 分配临时缓冲区 */
if (buf) {
scsi_sg_copy_to_buffer(cmd, buf, param_len); /* 从sg列表复制数据 */
kfree(buf); /* 释放临时缓冲区 */
}
cmd->result = DID_OK << 16; /* 设置命令结果为成功 */
scsi_set_resid(cmd, 0); /* 没有剩余数据 */
return 0;
}
|
关键说明:
第9-16行:解析CDB,提取主机请求的分配长度(alloc_len)、禁止块描述符标志(dbd)、请求的页面代码(pcode)以及页面控制字段(pcontrol)。
第20-25行:进行协议合法性校验。若主机请求“保存值(3)”而虚拟磁盘不支持,则主动构造ILLEGAL_REQUEST的Sense数据,并返回CHECK_CONDITION状态拒绝该命令。
第28-32行:构造MODE SENSE(6)标准的4字节响应头,填充介质类型、设备特定参数以及后续块描述符的长度。
第35-47行:若主机未禁止块描述符,则继续填充8字节的块描述符,以大端序上报虚拟磁盘的总扇区数和单扇区字节数。
第50-91行:根据主机请求的页面代码(pcode)动态构造模式页面数据。支持返回缓存页面(0x08)和读写错误恢复页面(0x01)。
第94-95行:计算实际生成的响应长度,并将其截断至主机请求的alloc_len范围内,最后回填响应头第0字节的“模式数据长度”字段,协议规定为总长度减1。
第97-100行:设置DMA方向为设备到主机,调用SG拷贝接口将构造好的模式页数据上报给内核,设置成功状态并计算残余数据长度。
第112-113行:解析CDB,提取主机下发的模式参数数据总长度(param_len)。
第117行:指定数据传输方向为主机到设备。
第120-124行:分配临时内核缓冲区,调用SG拷贝接口将主机下发的模式参数数据读出并丢弃。虚拟磁盘无需真实修改硬件配置,但必须消耗掉主机发来的数据以完成协议规定的命令交互流程。
第126-127行:设置命令成功状态码(DID_OK),并将残余数据长度(resid)置0,向内核表明主机下发的数据已被全部接收并处理完毕。
10字节的MODE_SENSE/MODE_SELECT
两者在实现逻辑上完全一致,区别仅在于协议格式上的扩展。以下黄色标注部分为和6字节差异部分。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 | /*
* 执行SCSI MODE SENSE(10)命令
* 返回模式页面数据,包括缓存信息
* @param cmd: SCSI命令结构体
* @return: 返回响应数据长度
*/
static int do_mode_sense_10(struct scsi_cmnd *cmd)
{
unsigned char *cdb = cmd->cmnd; /* 获取命令描述块指针 */
u32 alloc_len = get_unaligned_be16(cdb + 7); /* alloc_len是cdb[7:8]的2字节大端序值 */
int dbd = (cdb[1] & 0x08) != 0; /* DBD位:禁止块描述符 */
int pcode = cdb[2] & 0x3F; /* 页面代码 */
int pcontrol = (cdb[1] >> 6) & 0x03; /* 页面控制字段 */
unsigned char buf[64]; /* 64字节响应缓冲区 */
int offset = 0; /* 当前写入位置偏移 */
int len; /* 实际响应长度 */
memset(buf, 0, 64); /* 清零缓冲区 */
/* 页面控制:0=当前值, 1=可更改值, 2=默认值, 3=保存值(不支持) */
if (pcontrol == 3) {
set_sense_data(cmd, ILLEGAL_REQUEST, 0x20, 0x00);
cmd->result = (DID_OK << 16) | SAM_STAT_CHECK_CONDITION;
return 0;
}
/* MODE SENSE(10)响应头 */
buf[0] = 0; /* 模式数据长度高字节 */
buf[1] = 0; /* 模式数据长度低字节 */
buf[2] = 0; /* 介质类型(0=无介质) */
buf[3] = 0x10; /* 设备特定:DPOFUA=1 */
buf[7] = dbd ? 0 : 8; /* 块描述符长度 */
offset = 8; /* 头8字节已填充 */
/* 块描述符(8字节) */
if (!dbd) {
/* 块数量(大端序) */
buf[offset++] = (VIRTUAL_DISK_SECTORS >> 24) & 0xFF;
buf[offset++] = (VIRTUAL_DISK_SECTORS >> 16) & 0xFF;
buf[offset++] = (VIRTUAL_DISK_SECTORS >> 8) & 0xFF;
buf[offset++] = VIRTUAL_DISK_SECTORS & 0xFF;
/* 保留 */
buf[offset++] = 0;
buf[offset++] = 0;
/* 块长度(大端序) */
buf[offset++] = (VIRTUAL_SECTOR_SIZE >> 8) & 0xFF;
buf[offset++] = VIRTUAL_SECTOR_SIZE & 0xFF;
}
/* 处理特定页面代码 */
switch (pcode) {
case 0x00: /* 所有页面 */
case 0x08: /* 缓存页面 */
/* 缓存页面:页面代码=0x08, 页面长度=0x12(18字节) */
buf[offset++] = 0x08; /* 页面代码 */
buf[offset++] = 0x12; /* 页面长度(18) */
buf[offset++] = 0x14; /* 缓存控制:WCE=1, RCD=1 */
buf[offset++] = 0x00; /* 缓存大小(MSB) - 未使用 */
buf[offset++] = 0xff; /* 缓存大小 */
buf[offset++] = 0xff; /* 缓存大小 */
buf[offset++] = 0x00; /* 缓存大小(LSB) */
buf[offset++] = 0x00; /* 阈值 */
buf[offset++] = 0xff; /* 预取大小 */
buf[offset++] = 0xff; /* 预取大小 */
buf[offset++] = 0xff; /* 预取上限 */
buf[offset++] = 0xff; /* 预取上限 */
buf[offset++] = 0x80; /* 缓存禁用(CD)=0, FUA=1 */
buf[offset++] = 0x14; /* 缓存控制(与字节2相同) */
buf[offset++] = 0x00; /* 保留 */
buf[offset++] = 0x00; /* 保留 */
buf[offset++] = 0x00; /* 保留 */
buf[offset++] = 0x00; /* 保留 */
buf[offset++] = 0x00; /* 保留 */
break;
case 0x01: /* 读写错误恢复页面 */
/* 简单的错误恢复页面 */
buf[offset++] = 0x01; /* 页面代码 */
buf[offset++] = 0x0a; /* 页面长度(10) */
buf[offset++] = 0x00; /* 错误恢复控制 */
buf[offset++] = 0x00; /* 读重试次数 */
buf[offset++] = 0x00; /* 校正跨度 */
buf[offset++] = 0x00; /* 磁头偏移次数 */
buf[offset++] = 0x00; /* 数据选通偏移次数 */
buf[offset++] = 0x00; /* 写重试次数 */
buf[offset++] = 0x00; /* 恢复时间限制 */
buf[offset++] = 0x00; /* 恢复时间限制 */
buf[offset++] = 0x00; /* 保留 */
break;
default:
/* 未知页面 - 只返回头 */
break;
}
/* 设置总长度 */
len = min(offset, (int)alloc_len);
put_unaligned_be16(len - 2, buf); /* 模式数据长度 = 总长度 - 2 */
cmd->sc_data_direction = DMA_FROM_DEVICE; /* 设置数据传输方向 */
scsi_sg_copy_from_buffer(cmd, buf, len); /* 将数据复制到sg列表 */
cmd->result = DID_OK << 16; /* 设置命令结果为成功 */
scsi_set_resid(cmd, alloc_len - len); /* 设置剩余长度 */
return len; /* 返回实际传输长度 */
}
/*
* 执行SCSI MODE SELECT(10)命令
* @param cmd: SCSI命令结构体
* @return: 返回0表示成功
*/
static int do_mode_select_10(struct scsi_cmnd *cmd)
{
unsigned char *cdb = cmd->cmnd; /* 获取命令描述块指针 */
u32 param_len = get_unaligned_be16(cdb + 7); /* 参数长度在cdb[7:8] */
unsigned char *buf; /* 临时缓冲区指针 */
/* 对于虚拟磁盘,接受但忽略模式选择 */
cmd->sc_data_direction = DMA_TO_DEVICE; /* 设置数据传输方向 */
/* 读取并丢弃数据 */
buf = kmalloc(param_len, GFP_KERNEL); /* 分配临时缓冲区 */
if (buf) {
scsi_sg_copy_to_buffer(cmd, buf, param_len); /* 从sg列表复制数据 */
kfree(buf); /* 释放临时缓冲区 */
}
cmd->result = DID_OK << 16; /* 设置命令结果为成功 */
scsi_set_resid(cmd, 0); /* 没有剩余数据 */
return 0;
}
|
7.7.1.11. 磁盘读写命令处理¶
该部分实现SCSI读写命令的处理,兼容6字节老旧读写命令与10字节标准读写命令,完成LBA地址解析、参数合法性校验、并发保护、数据拷贝、IO统计等核心操作,模拟真实磁盘硬件的数据读写行为。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 | /*
* 执行SCSI READ命令(6字节或10字节)
* @param cmd: SCSI命令结构体
* @return: 返回读取的字节数或0表示失败
*/
static int do_read(struct scsi_cmnd *cmd)
{
struct vhost_data *vhost = host_to_vhost(cmd->device->host); /* 获取虚拟主机数据 */
unsigned char *cdb = cmd->cmnd; /* 获取命令描述块指针 */
u32 lba; /* 逻辑块地址 */
u32 num_sectors; /* 扇区数量 */
unsigned long offset; /* 缓冲区偏移 */
int ret = 0; /* 返回值 */
if (cmd->cmd_len == 6) {
/* READ(6)命令格式 */
lba = ((cdb[1] & 0x1F) << 16) | (cdb[2] << 8) | cdb[3]; /* LBA在cdb[1:3] */
num_sectors = cdb[4]; /* 扇区数在cdb[4] */
} else {
/* READ(10)命令格式 */
lba = get_be32(&cdb[2]); /* LBA在cdb[2:5] */
num_sectors = get_be16(&cdb[7]); /* 扇区数在cdb[7:8] */
}
/* 验证LBA和扇区数量 */
if (lba + num_sectors > VIRTUAL_DISK_SECTORS) {
set_sense_data(cmd, ILLEGAL_REQUEST, 0x21, 0x00); /* LBA越界 */
cmd->result = (DID_OK << 16) | SAM_STAT_CHECK_CONDITION;
return 0;
}
offset = lba * VIRTUAL_SECTOR_SIZE; /* 计算缓冲区偏移 */
cmd->sc_data_direction = DMA_FROM_DEVICE; /* 设置数据传输方向 */
spin_lock(&vhost->lock); /* 获取自旋锁 */
/* 从虚拟磁盘复制数据到scatter-gather列表(DMA_FROM_DEVICE: 从buf到sg) */
ret = scsi_sg_copy_from_buffer(cmd, vhost->disk_buffer + offset,
num_sectors * VIRTUAL_SECTOR_SIZE);
spin_unlock(&vhost->lock); /* 释放自旋锁 */
vhost->sectors_read += num_sectors; /* 更新读取统计 */
if (ret < 0) {
cmd->result = DID_ERROR << 16; /* 设置错误结果 */
return 0;
}
cmd->result = DID_OK << 16; /* 设置成功结果 */
scsi_set_resid(cmd, 0); /* 没有剩余数据 */
return ret; /* 返回读取的字节数 */
}
/*
* 执行SCSI WRITE命令(6字节或10字节)
* @param cmd: SCSI命令结构体
* @return: 返回写入的字节数或0表示失败
*/
static int do_write(struct scsi_cmnd *cmd)
{
struct vhost_data *vhost = host_to_vhost(cmd->device->host); /* 获取虚拟主机数据 */
unsigned char *cdb = cmd->cmnd; /* 获取命令描述块指针 */
u32 lba; /* 逻辑块地址 */
u32 num_sectors; /* 扇区数量 */
unsigned long offset; /* 缓冲区偏移 */
int ret = 0; /* 返回值 */
if (cmd->cmd_len == 6) {
/* WRITE(6)命令格式 */
lba = ((cdb[1] & 0x1F) << 16) | (cdb[2] << 8) | cdb[3]; /* LBA在cdb[1:3] */
num_sectors = cdb[4]; /* 扇区数在cdb[4] */
} else {
/* WRITE(10)命令格式 */
lba = get_be32(&cdb[2]); /* LBA在cdb[2:5] */
num_sectors = get_be16(&cdb[7]); /* 扇区数在cdb[7:8] */
}
/* 验证LBA和扇区数量 */
if (lba + num_sectors > VIRTUAL_DISK_SECTORS) {
set_sense_data(cmd, ILLEGAL_REQUEST, 0x21, 0x00); /* LBA越界 */
cmd->result = (DID_OK << 16) | SAM_STAT_CHECK_CONDITION;
return 0;
}
offset = lba * VIRTUAL_SECTOR_SIZE; /* 计算缓冲区偏移 */
cmd->sc_data_direction = DMA_TO_DEVICE; /* 设置数据传输方向 */
spin_lock(&vhost->lock); /* 获取自旋锁 */
/* 从scatter-gather列表复制数据到虚拟磁盘(DMA_TO_DEVICE: 从sg到buf) */
ret = scsi_sg_copy_to_buffer(cmd, vhost->disk_buffer + offset,
num_sectors * VIRTUAL_SECTOR_SIZE);
spin_unlock(&vhost->lock); /* 释放自旋锁 */
vhost->sectors_written += num_sectors; /* 更新写入统计 */
if (ret < 0) {
cmd->result = DID_ERROR << 16; /* 设置错误结果 */
return 0;
}
cmd->result = DID_OK << 16; /* 设置成功结果 */
scsi_set_resid(cmd, 0); /* 没有剩余数据 */
return ret; /* 返回写入的字节数 */
}
|
关键说明:
第15-23行:根据命令长度解析CDB。对于READ(6),LBA分布在cdb[1:3],扇区数在cdb[4];对于READ(10),LBA和扇区数分别位于cdb[2:5]和cdb[7:8]。
第26-30行:检查LBA是否超出虚拟磁盘的物理容量。若发生越界,调用set_sense_data构造SCSI标准错误码ILLEGAL_REQUEST(ASC=0x21表示Logical block address out of range),并将命令状态标记为SAM_STAT_CHECK_CONDITION。这会通知SCSI中间层“设备遇到了需要检查条件的错误”,从而触发上层的错误处理机制。
第33行:指定数据传输方向为设备到主机。
第38-39行:将设备端的连续内存数据拷贝到主机端由多个非连续物理页组成的SG列表中。
第50-52行:设置命令成功状态码(DID_OK),并将残余数据长度(resid)置0,表明主机请求的数据量已全部成功传输,没有残余。
第69-77行:逻辑与do_read一致,负责从CDB中提取主机请求写入的目标LBA和扇区数量,为后续的内存偏移计算做准备。
第80-84行:防止主机下发恶意或错误的越界写请求。
第87行:指定数据传输方向为主机到设备。
第92-93行:将主机端分散在SG列表各个物理页中的写入数据,“汇聚”并拷贝到设备端连续的disk_buffer指定偏移处。
第104-106行:设置命令成功状态码(DID_OK),并将残余数据长度(resid)置0,表示主机下发的所有写入数据均已被设备成功接收并写入。
7.7.1.12. 数据校验与缓存同步¶
该部分实现数据校验、缓存同步命令的处理,主要用于校验磁盘LBA读写合法性、模拟磁盘缓存数据同步操作。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | /*
* 执行SCSI VERIFY命令
* @param cmd: SCSI命令结构体
* @return: 返回0表示成功
*/
static int do_verify(struct scsi_cmnd *cmd)
{
unsigned char *cdb = cmd->cmnd; /* 获取命令描述块指针 */
u32 lba = get_be32(&cdb[2]); /* LBA在cdb[2:5] */
u32 num_sectors = get_be16(&cdb[7]); /* 扇区数在cdb[7:8] */
/* 验证LBA和扇区数量 */
if (lba + num_sectors > VIRTUAL_DISK_SECTORS) {
set_sense_data(cmd, ILLEGAL_REQUEST, 0x21, 0x00); /* LBA越界 */
cmd->result = (DID_OK << 16) | SAM_STAT_CHECK_CONDITION;
return 0;
}
cmd->result = DID_OK << 16; /* 设置成功结果 */
return 0;
}
/*
* 执行SCSI SYNCHRONIZE CACHE命令
* @param cmd: SCSI命令结构体
* @return: 返回0表示成功
*/
static int do_synchronize_cache(struct scsi_cmnd *cmd)
{
/* 虚拟磁盘不需要同步缓存(没有真实缓存) */
cmd->result = DID_OK << 16; /* 设置成功结果 */
return 0;
}
|
关键说明:
第13-17行:执行LBA地址越界校验,对超出虚拟磁盘容量的非法校验请求,调用异常函数上报SCSI协议错误。
第19行:固定返回DID_OK成功状态,模拟真实磁盘数据校验无误、存储完整性的正常反馈。
第31行:虚拟设备无物理缓存,直接返回执行成功。
7.7.1.13. EH错误处理¶
该部分实现SCSI标准EH错误处理回调函数,对应真实硬件的异常复位、命令中止功能,当内核检测到命令超时、设备异常时,调用本接口完成命令中止、主机重置、状态恢复等操作。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | /*
* 中止处理函数 - 当命令需要中止时调用
* @param cmd: 需要中止的SCSI命令
* @return: 返回SUCCESS表示成功
*/
static int vhost_eh_abort_handler(struct scsi_cmnd *cmd)
{
dev_info(&cmd->device->host->shost_gendev, "Abort command: opcode 0x%02x\n",
cmd->cmnd[0]);
/* 对于虚拟驱动,直接完成命令并返回错误 */
cmd->result = DID_ABORT << 16; /* 设置中止结果 */
vhost_scsi_done(cmd); /* 完成命令 */
return SUCCESS; /* 返回成功 */
}
/*
* 主机重置处理函数 - 当主机需要重置时调用
* @param cmd: SCSI命令,用于获取主机信息
* @return: 返回SUCCESS表示成功
*/
static int vhost_eh_host_reset_handler(struct scsi_cmnd *cmd)
{
struct vhost_data *vhost = host_to_vhost(cmd->device->host); /* 获取虚拟主机数据 */
dev_info(&cmd->device->host->shost_gendev, "Host reset requested\n");
/* 重置统计信息 */
spin_lock(&vhost->lock); /* 获取自旋锁 */
vhost->sectors_read = 0; /* 清零读取统计 */
vhost->sectors_written = 0; /* 清零写入统计 */
spin_unlock(&vhost->lock); /* 释放自旋锁 */
return SUCCESS; /* 返回成功 */
}
|
关键说明:
第8-9行:打印异常命令信息,便于故障定位。
第12行:赋值命令中止状态码,标记底层命令异常终止。
第13行:调用命令收尾接口,完成异常IO资源回收。
第15行:返回执行成功,告知内核底层中止处理操作完成。
第30-33行:加锁清空底层IO统计数据,重置设备运行状态,模拟硬件主机软复位效果。
第35行:返回执行成功,告知内核底层异常恢复操作完成。
7.7.2. Makefile说明¶
本节实验使用的Makefile如下所示,编写该Makefile时,只需要根据实际情况修改变量KERNEL_DIR、CROSS_COMPILE和obj-m即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | #指定内核路径,可以是相对路径或绝对路径
KERNEL_DIR=../../kernel/
# KERNEL_DIR=/home/guest/LubanCat_Linux_Generic_Full_SDK/kernel-6.1
# KERNEL_DIR=/home/guest/LubanCat_Linux_rk3588_SDK/kernel
#指定目标架构为arm64
ARCH=arm64
#指定交叉编译工具链的前缀
CROSS_COMPILE=aarch64-linux-gnu-
# CROSS_COMPILE=/home/guest/LubanCat_Linux_Generic_Full_SDK/prebuilts/gcc/linux-x86/aarch64/gcc-arm-10.3-2021.07-x86_64-aarch64-none-linux-gnu/bin/aarch64-none-linux-gnu-
#导出为环境变量
export ARCH CROSS_COMPILE
#指定要编译的内核模块目标文件
obj-m := virtual_scsi_host.o
#all :默认目标,执行时会编译驱动模块
#$(MAKE) :调用make工具
#-C $(KERNEL_DIR) :指定的内核源码目录
#M=$(CURDIR) :模块的源码位于当前目录
#modules :编译模块
all:
$(MAKE) -C $(KERNEL_DIR) M=$(CURDIR) modules
.PHONE:clean
#清理编译生成的文件
clean:
$(MAKE) -C $(KERNEL_DIR) M=$(CURDIR) clean
|
7.7.3. 编译驱动¶
在实验目录下输入 make 即可编译驱动,编译得到内核模块virtual_scsi_host.ko。
7.7.4. 程序运行结果¶
如出现 Permission denied 或类似字样,请注意用户权限,大部分操作硬件外设的功能,几乎都需要root用户权限,简单的解决方案是在执行语句前加入sudo或以root用户运行程序。
7.7.4.1. 实验操作¶
使用以下命令加载驱动:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | # 加载驱动
sudo insmod virtual_scsi_host.ko
### 信息输出如下
# 驱动初始化入口打印
[ 24.008222] virtual_scsi_host: Virtual SCSI Host Driver v1.0 initializing...
# 虚拟磁盘内存分配成功,16MB、总扇区32768、单扇区512字节
[ 24.035672] virtual_scsi_host: Allocated virtual disk: 16 MB (32768 sectors, 512 bytes/sector)
# 内核分配1号SCSI主机适配器host1,驱动主机注册成功
[ 24.036243] scsi host1: virtual_scsi_host
# 驱动所有资源初始化全部完成,读写统计初始化为0
[ 24.036729] virtual_scsi_host: Virtual SCSI Host initialized successfully!
[ 24.036751] virtual_scsi_host: Statistics: sectors_read=0, sectors_written=0
# SCSI拓扑:host1:channel0:target0:lun0,设备类型为磁盘 Direct-Access,ANSI版本为6
[ 24.037144] scsi 1:0:0:0: Direct-Access LubanCat VIRTUAL DISK 1.0 PQ: 0 ANSI: 6
# 块设备层识别虚拟磁盘,分配块设备节点/dev/sda,识别总扇区32768,容量16MiB
[ 24.039837] sd 1:0:0:0: [sda] 32768 512-byte logical blocks: (16.8 MB/16.0 MiB)
# 磁盘未开启写保护,支持读写操作
[ 24.040010] sd 1:0:0:0: [sda] Write Protect is off
# 以下两行是信息性消息,并非错误,只是说明内核不会利用写缓存来优化写入操作,所有写操作会立即写入虚拟磁盘缓冲区
[ 24.040091] sd 1:0:0:0: [sda] No Caching mode page found
[ 24.040111] sd 1:0:0:0: [sda] Assuming drive cache: write through
|
查看系统SCSI总线设备和块设备节点:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | # 查看系统SCSI总线设备需要安装lsscsi工具
sudo apt update && sudo apt install lsscsi
# 列出所有SCSI主机与LUN设备
lsscsi
# 信息输出如下
[1:0:0:0] disk LubanCat VIRTUAL DISK 1.0 /dev/sda
# 列出所有块设备节点
lsblk
# 信息输出如下
NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINTS
sda 8:0 0 16M 0 disk
mmcblk0 179:0 0 29.3G 0 disk
├─mmcblk0p1 179:1 0 8M 0 part
├─mmcblk0p2 179:2 0 128M 0 part /boot
└─mmcblk0p3 179:3 0 29.2G 0 part /
mmcblk0boot0 179:32 0 4M 1 disk
mmcblk0boot1 179:64 0 4M 1 disk
zram0 254:0 0 0B 0 disk
|
从输出信息可以看到SCSI地址:host 1,通道 0,target 0,lun 0;设备类型磁盘;块设备文件/dev/sda,大小16M,和物理磁盘并列。
使用sg系列工具直接下发SCSI CDB,调用底层驱动queuecommand接口,验证各命令处理函数:
1 2 | # 安装sg系列工具
sudo apt update && sudo apt install sg3-utils
|
INQUIRY 设备信息查询:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | # INQUIRY 设备信息查询验证
sudo sg_inq /dev/sda
# 信息输出如下
standard INQUIRY:
PQual=0 PDT=0 RMB=0 LU_CONG=0 hot_pluggable=0 version=0x06 [SPC-4]
[AERC=0] [TrmTsk=0] NormACA=0 HiSUP=0 Resp_data_format=2
SCCS=0 ACC=0 TPGS=0 3PC=0 Protect=0 [BQue=0]
EncServ=0 MultiP=0 [MChngr=0] [ACKREQQ=0] Addr16=0
[RelAdr=0] WBus16=0 Sync=0 [Linked=0] [TranDis=0] CmdQue=0
length=36 (0x24) Peripheral device type: disk
Vendor identification: LubanCat
Product identification: VIRTUAL DISK
Product revision level: 1.0
|
PQual=0:来自inquiry_data[0]的高3位,0表示设备已连接且逻辑单元存在。
PDT=0:来自inquiry_data[0]的低5位,0表示直接访问块设备,即磁盘。
RMB=0:来自inquiry_data[1]的最高位,0表示介质不可移除。
LU_CONG=0:逻辑单元拥塞位,保留,通常为0。
hot_pluggable=0:热插拔,0表示不支持热插拔。
version=0x06:ANSI版本,0x06对应SPC-4。
可以看到设备被正确识别为非可移除、SPC-4、直接访问块设备,厂商和产品信息与inquiry_data数组设置一致。
READ_CAPACITY 容量查询:
1 2 3 4 5 6 7 8 9 | # 容量查询
sudo sg_readcap /dev/sda
# 信息输出如下
Read Capacity results:
Last LBA=32767 (0x7fff), Number of logical blocks=32768
Logical block length=512 bytes
Hence:
Device size: 16777216 bytes, 16.0 MiB, 0.02 GB
|
返回的Last LBA=32767、Logical block length=512,设备容量16.0 MiB,与驱动中定义的VIRTUAL_DISK_SECTORS和VIRTUAL_SECTOR_SIZE完全一致。
TEST_UNIT_READY 就绪检测:
1 2 3 4 5 | # 就绪检测
sudo sg_turs -v /dev/sda
# 信息输出如下
test unit ready cdb: [00 00 00 00 00 00]
|
可以看到正常返回ready。
MODE_SENSE/MODE_SELECT 模式命令:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | # 读取缓存模式页,-p 8指定读取缓存模式页,对应0x08模式页
sudo sg_modes -p 8 /dev/sda
####### 信息输出如下
# 从MODE_SENSE应答中携带设备厂商、产品、固件版本
LubanCat VIRTUAL DISK 1.0 peripheral_type: disk [0x0]
Mode parameter header from MODE SENSE(10):
# 数据总长度35字节,介质类型为固定磁盘,写保护关闭,支持FUA强制写透功能,不支持超长LBA扩展标识,驱动填充8字节标准块描述符
Mode data length=35, medium type=0x00, WP=0, DpoFua=1, longlba=0
Block descriptor length=8
# 前4字节00 00 80 00(大端序)= 十进制32768,匹配宏VIRTUAL_DISK_SECTORS
# 后4字节00 00 02 00 = 十进制512,匹配宏VIRTUAL_SECTOR_SIZE
> Direct access device block descriptors:
Density code=0x0
00 00 00 80 00 00 00 02 00
# 缓存模式页
>> Caching, page_control: current
00 08 12 14 00 ff ff 00 00 ff ff ff ff 80 14 00 00
10 00 00 00
#######
# 测试MODE_SELECT,参考命令如下
sudo sg_wr_mode --force -p 8 -c 08,12,14 /dev/sda
|
从输出信息可以看到块描述符和缓存模式页和驱动中填充的一致。
VERIFY 扇区校验命令:
1 2 3 4 5 6 7 8 9 10 | # 校验LBA 0,共1000个扇区,正常无信息输出
sudo sg_verify -l 0 -c 1000 /dev/sda
# 验证LBA越界报错,传入超出磁盘最大扇区
sudo sg_verify -l 40000 -c 10 /dev/sda
# 信息输出如下,LBA超出范围
VERIFY(10): LBA out of range
failed near lba=40000 [0x9c40]
sg_verify failed: LBA out of range
|
SYNCHRONIZE_CACHE 缓存同步:
1 2 | # 验证缓存同步,正常无信息输出
sudo sg_sync /dev/sda
|
READ/WRITE命令:
1 2 3 4 5 6 7 | # 写入随机测试数据
sudo sg_dd if=/dev/urandom of=/dev/sda bs=512 count=10
# 读取数据到文件
sudo sg_dd if=/dev/sda of=test_data.bin bs=512 count=10
#无信息报错即为正常
|
虚拟块设备格式化与挂载测试:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | # 格式化为ext4文件系统
sudo mkfs.ext4 /dev/sda
# 信息输出如下
mke2fs 1.47.0 (5-Feb-2023)
Creating filesystem with 16384 1k blocks and 4096 inodes
Filesystem UUID: 5052d0bc-e26b-49f7-84b5-8ab8cc330d88
Superblock backups stored on blocks:
8193
Allocating group tables: done
Writing inode tables: done
Creating journal (1024 blocks): done
Writing superblocks and filesystem accounting information: done
# 挂载虚拟磁盘
sudo mount /dev/sda /mnt/
# 信息输出如下
[ 40.064055] EXT4-fs (sda): mounting with "discard" option, but the device does not support discard
[ 40.064108] EXT4-fs (sda): mounted filesystem with ordered data mode. Quota mode: disabled.
# 查看系统挂载情况
df -h
# 信息输出如下
Filesystem Size Used Avail Use% Mounted on
udev 962M 8.0K 962M 1% /dev
tmpfs 196M 1.2M 195M 1% /run
/dev/mmcblk0p3 29G 2.1G 26G 8% /
tmpfs 977M 0 977M 0% /dev/shm
tmpfs 5.0M 12K 5.0M 1% /run/lock
tmpfs 977M 4.0K 977M 1% /tmp
/dev/mmcblk0p2 124M 55M 64M 47% /boot
tmpfs 196M 0 196M 0% /run/user/0
/dev/sda 14M 14K 13M 1% /mnt
|
可以看到sda虚拟块设备可正常格式化为ext4文件系统,可以被正常挂载。
使用以下命令读写测试:
1 2 3 4 5 6 7 8 9 10 11 | #进入挂载目录
cd /mnt/
#写入测试文件
sudo sh -c "echo virtual_scsi_host > test.txt"
#读取测试文件
cat test.txt
#信息打印如下
virtual_scsi_host
|
可以看到可以正常创建和读取文件。
使用以下命令卸载设备与模块:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | #返回家目录
cd ~
#卸载挂载点
sudo umount /mnt/
#卸载内核模块
sudo rmmod virtual_scsi_host
#信息打印如下
[ 164.367897] virtual_scsi_host: Unloading Virtual SCSI Host Driver...
[ 164.368012] virtual_scsi_host: Final statistics:
[ 164.368026] virtual_scsi_host: Sectors read: 3804
[ 164.368039] virtual_scsi_host: Sectors written: 4616
[ 164.482600] virtual_scsi_host: Virtual SCSI Host Driver unloaded successfully!
|
可以从信息输出看到主机从虚拟磁盘读取了3804个扇区,因为每个扇区512字节,总共读取了约1.95 MB的数据。 主机向虚拟磁盘写入了4616个扇区,总计约2.36 MB的数据。所有资源都已正确释放,模块成功从内核移除。
7.7.5. 实验注意事项¶
所有SCSI命令CDB格式、Sense错误码、数据帧长度需遵循SPC-4协议,保证与内核SCSI子系统协议适配。
统一使用网络大端序封装LBA、数据长度等参数,避免大小端字节序错乱问题。
针对非法命令、越界LBA地址,必须返回标准ILLEGAL_REQUEST错误,完善协议异常处理。
内核内存拷贝、SG散列缓冲区操作需调用内核标准接口,禁止直接裸内存读写,避免内存越界风险。
虚拟磁盘缓冲区读写操作必须加自旋锁,保障多线程并发IO的数据一致性,防止数据错乱。