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

Linux内核工程导论——存储:USB

2015-10-08 20:44 369 查看

kernel USB驱动层

首先USB(UniversalSerial Bus)是一种传输协议,并不是一种数据协议,也没有任何语义上的指令意义。USB传输协议所传输的SCSI命令才是各个存储设备所能理解的命令,USB的责任就是将这些命令送达并且返回命令所要求的数据。所以,USB传输协议是不认识SCSI指令的,它的任务只是将上层的任何数据以USB的传输方式送达。

USB背景知识

USB作为一种传输协议,主要有三个优点:集成电源、造价便宜、支持广泛。这里要说明的是,论速度USB不算最快;论便宜,USB不算最便宜;USB软件支持系统的复杂性,在所有的传输协议里首屈一指的复杂。但是USB依然被广泛的应用,以致成为全世界的事实数据标准,就连Intel和苹果共同推广的速度极高的thunderbird传输协议也无法撼动USB的地位,内部的原因主要有两个:集成电源和支持广泛。集成电源让其不仅可以作为数据接口,也可以作为充电接口存在,在移动设备的充电方式上,USB口已经成为了事实上标准。支持广泛上,USB可能并不是因为好所以才被广泛支持的,而是因为有广泛的大商家支持,所以才发展的如此顺利。Intel、IBM、Microsoft、compaq等一干具有影响力的大型公司就是USB的创始者。

USB是金字塔型的,与SCSI一样,最上层是总线的硬件控制器芯片:USB host,约定的,根host下必须挂一个hub。USB hub的存在让USB系统组成一颗树,可以自由的扩展。

图:USB金字塔图

USB传输方式
那究竟什么是USB的传输方式呢?USB分为1.0,1.1,2.0,3.0和3.1几个版本。

USB子系统上层(USB设备驱动层)
作为模块的USB storage
USB子系统的上层就是实际的驱动程序,是要注册到系统的驱动程序列表的结构体。对storage来说,在drivers/usb/storage/usb.c中有完整的模块初始化和卸载函数。

USB与SCSI的对接
USB的每个设备实际上都是SCSI的一个scsi host,所以scsi向USB传递命令都是直接通过调用这个scsi host所规定的接口函数。最重要的是queuecommand函数,这个函数将从scsi传来的命令实际挂载到USB子系统内部的结构体,也是USB的最上层结构体(struct us_data)。值得注意的是,这个queuecommand的函数接口虽然是在scsi子系统定义的,但是在USB子系统中其具体的实现却是在USB子系统中。通过这一步USB子系统将来自scsi层的命令传输到了本层。但是,这时,该命令仍然没有执行。

另外,每个us_data同时只能有一个命令,如果当前us_data已经有命令了,queuecommand将返回错误。us_data是USB子系统上层调度的实体,并不代表任何的具体设备。

而,这个us_data是如何与scsi host关联起来的呢?在Scsi_Host结构体的最下面,有一个域叫做unsigned long hostdata[0]。整个us_data结构体就是放在这里的,所以可以通过Scsi_Host直接找到其对应的us_data,也就是唯一的USB设备。

如果阅读代码会发现,在scsiglue.c中定义scsi的接口数据结构并不是直接定义的Scsi_Host,而是定义的struct scsi_host_template。这是SCSI的结构决定的,只需要定义这个,SCSI子系统会根据这个生成对应的Scsi_Host结构体。

USB存储设备的种类

USB Storage执行SCSI命令
这里以USBStorage为例讲述。USB设备被关注最多的就是存储设备,USB的存储设备在USB子系统中位于drivers/usb/storage子目录。

真正执行us_data中命令的是usb-storage内核线程。该线程可以有多个,其启动的参数就是传入一个us_data。

该线程会做一些列检查,例如当前是否有命令,没有的话退出。最主要的,检测有命令要执行时,调用us_data结构体中注册的函数proto_handler执行。

从这里可以看出linux内核的数据结构为核心的设计思想。所有的操作和操作所需要的数据都是数据结构中,但是什么时候调用这些操作,调用操作的结果怎么存储到数据结构中,则是通过一些外部的函数或线程进行的。所有的代码都围绕着数据结构为其打工,周边代码存在的目的是让数据结构动起来。

