《Linux那些事儿之我是USB》我是U盘(29)彼岸花的传说(八)

版权声明:本文为博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/fudan_abc/article/details/7010159

对于use_sg为0的情况,我们接下来再看168行,offset是函数调用传递进来的参数,注释里说得很清楚,就是用来标志偏移量的,每次复制几个字节它就增加几个字节,最大它也不能超过request_bufflen,这是显然的。usb_stor_access_xfer_buf()这个函数所做的事情就是从srb->request_buffer往buffer里边复制数据,或者反过来从buffer往srb->request_buffer,然后返回复制了多少个字节。对于offset大于等于request_bufflen的情况,当然就直接返回0了,因为request_buffer已经满了。

参数enumxfer_buf_dir dir标志的正是传输方向,这个数据类型是在drivers/usb/storage/protocol.h中被定义的:

51 /* struct scsi_cmnd transfer buffer accessutilities */

52 enum xfer_buf_dir       {TO_XFER_BUF, FROM_XFER_BUF};

这个参数其实是很简单的一个枚举数据类型,含义也很简单:一个表示向srb->request_buffer里边复制,TO_XFER_BUF;另一个表示从srb->request_buffer里边往外复制,FROM_XFER_BUF。(题外话:XFER就是TRANSFER的意思,外国人喜欢这样缩写。刚进Intel时老板专门给了我一个Excel文件,里边全是Intel内部广泛使用的英文缩写,不在Intel待一段时间基本没法理解。)其中定义成枚举数据类型也是很有必要的,因为数据传输肯定得有且仅有一个方向。而此情此景,我们传进来的是前者,所以171行判断之后会执行172行,从buffer里边复制 cnt个字节到(unsignedchar *) srb->request_buffer + *offset去。cnt在170行确定,min函数不用说也知道,取最小值,不过linux内核中确实有定义这个函数,在include/linux/kernel.h中。

而170行比较的是一个buflen,一个srb->request_bufflen-*offset,咱们这次要传送的数据长度是buflen,但是显然也不能够超过后者,所以就取其中小的值,调用memcpy 复制,然后177行*offset加上复制的字节cnt,对于这种不采用scatter gather方式的传输,那么到这里就可以返回了,直接就到240行,返回cnt即可。

但是对于使用scattergather方式的传输,情况当然不一样了。从186行开始往下看,显然如我们所说,得定义一个struct scatterlist结构体的指针,由于struct scatterlist是和体系结构有关的,以i386的为例,include/asm-i386/scatterlist.h:

 6struct scatterlist {

 7    struct page         *page;

 8    unsigned int        offset;

 9    dma_addr_t          dma_address;

10     unsigned int        length;

11 };

这个结构并不复杂,其中page指针通常指向将要进行scatter gather操作的buffer。而length表示buffer的长度,offset表示buffer在page内的偏移量。187行,定义一个structscatterlist指针sg,然后令它指向(structscatterlist*)srb->request_buffer+*index,搜索一下内核代码就可以知道,每次*index都是被初始化为0然后才调用usb_stor_access_xfer_buf()的。195行,cnt设为0,196行开始进入循环,循环的条件是cnt小于buflen,同时*index小于srb->use_sg, srb->use_sg在前面说过了,只要它不是0,那么它里边的值就代表了scatter gather传输时数组元素的个数。

请注意,187行这里让sg等于srb->request_buffer,(当然还要加上*index,如果index不为0的话),那么request_buffer究竟是什么?对于使用scatter/gather传输的情况,request_buffer里边实际上是一个数组,每一个元素都是一个structscatterlist的指针,而每一个scatterlist指针的page里边包含了一些page(而不是一个page),而offset里边包含的是每一个DMA buffer的总的偏移量,它由两部分组成,高位部分标志着page号,低位部分标志着具体某个page中的偏移量,高位低位由PAGE_SHIFT宏来划分。不同的硬件平台PAGE_SHIFT值不一样,因为不同的硬件平台page的大小也不一样,即这里的PAGE_SIZE不一样。目前比较前卫的硬件平台其page size有4KB或者8KB的,而PAGE_SHIFT也就是12或者13。换而言之,sg->offset去掉低12位或者低13位就是page号,而低12位或者低13位恰恰是在该page内的偏移量。之所以可以把一个sg->offset起两个作用,正是因为page size只需要12位或者13位就足够了,或者说偏移量本身只有12位或者13位,而一个int型变量显然可以包含比12位或13位更多的信息。我们最终是要把buffer(即前面说的那个36个Bytes的标准的INQUIRY data buffer)里边的信息复制至DMAbuffer中,buffer我们已经知道,它就是usb_stor_access_xfer_buf()函数传递进来的参数,而DMA buffer在哪呢?只要我们知道它在哪个page中,知道它的offset,那么有一个函数可以帮助我们获得它对应的内核虚拟地址,而这个地址正是我们需要的,有了它,我们就能调用memcpy函数来复制数据了。

