首先USB(UniversalSerial Bus)是一种传输协议,并不是一种数据协议,也没有任何语义上的指令意义。USB传输协议所传输的SCSI命令才是各个存储设备所能理解的命令,USB的责任就是将这些命令送达并且返回命令所要求的数据。所以,USB传输协议是不认识SCSI指令的,它的任务只是将上层的任何数据以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分为1.0,1.1,2.0,3.0和3.1几个版本。
USB子系统的上层就是实际的驱动程序,是要注册到系统的驱动程序列表的结构体。对storage来说,在drivers/usb/storage/usb.c中有完整的模块初始化和卸载函数。
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结构体。
这里以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?从而理论上就可以大幅度的提高速度。
当这个驱动扫描函数被调用时(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部分内容的填充可以后续完成。
分析第一步:从哪里入手
综上可知,一个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中可以找到相关的代码。
分析第三步
设备识别过程
首先,无论这个设备是存储设备还是打印机等设备。最先经过的都是core/hub.c。我们从一个不是最底层的函数开始,再逐步深入。
hub.c的入口函数是hub_thread线程函数,该函数循环调用hub_events函数处理hub事件,我们暂时不关心事件是如何产生,只关心如何处理。
hub_events中可以处理很多事件,与设备识别过程相关的最重要的是hub_port_connect_change函数,用于处理端口的逻辑连接或者物理连接发生变化的情况。