那proto_handler究竟调用的什么呢?USB子系统的存储部分根据SC(subclass)类型不同定义了不同的proto_handler。

#define US_SC_RBC 0x01 /* Typically, flash devices */

#define US_SC_8020 0x02 /* CD-ROM */

#define US_SC_QIC 0x03 /* QIC-157 Tapes */

#define US_SC_UFI 0x04 /* Floppy */

#define US_SC_8070 0x05 /* Removable media */

#define US_SC_SCSI 0x06 /* Transparent */

#define US_SC_LOCKABLE 0x07 /* Password-protected */

#define US_SC_ISD200 0xf0 /* ISD200 ATA */

#define US_SC_CYP_ATACB 0xf1 /* Cypress ATACB */

#define US_SC_DEVICE 0xff /* Use device's value */

实际上,虽然分类了这么多,但是处理函数只有两种可能。是多对一的关系。最终实际上调用的函数us_data结构体的注册函数:transport。

实际的transport根据协议不同还有两个函数(但是有三种USB协议)

#define US_PR_CBI 0x00 /* Control/Bulk/Interrupt */

#define US_PR_CB 0x01 /* Control/Bulk w/o interrupt */

#define US_PR_BULK 0x50 /* bulk only */

但是原理都是一致的,生成并填充一个urb,然后提交。URB是USB core(USB总线驱动)中的内容。稍后再讨论。对于磁盘等存储设备,对应的是US_PR_BULK模式。

US_PR_BULK模式

首先要介绍USB会向设备发送的三种命令:CBW(CommandBlock Wrapper)、CSW(Command Status Wrapper)和数据。

无论如何,其都会首先发送CBW,只有在有数据的时候才会发送数据体。最后再发送CSW获得设备的命令执行情况。最后根据CSW返回的设备情况向上报告当前命令的执行是否成功。

可以看出,这是一个损耗很高的过程,所以当发送数据时应尽量发送多的数据。实际的发送代码位于drivers/usb/storage/transport.c中。

这里,我们忽然想到,是否可以让其发送多次CBW而只获得一次CSW?从而理论上就可以大幅度的提高速度。

USB Storage设备发现过程
当这个驱动扫描函数被调用时(storage_probe),就会进行扫描发现过程。值得注意的是这里的设备首先也是一个scsi设备,所以扫描完毕需要调用让scsi子系统也针对此设备进行扫描和数据填充。

storage_probe包括usb_stor_probe1,usb_stor_probe2两个阶段,完成对scsi_host为USB storage的us_data初始化时。在usb_stor_probe2末尾时还会启动另外一个内核线程usb-stor-scan。这个内核线程会实际调用scsi的扫描接口,填充scsi_host的其他域。这里为何使用的是线程来?是延迟的一种手段,不让内核在这里阻塞,对scsi部分内容的填充可以后续完成。

实现一个非SCSI直接调用USB storage接口的函数
分析第一步:从哪里入手

综上可知,一个USB子系统与SCSI层的对接靠的是scsiglue.c文件中的函数,其主要是实现了queuecommand函数。所以我们要做的是直接生成一个struct scsi_cmnd结构体,插入到对应的struct us_data的srb中。

首先,我们要搞清楚scsiglue中有多少USB相关的操作被挂接到了scsi上。真正的函数执行有6个:

queuecommand:将SCSICommand放入USB队列

command_abort:取消在USB队列中的ScsiCommand

device_reset: 设备复位时候调用的复位函数

bus_reset:总线复位时调用的复位函数

slave_alloc:发现设备时,最早调用的函数,用来为设备的生成提前分配设备驱动相关的内存

slave_configure:发现设备结束后,调用该函数进行最后的设备相关的配置

综上,可以看出,这6个函数中,其他5个都可以直接不动的让scsi使用,我们要做的就是让scsiqueuecommand与USB相关的断开,而使用我们的。甚至也不必要断开,只要两个不发生冲突就可以了。

