異構(gòu)計算關(guān)鍵技術(shù)之內(nèi)存管理與DMA(一)
誕生伊始,計算機(jī)處理能力就處于高速發(fā)展中。及至最近十年,隨著大數(shù)據(jù)、區(qū)塊鏈、AI 等新技術(shù)的持續(xù)火爆,人們?yōu)樘嵘嬎闾幚硭俣雀前l(fā)展了多種不同的技術(shù)思路。大數(shù)據(jù)受惠于分布式集群技術(shù),區(qū)塊鏈帶來了專用處理器(Application-Specific IC, ASIC)的春天,AI 則讓大眾聽到了“異構(gòu)計算”這個計算機(jī)界的學(xué)術(shù)名詞。
“異構(gòu)計算”(Heterogeneous computing),是指在系統(tǒng)中使用不同體系結(jié)構(gòu)的處理器的聯(lián)合計算方式。在 AI 領(lǐng)域,常見的處理器包括:CPU(X86,Arm,RISC-V 等),GPU,F(xiàn)PGA 和 ASIC。(按照通用性從高到低排序)。

本系列文章將介紹異構(gòu)計算涉及到的內(nèi)存管理技術(shù)、DMA技術(shù)。結(jié)合驅(qū)動開發(fā)、FPGA/ASIC PCIe DMA engine的實例代碼進(jìn)行詳細(xì)講解多線程、DMA scatter-gather list、PCIe TLP等核心技術(shù)。
本章將介紹核心的基本概念:主要包括進(jìn)程空間、頁機(jī)制、DMA類型。
一、什么是異構(gòu)計算
同構(gòu)計算或者說通用計算性能的發(fā)展已經(jīng)遠(yuǎn)遠(yuǎn)跟不上應(yīng)用的需求,如近幾年的國內(nèi)的天河2A和神威超算都屬于異構(gòu)超算,接下來幾年研發(fā)的超算也都屬于異構(gòu)超算,可見,異構(gòu)超算已經(jīng)成為中美兩國超算領(lǐng)域的趨勢。
這里我們引用網(wǎng)上的一個經(jīng)典“廚房論”異構(gòu)計算。
在飯店的廚房,通常會有一個大廚(CPU),它會做各種菜(兼容性極好),但是如果做菜之前的大量重復(fù)動作(洗菜、切菜)導(dǎo)致它一天做菜的份數(shù)明顯減少。
并且,由于最近(人工智能時代到來)客人點菜要求越來越高(花樣菜式),大廚開始不堪負(fù)重。


本來顧客大多要的「炒白菜」,現(xiàn)在一個個都想吃「上湯娃娃菜」。
一道是家常菜,一道是國宴菜。然而后者復(fù)雜程度(大量數(shù)據(jù)復(fù)雜處理)遠(yuǎn)遠(yuǎn)不是前者所能比較。
于是,大廚想著,一大菜我一個做著麻煩,但是我可以請個幫手(協(xié)處理器)。比如在切菜方面,這個幫手可以同時處理很多菜品(并行計算),而且很熟練,速度很快(低延時)。
于是,一個負(fù)責(zé)切菜,一個負(fù)責(zé)做菜,分工明確。當(dāng)然,大廚挑選這個幫手也是精挑細(xì)選,主要體現(xiàn)在以下方面:
1. 多樣的菜品處理能力,如洗菜切菜一體化(算法性能)——協(xié)處理器需要能全面支持需要用到的場景關(guān)鍵算法。
2. 支持同時、快速加工(數(shù)據(jù)并行和低延時處理能力)——協(xié)處理器需要有大量并行通道,且每個通道支持低延時的數(shù)據(jù)處理。
3. 便于大廚操作和菜品存?。ń涌谛阅埽椭魈幚砥骱芊奖愕臄?shù)據(jù)交互
4. 學(xué)習(xí)能力強(qiáng),新菜式也能學(xué)會(配置靈活)——協(xié)處理器可以針對計算需求升級迭代
5. 一天別吃太多(功耗低)——協(xié)處理器更低的功耗意味著更低的運(yùn)行成本,更小的空間占用和更簡單的熱處理方案。
就這樣,我們將一個復(fù)雜的工作(做菜)分解成了適合各個processor(工人師傅)任務(wù),最后我們再把他們的結(jié)果(成品)封裝一下即可(成品)。
二、進(jìn)程地址空間布局
在Linux中每個進(jìn)程都擁有獨立的虛擬地址空間,每個虛擬地址空間都可以認(rèn)為自己擁有全部內(nèi)存,比如32位系統(tǒng)中的進(jìn)程就認(rèn)為自己擁有4GB的內(nèi)存。
Linux把一個進(jìn)程空間中劃分為用戶空間和內(nèi)核空間兩段。在32位系統(tǒng)中給用戶空間劃分3GB,給內(nèi)核空間劃分1GB;64位系統(tǒng)中其實目前只用了48位,用戶空間和內(nèi)核空間平分256TB(從4.11內(nèi)核開始擴(kuò)展到57位)。

