本文是近期學(xué)習(xí)CMA模塊的一個學(xué)習(xí)筆記,方便日后遺忘的時候,回來查詢以便迅速恢復(fù)上下文。
學(xué)習(xí)的基本方法是這樣的:一開始,我自己先提出了若干的問題,然后帶著這些問題查看網(wǎng)上的資料,代碼,最后整理形成這樣以問題為導(dǎo)向的index,順便也向笨叔叔致敬。笨叔叔寫了一本書叫做《奔跑吧Linux內(nèi)核》,采用了問答的方式描述了4.x Linux內(nèi)核中的進程管理、內(nèi)存管理,同步和中斷子系統(tǒng)。7月將和大家見面,敬請期待。
閱讀本文最好手邊有一份linux source code,我使用的是4.4.6版本。
一、什么是CMA
CMA,Contiguous Memory Allocator,是內(nèi)存管理子系統(tǒng)中的一個模塊,負(fù)責(zé)物理地址連續(xù)的內(nèi)存分配。一般系統(tǒng)會在啟動過程中,從整個memory中配置一段連續(xù)內(nèi)存用于CMA,然后內(nèi)核其他的模塊可以通過CMA的接口API進行連續(xù)內(nèi)存的分配。CMA的核心并不是設(shè)計精巧的算法來管理地址連續(xù)的內(nèi)存塊,實際上它的底層還是依賴內(nèi)核伙伴系統(tǒng)這樣的內(nèi)存管理機制,或者說CMA是處于需要連續(xù)內(nèi)存塊的其他內(nèi)核模塊(例如DMA mapping framework)和內(nèi)存管理模塊之間的一個中間層模塊,主要功能包括:
1、解析DTS或者命令行中的參數(shù),確定CMA內(nèi)存的區(qū)域,這樣的區(qū)域我們定義為CMA area。
2、提供cma_alloc和cma_release兩個接口函數(shù)用于分配和釋放CMA pages
3、記錄和跟蹤CMA area中各個pages的狀態(tài)
4、調(diào)用伙伴系統(tǒng)接口,進行真正的內(nèi)存分配。
二、內(nèi)核中為何建立CMA模塊?
Linux內(nèi)核中已經(jīng)提供了各種內(nèi)存分配的接口,為何還有建立CMA這種連續(xù)內(nèi)存分配的機制呢?
我們先來看看內(nèi)核哪些模塊有物理地址連續(xù)的需求。huge page模塊需要物理地址連續(xù)是顯而易見的。大家都熟悉的處理器(不要太古老),例如ARM64,其內(nèi)存管理單元都可以支持多個頁面大小(4k、64K、2M或者更大的page size),但在大多數(shù)CPU架構(gòu)上,Linux內(nèi)核總是傾向使用最小的page size,即4K page size。Page size大于4K的page統(tǒng)稱為“huge page”。對于一個2M的huge page,MMU會把一個連續(xù)的2M的虛擬地址mapping到連續(xù)的、2M的物理地址上去,當(dāng)然,這2M size的物理地址段必須是由512個地址連續(xù)的4k page frame組成。
當(dāng)然,更多的連續(xù)內(nèi)存的分配需求來自形形色色的驅(qū)動。例如現(xiàn)在大家的手機都有視頻功能,camer功能,這類驅(qū)動都需要非常大塊的內(nèi)存,而且有DMA用來進行外設(shè)和大塊內(nèi)存之間的數(shù)據(jù)交換。對于嵌入式設(shè)備,一般不會有IOMMU,而且DMA也不具備scatter-getter功能,這時候,驅(qū)動分配的大塊內(nèi)存(DMA buffer)必須是物理地址連續(xù)的。
順便說一句,huge page的連續(xù)內(nèi)存需求和驅(qū)動DMA buffer還是有不同的,例如在對齊要求上,一個2M的huge page,其底層的2M 的物理頁面的首地址需要對齊在2M上,一般而言,DMA buffer不會有這么高的對齊要求。因此,我們這里講的CMA主要是為設(shè)備驅(qū)動準(zhǔn)備的,huge page相關(guān)的內(nèi)容不在本文中描述。
我們來一個實際的例子吧:我的手機,像素是1300W的,一個像素需要3B,那么拍攝一幅圖片需要的內(nèi)存大概是1300W x 3B = 26MB。通過內(nèi)存管理系統(tǒng)分配26M的內(nèi)存,壓力可是不小。當(dāng)然,在系統(tǒng)啟動之處,伙伴系統(tǒng)中的大塊內(nèi)存比較大,也許分配26M不算什么,但是隨著系統(tǒng)的運行,內(nèi)存不斷的分配、釋放,大塊內(nèi)存不斷的裂解,再裂解,這時候,內(nèi)存碎片化導(dǎo)致分配地址連續(xù)的大塊內(nèi)存變得不是那么的容易了,怎么辦?作為驅(qū)動工程師,我們有兩個選擇:其一是在啟動時分配用于視頻采集的DMA buffer,另外一個方案是當(dāng)實際使用camer設(shè)備的時候分配DMA buffer。前者的選擇是可靠的,但它有一個缺點,即當(dāng)照相機不使用時(大多數(shù)時間內(nèi)camera其實都是空閑的),預(yù)留的那些DMA BUFFER的內(nèi)存實際上是浪費了(特別在內(nèi)存配置不大的系統(tǒng)上更是如此)。后一種選擇不會浪費內(nèi)存,但是不可靠,隨著內(nèi)存碎片化,大的、連續(xù)的內(nèi)存分配變得越來越困難,一旦內(nèi)存分配失敗,camera功能就會缺失,估計用戶不會答應(yīng)。
這就是驅(qū)動工程師面臨的困境,為了解決這個問題,各個驅(qū)動各出奇招,但是都不能非常完美的解決問題。最終來自Michal Nazarewicz的CMA補丁將可以把各個驅(qū)動工程師的煩惱“一洗了之”。對于CMA 內(nèi)存,當(dāng)前驅(qū)動沒有分配使用的時候,這些memory可以內(nèi)核的被其他的模塊使用(當(dāng)然有一定的要求),而當(dāng)驅(qū)動分配CMA內(nèi)存后,那些被其他模塊使用的內(nèi)存需要吐出來,形成物理地址連續(xù)的大塊內(nèi)存,給具體的驅(qū)動來使用。
三、CMA模塊的藍圖是怎樣的?
了解一個模塊,先不要深入細(xì)節(jié),我們先遠(yuǎn)遠(yuǎn)的看看CMA在整個系統(tǒng)中的位置。雖然用于解決驅(qū)動的內(nèi)存分配問題,但是驅(qū)動并不會直接調(diào)用CMA模塊的接口,而是通過DMA mapping framework來間接使用CMA的服務(wù)。一開始,CMA area的概念是全局的,通過內(nèi)核配置參數(shù)和命令行參數(shù),內(nèi)核可以定位到Global CMA area在內(nèi)存中的起始地址和大小(注:這里的Global的意思是針對所有的driver而言的)。并在初始化的時候,調(diào)用dma_contiguous_reserve函數(shù),將指定的memory region保留給Global CMA area使用。人性是貪婪的,驅(qū)動亦然,很快,有些驅(qū)動想吃獨食,不愿意和其他驅(qū)動共享CMA,因此出現(xiàn)兩種CMA area:Global CMA area給大家共享,而per device CMA可以給指定的一個或者幾個驅(qū)動使用。這時候,命令行參數(shù)不是那么合適了,因此引入了device tree中的reserved memory node的概念。當(dāng)然,為了兼容,內(nèi)核仍然支持CMA的command line參數(shù)。
三、CMA模塊如何管理和配置CMA area?
在CMA模塊中,struct cma數(shù)據(jù)結(jié)構(gòu)用來抽象一個CMA area,具體定義如下:
struct cma {?
??? unsigned long?? base_pfn;?
??? unsigned long?? count;?
??? unsigned long?? *bitmap;?
??? unsigned int order_per_bit; /* Order of pages represented by one bit */?
??? struct mutex??? lock;?
};
cma模塊使用bitmap來管理其內(nèi)存的分配,0表示free,1表示已經(jīng)分配。具體內(nèi)存管理的單位和struct cma中的order_per_bit成員相關(guān),如果order_per_bit等于0,表示按照一個一個page來分配和釋放,如果order_per_bit等于1,表示按照2個page組成的block來分配和釋放,以此類推。struct cma中的bitmap成員就是管理該cma area內(nèi)存的bit map。count成員說明了該cma area內(nèi)存有多少個page。它和order_per_bit一起決定了bitmap指針指向內(nèi)存的大小。base_pfn定義了該CMA area的起始page frame number,base_pfn和count一起定義了該CMA area在內(nèi)存在的位置。
我們前面說過了,CMA模塊需要管理若干個CMA area,有g(shù)loal的,有per device的,代碼如下:
struct cma cma_areas[MAX_CMA_AREAS];
每一個struct cma抽象了一個CMA area,標(biāo)識了一個物理地址連續(xù)的memory area。調(diào)用cma_alloc分配的連續(xù)內(nèi)存就是從CMA area中獲得的。具體有多少個CMA area是編譯時決定了,而具體要配置多少個CMA area是和系統(tǒng)設(shè)計相關(guān),你可以為特定的驅(qū)動準(zhǔn)備一個CMA area,也可以只建立一個通用的CMA area,供多個驅(qū)動使用(本文重點描述這個共用的CMA area)。
房子建好了,但是還空著,要想金屋藏嬌,還需要一個CMA配置過程。配置CMA內(nèi)存區(qū)有兩種方法,一種是通過dts的reserved memory,另外一種是通過command line參數(shù)和內(nèi)核配置參數(shù)。
device tree中可以包含reserved-memory node,在該節(jié)點的child node中,可以定義各種保留內(nèi)存的信息。compatible屬性是shared-dma-pool的那個節(jié)點是專門用于建立 global CMA area的,而其他的child node都是for per device CMA area的。
Global CMA area的初始化可以參考定義如下:
RESERVEDMEM_OF_DECLARE(cma, "shared-dma-pool", rmem_cma_setup);
具體的setup過程倒是比較簡單,從device tree中可以獲取該memory range的起始地址和大小,調(diào)用cma_init_reserved_mem函數(shù)即可以注冊一個CMA area。需要補充說明的是:CMA對應(yīng)的reserved memory節(jié)點必須有reusable屬性,不能有no-map的屬性。具體reusable屬性的reserved memory有這樣的特性,即在驅(qū)動不使用這些內(nèi)存的時候,OS可以使用這些內(nèi)存(當(dāng)然有限制條件),而當(dāng)驅(qū)動從這個CMA area分配memory的時候,OS可以reclaim這些內(nèi)存,讓驅(qū)動可以使用它。no-map屬性和地址映射相關(guān),如果沒有no-map屬性,那么OS會為這段memory創(chuàng)建地址映射,象其他普通內(nèi)存一樣。但是有no-map屬性的往往是專用于某個設(shè)備驅(qū)動,在驅(qū)動中會進行io remap,如果OS已經(jīng)對這段地址進行了mapping,而驅(qū)動又一次mapping,這樣就有不同的虛擬地址mapping到同一個物理地址上去,在某些ARCH上(ARMv6之后的cpu),會造成不可預(yù)知的后果。而CMA這個場景,reserved memory必須要mapping好,這樣才能用于其他內(nèi)存分配場景,例如page cache。
per device CMA area的注冊過程和各自具體的驅(qū)動相關(guān),但是最終會dma_declare_contiguous這個接口函數(shù),為一個指定的設(shè)備而注冊CMA area,這里就不詳述了。
通過命令行參數(shù)也可以建立cma area。我們可以通過cma=nn[MG]@[start[MG][-end[MG]]]這樣命令行參數(shù)來指明Global CMA area在整個物理內(nèi)存中的位置。在初始化過程中,內(nèi)核會解析這些命令行參數(shù),獲取CMA area的位置(起始地址,大小),并調(diào)用cma_declare_contiguous接口函數(shù)向CMA模塊進行注冊(當(dāng)然,和device tree傳參類似,最終也是調(diào)用cma_init_reserved_mem接口函數(shù))。除了命令行參數(shù),通過內(nèi)核配置(CMA_SIZE_MBYTES和CMA_SIZE_PERCENTAGE)也可以確定CMA area的參數(shù)。
四、memblock、CMA和伙伴系統(tǒng)的初始化順序是怎樣的?
套用一句廣告詞:CMA并不進行內(nèi)存管理,它只是”內(nèi)存管理機制“的搬運工。也就是說,CMA area的內(nèi)存最終還是要并入伙伴系統(tǒng)進行管理。在這樣大方向的指導(dǎo)下,CMA模塊的初始化必須要在適當(dāng)?shù)臅r機,以適當(dāng)?shù)姆绞讲迦氲絻?nèi)存管理(包括memblock和伙伴系統(tǒng))初始化過程中。
內(nèi)存管理子系統(tǒng)進行初始化的時候,首先是memblock掌控全局的,這時候需要確定整個系統(tǒng)的的內(nèi)存布局,簡單說就是了解整個memory的分布情況,哪些是memory block是memory type,哪些memory block是reserved type。毫無疑問,CMA area對應(yīng)的當(dāng)然是reserved type。最先進行的是memory type的內(nèi)存塊的建立,可以參考如下代碼:
setup_arch--->setup_machine_fdt--->early_init_dt_scan--->early_init_dt_scan_nodes--->memblock_add
隨后會建立reserved type的memory block,可以參考如下代碼:
setup_arch--->arm64_memblock_init--->early_init_fdt_scan_reserved_mem--->__fdt_scan_reserved_mem--->memblock_reserve
完成上面的初始化之后,memblock模塊已經(jīng)通過device tree構(gòu)建了整個系統(tǒng)的內(nèi)存全貌:哪些是普通內(nèi)存區(qū)域,哪些是保留內(nèi)存區(qū)域。對于那些reserved memory,我們還需要進行初始化,代碼如下:
setup_arch--->arm64_memblock_init--->early_init_fdt_scan_reserved_mem--->fdt_init_reserved_mem--->__reserved_mem_init_node
上面的代碼會scan內(nèi)核中的一個特定的section(還記得前面RESERVEDMEM_OF_DECLARE的定義嗎?),如果匹配就會調(diào)用相應(yīng)的初始化函數(shù),而對于Global CMA area而言,這個初始化函數(shù)就是rmem_cma_setup。當(dāng)然,如果有需要,具體的驅(qū)動也可以定義自己的CMA area,初始化的思路都是一樣的。
至此,通過device tree,所有的內(nèi)核模塊要保留的內(nèi)存都已經(jīng)搞清楚了(不僅僅是CMA保留內(nèi)存),是時候通過命令行參數(shù)保留CMA內(nèi)存了,具體的調(diào)用如下:
setup_arch--->arm64_memblock_init--->dma_contiguous_reserve
實際上,在構(gòu)建CMA area上,device tree的功能已經(jīng)完全碾壓命令行參數(shù),因此dma_contiguous_reserve有可能沒有實際的作用。如果沒有通過命令行或者內(nèi)核配置文件來定義Global CMA area,那么這個函數(shù)調(diào)用當(dāng)然不會起什么作用,如果device tree已經(jīng)設(shè)定了Global CMA area,那么其實dma_contiguous_reserve也不會真正reserve memory(device tree優(yōu)先級高于命令行)。
如果有配置命令行參數(shù),而且device tree并沒有設(shè)定Global CMA area,那么dma_contiguous_reserve才會真正有作用。那么根據(jù)配置參數(shù)可以有兩種場景:一種是CMA area是固定位置的,即參數(shù)給出了確定的起始地址和大小,這種情況比較簡單,直接調(diào)用memblock_reserve就OK了,另外一種情況是動態(tài)分配的,這時候,需要調(diào)用memblock的內(nèi)存分配接口memblock_alloc_range來為CMA area分配內(nèi)存。
memblock始終是初始化階段的內(nèi)存管理模塊,最終我們還是要轉(zhuǎn)向伙伴系統(tǒng),具體的代碼如下:
start_kernel--->mm_init--->mem_init--->free_all_bootmem--->free_low_memory_core_early--->__free_memory_core
在上面的過程中,free memory被釋放到伙伴系統(tǒng)中,而reserved memory不會進入伙伴系統(tǒng),對于CMA area,我們之前說過,最終被由伙伴系統(tǒng)管理,因此,在初始化的過程中,CMA area的內(nèi)存會全部導(dǎo)入伙伴系統(tǒng)(方便其他應(yīng)用可以通過伙伴系統(tǒng)分配內(nèi)存)。具體代碼如下:
core_initcall(cma_init_reserved_areas);
至此,所有的CMA area的內(nèi)存進入伙伴系統(tǒng)。
五、CMA是如何工作的?
1、準(zhǔn)備知識
如果想要了解CMA是如何運作的,你可能需要知道一點點關(guān)于migrate types和pageblocks的知識。當(dāng)從伙伴系統(tǒng)請求內(nèi)存的時候,我們需要提供了一個gfp_mask的參數(shù)。它有很多的功能,不過在CMA這個場景,它用來指定請求頁面的遷移類型(migrate type)。migrate type有很多中,其中有一個是MIGRATE_MOVABLE類型,被標(biāo)記為MIGRATE_MOVABLE的page說明該頁面上的數(shù)據(jù)是可以遷移的。也就是說,如果需要,我們可以分配一個新的page,copy數(shù)據(jù)到這個new page上去,釋放這個page。而完成這樣的操作對系統(tǒng)沒有任何的影響。我們來舉一個簡單的例子:對于內(nèi)核中的data section,其對應(yīng)的page不是是movable的,因為一旦移動數(shù)據(jù),那么內(nèi)核模塊就無法訪問那些頁面上的全局變量了。而對于page cache這樣的頁面,其實是可以搬移的,只要讓指針指向新的page就OK了。
伙伴系統(tǒng)不會跟蹤每一個page frame的遷移類型,實際上它是按照pageblock為單位進行管理的,memory zone中會有一個bitmap,指明該zone中每一個pageblock的migrate type。在處理內(nèi)存分配請求的時候,一般會首先從和請求相同migrate type(gfp_mask)的pageblocks中分配頁面。如果分配不成功,不同migrate type的pageblocks中也會考慮,甚至可能改變pageblock的migrate type。這意味著一個non-movable頁面請求也可以從migrate type是movable的pageblock中分配。這一點CMA是不能接受的,所以我們引入了一個新的migrate type:MIGRATE_CMA。這種遷移類型具有一個重要性質(zhì):只有可移動的頁面可以從MIGRATE_CMA的pageblock中分配。
2、初始化CMA area
static int __init cma_activate_area(struct cma *cma)?
{?
??? int bitmap_size = BITS_TO_LONGS(cma_bitmap_maxno(cma)) * sizeof(long);?
??? unsigned long base_pfn = cma->base_pfn, pfn = base_pfn;?
??? unsigned i = cma->count >> pageblock_order;?
??? struct zone *zone; -----------------------------(1)
cma->bitmap = kzalloc(bitmap_size, GFP_KERNEL); ----分配內(nèi)存
zone = page_zone(pfn_to_page(pfn)); ---找到page對應(yīng)的memory zone
do {--------------------------(2)?
??????? unsigned j;
base_pfn = pfn;?
??????? for (j = pageblock_nr_pages; j; --j, pfn++) {-------------(3)?
??????????? if (page_zone(pfn_to_page(pfn)) != zone)?
??????????????? goto err;?
??????? }?
??????? init_cma_reserved_pageblock(pfn_to_page(base_pfn));----------(4)?
??? } while (--i);
mutex_init(&cma->lock);
return 0;
err:?
??? kfree(cma->bitmap);?
??? cma->count = 0;?
??? return -EINVAL;?
}
(1)CMA area有一個bitmap來管理各個page的狀態(tài),這里bitmap_size給出了bitmap需要多少的內(nèi)存。i變量表示該CMA area有多少個pageblock。
(2)遍歷該CMA area中的所有的pageblock。
(3)確保CMA area中的所有page都是在一個memory zone內(nèi),同時累加了pfn,從而得到下一個pageblock的初始page frame number。
(4)將該pageblock導(dǎo)入到伙伴系統(tǒng),并且將migrate type設(shè)定為MIGRATE_CMA。
2、分配連續(xù)內(nèi)存
cma_alloc用來從指定的CMA area上分配count個連續(xù)的page frame,按照align對齊。具體的代碼就不再分析了,比較簡單,實際上就是從bitmap上搜索free page的過程,一旦搜索到,就調(diào)用alloc_contig_range向伙伴系統(tǒng)申請內(nèi)存。需要注意的是,CMA內(nèi)存分配過程是一個比較“重”的操作,可能涉及頁面遷移、頁面回收等操作,因此不適合用于atomic context。
3、釋放連續(xù)內(nèi)存
分配連續(xù)內(nèi)存的逆過程,除了bitmap的操作之外,最重要的就是調(diào)用free_contig_range,將指定的pages返回伙伴系統(tǒng)。
參考文獻:
LWN上的若干和CMA相關(guān)的文檔,包括:
1、A deep dive into CMA
2、A reworked contiguous memory allocator
3、CMA and ARM
4、Contiguous memory allocation for drivers
評論
查看更多