我们的方案应该让原来的SCSi也发挥作用,如此,就算我们的路径不工作,scsi路径也可以正常驱动USB工作。所以我们要另外写一个函数调用USB的queuecommand函数即可,而这个函数的处理一个scsi command之外,还需要一个回调函数,用来通知命令的执行结果。

所以,问题的关键是如何构造scsicommand以及提供一个回调函数。

分析第二步:构造scsi command

由于SCSI command有很多域,但并不是所有的域都被USB子系统所利用。追踪USB的代码,可以发现,其有用的域有如下几个:

struct scsi_cmnd {

struct scsi_device *device; //代表SCSI设备,对于USB来说,能够通过它获得其最末尾的us_data,并且要使用其一些域进行从属判断,所以可以直接使用系统原有的。

struct list_head list; /* scsi_cmnd participates in queue lists */

struct list_head eh_entry; /* entry for the host eh_cmd_q */

int eh_eflags; /* Used by error handlr */

/*

* A SCSI Command is assigned a nonzero serial_number before passed

* to the driver's queue command function. The serial_number is

* cleared when scsi_done is entered indicating that the command

* has been completed. It is a bug for LLDDs to use this number

* for purposes other than printk (and even that is only useful

* for debugging).

*/

unsigned long serial_number;

/*

* This is set to jiffies as it was when the command was first

* allocated. It is used to time how long the command has

* been outstanding

*/

unsigned long jiffies_at_alloc;

int retries;

int allowed;

unsigned char prot_op;

unsigned char prot_type;

enum dma_data_direction sc_data_direction;//表示数据的流向(从总线到设备还是从设备到总线)

unsigned short cmd_len; //要发送的命令的长度(指的是cmnd所指向的长度,并非数据体的长度)

unsigned char *cmnd; //实际的要执行的命令类型

struct scsi_data_buffer sdb; //实际命令的体,这也是我们要构造的主体。可以存在只有数据的命令,叫bulk传输。如果没有命令头,而使用的是bulk传输,就没有命令头的开销。这个域的长度也包含在这个结构体中。

struct scsi_data_buffer *prot_sdb;

unsigned underflow; /* Return error if less than

this amount is transferred */

unsigned transfersize; /* How much we are guaranteed to

transfer with each SCSI transfer

(ie, between disconnect /

reconnects. Probably == sector

size */

struct request *request; /* The command we are

working on */

#define SCSI_SENSE_BUFFERSIZE 96

unsigned char *sense_buffer; //这是一种sense功能

void (*scsi_done) (struct scsi_cmnd *); //命令完成后调用的函数,我们可以将其截断成我们自己的发送处理函数

/*

* The following fields can be written to by the host specific code.

* Everything else should be left alone.

*/

struct scsi_pointer SCp; /* Scratchpad used by some host adapters */

unsigned char *host_scribble; /* The host adapter is allowed to

* call scsi_malloc and get some memory

* and hang it here. The host adapter

* is also expected to call scsi_free

* to release this memory. (The memory

* obtained by scsi_malloc is guaranteed

* to be at an address < 16Mb). */

int result; //本条命令的处理结果(一系列预定义的宏)

unsigned char tag; /* SCSI-II queued command tag */

};

由于usb-storage线程的运行时要传入一个us_data结构体,这个结构体和其要处理的command的device域的最下面的结构体应该是同一个,所以我们要利用该us_data。

由于在usb-storage中有对scsi host的锁。所以,我们在处理我们的scsi command的时候,同时也制止了本设备下发命令,这正好是我们所希望看到的。

构造scsi命令可以参考scsi驱动中的做法,在sd.c中可以找到相关的代码。

分析第三步

USB子系统的中层(USB core)
USB子系统的下层
设备识别过程

首先,无论这个设备是存储设备还是打印机等设备。最先经过的都是core/hub.c。我们从一个不是最底层的函数开始,再逐步深入。

hub.c的入口函数是hub_thread线程函数,该函数循环调用hub_events函数处理hub事件,我们暂时不关心事件是如何产生,只关心如何处理。

hub_events中可以处理很多事件,与设备识别过程相关的最重要的是hub_port_connect_change函数,用于处理端口的逻辑连接或者物理连接发生变化的情况。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息