.text段存放二進(jìn)制代碼;
.rodata段存放只讀數(shù)據(jù),例如const和char *字符串;
.data段存放已初始化的全局變量;
.bss段存放未初始化的全局變量;
Heap是堆內(nèi)存,向高地址生長,由malloc函數(shù)分配,free函數(shù)釋放;
*.dll、*.so存放運(yùn)行時的共享庫,例如動態(tài)編譯使用printf的程序,運(yùn)行時會調(diào)用這里存放的庫文件;
Stack是棧內(nèi)存,向低地址生長,函數(shù)跳轉(zhuǎn)的現(xiàn)場保護(hù)、局部變量、一些中斷現(xiàn)場保護(hù)等數(shù)據(jù)存放在棧中。
想看這些段的話可以去linux中寫個C程序,編譯后再反匯編就能看見了。
虛擬地址:
即使是現(xiàn)代操作系統(tǒng)中,內(nèi)存依然是計算機(jī)中很寶貴的資源,看看你電腦幾個T固態(tài)硬盤,再看看內(nèi)存大小就知道了。
為了充分利用和管理系統(tǒng)內(nèi)存資源,Linux采用虛擬內(nèi)存管理技術(shù),利用虛擬內(nèi)存技術(shù)讓每個進(jìn)程都有4GB互不干涉的虛擬地址空間。
進(jìn)程初始化分配和操作的都是基于這個「虛擬地址」,只有當(dāng)進(jìn)程需要實際訪問內(nèi)存資源的時候才會建立虛擬地址和物理地址的映射,調(diào)入物理內(nèi)存頁。
打個不是很恰當(dāng)?shù)谋确?,這個原理其實和現(xiàn)在的某某網(wǎng)盤一樣。假如你的網(wǎng)盤空間是1TB,真以為就一口氣給了你這么大空間嗎?那還是太年輕,都是在你往里面放東西的時候才給你分配空間,你放多少就分多少實際空間給你,但你和你朋友看起來就像大家都擁有1TB空間一樣。
虛擬地址的好處:
- 避免用戶直接訪問物理內(nèi)存地址,防止一些破壞操作,保護(hù)系統(tǒng);
- 每個進(jìn)程都被分配了4GB的虛擬內(nèi)存,用戶應(yīng)用程序可使用比實際物理內(nèi)存更大的地址空間;
4GB的進(jìn)程虛擬地址空間被分成兩部分:“用戶空間”和“內(nèi)核空間”。

