您的位置:首页 > 运维架构 > Linux

linux设备驱动程序和软硬件交互——以3w-9xxxx RAID卡驱动为例

2015-12-11 13:15 441 查看
3w-9xxx驱动程序包括3w-9xxx.h头文件和3w-9xxx.c主C程序,是AMCC公司3ware 9000 Storage Controller device(RAID卡系列)的驱动程序。在源文件的开头的注释中这样写的:

3w-9xxx.h -- 3ware 9000 Storage Controller device driver for Linux.
3w-9xxx.c -- 3ware 9000 Storage Controller device driver for Linux.


3ware 9000 Storage Controller device是一种PCI接口的RAID卡,因此,这个驱动程序本质上是一个PCI驱动程序。该PCI RAID卡后面接的是SCSI磁盘,走的是SCSI这套协议。我们知道,操作系统中SCSI子系统可以分为3层,分别是SCSI高层、SCSI中间层和SCSI底层。SCSI高层对应具体的块设备驱动,如磁盘驱动(sd.c),光盘驱动(sr.c)等。在这层中,需要支持设备要求支持的SCSI命令(虽然SCSI协议SBC中为块设备定义了统一的命令集,但是针对各种不同的块设备如磁盘、光盘等,还可以定义自己的命令),将块设备注册到块操作系统子系统中也是在该层完成的。SCSI提供一些公共的函数,包括错误处理和恢复,SCSI总线扫描等。SCSI底层是最底层的驱动程序,和硬件设备直接打交道来控制设备,因此和硬件设备的接口有关。比如我们这里是一个PCI的RAID卡,因此它是一个PCI驱动,位于SCSI 3层中的底层。该层需要初始化上层的业务逻辑,并将上层的命令发送到设备上去,因此,该层需要知道硬件设备暴露给它的编程接口,也就是各种控制和状态寄存器、其他特定的寄存器等。

下面根据我自己的理解,以读过程为例,简要的描述一下应用程序和硬件块设备之间进行数据交换的流程。首先,应用程序调用库函数fread或者直接调用read系统调用产生读请求,在这里读请求可以理解成需要从文件的某个偏移处读取多长的数据到用户的缓冲区中。上面的两个函数会调用sys_read进入到内核中,从这里开始进程在系统空间中运行。sys_read又会调用具体的文件系统注册的函数对请求进行处理,首先查看请求对应的数据是否在该文件的PAGE CACHE中存在,如果请求的数据已经在PAGE CACHE中,则直接将数据返回给用户空间的缓冲区中,这次请求结束。如果不再PAGE CACHE中,文件系统将经过处理后的请求封装成bio请求格式,发送到块层。bio请求包括读请求的逻辑扇区地址(LBA),读到内存的地址和长度(可能包含多个内存片段)。块层首先查看该bio请求是否能合并到请求队列的某个请求中,若能则合并请求,若不能则为该bio生成一个新的request请求经过重排序后加入到请求队列中。到这里,请求已经到了请求队列中,等待进一步的处理。操作系统的后台线程会定期(还有其他可能触发的条件)的从请求队列中取下请求交给SCSI子系统进行处理,包括调用SCSI上层的块设备驱动准备SCSI命令、准备用于DMA的聚散列表等,最后通过SCSI底层也就是底层驱动将命令发送到设备端。底层驱动往往是通过PIO的方式写设备端的寄存器将命令发送下去,或者将命令的地址写到设备的某个寄存器中,设备再通过DMA的方式从该地址把命令读下去。最终的结果是命令在设备端被解析、执行,然后设备将读取的数据通过DMA的方式写到主机端内核缓冲区或者直接到用户空间缓存(聚散DMA方式),并通过中断的方式通知主机请求执行完成。在中断服务程序中会唤醒之前在该数据上等待的进程继续执行。这样整个请求处理结束,fread或者read函数执行完成,应用程序继续往下执行。

上面只是个人的理解,欢迎大家批评指正。

