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协议。

 

posted @ 2023-03-14 23:19  free-锻炼身体  阅读(666)  评论(0编辑  收藏  举报