1. 前言
我們在那些需要和用戶空間交互大量數據的子系統(例如MMC[1]、Video、Audio等)中,經常看到scatterlist的影子。對我們這些“非英語母語”的人來說,初見這個詞匯,腦袋瞬間就蒙圈了。scatter可翻譯成“散開、分散”,list是“列表”的意思,因而scatterlist可翻譯為“散列表”。“散列表”又是什么?太抽象了!
之所以抽象,是因為這個詞省略了主語----物理內存(Physical memory),加上后,就好理解了多了,既:物理內存的散列表。再通俗一些,就是把一些分散的物理內存,以列表的形式組織起來。那么,也許你會問,有什么用處呢?
當然有用,具體可參考本文后續的介紹。
2. scatterlist產生的背景
我沒有去考究scatterlist API是在哪個kernel版本中引入的(年代太久遠了),憑猜測,我覺得應該和MMU有關。因為在引入MMU之后,linux系統中的軟件將不得不面對一個困擾(下文將以圖片1中所示的系統架構為例進行說明):
假設在一個系統中(參考下面圖片1)有三個模塊可以訪問memory:CPU、DMA控制器和某個外設。CPU通過MMU以虛擬地址(VA)的形式訪問memory;DMA直接以物理地址(PA)的形式訪問memory;Device通過自己的IOMMU以設備地址(DA)的形式訪問memory。
然后,某個“軟件實體”分配並使用了一片存儲空間(參考下面圖片2)。該存儲空間在CPU視角上(虛擬空間)是連續的,起始地址是va1(實際上,它映射到了3塊不連續的物理內存上,我們以pa1,pa2,pa3表示)。
那么,如果該軟件單純的以CPU視角訪問這塊空間(操作va1),則完全沒有問題,因為MMU實現了連續VA到非連續PA的映射。
不過,如果軟件經過一系列操作后,要把該存儲空間交給DMA控制器,最終由DMA控制器將其中的數據搬移給某個外設的時候,由於DMA控制器只能訪問物理地址,只能以“不連續的物理內存塊”為單位遞交(而不是我們所熟悉的虛擬地址)。
此時,scatterlist就誕生了:為了方便,我們需要使用一個數據結構來描述這一個個“不連續的物理內存塊”(起始地址、長度等信息),這個數據結構就是scatterlist(具體可參考下面第3章的說明)。而多個scatterlist組合在一起形成一個表(可以是一個struct scatterlist類型的數組,也可以是kernel幫忙抽象出來的struct sg_table),就可以完整的描述這個虛擬地址了。
最后,從本質上說:scatterlist(數組)是各種不同地址映射空間(PA、VA、DA、等等)的媒介(因為物理地址是真實的、實在的存在,因而可以作為通用語言),借助它,這些映射空間才能相互轉換(例如從VA轉到DA)。
圖片1 cpu_dma_device_memory
圖片2 cpu_view_memory
3. scatterlist API介紹
3.1 struct scatterlist
struct scatterlist用於描述一個在物理地址上連續的內存塊(以page為單位),它的定義位於“include/linux/scatterlist.h”中,如下:
| struct scatterlist { |
page_link,指示該內存塊所在的頁面。bit0和bit1有特殊用途(可參考后面的介紹),因此要求page最低4字節對齊。
offset,指示該內存塊在頁面中的偏移(起始位置)。
length,該內存塊的長度。dma_address,該內存塊實際的起始地址(PA,相比page更接近我們人類的語言)。
dma_length,相應的長度信息。
3.2 struct sg_table
在實際的應用場景中,單個的scatterlist是沒有多少意義的,我們需要多個scatterlist組成一個數組,以表示在物理上不連續的虛擬地址空間。通常情況下,使用scatterlist功能的模塊,會自行維護這個數組(指針和長度),例如[2]中所提到的struct mmc_data:
| struct mmc_data { … unsigned int sg_len; /* size of scatter list */ |
不過呢,為了使用者可以偷懶,kernel抽象出來了一個簡單的數據結構:struct sg_table,幫忙保存scatterlist的數組指針和長度:
| struct sg_table { |
其中sgl是內存塊數組的首地址,orig_nents是內存塊數組的size,nents是有效的內存塊個數(可能會小於orig_nents)。
以上心思都比較直接,不過有一點,我們要仔細理解:
scatterlist數組中到底有多少有效內存塊呢?這不是一個很直觀的事情,主要有如下2個規則決定:
1)如果scatterlist數組中某個scatterlist的page_link的bit0為1,表示該scatterlist不是一個有效的內存塊,而是一個chain(鉸鏈),指向另一個scatterlist數組。通過這種機制,可以將不同的scatterlist數組鏈在一起,因為scatterlist也稱作chain scatterlist。
2)如果scatterlist數組中某個scatterlist的page_link的bit1為1,表示該scatterlist是scatterlist數組中最后一個有效內存塊(后面的就忽略不計了)。
3.3 API介紹
理解了scatterlist的含義之后,再去看“include/linux/scatterlist.h”中的API,就容易多了,例如(簡單介紹一下,不再詳細分析):
| #define sg_dma_address(sg) ((sg)->dma_address) #ifdef CONFIG_NEED_SG_DMA_LENGTH |
sg_dma_address、sg_dma_len,獲取某一個scatterlist的物理地址和長度。
| #define sg_is_chain(sg) ((sg)->page_link & 0x01) |
sg_is_chain可用來判斷某個scatterlist是否為一個chain,sg_is_last可用來判斷某個scatterlist是否是sg_table中最后一個scatterlist。
sg_chain_ptr可獲取chain scatterlist指向的那個scatterlist。
| static inline void sg_assign_page(struct scatterlist *sg, struct page *page) static inline void sg_set_page(struct scatterlist *sg, struct page *page, unsigned int len, unsigned int offset) static inline struct page *sg_page(struct scatterlist *sg) static inline void sg_set_buf(struct scatterlist *sg, const void *buf, unsigned int buflen) #define for_each_sg(sglist, sg, nr, __i) \ for (__i = 0, sg = (sglist); __i < (nr); __i++, sg = sg_next(sg)) static inline void sg_chain(struct scatterlist *prv, unsigned int prv_nents, struct scatterlist *sgl) static inline void sg_mark_end(struct scatterlist *sg) static inline void sg_unmark_end(struct scatterlist *sg) static inline dma_addr_t sg_phys(struct scatterlist *sg) static inline void *sg_virt(struct scatterlist *sg) |
sg_assign_page,將page賦給指定的scatterlist(設置page_link字段)。
sg_set_page,將page中指定offset、指定長度的內存賦給指定的scatterlist(設置page_link、offset、len字段)。
sg_page,獲取scatterlist所對應的page指針。
sg_set_buf,將指定長度的buffer賦給scatterlist(從虛擬地址中獲得page指針、在page中的offset之后,再調用sg_set_page)。for_each_sg,遍歷一個scatterlist數組(sglist)中所有的有效scatterlist(考慮sg_is_chain和sg_is_last的情況)。
sg_chain,將兩個scatterlist 數組捆綁在一起。
sg_mark_end、sg_unmark_end,將某個scatterlist 標記(或者不標記)為the last one。
sg_phys、sg_virt,獲取某個scatterlist的物理或者虛擬地址。
等等(不再羅列了,感興趣的同學直接去看代碼就行了)。