之前自己了解了一下SCSI协议,还看了linux SCSI子系统相关的知识。我们知道,对于连接在PCI总线上的设备如显卡、网卡等(这里是PCI RAID卡),我们应该在PCI驱动的probe函数中初始化上层的业务逻辑,该RAID卡充当的是SCSI主机适配器的角色,我们在probe函数中要为该适配器分配Scsi_Host结构,并提供主机适配器模板(scsi_host_template),然后将Scsi_Host添加到SCSI子系统中,再调用scsi_scan_host扫描适配器后面的SCSI总线,将后面的SCSI设备添加到SCSI子系统中,这就是我们前面所指的业务逻辑。前面我们提到过,内核线程会定期从request_queue中把请求取出交给SCSI各层处理,最后通过调用queue_command函数将命令提交到底层驱动的命令队列中。我们知道,每个线程在提交了bio的请求后,bio对应的内存页或者缓冲区会被加锁,对应的线程会睡眠等待,往往在设备完成此次IO后产生中断,再通过中断服务程序最终调用bio_endio函数解锁锁定的内存页或者缓冲区,对应睡眠的线程会被唤醒,继续执行。但是,处理请求的内核线程往往只是通过底层驱动注册实现的queue_command函数将request请求提交到底层驱动队列(很多书籍上是这么说的),那又是那个线程把底层驱动队列里面的命令发送到设备上去的咧?我之前一直对这个问题存在疑问,所以打算自己亲自看看底层驱动的代码,弄清楚到底底层驱动是怎样处理的,还一个是弄清楚底层驱动怎样支持本地命令队列(NCQ)的。这也是我为什么要看3w-9xxx驱动程序的源代码的原因。

我所看的是linux 2.6.32内核下3w-9xxx驱动程序的源代码。到目前为止,内核的最新稳定版本是3.12.4,我也对比了2.6.32和3.12.4两个版本下3w-9xxx驱动,它们是一样的,也就是说没有随着内核版本的改进而出现新的版本。

下面言归正传,介绍3w-9xxx驱动是如何工作的。

在介绍驱动前,我们看看设备端(RAID卡)有哪些重要的寄存器,因为驱动需要与这些寄存器直接打交道。

在3w-9xxx.h头文件中有如下宏定义。

/* Control register bit definitions */

#define TW_CONTROL_CLEAR_HOST_INTERRUPT            0x00080000

#define TW_CONTROL_CLEAR_ATTENTION_INTERRUPT   0x00040000

#define TW_CONTROL_MASK_COMMAND_INTERRUPT      0x00020000

#define TW_CONTROL_MASK_RESPONSE_INTERRUPT     0x00010000

#define TW_CONTROL_UNMASK_COMMAND_INTERRUPT    0x00008000

#define TW_CONTROL_UNMASK_RESPONSE_INTERRUPT   0x00004000

#define TW_CONTROL_CLEAR_ERROR_STATUS       0x00000200

#define TW_CONTROL_ISSUE_SOFT_RESET               0x00000100

#define TW_CONTROL_ENABLE_INTERRUPTS           0x00000080

#define TW_CONTROL_DISABLE_INTERRUPTS          0x00000040

#define TW_CONTROL_ISSUE_HOST_INTERRUPT             0x00000020

#define TW_CONTROL_CLEAR_PARITY_ERROR          0x00800000

#define TW_CONTROL_CLEAR_QUEUE_ERROR           0x00400000

#define TW_CONTROL_CLEAR_PCI_ABORT             0x00100000


