SCSI、ATA与T10的SAS
1981年SCSI产生。后一致与ATA并行竞争发展。例如串行的SATA的出现就导致了串行的SCSI:SAS的出现。SCSI目前的最新标准是T10,而RAID则是一种组织多个磁盘的冗余备份或加速的高层次并行架构,无论是ATA还是SCSI都支持RAID。ATA和SCSI关注的主要是如何扩展磁盘,如何传输数据。
由于长期的竞争和互相学习,ATA与SCSI趋同。物理上的不同不在本文的关注点内(ATA胜出),但上层的命令全部使用SCSI的命令定义。
linux系统中,你会经常见到SCSI,如果你去百度,你会得到很多SCSI相关的内容,实际上大部分Linux用户可能只是使用了SCSI的命令。一些SCSI的背景应该普及一下。1986年,SCSI总线标准化,定义了8位并行的总线协议SCSI-1,1994年推出了支持16位数据总线的SCSI-2,近代又出现了SCSI-3。就是这个SCSI-3,不像之前的协议重点在定义物理层,SCSI-3是一个文档集(现代的标准都倾向于用文档集了),其中有一个就是SCSI的命令集,后来这个命令集单独发展,被其他物理标准(如ATA)也广泛用来做命令标准(T10)。
ATA本来是试图移植高端昂贵的SCSI为一个便宜简单的版本而诞生的,诞生后独立发展。1992年推出ATA-2,1995年又退出ATA-3。后来又有了ATA-4、ATA-5、ATA-6,这些标准一直是在提高支持磁盘的容量,数据传输的速度还有一些监控诊断等功能。
并行时代SCSI性能上比ATA强,但进入串行时代,ATA的优势凸显,结果是物理层ATA胜出,命令集SCSI胜出。于是一个使用ATA物理层,使用SCSI的命令层的标准诞生了:T10委员会制定的SAS(Serial AttachedSCSI)。自此SCSI退出历史舞台。
SAS与2002年诞生,与PCI、usb总线一样,SAS也抛弃了总线架构,使用了交换架构。
问题还有一点,就是SCSI完全退出,SAS使用了SATA的物理层,但是SATA并没有完全合并进SAS,仍独立发展,SATA的物理层仍与SAS继续竞争(已有SATAII和SATA III),ATA的命令也在独立发展,然而目前大部分ATA设备也都支持SCSI的命令。目前的市场情况是SATA由于性价比占据了低端市场,SAS由于高性能占据了高端市场。全部使用SCSI命令。我们购买的普通PC机一般留有SATA接口,只可以插SATA硬盘。而如果主板上有SAS接口,则既可以插SATA,又可以插SAS硬盘。主板之所以普遍不集成SAS接口的原因是因为成本,SAS太贵。SATA完全可以满足个人用户。
SCSI命令族
SCSI命令包括基本命令和设备相关的命令。基本命令是所有SCSI设备都应提供的,术语叫做SCSI Primary Commands(SPC),最新的SPC版本时SPC-4第20a次修改,广泛使用的SPC-3目前是第23次修改版本。
SCSI命令集定义了一个命令模型,客户端(PC)发送一个SCSI命令给服务端(磁盘),数据格式是固定的,叫做CDB,里面有命令类型和参数。磁盘根据命令的执行情况设置sense key和sense code,然而这两个返回值并不会返回给PC,需要PC再发一条指令去获取才会返回。这样的设计本身是为了加速,然而linux在实现usb驱动的时候执行了一条命令就去获取一次结果,这个机制反而成了累赘(linux中很多如此的做法,因为商业版本的模块提供了更快更好的驱动,例如ntfs、电梯算法等)。
虽然ATA有自己的命令族,但由于ATA设备兼容SCSI命令族,所以linux内部全部以SCSI命令发送。
命令的解析
所有CDB命令的的第一个字节是一样,该字节分为两部分,第一部分group code可以用来表示本命令的总长度,可以看出共有6、10、12、16四中长度。第二部分表示具体的命令,有的命令有子命令,处理程序根据command code就可以找到与之对应的命令格式进行解析。
大部分命令都有其作用的地址,其地址在CDB中的表示是一个序号,表示一个分区的逻辑块的序号,从0开始到分区的最大值。
sense
sense这个词是scsi命令定义的,当执行一个scsi指令遇到错误的时候调用REQUEST SENSE给scsi设备,设备就会返回sense数据,sense数据包含具体的上一次出错原因。
当然,凡是标准定义的出错原因,必定都预定义好了。出错原因只能是预定义的一种。由于所有SCSI设备共享这个sense数据,所以这个错误必须能够涵盖各种设备,这也就决定了这个sense数据是分层次的。
上图是sense的数据格式,第一行是与所有其他回复命令共享的,因为要辨别回复的种类。对于sense 数据,RESPONSE CODE固定为72h或73h。
SENSEKEY是第一层大类。描述的比较宏观,得不到具体的信息。例如NO SENSE表示没问题,RECOVERED ERROR表示命令已经正常执行,但带来一些错误,这些错误也是可以恢复的。NOT READY表示设备还未准备就绪,MEDIUM ERROR表示传输和存储媒体上的错误。等等这样的有14种。
用户端直接调用scsi命令
我们知道在内核中对SCSI命令的转换发生在SCSI层。而SCSI分为3层,最上层是针对不同的设备的,sd表示磁盘,sr表示光盘,st表示磁带,sg表示通用。我们要关注的就是这个通用。文件系统向下调用磁盘中的文件需要用到的是sd,而sg内核驱动的存在使我们可以不使用文件系统,直接在用户空间调用scsi命令。
你可以自己写程序使用sg模块暴漏出来的API来发送SCSI命令,也可以使用已有的程序。这个程序集是sg3_utils。你可以apt或者yum安装这个包。安装之后键入sg,你会发现有很多sg开头的程序:
sg_inq/dev/sg0 查询sg0的信息,通常可作为硬件的ping。sg_raw可以发送用户自己定义的任意格式的软件。
SCSI(Small Computer System Interface)层
SCSI协议按照历史分为SCSI-1(5MB/S)、SCSI-2(20MB/S)、SCSI-3(并行多个子版本,速度过百兆),但这些都不是Linux系统中所指的SCSI,Linux中的软体SCSI只包含SCSI命令,其内容是根据数据请求构造命令,执行命令,反馈命令执行的结果。
所有的存储设备都是使用SCSI命令,只是数据传输的方式不一致,因此Linux对所有块设备的访问都通过SCSI层,在内核中,SCSI层包含上中下三层。
上层的作用有三个:区分不同的SCSI设备;将通用块层的数据请求转变为SCSI命令;向通用块层返回其数据请求的执行结果。
中层的作用有五个:抽象下层不同总线的操作为统一的API;提供注册和管理多个不同种类型下层总线设备;为下层的设备提供错误和超时处理能力;将来自上层SCSI命令进行排列并维护该队列;向较高层报告其SCSI命令执行的结果。这一层次与SCSI的specification直接相关,可以说是对SCSI总线的驱动实现。
下层的作用是唯一的:定义针对各种不同的SCSI适配器的操作接口,但是都对应的向上注册映射到中间层的标准API上。
由于磁盘是接收SCSI命令的,通用块层并不知道SCSI命令的存在。
上下层的逻辑图
下层
下层的SCSI适配器种类较多,但是在PC中一般是插在PCI-E上的SCSI host适配器(也叫做总线控制器),该host为物理上SCSI的总线的起点,但是却是内核中SCSI驱动的终点,一般是一个SCSI总线控制芯片。每个这种host可以支持多个channel,每个channel上可以连接多个SCSI节点(node),每个节点可能包括多个设备,这些挂载在SCSI总线上的设备,称为LUN(Logical Unit)。
但是在linux的scsi子系统中,scsihost不但可以指SCSI标准中规定的scsi总线,还有可能是一个虚拟的scsi总线设备,任何人都可以实现了scsi host所需要的函数操作,从而定义一个符合自己要求的scsi虚拟设备。还有可能是一个USB总线控制器,这样所有通过USB插口接入本机的设备只需要实现一个scsi host对应操作即可挂载到内核的接触范围。也可以是一个IDE总线控制器,如此,所有在该总线上的设备也可以挂载scsi子系统中了。由于现在的硬件个PCI总线一般作为主要总线存在,类似USB总线控制器,SCSI总线控制器等这些附加总线首先是作为一个PCI设备挂载到PCI总线上的,这种作为PCI设备的总线控制器叫做HBA(主机总线适配器)。
内核驱动代码也必须对未来有可能接入系统的各种设备进行定义。对于LUN的定义位于中间层的scsi_device结构体。而对于node的定义是中间层的scsi_target结构体,channel没有对应的结构体。
系统中也有可能同时存在多个SCSI控制芯片,也即多个SCSIhost。对于如何定位每个LUN设备就需要一种编码方式。根据拓扑结构可以很容易的知道定位的编码方式是:host_id: channel_id: node_id:lun_id。这些ID的生成方式不讨论,但是根据每个各设备的编号就可以定位到具体的单个lun设备了。
上下层的物理图
中层
从下层的描述可以知道,只有scsi_host以及对应物理上的SCSI总线控制器属于下层,而scsi_target和scsi_device则属于中层的数据结构,并且中层也定义了scsi_host的标准操作(scsi_host_template结构体)。该结构体的具体函数内容由下层定义。
scsi_host_template中有一个很重要的函数:queuecommand。该函数可以对应不同的scsi_host中对应的不同的操作。其意义都是将上层提交来的命令加入命令队列进行处理。这个队列的深度可以只有1,也可以支持多个命令的排列。这是不同的scsi host实现各自决定的。由于scsi_target和scsi_device在本层实现,这里还需要根据命令的执行情况动态的更新所存储的对应设备的信息。
这里要特别注意的是scsi_target和scsi_device并不是存在本机的物理实体,而是外设在本机内存中的建模,表示了外设的状况。
上层
较高层区分不同的SCSI设备类型,典型的包括磁盘(sd)、磁带(st)、CD(sr)还有一个通用设备(sg),分别定义了这4个驱动,括号内是内核对其的简称。不同的设备可以将针对不同设备的特定请求转化为对应的SCSI命令。特殊的,通用设备(sg)不对应任何具体的设备类型,用户端可以直接使用sg提供的接口向任何支持SCSI命令的设备发送SCSI命令,典型的用户端支持程序是sg3utils包。
通过中层的讨论,具备了上层如何将通用块层下传的理论基础。上层从通用块层接收到了数据访问的请求,将其转化为SCSI命令,这个命令在上层中定义为scsi_cmnd结构体。然后调用中间层的scsi_host_template结构体中定义的queuecommand接口,将此命令交付中层处理。
在命令处理结束,本层的回调函数会被以软中断的形式调用,以处理与命令相关的后续操作和通知通用块层该条命令的执行结果。
SCSI命令
request
SCSI命令有256种,每个命令被组织成CDB的数据结构发送给磁盘,每个磁盘都识别SCSI命令的CDB结构。CDB结构包含了命令类型和命令参数。大体包含如下域:
l Operation Code:就是命令的256种类型的一种。而这个域被进一步拆解为group code和command code,前面的group code表示的是CDB的总长度,一共有8种,不过预定义的只有6,10,12,16长度的CDB
l Control:所有命令都有的,携带标志位,可厂商定义
以下都不是必须的,而是不同的命令对应有不同的域
l Service Action:这是子命令,并不是所有CDB都有,在某个Operation Code下的不同子命令用这个域区分
l Logical Block Address:这是磁盘的逻辑地址,用来表示命令操作的内容。
l Transfer Length:这是在传送数据时用来表示要传送的数据的长度
l Parameter list length:命令的参数的长度
l Allocation Length:说明客户端可用的接收缓存的大小
response
对CDB命令的响应命令叫sense。但是这个响应可不是自动产生的,需要scsi设备主动使用sense request命令去查询。所以对于发送request方来说,命令的执行结束分为两个阶段,发送成功和磁盘设备执行成功。函数调用结束的状态只表示是本机发送该命令的结果状态,而不表示实际磁盘设备的执行情况。如果需要获得执行情况,需要去手动获取sense数据。
目前的linux的scsi实现就是这两个阶段的回调,一个是处理本机处理结果,另一个是发送sense request查询设备的执行结果,才会继续向下执行。
scsi_cmnd
承载CDB的是scsi_cmnd结构体,但是不要误会的一点是:CDB中定义了很多结构域,但是这些结构域丝毫没有对应到scsi_cmnd结构体中,而在这个结构体中,CDB仅仅是以一个字符串指针的形式存在的。既然如此,那这个结构体有何作用?
答案是管理作用。
l 唯一定位与追踪:给每个命令分配一个唯一的ID:serial_number,在命令生成时产生,执行结束时销毁
l 结果处理:eh_eflags,underflow(如果实际传输的数据少于这个值就会返回错误),result
l 命令属性:创建时间(jiffies_at_alloc)、命令被重试的次数(retries)、保证传输的大小(transfersize,在发生故障或是断开连接前保证的最小传输单元)
l 上下文指针:设备指针(scsi_device)、通用块层的请求指针(request)、执行这个命令的工作队列(abort_work)、
l 命令体:cmnd、cmd_len、sdb、prot_sdb、盛放命令的执行结果(sense_buffer),回调函数(scsi_done)
l 为其他组件提供的数据存储:host_scribble(host可以申请内存放在这里,也可以释放)
struct scsi_cmnd {
structscsi_device *device;
structlist_head list; /* scsi_cmndparticipates in queue lists */
structlist_head eh_entry; /* entry for the host eh_cmd_q */
structdelayed_work abort_work;
inteh_eflags; /* Used by errorhandlr */
unsignedlong serial_number;
unsignedlong jiffies_at_alloc;
intretries;
intallowed;
unsignedchar prot_op;
unsignedchar prot_type;
unsignedchar prot_flags;
unsignedshort cmd_len;
enumdma_data_direction sc_data_direction;
unsignedchar *cmnd;
structscsi_data_buffer sdb;
structscsi_data_buffer *prot_sdb;
unsignedunderflow;
unsignedtransfersize;
structrequest *request;
#define SCSI_SENSE_BUFFERSIZE 96
unsignedchar *sense_buffer;
void(*scsi_done) (struct scsi_cmnd *);
structscsi_pointer SCp; /* Scratchpad used bysome host adapters */
unsignedchar *host_scribble;
intresult; /* Status code from lowerlevel driver */
unsignedchar tag; /* SCSI-II queued command tag*/
};
数据存储机制
数据完整性检查
scsi流行的数据完整性检查有两种方式:DIF、DIX。
DIF机制需要收到磁盘和访问磁盘的操作系统的双方面支持。其在每个sector后面加8个字节的保护信息。这样一个sector的大小就变成了520(原来是512)。而这8个字节的计算是要在HBA硬件中完成的,然后由HBA一起传送给磁盘设备。不仅有DIF,还有其它种类的数据完整性方式,都是添加额外的数据到sector,特殊的,文件系统通过合理的安排数据,甚至可以不增加sector的大小,而利用已有的数据空间,安排一部分出来做完整性信息的存储,但是目前的磁盘都有提供了额外的空间,所以文件系统的做法就很大程度没必要了,但是想要获得更大的辅助空间,就可以利用数据交织和磁盘提供的有限的额外位来提供更多的位。额外的位也并不一定用来存储校验信息,可以是tag,由文件系统决定存放什么。
而对于HBA不支持自动计算DIF的设备,就需要在内核中计算,然后由内核一起传递给磁盘设备。这种内核中计算校验信息的机制叫做DIX(Data Integraty Extension)。
磁盘驱动
磁盘驱动的文件时sd.c和sd.h,抽象的设备结构体是structscsi_disk,而这个设备是更上层的gendisk的一种。
读写函数初始化(sd.c所在的上层层面)
sd设备的读写初始化函数是sd_setup_read_write_cmnd(structscsi_cmnd *SCpnt)。我们可以做些优化,所以这里就边分析边优化。
l 获得要读写的内存位置和大小
l 初始化命令携带的数据存储结构(初始化scatterlist,将bio中携带的上层数据映射到scatterlist中)
l 检查读写命令的是否错误(设备是否在线,读写大小是否超出界限,设备正在发生变化)
l 处理特殊情况(sd卡不能连续读取最后几个sector、 读写的大小的最低位和设备实际的最小sector大小不一致(上层提交下来的全部是512为单位的读写单元))
l 实际生成命令
n 读全部使用READ_6,写全部使用WRITE_6初始化命令头部
n 写数据需要数据完整性检查(未知作用)
n DIF/DIX检查和处理(这是数据完整的特性,可以在数据后存储校验值)
n 根据读写大小重新初始化命令头部为READ_10、READ_12、READ_16等
l 初始化命令的其他域
通过之上的逻辑可以看出,如下可以提高效率:
l 特殊情况处理(不大)
l 去掉DIF/DIX检查(极大)
注意的是这里只有初始化读写的函数,没有实际的发送函数,实际的发送是由更底层的驱动(如USB)执行的。
回调函数
由于我们现在位于sd.c位于的上层层面,这一层面生成命令,命令执行完后发生回调。当然在此之前,底层也会有向下的发送和向上的回调发生,但是这里不考虑。
回调函数是sd_done。在这里的时候sense数据已经获得到了,当然是由底层的回调或得到的,但是对sense数据进行判断和处理却是在这里执行。
scsi_driver
这些实际的执行函数构成了structscsi_driver结构体的域,也就是说,这些操作就是这个层次的scsi驱动。
static structscsi_driver sd_template = {
.owner =THIS_MODULE,
.gendrv = {
.name = "sd",
.probe = sd_probe,
.remove = sd_remove,
.shutdown = sd_shutdown,
.pm = &sd_pm_ops,
},
.rescan =sd_rescan,
.init_command = sd_init_command,
.uninit_command = sd_uninit_command,
.done =sd_done,
.eh_action = sd_eh_action,
};
一个驱动包括了设备的检测、电源管理,添加和删除、扫描,使用和错误处理等。
总结
我们可以发现内核中对上中下的划分与物理概念是相反的。最上层是最抽象的设备类型,中间层是不关心设备类型的通用设备的定义,最下层是不关心设备定义的传输方式接口。
数据流图
scsi设备的扫描和初始化
涉及到的内核数据结构
代码结构
scsi的驱动位于drivers/scsi/下。主要文件有:
sd.h, sd.c:
sg.h, sg.c:
sr.h, sr.c:
st.h, st.c:
scsi_wait_scan.c
scsi_scan.c
scsi.c
scsi_lib.c:定义了需要的各种函数,例如回调处理,队列管理,状态和模式管理等。
其他的诸如以scsi_开头的很多文件,如cam, tgt, proc,都是满足系统的特定接口需求的填充函数,不作为核心功能讨论。