2017-07-09
今天周末,閑來無事聊聊linux內核內存分配那點事……重點在於分析vmalloc的執行 流程
以傳統x86架構為例,內核空間內存(3G-4G)主要分為三大部分:DMA映射區,一致映射區、高端內存區。其中前兩者占據低端892M,而剩下的128M作為高端內存區。DMA映射區涉及到外部設備,咱們暫且不討論,那么就剩下一致映射區和高端內存區。一致映射區的虛擬地址均一一對應了物理頁框,因此此區間虛擬地址的訪問可以直接通過偏移量得到物理內存而不需進行頁表的轉換。但是1G內核地址空間說實話有些捉襟見肘,如果都用作一致映射,那么當物理內存大於4G時,內核仍然無法利用。鑒於此,留下128M的地址空間作為高端內存,扮演着臨時映射的作用。回想下PAE模式的原理,是不是有些相似呢?一致映射區既然都已經關聯了物理內存就可以通過slab緩存來管理,以加速分配。而高端內存這點有些類似於用戶進程的內存分配,但又並不完全相同,后面咱們會講到。在這里咱們先回憶下用戶空間內存分配流程,一個普通的進程調用malloc函數分配一段地址空間,有可能在 堆中,如果內存過大海有可能在mmap映射區,同時會由一個vm_area_struct記錄下本次分配出去的地址空間信息,如大小,起始地址等由於進程獨享虛擬地址空間,所以這些vm_area_struct都是按照進程為單位進行管理的。這也沒毛病。此時僅僅是在進程管理虛擬內存的數據結構中記錄了下這塊虛擬地址空間被分配出去了,然而此時和物理內存還沒管理,在真正發生讀寫的時候就會分配物理內存、填充頁表。然而在內核中,所有進程共享唯一的內核地址空間,所以內核地址空間需要統一管理。
下面我們根據源碼分析下vmalloc的具體流程
void *vmalloc(unsigned long size) { return __vmalloc(size, GFP_KERNEL | __GFP_HIGHMEM, PAGE_KERNEL); }
該函數直接封裝了__vmalloc_node,而__vmalloc_node又封裝了__vmalloc_node_range,我們直接從__vmalloc_node_range函數看起
void *__vmalloc_node_range(unsigned long size, unsigned long align, unsigned long start, unsigned long end, gfp_t gfp_mask, pgprot_t prot, int node, const void *caller) { struct vm_struct *area; void *addr; unsigned long real_size = size; size = PAGE_ALIGN(size); if (!size || (size >> PAGE_SHIFT) > totalram_pages) goto fail; /*在高端內存區分配一個vm_struct並初始化*/ area = __get_vm_area_node(size, align, VM_ALLOC | VM_UNLIST, start, end, node, gfp_mask, caller); if (!area) goto fail; /*為area分配管理page的數組,並通過伙伴系統分配物理頁面*/ addr = __vmalloc_area_node(area, gfp_mask, prot, node, caller); if (!addr) return NULL; /* * In this function, newly allocated vm_struct has VM_UNLIST flag. * It means that vm_struct is not fully initialized. * Now, it is fully initialized, so remove this flag here. */ clear_vm_unlist(area); /* * A ref_count = 3 is needed because the vm_struct and vmap_area * structures allocated in the __get_vm_area_node() function contain * references to the virtual address of the vmalloc'ed block. */ kmemleak_alloc(addr, real_size, 3, gfp_mask); return addr; fail: warn_alloc_failed(gfp_mask, 0, "vmalloc: allocation failure: %lu bytes\n", real_size); return NULL; }
前面說和用戶空間進程分配內存類似的點就在於高端內存區的管理同樣需要數據結構,和用戶空間對應,內核使用vm_struct。只是內核所有的vm_struct放在一起,與單個進程無關。該函數中首先對size進行了頁面對齊設置,然后檢測size的合法性。這都不需多說。接着調用__get_vm_area_node分配一個vm_struct結構,然后調用__vmalloc_area_node分配一個管理page結構的數組,並通過伙伴系統,分配物理頁面並填充該數組。由此可見,高端內存區的分配和用戶空間進程的不同之處,就是這里並不是等待訪問的時候分配物理內存,而是在分配的時候就進行了填充。
__get_vm_area_node
static struct vm_struct *__get_vm_area_node(unsigned long size, unsigned long align, unsigned long flags, unsigned long start, unsigned long end, int node, gfp_t gfp_mask, const void *caller) { struct vmap_area *va; struct vm_struct *area; /*不能處於中斷上下文*/ BUG_ON(in_interrupt()); if (flags & VM_IOREMAP) { int bit = fls(size); if (bit > IOREMAP_MAX_ORDER) bit = IOREMAP_MAX_ORDER; else if (bit < PAGE_SHIFT) bit = PAGE_SHIFT; align = 1ul << bit; } size = PAGE_ALIGN(size); if (unlikely(!size)) return NULL; /*分配一個vm_struct結構*/ area = kzalloc_node(sizeof(*area), gfp_mask & GFP_RECLAIM_MASK, node); if (unlikely(!area)) return NULL; /* * We always allocate a guard page. */ size += PAGE_SIZE; /*分配一塊虛擬地址空間*/ va = alloc_vmap_area(size, align, start, end, node, gfp_mask); if (IS_ERR(va)) { kfree(area); return NULL; } /* * When this function is called from __vmalloc_node_range, * we add VM_UNLIST flag to avoid accessing uninitialized * members of vm_struct such as pages and nr_pages fields. * They will be set later. */ if (flags & VM_UNLIST) /*初始化area*/ setup_vmalloc_vm(area, va, flags, caller); else insert_vmalloc_vm(area, va, flags, caller); return area; }
該函數完成vm_struct結構的分配並初始化,開始便判斷是否處於中斷上下文,在中斷上下文有可能會出問題,因為物理頁面的獲取並不能保證一定可以立刻得到,如果物理頁面不足會造成睡眠,而中斷上下文是不能睡眠的。IOREMAP的情況我們不考慮先,再次對size進行頁面對齊,實際上前面已經設置過了,然后調用了kzalloc_node函數得到一個結構,這里為何不用slab緩存??有可能是vmalloc並不會頻繁的執行(猜想)。然后size加了一個page的大小主要做各個區間之間的隔離。接着調用alloc_vmap_area函數在虛擬地址空間分配一段空間,這點就類似於我們在用戶空間通過malloc函數分配了,僅僅分配虛擬地址空間。內核中一段虛擬地址空間通過vmap_area管理,分配完成后通過setup_vmalloc_vm利用vmap_area對vm_struct進行初始化。
__vmalloc_area_node
static void *__vmalloc_area_node(struct vm_struct *area, gfp_t gfp_mask, pgprot_t prot, int node, const void *caller) { const int order = 0; struct page **pages; unsigned int nr_pages, array_size, i; gfp_t nested_gfp = (gfp_mask & GFP_RECLAIM_MASK) | __GFP_ZERO; /*需要分配的頁面數量*/ nr_pages = (area->size - PAGE_SIZE) >> PAGE_SHIFT; /*需要一個數組管理涉及到的物理頁面地址即page指針,這個數組的大小*/ array_size = (nr_pages * sizeof(struct page *)); area->nr_pages = nr_pages; /* Please note that the recursion is strictly bounded. */ /*如果數組的大小大於一個頁面*/ if (array_size > PAGE_SIZE) { pages = __vmalloc_node(array_size, 1, nested_gfp|__GFP_HIGHMEM, PAGE_KERNEL, node, caller); area->flags |= VM_VPAGES; } else { pages = kmalloc_node(array_size, nested_gfp, node); } area->pages = pages; area->caller = caller; if (!area->pages) { remove_vm_area(area->addr); kfree(area); return NULL; } for (i = 0; i < area->nr_pages; i++) { struct page *page; gfp_t tmp_mask = gfp_mask | __GFP_NOWARN; if (node < 0) page = alloc_page(tmp_mask); else page = alloc_pages_node(node, tmp_mask, order); if (unlikely(!page)) { /* Successfully allocated i pages, free them in __vunmap() */ area->nr_pages = i; goto fail; } area->pages[i] = page; } if (map_vm_area(area, prot, &pages)) goto fail; return area->addr; fail: warn_alloc_failed(gfp_mask, order, "vmalloc: allocation failure, allocated %ld of %ld bytes\n", (area->nr_pages*PAGE_SIZE), area->size); vfree(area->addr); return NULL; }
這段代碼倒沒有什么難度,首先根據實際的虛擬地址空間大小計算頁面數量,然后計算page指針數組的大小,如果在一個頁面以內,就在一致映射區直接分配,這樣更快。如果大於一個page,就 還在高端內存區分配。之后在vm_struct和pages數組建立關聯,當然如果page指針數組沒分配好則及時返回。如果一切OK,則進入一個循環,為每個虛擬頁框分配物理頁面,這里只是分配了和虛擬頁面數組相等的物理頁面,並沒有建立關聯,如果指定了分配節點(NUMA下),則從指定的節點分配,否則默認從當前節點分配。到這里我們又可以發現高端內存區的內存分配和一致映射區內存分配不同之處,就是高端內存區虛擬地址空間連續的,而對應的物理地址空間卻是離散的。一致映射區都是連續的。在分配好物理頁面后就調用map_vm_area為頁面建立映射,這個過程就是填充頁表的過程,我們就不深入看了。
內核中高端內存的管理
如前所述,內核地址空間的分配通過vm_struct結構管理,該結構類似於進程地址空間的vm_area_struct結構。不同的是由於所有進程共享內核地址空間,內核中所有vm_struct放在一起管理,構成一條鏈表。同時,內核把負責具體映射的j信息抽離成一個結構vmap_area,該結構在內核通過紅黑樹和雙向鏈表管理,在此之前我們先看下vm_struct和vmap_area結構,
struct vm_struct { struct vm_struct *next; void *addr; unsigned long size; unsigned long flags; struct page **pages; unsigned int nr_pages; phys_addr_t phys_addr; const void *caller; };
所有的vm_struct通過next鏈接,addr為該區間的地址地址,size為該區間的大小,flags是一些標志位,page是一個page指針數組,記錄該區間映射的所有物理頁面,nr_pages記錄該區間包含的頁面數量,caller是一個函數指針__builtin_return_address(0),這個玩意暫時不太了解。
struct vmap_area { unsigned long va_start;//區間的起始地址 unsigned long va_end;//區間的結束地址 unsigned long flags;//標志位 struct rb_node rb_node; /*紅黑樹節點*/ /* address sorted rbtree */ struct list_head list; /*鏈表節點*/ /* address sorted list */ struct list_head purge_list; /* "lazy purge" list */ struct vm_struct *vm; struct rcu_head rcu_head; };
具體地址空間的管理是通過vmap_area管理的,該結構記錄整個區間的起始和結束,相關字段上面已經解釋的比較清楚,這里就不多說。管理vmap_area結構的紅黑樹根節點為全局變量vmap_area_root,雙鏈表頭結點為vmap_area_list。下面我們看一下分配具體空間的函數alloc_vmap_area,還是分段分析
struct vmap_area *va; struct rb_node *n; unsigned long addr; int purged = 0; struct vmap_area *first; BUG_ON(!size); BUG_ON(size & ~PAGE_MASK); BUG_ON(!is_power_of_2(align)); va = kmalloc_node(sizeof(struct vmap_area), gfp_mask & GFP_RECLAIM_MASK, node); if (unlikely(!va)) return ERR_PTR(-ENOMEM);
開頭是一些驗證,size不能為0,必須是頁面對齊的,而且必須是2的指數。滿足條件就通過kmalloc_node分配一個vmap_area結構,基於內核地址空間共享特性,vmap_area需要加鎖操作。為了加速查找可用的空間,這里涉及到兩個靜態變量cached_hole_size和free_vmap_cache。從代碼來看free_vmap_cache是上次分配出去的vmap_area的紅黑樹節點,而cached_hole_size是free_vmap_cache最大的空洞。在紅黑樹和鏈表中,vmap_area都是有序排列的。
if (!free_vmap_cache || size < cached_hole_size || vstart < cached_vstart || align < cached_align) { nocache: cached_hole_size = 0; free_vmap_cache = NULL; } /* record if we encounter less permissive parameters */ cached_vstart = vstart; cached_align = align;
如果free_vmap_cache為空,或者在free_vmap_cache存在size大於請求分配空間的大小,又或者請求分配的起始地址小於cached_vstart,或者要求的對齊大小小於cached_align,則就不利用cache,這時設置cached_hole_size=0,free_vmap_cache=NULL,然后更新cached_vstart和cached_align
if (free_vmap_cache) { first = rb_entry(free_vmap_cache, struct vmap_area, rb_node); addr = ALIGN(first->va_end, align); if (addr < vstart) goto nocache; if (addr + size - 1 < addr) goto overflow; } else { addr = ALIGN(vstart, align); if (addr + size - 1 < addr) goto overflow; n = vmap_area_root.rb_node; first = NULL; /*遍歷紅黑樹*/ while (n) { struct vmap_area *tmp; tmp = rb_entry(n, struct vmap_area, rb_node); /**/ if (tmp->va_end >= addr) { first = tmp; if (tmp->va_start <= addr) break; n = n->rb_left; } else n = n->rb_right; } if (!first) goto found; }
如果free_vmap_cache存在,則設置first節點為free_vmap_cache代表的vmap_area,該vmap_area代表區間的結束地址要大於等於vstart才可以稱作是first。不滿足條件只能設置no_cache,然后從頭遍歷了。而如果free_vmap_cache不存在,則從vstart設置對齊后的地址為起始地址開始遍歷紅黑樹,有可能得到兩種結果1、找到一個vmap_area,其代表區間的結束地址要大於等於vstart並且起始地址小於等於vstart,就找到了first節點。2、vstart地址大於紅黑樹中所有節點的結束地址,起始就是vstart在已經分配的最大的地址后面,那這樣就不用遍歷了,直接滿足條件。
如果是第一種情況,則需要從first節點往后遍歷,這個過程會更新cached_hole_size,從代碼看是一直遍歷到鏈表最后,如果剩余的空間足夠分配,則成功,否則失敗。不太明白的是從頭開始遍歷並沒有想到利用最大空洞,而是總是遍歷到最后,這點着實不解。
找到之后就設置vmap_area的相關記錄信息,並調用__insert_vmap_area插入到紅黑樹和鏈表中。
以馬內利!
參考資料:
linux內核源碼