从上面的定义可以看出,设备端有一个32位的控制寄存器,每一位的含义可以通过宏定义的名称看出来。该控制寄存器包括一些中断清除、错误状态清除、中断屏蔽和中断使能等控制位,也就是通过写这些位可以完成这些功能。并且我们要看到,设备会存在4中类型的中断,包括HOST INTERRUPT、ATTENTION INTERRUPT、COMMAND INTERRUPT和RESPONSE INTERRUPT。HOST INTERRUPT我目前没有看到有什么情况下会产生,并且中断服务程序中在出现这种中断的情况下只是将该中断清空掉,并没有做任何特殊处理,所以大家不用关心该种类型的中断。ATTENTION INTERRUPT是在设备端出现某种错误或者异步事件的情况下产生的中断,产生该中断后主机会发送REQUEST SENSE命令到设备端来获取错误或者异步事件的具体信息,关于该命令可以参考SCSI协议。COMMAND INTERRUPT是在设备端的命令队列为空的情况下,也就是设备端的命令都执行完了或者设备端命令队列不为满了,需要主机发送新的命令来执行,在这种情况下主机会将驱动队列中缓存的命令尽可能的发送到设备端。这里需要提醒大家的是,驱动不仅支持SCSI命令,还有自定义的命令,从下面宏定义可以看出。在设备端完成主机发送过来的一条命令(或者请求)之后,如数据已经被读入到主机端或者被写入到设备上,设备端就会产生一条reponse消息放到设备端的response队列中,这种情况下回产生RESPONSE INTERRUPT。该reponse消息其实就是命令的标签(TAG),前面每条发送下来的命令都是有一个TAG的(参考本地命令队列NCQ),表示之前发送的标记为TAG的命令已经完成。主机端接收到该中断后,会从设备端的reponse队列中读取reponse消息,从而知道之前发送的那条命令已经完成了,因此可以唤醒之前在该请求上睡眠的进程继续执行。然后继续从reponse队列中读取下一个reponse,直到reponse队列为空,和发送命令一样,成批的处理,减少中断的次数,从而减少开销。上面中断后的处理是根据twa_interrupt中断服务程序的来的,有兴趣的读者可以自己看看。

/* Command packet opcodes used by the driver */

#define TW_OP_INIT_CONNECTION 0x1

#define TW_OP_GET_PARAM              0x12

#define TW_OP_SET_PARAM               0x13

#define TW_OP_EXECUTE_SCSI    0x10

#define TW_OP_DOWNLOAD_FIRMWARE 0x16

#define TW_OP_RESET             0x1C

/* Status register bit definitions */

#define TW_STATUS_MAJOR_VERSION_MASK           0xF0000000

#define TW_STATUS_MINOR_VERSION_MASK           0x0F000000

#define TW_STATUS_PCI_PARITY_ERROR           0x00800000

#define TW_STATUS_QUEUE_ERROR                  0x00400000

#define TW_STATUS_MICROCONTROLLER_ERROR           0x00200000

#define TW_STATUS_PCI_ABORT                 0x00100000

#define TW_STATUS_HOST_INTERRUPT             0x00080000

#define TW_STATUS_ATTENTION_INTERRUPT           0x00040000

#define TW_STATUS_COMMAND_INTERRUPT           0x00020000

#define TW_STATUS_RESPONSE_INTERRUPT             0x00010000

#define TW_STATUS_COMMAND_QUEUE_FULL                0x00008000

#define TW_STATUS_RESPONSE_QUEUE_EMPTY              0x00004000

#define TW_STATUS_MICROCONTROLLER_READY            0x00002000

#define TW_STATUS_COMMAND_QUEUE_EMPTY            0x00001000

#define TW_STATUS_EXPECTED_BITS                  0x00002000

#define TW_STATUS_UNEXPECTED_BITS            0x00F00000

#define TW_STATUS_VALID_INTERRUPT              0x00DF0000


上面的宏定义定义了状态寄存器的一些位,主要包括是否有中断(4中类型,前面说过)产生,设备端的命令队列或者响应队列空满标示及一些反应错误的状态位。

/* Macros */

#define TW_CONTROL_REG_ADDR(x) (x->base_addr)

#define TW_STATUS_REG_ADDR(x) ((unsigned char __iomem *)x->base_addr + 0x4)

#define TW_COMMAND_QUEUE_REG_ADDR(x) (sizeof(dma_addr_t) > 4 ? ((unsigned char __iomem *)x->base_addr + 0x20) : ((unsigned char __iomem *)x->base_addr + 0x8))

#define TW_COMMAND_QUEUE_REG_ADDR_LARGE(x) ((unsigned char __iomem *)x->base_addr + 0x20)

