1. 程式人生 > >《Linux那些事兒之我是USB》我是U盤(29)彼岸花的傳說(八)

《Linux那些事兒之我是USB》我是U盤(29)彼岸花的傳說(八)

對於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打交道時認識的。