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

来源:互联网 发布:c语言是干什么的 编辑:程序博客网 时间:2024/06/01 07:28

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函数,用于处理端口的逻辑连接或者物理连接发生变化的情况。

         

1 0
原创粉丝点击