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