#define TW_RESPONSE_QUEUE_REG_ADDR(x) ((unsigned char __iomem *)x->base_addr + 0xC)

#define TW_RESPONSE_QUEUE_REG_ADDR_LARGE(x)


上面的宏可以看出,驱动能看见的只有4个(ADDR_LARGE不考虑)寄存器,包括命令寄存器、状态寄存器、命令队列寄存器和响应队列寄存器。主机端驱动通过对这4个寄存器的读写完成对设备的控制和设备上面数据的读写。下面根据我自己的理解对整个驱动和设备之间的协同工作进行说明,来描述读写流程,如图1所示。



图1 软硬件架构图

上面的架构图中省去了许多其他部分,但我认为不会影响大家的理解。

当应用程序产生读写请求后,会经过文件系统、块层和SCSI高层和中间层的处理转换成SCSI格式的命令请求,最后通过queue_command函数将请求发送到底层驱动。底层驱动接收到请求后,会将SCSI命令转换成自定义的命令包TW_Command_Full(包括适配器自身的管理命令和SCSI命令),如果此时硬件设备的命令队列没有满并且驱动端的命令队列中没有其他等待处理的命令,那么驱动就将这个命令包发送到硬件设备上。该过程的本质是将命令包的主机物理地址(映射到总线的地址)发送到设备的命令队列寄存器中。设备端发现有写命令队列寄存器的动作后,便立刻将寄存器中的值读入到设备端的命令队列中,这样就将主机端的命令发送到了设备端的命令队列中。如果设备端命令队列已满或者主机的命令队列中有等待处理的命令包,那么便立刻返回,SCSI层会将取下的request请求重新插入到块层request_queue队列的最前面,然后重试这个请求。命令调度器会从命令队列中取出最合适的请求来执行。适配器将命令发送到后面链接的SCSI总线上,并设置好DMA相关的内部寄存器,SCSI总线后面的SCSI设备接收到SCSI命令后,假如是读命令,SCSI设备将数据返回到适配器,然后适配器通过DMA的方式将数据传输到主机端内核缓冲区或者聚散列表对应的内存页中。然后适配器产生一个response消息到response队列中用来标示对应的命令已经完成。最后产生一个中断到主机端。

当主机端接收中断后,执行中断服务程序。中断服务程序首先会读取适配器的状态寄存器,看看具体是哪种中断。此时中断服务程序会发现是response中断引起的。中断服务程序会进行一些后处理的操作,包括释放命令队列中占用的命令包及其他一些清理操作,最后会调用注册的scsi_done函数结束中断服务程序。而scsi_done最终会唤醒之前睡眠在该请求上的进程,关于scsi_done的详细介绍可以参考一些书籍,这里不多说啦。

前面介绍了一个请求是如何经过驱动、适配器最终到达设备然后返回的过程。那驱动端队列中的请求时什么时候发送到设备端队列中去的。假如前面我们描述的命令已经发送到设备端的命令队列中,现在又来了SCSI命令或者适配器管理命令,如果设备端队列还没有满,则驱动会直接把命令发送到设备端命令队列中去。持续这样直到设备端命令队列为满,如果这时来了管理命令,那么驱动会把这些命令放到一个叫做pending队列中(其实就是将请求的id号标示在这个队列中)。当设备端执行并完成一些命令后,设备端命令队列又不为满,这时设备会产生COMMAND INTERRUPT,在中断服务程序中会将pending队列中等待处理的命令发送到设备的命令队列中,直到设备的命令队列又变为满。当有来自上层的SCSI命令时,如果设备端命令队列为满或者主机端的pending队列中有待处理的管理命令,那么主机驱动放弃请求对应的命令包TW_Command_Full,然后返回。SCSI层会将取出的request请求重新排入到request_queue最前面,等待下一次处理。

上面是对驱动工作流程和设备端工作原理的简单分析,至于更具体的方面就需要自己去看看代码了。第一次写些东西分享出来,写了非常随性,希望大家不要介意,若有错误的地方欢迎大家指正。

加勒比海盗

cxneu@sina.com

2014.1.21
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: