作者簡介:偉林,中年碼農(nóng),從事過電信、手機(jī)、安全、芯片等行業(yè),目前依舊從事Linux方向開發(fā)工作,個(gè)人愛好Linux相關(guān)知識分享。
Buddy 簡介
內(nèi)存是計(jì)算機(jī)系統(tǒng)中最重要的核心資源之一,Buddy 系統(tǒng)是 Linux 最底層的內(nèi)存管理機(jī)制,它使用 Page 粒度來管理內(nèi)存。通常情況下一個(gè) Page 的大小為 4K,在 Buddy 系統(tǒng)中分配、釋放、回收的最小單位都是 Page。
上圖是 Buddy 系統(tǒng)的內(nèi)部組織結(jié)構(gòu),本篇文章只關(guān)心未分配區(qū)域Free區(qū)域的管理,下篇文章再來分析可回收區(qū)域的管理。
統(tǒng)的內(nèi)存總大小動輒幾G幾十G,不同的內(nèi)存區(qū)域也有不同的特性。Buddy 使用層次化的結(jié)構(gòu)把這些特性給組織起來:
1、Node。在 NUMA 架構(gòu)下存在多個(gè) Memory 和 CPU 節(jié)點(diǎn),不同 CPU 訪問不同 Memory 節(jié)點(diǎn)的速度是不一樣的,使用 Node 的形式把各個(gè) Memory 節(jié)點(diǎn)的內(nèi)存管理起來。
2、Zone。某些外設(shè)只能訪問低 16M 的物理地址,某些外設(shè)只能訪問低 4G 的物理地址,32bit 的內(nèi)核空間只能直接映射低 896M 物理地址。根據(jù)這些地址空間的限制,把同一個(gè) node 內(nèi)的內(nèi)存再劃分成多個(gè) zone 。
3、Order Freelist。按照空閑內(nèi)存塊的長度,把內(nèi)存掛載到不同長度的 freelist 鏈表中。freelist 的長度是以 (2^order x Page) 來遞增的,即 1 page、2 page、4 page … 2^n,通常情況下最大 order 為10 對應(yīng)的空閑內(nèi)存大小為 4M bytes。在分配時(shí),如果一個(gè)空閑塊的大小不能被任一長度整除,它就從大到小逐個(gè)分解成多個(gè) (2^order x Page) 塊來掛載;在釋放時(shí),首先把內(nèi)存釋放到對應(yīng)長度的鏈表中,隨后看看和該內(nèi)存大小相同、地址相鄰的兄弟塊(Buddy)是不是free的,如果可以和 buddy 塊合并成一個(gè)大塊掛載到更高一階的鏈表,在掛載的時(shí)候繼續(xù)嘗試合并。這就是 Buddy 的核心思想,已2的冪個(gè) page 的長度來管理內(nèi)存方便分配和釋放,最核心的目的就是減少內(nèi)存的碎片化。
4、Migrate Type。為了進(jìn)一步減少碎片化,系統(tǒng)對內(nèi)存按照遷移類型進(jìn)行了分類,最基本的遷移類型有:不可移動(unmovable)、可移動(movable)、可回收(reclaimable)。初始的最大塊空閑內(nèi)存都是 unmovable 的,如果其中一小塊分配給了 reclaimable ,那么剩下的內(nèi)存都變成了 reclaimable。這樣壞的類型和壞的類型集中到了一起,避免壞情況的擴(kuò)散從而造成多個(gè) Free 區(qū)域無法合并的情況。
5、PerCPU 1 Page Cache。大于 1 Page 的內(nèi)存分配大多發(fā)生在內(nèi)核態(tài),而用戶態(tài)的內(nèi)存分配使用的是缺頁機(jī)制所以分配的大小一般是 1 Page。針對大小為 1 Page 的內(nèi)存分配,系統(tǒng)設(shè)計(jì)了一個(gè)免鎖的 PerCPU cache 來支撐。1 Page (Order = 0) 的空閑內(nèi)存優(yōu)先釋放到 PCP 中,超過了一定 batch 才會釋放到 Order Freelist當(dāng)中;同樣 1 Page 的內(nèi)存也優(yōu)先在 PCP 中分配。
Buddy 初始化
Struct Page 初始化
以 Page 大小的粒度來管理內(nèi)存,一個(gè) Page 對應(yīng)的物理內(nèi)存稱為頁框 (Page Frame)。另外為了應(yīng)對復(fù)雜的管理,系統(tǒng)給每個(gè) Page 還分配了一個(gè)管理結(jié)構(gòu) struct page,系統(tǒng)在初始化時(shí)會預(yù)留這部分的物理內(nèi)存并且映射到 vmemmap 區(qū)域 (參考:內(nèi)核地址空間布局),內(nèi)核根據(jù)物理頁幀的編號 pfn 就能在 vmemmap 區(qū)域中找到對應(yīng)的 struct page 結(jié)構(gòu)。
struct page 結(jié)構(gòu)存儲了很多信息 (參考:Page 頁幀管理詳解)。在 sparse_init() 時(shí)已經(jīng)把所有的struct page 結(jié)構(gòu)清零,zone_sizes_init() 初始化時(shí)主要初始化兩部分信息:
將 page->flags 中保存的 setcion、node、zone 設(shè)置成對應(yīng)的 index,這樣后續(xù)操作 struct page 結(jié)構(gòu)時(shí)就能快速的找到對應(yīng)的 setcion、node、zone 而不需要重新根據(jù) pfn 來進(jìn)行計(jì)算。page->flags 中的 flag 部分初始化為 0。
另外給 page->_refcount、_mapcount、_last_cpupid、lru 等成員都進(jìn)行了初始化。
start_kernel() → setup_arch() → x86_init.paging.pagetable_init() → native_pagetable_init() → paging_init() → zone_sizes_init() → free_area_init_nodes() → free_area_init_node() → free_area_init_core():
|→ memmap_init() → memmap_init_zone()
void __meminit memmap_init_zone(unsigned long size, int nid, unsigned long zone,
unsigned long start_pfn, enum memmap_context context)
{
for (pfn = start_pfn; pfn < end_pfn; pfn++) {
/* (2) 當(dāng)前是一個(gè)pageblock的第一個(gè)page */
if (!(pfn & (pageblock_nr_pages - 1))) {
struct page *page = pfn_to_page(pfn);
/* (2.1) 初始化對應(yīng)的 struct page 結(jié)構(gòu) */
__init_single_page(page, pfn, zone, nid,
context != MEMMAP_HOTPLUG);
/* (2.2) 初始化時(shí)把所有pageblock的migratetype設(shè)置成MIGRATE_MOVABLE */
set_pageblock_migratetype(page, MIGRATE_MOVABLE);
cond_resched();
/* (3) pageblock中的其他page */
} else {
/* (3.1) 初始化對應(yīng)的 struct page 結(jié)構(gòu) */
__init_single_pfn(pfn, zone, nid,
context != MEMMAP_HOTPLUG);
}
}
}
↓
__init_single_pfn()
↓
static void __meminit __init_single_page(struct page *page, unsigned long pfn,
unsigned long zone, int nid, bool zero)
{
/* (2.1.1) 如果需要,對struct page結(jié)構(gòu)清零 */
if (zero)
mm_zero_struct_page(page);
/* (2.1.2) 設(shè)置page->flags中的setcion index、node index、zone index */
set_page_links(page, zone, nid, pfn);
/* (2.1.3) 設(shè)置page->_refcount = 1 */
init_page_count(page);
/* (2.1.4) 設(shè)置page->_mapcount = -1 */
page_mapcount_reset(page);
/* (2.1.5) 設(shè)置page->_last_cpupid = -1 */
page_cpupid_reset_last(page);
/* (2.1.6) 初始化page->lru */
INIT_LIST_HEAD(&page->lru);
#ifdef WANT_PAGE_VIRTUAL
/* The shift won't overflow because ZONE_NORMAL is below 4G. */
if (!is_highmem_idx(zone))
set_page_address(page, __va(pfn << PAGE_SHIFT));
#endif
}
2、分配并初始化 zone->pageblock_flags:文章開始時(shí)說了 migrate type 的概念。系統(tǒng)把內(nèi)存劃分成多個(gè) pageblock,一個(gè) pageblock 即對應(yīng) (2^max_order x Page),每個(gè) pageblock 擁有自己的 migrate type。
系統(tǒng)以 zone 為單位分配空間來保存所有 pageblock 的 migrate type:
start_kernel() → setup_arch() → x86_init.paging.pagetable_init() → native_pagetable_init() → paging_init() → zone_sizes_init() → free_area_init_nodes() → free_area_init_node() → free_area_init_core():
|→ setup_usemap()
static void __init setup_usemap(struct pglist_data *pgdat,
struct zone *zone,
unsigned long zone_start_pfn,
unsigned long zonesize)
{
unsigned long usemapsize = usemap_size(zone_start_pfn, zonesize);
zone->pageblock_flags = NULL;
if (usemapsize)
/* (1) 分配存儲當(dāng)前zone里所有pageblock的migrate標(biāo)志 */
zone->pageblock_flags =
memblock_virt_alloc_node_nopanic(usemapsize,
pgdat->node_id);
}
pageblock 的初始 migrate type 為 MIGRATE_MOVABLE:
void __meminit memmap_init_zone(unsigned long size, int nid, unsigned long zone,
unsigned long start_pfn, enum memmap_context context)
{
for (pfn = start_pfn; pfn < end_pfn; pfn++) {
/* (2) 當(dāng)前是一個(gè)pageblock的第一個(gè)page */
if (!(pfn & (pageblock_nr_pages - 1))) {
struct page *page = pfn_to_page(pfn);
/* (2.1) 初始化對應(yīng)的 struct page 結(jié)構(gòu) */
__init_single_page(page, pfn, zone, nid,
context != MEMMAP_HOTPLUG);
/* (2.2) 初始化時(shí)把所有pageblock的migratetype設(shè)置成MIGRATE_MOVABLE */
set_pageblock_migratetype(page, MIGRATE_MOVABLE);
cond_resched();
/* (3) pageblock中的其他page */
} else {
/* (3.1) 初始化對應(yīng)的 struct page 結(jié)構(gòu) */
__init_single_pfn(pfn, zone, nid,
context != MEMMAP_HOTPLUG);
}
}
}
pageblock 中第一個(gè)分配的內(nèi)存的 migrate type 決定了整個(gè) pageblock 的 migrate type。
Buddy 初始化
在內(nèi)核啟動過程中在 Buddy 初始化以前,系統(tǒng)使用一個(gè)簡便的 Memblock 機(jī)制來管理內(nèi)存。在 Buddy 數(shù)據(jù)結(jié)構(gòu)準(zhǔn)備好后,需要把 Memblock 中的內(nèi)存釋放到 Buddy 當(dāng)中。
這就是 Buddy 系統(tǒng)初始的狀態(tài),除了保留的內(nèi)存,其他的內(nèi)存都處于 Free 狀態(tài):
start_kernel() → mm_init() → mem_init() → free_all_bootmem():
unsigned long __init free_all_bootmem(void)
{
unsigned long pages;
/* (1) 將每個(gè)node每個(gè)zone管理的page清零:z->managed_pages = 0 */
reset_all_zones_managed_pages();
/* (2) 將memblock中的內(nèi)存轉(zhuǎn)移到buddy系統(tǒng)中 */
pages = free_low_memory_core_early();
totalram_pages += pages;
return pages;
}
↓
static unsigned long __init free_low_memory_core_early(void)
{
unsigned long count = 0;
phys_addr_t start, end;
u64 i;
memblock_clear_hotplug(0, -1);
/* (2.1) 遍歷memblock中的保留內(nèi)存,將其對應(yīng)的`struct page`結(jié)構(gòu)page->flags設(shè)置PG_reserved標(biāo)志 */
for_each_reserved_mem_region(i, &start, &end)
reserve_bootmem_region(start, end);
/*
* We need to use NUMA_NO_NODE instead of NODE_DATA(0)->node_id
* because in some case like Node0 doesn't have RAM installed
* low ram will be on Node1
*/
/* (2.2) 遍歷memblock中尚未分配的內(nèi)存,將其釋放到buddy系統(tǒng)中 */
for_each_free_mem_range(i, NUMA_NO_NODE, MEMBLOCK_NONE, &start, &end,
NULL)
count += __free_memory_core(start, end);
return count;
}
↓
__free_memory_core()
↓
static void __init __free_pages_memory(unsigned long start, unsigned long end)
{
int order;
/* (2.2.1) 對需要釋放的區(qū)域,拆分成盡可能大的 2^order 內(nèi)存塊去釋放 */
while (start < end) {
order = min(MAX_ORDER - 1UL, __ffs(start));
/* (2.2.1.1) 計(jì)算最大的釋放長度(2^order)page */
while (start + (1UL << order) > end)
order--;
/* (2.2.1.2) 繼續(xù)釋放 */
__free_pages_bootmem(pfn_to_page(start), start, order);
start += (1UL << order);
}
}
↓
__free_pages_bootmem() → __free_pages_boot_core() → __free_pages()
具體的釋放細(xì)節(jié) __free_pages() 在下一節(jié)中解析。