所以197行到200行,就是计算出究竟是哪个page,究竟是多少offset,后者用被赋给了unsigned int变量poff,*offset是usb_stor_access_xfer_buf()函数传递进来的参数,它也可以控制我们要传送数据的DMA buffer对应的offset,不过这里我们传递进来的是0。所以不去关注。

201行对unsigned int sglen赋值,sg->length实际上就是DMA buffer的长度。所以显然我们复制的数据不能超过这个长度。如果我们还指定了*offset,就表明DMA buffer中从*offset开始装,那么就不能超过sg->length-*offset。

203行,由于我们现在还在while循环中,所以先看第一次执行到203行,这时cnt等于0,buflen就是data buffer的长度,传进来的是36。sglen表示了DMA buffer里面可以装多少数据,如果sglen比buflen要大,那么很好,一次就可以装满。因为这就好比buflen是一吨沙子,而sglen则表示装沙车载重两吨或者更多,比如三吨。这样206行和207行的作用就是做一个标记,比如sglen被用来记录实际装载了多少重量的沙子,而*offset则表示了装沙车用了多少了,如果还没卸货下次又要继续往里装那就装吧,反正没满就可以装。如果sglen比buflen要小或者刚好够大,那么*offset肯定就被设为0,因为这一车必然会被装满,要再装沙子只能调用下一辆车,所以同时sg和*index也自加。

然后来看219行了,sglen一开始肯定应该大于0,它表示的是实际装了多少数据,但是内存管理机制有一个限制,memcpy传输不能够跨页,也就是说不能跨page,一次最多就是把本page的内容复制,如果你的数据超过了一个page,那你还得再复制一次或者多次,因为这里使用了循环。

每一次真正复制的长度用plen来表示,所以233行和234行,cnt是计数的,所以要加上一个plen,而sglen也是一个计数的,但是它是反着计,所以它每次要减掉一个plen。而poff和page一个设为0,一个自加,这个道理很简单,从下一页开头进行继续复制。而220行再次调用强大的min()函数也正是为了保证每次复制不能跨页。

222行和228行kmap()和kunmap()出现了,道理也很简单。这对冤家是内核中的内存管理部门提供的重要函数,其中kmap()函数的作用就是传递给它一个struct page指针,它能返回这个page指针所指的page的内核虚拟地址,而有了这个page对应的虚拟地址,加上前面已经知道的偏移量,就可以调用memcpy函数来复制数据了。至于kunmap,凡是kmap()建立起来的良好关系必须由kunmap()来摧毁。

然后,224行到227行的两个memcpy无须再讲了。

240行,返回实际传输的字节数。

至此,usb_stor_access_xfer_buf()这个函数都讲完了。(注:应该说如果不是很有悟性的话,这段代码要看懂还是挺难的,要理解这个函数,必须明白这个双重循环,须知外循环是按sg entry来循环的,即一个sg entry循环一次,而内循环是按page来循环的,我们说过,一个sgentry可以有多个page,因为没有办法跨page映射,所以只能每个page映射一次,所以才需要循环。)

回到usb_stor_set_xfer_buf()中来,也只剩下一句话了,如果我们要传递的36个字节还不足以填满这辆装沙车的话,那就让我们记录下这辆车还能装多少沙子吧,srb->resid正是扮演着这个记录者的角色。

然后我们回到fill_inquiry_response()中来,这个函数也结束了。我们再一次回到usb_stor_control_thread()中来,这个函数总是隔一会儿又会出现。对于INQUIRY命令,咱们在fill_inquiry_response()之后,把srb->result设为了SAM_STAT_GOOD,它是scsi系统里面定义的一个宏,include/scsi/scsi.h中:

129 #define SAM_STAT_GOOD           0x00

其实就是0,完成了工作之后把srb->result设为0,以后scsi那边的函数会去检测,不用咱们管了。

然后我们进入到384行,更确切地说是388行,srb->scsi_done()函数会被调用。srb->scsi_done()实际上是一个函数指针,在queuecommand中令它等于scsi_done,我们被要求在完成了该做的事情之后调用这个函数,剩下的事情SCSI核心层会去处理。

针对queuecommand传递进来INQUIRY命令的情况,该做的就都做了。

最后单独解释一下:

首先,好端端的传输数据为什么要分散成好多个scatter gather list,这不是自找麻烦吗? SCSI层包括usb-storage之所以要使用scatter gather,是因为这个特性允许系统在一个SCSI命令中传输多个物理上不连续的buffers。

kmap()和kunmap()这两个函数是干什么的?为什么要映射?这个世界上有一个地址,叫做物理地址,还有一个地址叫做内核地址。struct page代表的是物理地址,内核用这个结构体来代表每一个物理page,或者说物理页,显然我们代码中不能直接操作物理地址,memcpy这个函数根本就不认识物理地址,它只能使用内核地址。所以我们需要把物理地址映射到内核地址空间中来。kmap()从事的正是这项伟大的工作。不过写过代码的人了解kmap()更多地是在和high memory打交道时认识的。
展开阅读全文

没有更多推荐了,返回首页