SCSI分层
Linux SCSI子系统和其他子系统一样,也是一种分层的架构。共分为三层,最底下是低层,代表适用于SCSI的物理接口的实际驱动器,例如各个厂商为设备特定的主机适配器(也被称为主机总线适配器,Host Bus Adapter HBA)开发的驱动。低层驱动主要作用是发现连接到主线适配器上的SCSI设备,在内存中为它们建立好数据结构,并提供消息传递接口,将SCSI命令的接收与发送解释为主机适配器操作。
最上面的一层是高层,代表各种SCSI设备类型的驱动(磁盘类型、磁带类型、CD类型等)。高层驱动会“认领”低层驱动发现的SCSI设备,为他们分配设备名,将对设备的request转换为SCSI命令,交由低层驱动处理。
中间层也称为公共层或统一层,包含了SCSI堆栈的高层和低层的一些公共服务函数。高层和低层通过调用中间层的函数来实现其功能,而中间层在执行过程中,也需要调用高层和低层注册的回调函数来做个性化处理。
Linux SCSI模型
如上图,Linux SCSI模型基于传统的并行SCSI,主机适配器连接主机IO总线(通常是PCI总线)和存储IO总线(这里是SCSI总线)。一台计算机可以有多个主机适配器,而主机适配器根据其通道数,可以控制一个或多个SCSI总线,一条总线可以有多个目标节点与之相连,并且一个目标节点可以有多个逻辑单元(logic unit, lu),每个逻辑单元有其编号lun(logic unit number)。在Linux语义中,用SCSI设备描述符表示具体的逻辑单元,但和我们通常谈到的SCSI设备(例如SCSI磁盘)是不一样的。一个SCSI磁盘应该被视作一个目标节点,在这样一个目标节点内,可以有多个逻辑单元,统一由磁盘控制器控制,这些逻辑单元才是真正作为IO终点的存储设备。但是,为了简单起见,大多数厂商生产的磁盘只做了基本实现,只实现了LUN0。
因此,确定了Host、channel、目标节点id、lun,即可唯一定位一个逻辑单元,在Linux中,可以表示为
举个栗子:
5:0:0:0 即表示,插在host5适配器上,0通道总线下第0个目标设备的第0个逻辑单元。
对于并行SCSI总线,其所能挂接的设备数目是受物理层,总线宽度限制的,依据在SCSI总线上可设置的地址数量,并行SCSI又细分为:8个地址的窄SCSI(3位寻址)和16个地址(4位寻址)的宽SCSI,SCSI主机适配器是不可或缺的,它占用一个总线地址(7),对于窄SCSI,能挂载的目标节点数为7,对于宽SCSI能挂载的目标节点数为15,因此SCSI目标节点ID范围为0~6或8~15(7为host)。
而对于使用交换式拓扑结构的SAS或者“虚拟”iSCSI来说,channel和id是纯粹逻辑概念,没有物理意义,它的channel和id的范围也没有限制。
SCSI子系统的主要功能是:
探测SCSI设备,在内存建立可供设备驱动使用的核心结构;
在sysfs文件系统中构造SCSI子系统目录树;
SCSI高层驱动绑定SCSI设备,在内存中构建对应的核心结构;
提供错误恢复API,在SCSI命令错误和超时后被调用。
SCSI数据结构
根据上面的介绍,要将物理设备抽象到SCSI子系统中,至少需要定义下Host、Target(目标设备)、Device(逻辑单元)的结构。
于是在Linux中,这些数据结构具有以下的关系:
scsi_host:描述主机适配器,在linux中可以安装多个型号相同的主机适配器,由同一个低层驱动管理。当SCSI主机适配器被发现时,SCSI低层驱动为它分配一个scsi_host描述符,该描述符中一部分可供scsi中间层使用,另一部分为scsi低层驱动专用。在分配好scsi_host后,将其添加到系统并启动探测过程,探明scsi_target和scsi_device,并完成相关域、链表的配置。
scsi_target:描述目标节点,scsi_device:描述逻辑单元,他们的hostdata域指向SCSI低层驱动专用信息。
更具体的暂时不在本文说。
UFS概述
UFS全名为Universal Flash Storage(通用闪存存储),是一种具有串行接口的简单、高性能的大容量存储设备,主要被用在移动系统中,介于主机处理和大容量存储设备之间。
UFS采用一种多层次的架构设计,如下图:
UAP:应用层,应用层由UFS命令集UCS、设备管理device manager和任务管理Task manager组成。命令集UCS将会处理从SCSI子系统传入的常规的命令,如read、write等,UFS可能支持多命令集。UFS被设计为协议无关的,当前标准下的UCS是基于SCSI命令集的,UFS选择了简化的SCSI命令集,当需要扩展UFS功能时,可以支持UFS本机命令集。Task Manager处理用于命令队列控制命令,Device Manager提供设备级的控制,如Query Request和更低级连接层UIC的控制。
UFS Device Manager:设备管理器,设备管理器主要承担两个职责,处理设备级的操作和管理设备级的配置。设备级操作包含设备电源管理、与数据相关的设置、启用后台操作以及其他特定设备操作。设备管理器通过维护和存储一组描述符来管理设备级的配置。设备管理器处理类似于query request的命令允许修改或检索设备的配置信息。
Service Access Points:服务接入点,上图中椭圆形的部分称为服务接入点,例如,设备管理器通过访问UTP层暴露的服务接入点UDM_SAP从而进行设备级的操作和配置。
例如:
UDM_SAP:响应UTP层定义的Query Request和Query Response功能。
UIO_SAP:UIC暴露的接入点,响应一些设备重置的功能。
UTP:传输协议层,向更高层提供服务,UPIU为“UFS Protocol Information Unit” ,UFS协议信息单元,用于在UTP层在UFShost和UFSdevice间交换。例如,如果host侧的UTP收到了来自应用层的请求,UTP会为此请求生成一个UPIU,并传输到设备侧UTP。
UIC:互连层,UFS的最低层级,用于处理UFS host和UFS device之间的连接,主要包括MIPI UniPro 数据链路层和MIPI M-PHY 物理层,进行实际的数据传输。
UFS系统模型如上图。
UFS在SCSI中
不难发现,UFS管理了具体的host和device,它应当属于SCSI的低层驱动。
图源/参考:
https://blog.csdn.net/jasonactions/article/details/116932302
在之前对scsi请求处理的分析中,我们梳理了scsi请求处理的流程:
可以发现,当scsi上层向下层派遣命令时,是通过queuecommand这个回调函数进行的,当处理完成时,调用scsi_done通知上层。
对应函数ufshcd_queuecommand:
static int ufshcd_queuecommand(struct Scsi_Host *host, struct scsi_cmnd *cmd)
{
struct ufs_hba *hba = shost_priv(host);
int tag = scsi_cmd_to_rq(cmd)->tag;
struct ufshcd_lrb *lrbp;
int err = 0;
trace_android_vh_ufs_mcq_map_tag(hba,
(scsi_cmd_to_rq(cmd)->mq_hctx->queue_num), &tag);
WARN_ONCE(tag < 0, "Invalid tag %d\n", tag);
if (!down_read_trylock(&hba->clk_scaling_lock))
return SCSI_MLQUEUE_HOST_BUSY;
switch (hba->ufshcd_state) {
case UFSHCD_STATE_OPERATIONAL:
case UFSHCD_STATE_EH_SCHEDULED_NON_FATAL:
break;
case UFSHCD_STATE_EH_SCHEDULED_FATAL:
/*
* pm_runtime_get_sync() is used at error handling preparation
* stage. If a scsi cmd, e.g. the SSU cmd, is sent from hba's
* PM ops, it can never be finished if we let SCSI layer keep
* retrying it, which gets err handler stuck forever. Neither
* can we let the scsi cmd pass through, because UFS is in bad
* state, the scsi cmd may eventually time out, which will get
* err handler blocked for too long. So, just fail the scsi cmd
* sent from PM ops, err handler can recover PM error anyways.
*/
if (hba->pm_op_in_progress) {
hba->force_reset = true;
set_host_byte(cmd, DID_BAD_TARGET);
cmd->scsi_done(cmd);
goto out;
}
fallthrough;
case UFSHCD_STATE_RESET:
err = SCSI_MLQUEUE_HOST_BUSY;
goto out;
case UFSHCD_STATE_ERROR:
set_host_byte(cmd, DID_ERROR);
cmd->scsi_done(cmd);
goto out;
}
hba->req_abort_count = 0;
err = ufshcd_hold(hba, true);
if (err) {
err = SCSI_MLQUEUE_HOST_BUSY;
goto out;
}
WARN_ON(ufshcd_is_clkgating_allowed(hba) &&
(hba->clk_gating.state != CLKS_ON));
lrbp = &hba->lrb[tag];
WARN_ON(lrbp->cmd);
lrbp->cmd = cmd;
lrbp->sense_bufflen = UFS_SENSE_SIZE;
lrbp->sense_buffer = cmd->sense_buffer;
lrbp->task_tag = tag;
lrbp->lun = ufshcd_scsi_to_upiu_lun(cmd->device->lun);
lrbp->intr_cmd = !ufshcd_is_intr_aggr_allowed(hba) ? true : false;
trace_android_vh_ufs_mcq_set_sqid(hba, scsi_cmd_to_rq(cmd)->mq_hctx->queue_num, lrbp);
ufshcd_prepare_lrbp_crypto(scsi_cmd_to_rq(cmd), lrbp);
trace_android_vh_ufs_prepare_command(hba, scsi_cmd_to_rq(cmd), lrbp,
&err);
if (err) {
lrbp->cmd = NULL;
ufshcd_release(hba);
goto out;
}
lrbp->req_abort_skip = false;
ufshpb_prep(hba, lrbp);
ufshcd_comp_scsi_upiu(hba, lrbp);
err = ufshcd_map_sg(hba, lrbp);
if (err) {
lrbp->cmd = NULL;
ufshcd_release(hba);
goto out;
}
ufshcd_send_command(hba, tag);
out:
up_read(&hba->clk_scaling_lock);
if (ufs_trigger_eh()) {
unsigned long flags;
spin_lock_irqsave(hba->host->host_lock, flags);
ufshcd_schedule_eh_work(hba);
spin_unlock_irqrestore(hba->host->host_lock, flags);
}
return err;
}