UEFI-SCSI驱动分析
简介
passthru协议,该协议也被成为直通协议。直通协议的目的就是将UEFI的IO通路,透传下去。为了了解UEFI下面SCSI协议,本文对UEFI的SCSI协议进行分析。
2. 代码分析
2.1. 代码位置
UEFI的SCSI协议层代码所在的位置如下所示:(本文使用的是MdeModulePkg库)
2.2. 数据结构
所有的驱动模型,少不了数据结构的参与。无论在UEFI下还是linux下,每一个驱动模型必然有一个记录它属性的结构体。每个驱动或者设备的加载都会对这个结构体进行赋值。基本SCSI设备的ID显示如下:
关键的SCSI BUS和DEVICE的数据结构显示如下:
Device的数据结构中,包含了六个数据结构。第一个是结构体的标记,第二个是执行了bus的stop函数后还能找到协议位置的函数:
Bus device和Io device是关键性的数据结构,与linux的驱动模型类似了。
2.3. SCSI_BUS函数接口
2.3.1. SCSIBusDriverBindingSupport
判断这个驱动是否支持指定的控制器
2.3.2. SCSIBusDriverBindingStart
执行控制器上的这个驱动:
2.3.3. SCSIBusDriverBindingStop
停止指定控制器上的该驱动
2.3.4. SsciBusComponentNameGetDriverName
获得驱动的名字
2.3.5. SsciBusComponentNameGetControllerName
获取控制器的名字
2.3.6. ScsiGetDeviceType
获取SCSI控制器上设备的类型:
2.3.7. ScsiGetDeviceLocation
检索SCSI设备的位置:
2.3.8. ScsiResetBus
重启SCSI控制器所在的BUS
2.3.9. ScsiResetDevice
重启设备handle所绑定的SCSI控制器
2.3.10. ScsiExecuteSCSICommand
对SCSI控制器发送一个SCSI包:
2.3.11. ScsiScanCreateDevice
在SCSI的bus上扫描设备,并且将ScsiIo协议,绑定上去
2.3.12. DiscoverScsiDevice
发现SCSI设备
2.4. 名词分析
按照UEFI的代码流程,SCSI驱动依旧是使用BUS和device的模型。每个SCSI的具体设备,会有一个SCSI的device handle;SCSI controller就是SCSI设备的驱动,每个驱动可以连接一个设备,也可以连接多个设备。
ResetDevice,就是重启绑定了device设备的驱动;
ResetBus,就是连接了SCSI controller的bus,会重启一组SCSI controller。
UEFI的SCI模型如下所示:
扫面SCSI bus下面的device并创建的时候,也就是对扫描到的handle上绑定SCSIO协议。SCSIIO协议的具体内容,显示如下:
对于SCSIIO协议,主要存在五中操作需求,分别对应是“获取设备类型”,“获取设备的位置”,“重启bus”,“重启device”和“下发SCSI指令”。
2.5. SCSI BUS主体流程
2.5.1. 入口函数
在SCSI驱动的初始化过程中,使用的以下的入口函数:
该入口函数中做的事情,也就是绑定SCSIBus的driverbinding接口。
2.5.2. support函数
完成对imageHandle安装了ScsiBus驱动后,有对对应的controllerHandle执行connect之后,便会执行注册上的support函数。
判断是否执行start函数的条件就是判断当前的环境下是否安装了extPassThru协议或者PassThru协议。
这里的逻辑判断,主要进行以下的几个判断步骤:
(1) 在该控制器上打开extPassThru协议,显示EFI_ALREADY_STARTED表示执行成功,直接返回
(2) 在打成功的情况下,确认是否是devicePath的尾节点或者没有RemainingDevicePath这个变量,若是的话,关闭协议,返回成功
(3) 在打成功的情况下,确认是否是devicePath的尾节点或者没有RemainingDevicePath这个变量,若不是的话,获取设备的target和lun信息,获取成功,返回成功。获取失败,继续执行
(4) 在不存在extPassThru协议情况下,判断是否存在ScsiPassThru协议。打开显示EFI_ALREADY_STARTED,直接返回成功
(5) 在不存在extPassThru协议情况下,判断是否存在ScsiPassThru协议。打开显示失败,直接返回失败
(6) 打开成功,存在RemainingDevicePath,且不是尾节点,获取target和lun信息,无论成功与否,返回status。
总结:对于不存在子设备的bus,判断是否support成功的标志,就是是否可以打开extPassThru协议;若是存在子设别的bus,在打开extPassThru协议成功后,需要执行GetTargetLun也成功。
2.5.3. Stop函数
(1) 当没有NumberOfChildren的时候,先从控制器上获取SCSI_BUG_DEVICE,获取失败,直接返错;
(2) 卸载SCSI bus协议
(3) 当前的SCSI bus数据支持ExtPassThru协议的话,关闭extPassThru协议和ScsiPassThru协议;否则只关闭ScsiPassThru协议
(4) 关闭devicepath协议,去掉了这个bus的节点信息,返回成功
(5) 当NumberOfChildren不是空的时候,执行上面的(1)-(4)步骤,但凡有一个设备的关闭失败,都返回失败。
总结:执行stop函数是否成功,主要判断extPassThru协议是否有关闭成功,并且关闭devicepath协议;若是存在子bus,需要对每个子bus也下关闭ExtPassThru协议和devicepath协议。
2.5.4. start函数
start函数是整个控制器功能实现的基础,整个start接口的核心代码如下所示:
在支持passthru协议的前提下,首先会执行以下GetNextTargetLun这个函数,确保函数接函数是可以使用的。第一次调用GetNextTargetLun函数的,向下传递的TargetId是0xff,申请的大小是0x16,具体代码如下:
这里调用GetNextTargetLun的逻辑是我们自己写的,主要逻辑如下所示:
若是第一次调用,传入的targetId是0xff。我们会将targetId的值设置为0,其他情况会将targetId进行加一的操作。
在确保scsi bus上的GetNextTargetLun是可用的前提下,将passThru协议中的AdapterId赋值给Scsitarget
结构体中的变量没具体如下:
扫描SCSI BUS下面的device,如果对应的device在这个bus下面,则对这个device安装SCSI的IO协议:
在这个start函数中,会依次获取每一个targetlun,当获取到的targetlun失败的时候,就会返回失败结束该流程,否则会依次扫描并且创建device设备。
2.5.5. ScsiScanCreateDevice函数
构建devicepath协议:扫描Scsi device的第一步,是进行device path的构建:
这里的逻辑,首先是调用passthru协议的BuildDevicePath协议,获取到scsi设备的子node节点。之后再将这个节点追加到devicepath后面。
接下来要做的是是创建具体的device Io协议:
在这个函数中,主要是进行了ScsiIo能力集的赋值,具体的赋值内容如下,具体的函数,会在设备发现的时候,进行指令的下发:
在完成device IO设备结构的申请后,进行该设备的发现,若是没有发现设备,退出:
2.5.6. DiscoverScsiDevice函数
该函数的第一项操作就是申请inqury和senseData数据,将对应的数据结构,进行清0操作:
如何判断当前的设备是存在的,具体代码显示如下:
在发现设备的过程中,确认一下什么情况是失败的,主要有以下几种情况:
(1) 返回成功,但是,targetStatus的值是check contion,且sensedate的error_code是0x70,sense_key是EFI_SCSI_SK_ILLEGAL_REQUEST。
(2) Status是EFI_BAD_BUFFER_SIZE、EFI_INVALID_PARAMETER或者EFI_UNSUPPORTED
(3) Status其他的情况下返回失败,重试两边依旧失败。
设备被发现的情况:
在inquiry结构中获取到了对应的信息,且获取到的信息是无误的情况下,设备发现成功。
2.5.7. 能力集的赋值
SCSI BUS的能力集主要体现在如下的位置:
在这个位置上,前面的四个函数都很好理解,具体内容如下:
对于指令下发,最终使用的函数是:
2.6. SCSI device函数接口
2.6.1. 入口函数
2.6.2. Support函数
Support函数中,首先获取,当前控制器上绑定的scsiIo协议,从scsiIo协议中获取当前设备的类型。当前数据类型是DISK、CDROM或者WLUN的时候,是支持的,否则不支持,退出。
2.6.3. Stop函数
在stop的时候,先判断当前的异步事件,是否都处理完成,只有处理完成,才会进行接下来的处理:
需要卸载的协议,主要有以下的内容:
2.6.4. Start函数
Start函数的第一步,时进行函数能力集的赋值,再就根据设备的类别,为BlkIo进行赋值。接下来的操作中,是为Scsi设备下发inquiry指令帧,查看检索该设备的信息:
在对SCSI设备下发inqury指令后,若返回成功,不会再下发inqury指令,失败,且不用重试的情况下,关闭协议,退出流程;否则再发送一次inqury指令。
对SCSI设备下发了inqury帧后,会调用函数ScsiDiskDetectMedia去检测SCSI设备的容量。检测成功的话,会检测设备是否需要安装BlockIo协议。若需要安装BlockIo协议,会进行协议的安装:
在该函数中,我们需要额外注意两个函数的执行内容,分别是ScsiDiskInquiryDevice函数和ScsiDiskDetectMedia函数。下面会着重进行分析。
2.6.5. ScsiDiskInquiryDevice
对于BUS的驱动,最终调用的执行inquiry的函数是:
函数调用过程的执行流程是:
对于BUS函数,调用inquiry指令,只会判断返回值是什么,从而确定是否发现设备。但是对于Disk的设备驱动,整个函数的指令流程却复杂的多,主要执行流程如下所示:
第一步:
对SCSI设备下发inquiry帧,注意,这个帧下发的过程中,第三个参数是没有赋值的:
只有在inquiry的返回值是EFI_SUCCESS或者EFI_WARN_BUFFER_TOO_SMALL的时候,才会执行到解析流程。
参数解析的第一步是解析当前的设备是否是可移动设备:
在解析设备是否是可移动设备后,会再次下发一次inqury帧,主要是用来判断当前的设备是否支持Block Limits VPD page(0xB0)。
获取到当前的设备的supportVpdPages后,需要进行参数的合理性判断:
方发现设备是支持VPD page的情况下,会继续下发inqury帧,从而获取的是VPD page的限制信息:
对于Block Limits VPD page,读取出来就是一些IO的限制信息,具体如下:
步骤2:返回值的判断
对于SCSI的inqury指令,也会一次判断status、hostStatus和targetStatus三个部分。具体内容与执行的流程如下:
第三步:其他出错情况的执行
在inquiry流程执行出错的情况下,会对senseKey进行解析,主要的解析流程如下:
2.6.6. ScsiDiskInquirtDevice函数
在start函数中,各种若是在指定的device的controller上安装IO指令,前提是执行ScsiDiskInquirtDevice函数。该函数的作用是检测并且却独设备的容量:
第一步:设置定时器
该函数操作的第一步,是进行事件的订阅,目前不知道,为什么进行这个事件的订阅:
第二步:检测当前device的状态
函数testUnitReady用来检测当前的设备是否处于ready状态,DetectMediaParsingSenseKeys用来检测Sensedata的值。
接下来会发送0x25这个指令,判断当前的设备的参数信息:
2.6.7. ScsiDiskTestUnitReady
该函数的作用是判断当前的device设备是否处于ready状态。第一步下发testUnit指令:
这个函数的作用是下发opcode 0,判断当前的设备是否处于就为的状态:
接下来是进行返回值的判断:
接下来会下发解析senseKey的指令:
该函数的所有操作如下所示:
该函数的作用是,是下发opcode3的指令,获取SCSI设备的senseKey:
但是对于SCSI的设备一般不会走到这里。
2.7. 总结
分析到这里,再看一下passthru协议的打印信息:
设备在初始化过程中,一共下发了三个inqury帧,一个ready帧,一个参数获取帧。下面依次说明各个帧下发的时间:
第一个:EFI_SCSI_OP_INQUIRY(0x12)
SCSI BUS驱动下发,判断当前的设备是否存在
第二个:EFI_SCSI_OP_INQUIRY(0x12)
SCSI DEVICE驱动下发,判断当前的设备是否存在
第三个:EFI_SCSI_OP_INQUIRY(0x12)
SCSI DEVICE驱动下发,判断当前的设备是否支持Block Limits VPD page
第四个:EFI_SCSI_OP_INQUIRY(0x12)
SCSI DEVICE驱动下发,当前的设备的Block Limits VPD page
第五个:EFI_SCSI_OP_TEST_UNIT_READY(0x0)
SCSI DEVICE驱动下发,判断当前的设备是否处于ready状态
第六个:EFI_SCSI_OP_READY_CAPACITY(0x25)
SCSI DEVICE驱动下发,获取device的容量信息
当获取capacity的指令发生错误,返回值是0xFFFFFFFF的时候,或者当前的磁盘大于2T的时候,会下发第七个指令EFI_SCSI_OP_READY_CAPACITY16(0x9e)
第七个:EFI_SCSI_OP_READY_CAPACITY16(0x9e)
SCSI DEVICE驱动下发,获取device的容量信息
下面是对大于2T的硬盘绑定驱动时候的代码执行过程:
对于小于2T的盘,在报盘的时候,会执行如下的指令:
至此位置,在扫盘过程中对磁盘下发的非读写指令都梳理清晰。非去写指令都是在SCSI BUS驱动和SCSI DISK驱动绑定的过程中下发的。
写道这里,大家可能有些对Block Limits VPD page,有些模糊,在SCSI的spec上进行查找:
我们可以将Block Limits VPD page理解为一个pgae,这个page可以为用户提供一些支持的操作。
进行lba和len转换的公式是:
2.8. 扩展
2.8.1. IO的下达流程
如何对设备下发IO。
在SCSI设备初始化的时候,进行了能力集的赋值,具体如下:
若是对某一个设备下发IO指令,直接打开这个设备的BlkIo协议,通过BlkIo协议进行数据的下发即可。因为每一个SCSI设备都是会绑定一个BlkIo协议的。
例如,我们想对系统中的某个blk下发IO。需要做以下两步:
(1) 获取设备的devicepath路径:
(2) 将devicepath路径,转换成为device handle:
(3) 通过device handle找到该设备上的BlkIo协议
(4) 通过BlkIo协议,对指定设备下发读写操作
由于SCSI disk驱动的能力集赋值,导致最后会调用到,我们自己书写的passthru协议。
2.8.2. IO的读操作
在IO能力集的赋值中,主要进行四个函数能力的赋值,read、write、flush和reset:
我们首先从ReadBlock的位置查看:
该函数输入五个参数,第一个参数是IO的协议指针,第二个是mediaId,第三个是输入的lba,第四个是输入的buffersize,最后一个参数是数据buffer。
该函数做的第一件事是,判断当前的函数是否是可移动设备,若是可移动设备,需要判断该设备是否出入ready状态,若是发现设备进行了插拔,需要重新安装ScsiIo协议:
目前我们的盘,设置的是非可移动设备,直接进行参数的判断,再进行指令的下发:
读数据的函数,接下来会走进ScsiDiskReadSectors函数:
该函数首先看当前的cdb是支持16进制读写的,还是10进制读写的。若是支持16进制读写,最大的IO数量是4M,若不支持16进制读写,最大的IO数量是64K。
一般大约2T的硬盘,是支持16进制读写的。
设置超时时间:
超时时间,按照将所有字节全部传输完成,在加上额外的30S。以这个时间作为最小的超时时间。
每个IO的读写会进行最大的两次尝试:
对于返回成功,以及不需要重试的返回失败,会退出该流程。
调整lba的参数:
当raid10或者raid16降低了输出的ByteCount,我们应该将读数据的SectorCount进行响应的减少,对于timeOut参数,不需要进行改变,大一些没有关系。
2.8.3. ScsiDiskRead
对于SCSI的读写操作,无论是否是16进制的,现在都采用了一种Backoff算法,该算法第一次使用正常的指令进行下发,当第一次的错误是media error、sense data或者其他的什么,会将传输长度减少到一半,继续执行。直到传出成功或者传输数据的长度,降低到1个sector的长度。具体的逻辑如下:
3. 总结
本文详细分析了UEFI SCSI驱动的加载与执行流程。在协议分析的过程总,主要围绕同步IO展开,没有涉及到异步IO的流程。主要涉及一下几个部分:
(1) 设备发现的逻辑,依旧安装host->channel->target->lun的方式来进行
(2) SCSI bus驱动负责发现channel下面的device设备
(3) 每个device设备上都安装了BlockIo协议,BlockIo协议是同步IO协议,BlockIo2是异步IO协议
(4) IO读写操作会设置最大超时时间以及使用了backoff算法
(5) UEFI SCSI设备的发现是以为channel为基础进行发现的,所以在passthru协议的绑定上,也是以channel为单位进行绑定的。有几个chnnel,绑定几个passthru协议。