物理地址:
不管是用戶空間還是內(nèi)核空間,使用的都是虛擬地址(邏輯地址),當(dāng)需進(jìn)程要實際訪存的時候,就由內(nèi)核的“請求分頁機(jī)制”,產(chǎn)生“缺頁異?!闭{(diào)入物理內(nèi)存頁。
把虛擬地址轉(zhuǎn)換成內(nèi)存的物理地址,這中間涉及利用MMU 內(nèi)存管理單元(Memory Management Unit)對虛擬地址分段和分頁(段頁式)地址轉(zhuǎn)換。

<font color=B871F78><h2>三、頁表概述</font></h2>
帶有MMU的處理器訪存過程主要是通過頁表來聯(lián)系虛擬地址和物理地址。頁表是內(nèi)存中的一片供MMU訪問的空間,每個進(jìn)程都有一個自己頁表,用來把自己進(jìn)程的虛擬地址映射到真正的內(nèi)存中。
頁表以頁為單位管理,MMU也以頁為單位進(jìn)行映射。虛擬地址空間和內(nèi)存都是分頁的,一頁大小為4KB(4K邊界是這么來的)。以32位地址為例,高20位是頁號,用于索引頁面、低12位是頁內(nèi)偏移,用于找到頁內(nèi)要訪問的那個存儲單元。如下圖所示:

不過都不用上面這種頁表,算一筆賬啊,一個頁表項4Byte,這個頁表有2^20項,那就是4MB,這還是一個進(jìn)程的,而整個物理內(nèi)存才4GB,顯然開銷太大了。所以實際應(yīng)用中都采用多級頁表,比如64位的Linux用的四級頁表,多級頁表的優(yōu)點是節(jié)省空間,因為并不是所有進(jìn)程的所有虛擬地址都會被用到,沒有用到的虛擬地址沒有必要建立虛擬地址到物理地址之間的映射關(guān)系;缺點是級數(shù)多,查找慢,所以才引入了TLB緩存。
四、kmalloc和vmalloc
kmalloc和vmalloc是Linux內(nèi)核提供的兩個在內(nèi)核空間分配內(nèi)存的API。
kmalloc能夠分配物理地址連續(xù)的內(nèi)存空間,然后把這片內(nèi)存空間的首地址映射為虛擬地址返回給內(nèi)核空間,不過kmalloc因為要求物理地址連續(xù),所以僅用于分配比較小的內(nèi)存空間,比如結(jié)構(gòu)體等。隨著系統(tǒng)運(yùn)行時間的越來越長,內(nèi)存碎片越來越多,kmalloc有分配失敗的可能。
vmalloc是分配虛擬地址連續(xù)的api,但是物理地址不一定連續(xù),用于分配大的內(nèi)存空間。不過vmalloc分配的內(nèi)存用起來效率不高,Linux社區(qū)不太推薦用它。
五、缺頁中斷概述
我們上面提到:當(dāng)需進(jìn)程要實際訪存的時候,就由內(nèi)核的“請求分頁機(jī)制”,產(chǎn)生“缺頁異常”調(diào)入物理內(nèi)存頁。
頁表中記錄著已經(jīng)分配空間的物理地址和虛擬地址間的映射關(guān)系,但如果虛擬地址空間中存在一個沒有被真正分配物理內(nèi)存的虛擬地址,那么訪問這個虛擬地址時MMU無法找到對應(yīng)的物理地址,這時MMU會向CPU拋出一個異常,即缺頁中斷。
比如上一小節(jié)說的kmalloc函數(shù),在調(diào)用kmalloc時,系統(tǒng)首先會在虛擬地址空間中分配一段虛擬空間,此時還沒有分配對應(yīng)的物理空間,然后訪問這個虛擬空間,此時MMU找不到對應(yīng)的物理地址,向CPU產(chǎn)生一個缺頁中斷,然后在缺頁中斷里才真正地分配物理內(nèi)存并建立映射關(guān)系。
SGDMA是分配多個連續(xù)的物理內(nèi)存塊,用鏈表聯(lián)系起來,把它們當(dāng)成一塊內(nèi)存用,優(yōu)點是不用要求大片物理地址連續(xù),故能進(jìn)行大批量數(shù)據(jù)傳輸;而且SGDMA因為有鏈表,所以當(dāng)鏈表所表述的所有內(nèi)存塊全傳輸完了才會發(fā)中斷,一批傳輸只要中斷一次(指的是數(shù)據(jù)傳輸完成中斷)。
SGDMA雖然牛,但Block DMA也不是完全沒用,比如PCIe的SGDMA應(yīng)用中,傳輸SG鏈表的緩沖區(qū)用的就是Block DMA(因為SG鏈表用的空間不大),而傳輸數(shù)據(jù)的那部分緩沖區(qū)用的才是SGDMA,所以SGDMA的使用其實離不開BDMA!
六、DMA類型
DMA分為塊DMA(Block DMA)和分散/聚集式DMA(Scatter-Gather DMA)。
Block DMA就是分配一個連續(xù)的物理內(nèi)存塊,然后進(jìn)行DMA,缺點是要求物理地址連續(xù),很難分出來較大的塊;而且如果想要傳輸多個內(nèi)存塊的數(shù)據(jù),需要每DMA完一個塊處理一次中斷,響應(yīng)中斷可是很麻煩的!
七、一致性DMA和流式DMA
DMA是直接內(nèi)存存取,也就是DMA控制器是直接操作內(nèi)存的,而CPU直接可見的存儲器是Cache,若DMA控制器改寫完內(nèi)存數(shù)據(jù)后CPU直接去讀那片內(nèi)存,其實讀到的是Cache中的老數(shù)據(jù),新數(shù)據(jù)已經(jīng)被DMA改了,這個稱為DMA一致性問題,解決問題的方法有兩種:
關(guān)了Cache直接讀內(nèi)存;
DMA結(jié)束后刷新Cache。
一致性DMA用的是第一種---關(guān)了cache,直接讀內(nèi)存,為一致性DMA分配緩沖區(qū)時,這片緩沖區(qū)會在頁表中被標(biāo)記上不帶Cache,CPU訪問時就從主存中去讀取了。上節(jié)說的BDMA就是一致性DMA。
流式DMA用的是第二種,有兩種情況:
1. 從內(nèi)存向外設(shè)DMA傳輸:首先CPU將要傳的數(shù)據(jù)寫入Cache,然后Cache把數(shù)據(jù)刷新進(jìn)內(nèi)存,再用內(nèi)存進(jìn)行DMA。
2. 從外設(shè)向內(nèi)存DMA傳輸:CPU設(shè)置目標(biāo)內(nèi)存緩沖區(qū)對應(yīng)的Cache line為臟數(shù)據(jù)(設(shè)置Cache上的dirty bit),就是無效數(shù)據(jù),CPU下次訪問這個Cache line時會從主存中重新刷進(jìn)來。
上一小節(jié)里的SGDMA就屬于流式DMA。
那么一致性DMA和流式DMA都在什么情景下用呢?
為什么實驗室端系統(tǒng)的PCIe使用一致性DMA來緩存SG鏈表呢而不把它也換成SGDMA呢?
如果CPU和DMA要頻繁地操作一塊固定的內(nèi)存區(qū)域,那么這個區(qū)域用一致性DMA比較好,因為這個內(nèi)存塊老進(jìn)行DMA,如果用流式DMA的話每進(jìn)行一次DMA就得刷一次Cache,刷新Cache很耗時間的,尤其是大片地刷。
八、Linux中建立一致性DMA
物理地址、虛擬地址和總線地址的關(guān)系:
物理地址就是內(nèi)存的物理地址;
虛擬地址就是虛擬空間的地址;
總線地址指的是總線內(nèi)部的地址,就像PCIe有它自己的總線地址,PCIe發(fā)起TLP請求時目的地址都是總線地址,是由**RC**映射成**物理地址**后才能寫進(jìn)內(nèi)存的。
Linux提供了建立一致性DMA的API,dma_alloc_coherent,其聲明如下:
/**
* Allocate DMA-coherent memory space and return both the kernel remapped
* virtual and bus address for that space.
*/
void *dma_alloc_coherent(struct device *dev, size_t size,
dma_addr_t *handle, gfp_t gfp)
{
...
}
這個API有兩個返回地址:
1. 一個是void*型的返回值,它返回的是一致性DMA緩沖區(qū)映射到內(nèi)核空間的虛擬首地址,方便驅(qū)動程序向緩沖區(qū)里寫描述符;
2. 另一個返回地址就是dma_addr_t*類型的指針,dma_addr_t是Linux中定義的總線地址類型(注意是總線地址不是物理地址),這個地址是一致性緩沖區(qū)在總線上的首地址,這是給外面的設(shè)備用的,萬不能在驅(qū)動中訪問這個地址
參數(shù)*dev是4.6節(jié)中說的device結(jié)構(gòu)體指針,指向設(shè)備;
參數(shù)size是緩沖區(qū)大小,真正的大小是2^size字節(jié);
flag是選擇分配空間如果不足時怎么辦,填宏GFP_ATOMIC是不阻塞等待,填GFP_KERNEL是阻塞等待。
Linux也為PCI設(shè)備驅(qū)動提供了分配一致性DMA的API,pci_alloc_consistent,定義如下:
static inline void *
pci_alloc_consistent(struct pci_dev *hwdev, size_t size,
dma_addr_t *dma_handle)
{
return dma_alloc_coherent(hwdev == NULL ? NULL : &hwdev->dev, size, dma_handle, GFP_ATOMIC);
}
可見,pci_alloc_consistent其實就是把dma_alloc_coherent又封裝了一下,調(diào)用時填入pci_dev結(jié)構(gòu)體指針就好了,flag被強(qiáng)制設(shè)為GFP_ATOMIC,即空間不足時不阻塞。
九、Linux中建立SGDMA
SG緩沖區(qū)是一個一個不連續(xù)的內(nèi)存塊,但塊內(nèi)是物理地址連續(xù)的,每個內(nèi)存塊是以頁為單位的,如下圖所示:

每個SG緩存塊都在其所屬的頁內(nèi),所以想要獲得一個SG緩存塊需要知道頁號、頁內(nèi)偏移(SG塊的首地址)和SG緩存塊長度。
Linux為SG緩存塊提供了一個描述類型,即scatterlist結(jié)構(gòu)體,其定義如下:
struct scatterlist {
#ifdef CONFIG_DEBUG_SG
unsigned long sg_magic;
#endif
unsigned long page_link;
unsigned int offset;
unsigned int length;
dma_addr_t dma_address;
#ifdef CONFIG_NEED_SG_DMA_LENGTH
unsigned int dma_length;
#endif
};
page_link指示該SG緩存塊所在的頁面;
offset為頁內(nèi)偏移;
length為SG緩存塊的長度;
dma_address是該內(nèi)存塊的實際起始地址(已經(jīng)映射為總線地址);
dma_length是對應(yīng)的長度信息(也是給DMA用的)。
一個scatterlist結(jié)構(gòu)體變量只是描述了一個SG緩存塊,如果想描述整個SG緩沖區(qū),需要定義一個scatterlist的結(jié)構(gòu)體數(shù)組,數(shù)組元素都是scatterlist結(jié)構(gòu)體,表示一個個緩存塊。
Linux提供了包括scatterlist結(jié)構(gòu)體數(shù)組的類型,即sg_table結(jié)構(gòu)體,不過RIFFA驅(qū)動沒有用它,而是直接定義的scatterlist結(jié)構(gòu)體數(shù)組。
SG緩存塊依附于頁,Linux使用struct page結(jié)構(gòu)體描述一個物理頁面,出于節(jié)省內(nèi)存的考慮,struct page中使用了大量的聯(lián)合體。
十、SG緩沖區(qū)的建立流程
SG緩沖區(qū)的建立流程如下所示:

(1)首先要明白的是,所謂SG緩沖區(qū)的建立不是說去申請SG緩沖區(qū)空間,SG緩沖區(qū)本身就在內(nèi)存中,為什么?
因為SG緩沖區(qū)本身就是從用戶空間傳進(jìn)內(nèi)核的一片數(shù)據(jù),在物理地址上自然是不連續(xù)且分塊的,這已經(jīng)滿足SG緩沖區(qū)的特征了,所以并不是去建立SG緩沖區(qū),而是將這片已經(jīng)存在的、不連續(xù)的普通內(nèi)存空間變成DMA真正可用的SG緩沖區(qū)。
(2) 首先需要計算這片從用戶空間傳進(jìn)來的數(shù)據(jù)用了多少頁,然后定義跟頁數(shù)一樣多個數(shù)的page指針,形成一個指針數(shù)組,此時這個page指針數(shù)組是個空指針數(shù)組,即不描述任何物理頁。
(3) 然后使用get_user_pages函數(shù)從SG緩沖區(qū)的虛擬首地址開始鎖定這些SG緩存塊所在的頁,防止頁表沖突時這些頁被系統(tǒng)換出到硬盤中,都換走了那還DMA啥啊。get_user_pages函數(shù)同時能夠?qū)age數(shù)組中的指針元素指向SG緩沖區(qū)的每個物理頁。此時page指針數(shù)組才真正地有用了。
(4) 創(chuàng)建scatterlist結(jié)構(gòu)體數(shù)組并按需分配空間(數(shù)組長度和頁數(shù)一致),用sg_init_table函數(shù)分配scatterlist結(jié)構(gòu)體數(shù)組的長度。注意此時scatterlist結(jié)構(gòu)體數(shù)組是空數(shù)組。
(5) 上一小節(jié)說到scatterlist結(jié)構(gòu)體描述SG緩存塊的,包含頁面、頁內(nèi)偏移、長度等信息,Linux提供了sg_set_page函數(shù)來把這些信息存入scatterlist結(jié)構(gòu)體中(遍歷scatterlist結(jié)構(gòu)體數(shù)組一個一個分配)。
(6) 最后調(diào)用dma_map_sg函數(shù)建立SGDMA映射,此時SGDMA的環(huán)境就建立好了。dma_map_sg函數(shù)定義在/asm/dma-mapping.h中,如下所示:
int dma_map_sg(struct device *dev, struct scatterlist *sglist,
int nents, enum dma_data_direction dir)
{
...
}
參數(shù)*dev是設(shè)備結(jié)構(gòu)體;
參數(shù)*sg是scatterlist結(jié)構(gòu)體數(shù)組;
參數(shù)nents是頁數(shù),也就是SG緩存塊的個數(shù);
參數(shù)direction是DMA傳輸方向,它是一個枚舉類型變量;
常用的是DMA_TO_DEVICE(內(nèi)存→外設(shè))和DMA_FROM_DEVICE(外設(shè)→內(nèi)存)。
DMA_TO_DEVICE用于PCIe的RxMEM傳輸、DMA_FROM_DEVICE用于PCIe的TxMEM傳輸。
十一、未完待續(xù)
下章將繼續(xù)介紹核心的基本概念:線程/進(jìn)程技術(shù)。
歡迎關(guān)注知乎:北京不北,+vbeijing_bubei,dou音:near.X
獲得免費答疑,長期技術(shù)交流。