專題:Linux內存管理專題html
關鍵詞:vmalloc、頁對齊、虛擬地址連續、物理不連續node
至此,已經介紹了集中內核中內存分配函數,在開始簡單作個對比總結Linux中經常使用內存分配函數的異同點,而後重點介紹了vmalloc相關的hole查找,頁面分配等等。linux
vmalloc的核心是在vmalloc區域中找到合適的hole,hole是虛擬地址連續的;而後逐頁分配內存來從物理上填充hole。api
vmalloc的gfp_maks和逐頁分配就決定了它的屬性:可能睡眠、虛擬地址連續、物理地址不連續、size對齊到頁;因此不適合小內存分配,開銷較大。數組
用戶/內核 | API名稱 | 物理連續? | 大小限制 | 單位 | 場景 | |
用戶空間 | malloc/calloc/realloc/free | 不保證 | 堆申請 | 字節 | calloc初始化爲0;realloc改變內存大小。 | |
alloca | 棧申請 | 字節 | 向棧申請內存 | |||
mmap/munmap | 將文件利用虛擬內存技術映射到內存中去。 | |||||
brk、sbrk | 虛擬內存到內存的映射。sbrk(0)返回program break地址,sbrk調整對的大小。 | |||||
內緩存 核安全 空數據結構 間 less |
vmalloc/vfree | 虛擬連續函數 物理不定 |
vmalloc區大小限制 | 頁 VMALLOC區域 |
可能睡眠,不能從中斷上下文中調用,或其餘不容許阻塞狀況下調用。 VMALLOC區域vmalloc_start~vmalloc_end之間,vmalloc比kmalloc慢,適用於分配大內存。 |
|
slab | kmalloc/kcalloc/krealloc/kfree | 物理連續 | 64B-4MB (隨slab而變) |
2^order字節 Normal區域 |
大小有限,不如vmalloc/malloc大。 最大/小值由KMALLOC_MIN_SIZE/KMALLOC_SHIFT_MAX,對應64B/4MB。 從/proc/slabinfo中的kmalloc-xxxx中分配,創建在kmem_cache_create基礎之上。 |
|
kmem_cache_create | 物理連續 | 64B-4MB | 字節大小,需對齊 Normal區域 |
便於固定大小數據的頻繁分配和釋放,分配時從緩存池中獲取地址,釋放時也不必定真正釋放內存。經過slab進行管理。 |
||
夥伴系統 | __get_free_page/__get_free_pages | 物理連續 | 4MB(1024頁) | 頁 Normal區域 |
__get_free_pages基於alloc_pages,可是限定不能使用HIGHMEM。 | |
alloc_page/alloc_pages/free_pages | 物理連續 | 4MB | 頁 Normal/Vmalloc均可 |
CONFIG_FORCE_MAX_ZONEORDER定義了最大頁面數2^11,一次能分配到的最大頁面數是1024。 |
在進行vmalloc代碼走讀以前,先簡單看一下兩個重要的數據結構:struct vm_struct(vmalloc描述符)和struct vmap_area(記錄在vmap_area_root中的vmalooc分配狀況和vmap_area_list列表中)。
struct vm_struct { struct vm_struct *next;----------下一個vm。 void *addr;--------------指向第一個內存單元虛擬地址 unsigned long size;----------該內存區對應的大小 unsigned long flags;---------vm標誌位,以下。 struct page **pages;---------指向頁面沒描述符的指針數組 unsigned int nr_pages;-------vmalloc映射的page數目 phys_addr_t phys_addr;-------用來映射硬件設備的IO共享內存,其餘狀況下爲0 const void *caller;----------調用vmalloc類函數的返回地址 };
其中VM_NO_GUARD表示不須要多分配一頁來作安全墊。
/* bits in flags of vmalloc's vm_struct below */ #define VM_IOREMAP 0x00000001 /* ioremap() and friends */ #define VM_ALLOC 0x00000002 /* vmalloc() */ #define VM_MAP 0x00000004 /* vmap()ed pages */ #define VM_USERMAP 0x00000008 /* suitable for remap_vmalloc_range */ #define VM_VPAGES 0x00000010 /* buffer for pages was vmalloc'ed */ #define VM_UNINITIALIZED 0x00000020 /* vm_struct is not fully initialized */ #define VM_NO_GUARD 0x00000040 /* don't add guard page */ #define VM_KASAN 0x00000080 /* has allocated kasan shadow memory */
vmap_area表示內核空間的vmalloc區域的一個vmalloc,由rb_node和list進行串聯。
struct vmap_area { unsigned long va_start;--------------malloc區的起始地址 unsigned long va_end;----------------malloc區的結束地址 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;------------------------------------------指向配對的vm_struct struct rcu_head rcu_head; };
vmalloc用於分配虛擬地址連續的內存空間,vzmalloc相對於vmalloc多了個0初始化。
同時vmalloc/vzmalloc分配的虛擬地址範圍在VMALLOC_START/VMALLOC_END之間。
void *vmalloc(unsigned long size) { return __vmalloc_node_flags(size, NUMA_NO_NODE, GFP_KERNEL | __GFP_HIGHMEM); } void *vzalloc(unsigned long size) { return __vmalloc_node_flags(size, NUMA_NO_NODE, GFP_KERNEL | __GFP_HIGHMEM | __GFP_ZERO); } static void *__vmalloc_node(unsigned long size, unsigned long align, gfp_t gfp_mask, pgprot_t prot, int node, const void *caller) { return __vmalloc_node_range(size, align, VMALLOC_START, VMALLOC_END, gfp_mask, prot, 0, node, caller); }
__vmalloc_node_range的主要工做是找到符合大小要求的空閒vmalloc區域的hole;分配頁面,並建立頁表映射關係。
下面是__vmalloc_node_range的主要子函數,反映了其主要工做內容。
__vmalloc_node_range----------------vmalloc的核心函數
__get_vm_area_node--------------找到符合大小的空閒vmalloc區域 alloc_vmap_area-------------從vmap_area_root中找到合適的hole,填充vmap_area結構體,並插入到vmap_area_root紅黑樹中 setup_vmalloc_vm------------將vmap_area的參數填入vm_struct __vmalloc_area_node-------------計算須要的頁面數,分配頁面,並建立頁表映射關係 alloc_page------------------分配頁面 map_vm_area-----------------創建PGD/PTE頁表映射關係
__vmalloc_node_range是vmalloc的核心函數:
void *__vmalloc_node_range(unsigned long size, unsigned long align, unsigned long start, unsigned long end, gfp_t gfp_mask, pgprot_t prot, unsigned long vm_flags, int node, const void *caller) { struct vm_struct *area; void *addr; unsigned long real_size = size; size = PAGE_ALIGN(size);----------------------------------------對地址進行了頁對齊,哪怕分配10B大小也分配一頁的空間。因此適合大內存分配。 if (!size || (size >> PAGE_SHIFT) > totalram_pages)-------------對size大小進行判斷,大於0小於總page數 goto fail; area = __get_vm_area_node(size, align, VM_ALLOC | VM_UNINITIALIZED | vm_flags, start, end, node, gfp_mask, caller);------申請並填充vm_struct結構體。 if (!area) goto fail; addr = __vmalloc_area_node(area, gfp_mask, prot, node);---------分配內存,創建頁面映射關係。 if (!addr) return NULL; ... /* * A ref_count = 2 is needed because vm_struct allocated in * __get_vm_area_node() contains a reference to the virtual address of * the vmalloc'ed block. */ kmemleak_alloc(addr, real_size, 2, gfp_mask);-------------------kmemleak記錄分配信息 return addr;----------------------------------------------------最後返回vmalloc分配區域的首地址 ... }
__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());------------------------------------------------------vmalloc不能中在中斷中被調用 if (flags & VM_IOREMAP) align = 1ul << clamp(fls(size), PAGE_SHIFT, IOREMAP_MAX_ORDER); size = PAGE_ALIGN(size);-----------------------------------------------------頁對齊操做 if (unlikely(!size)) return NULL; area = kzalloc_node(sizeof(*area), gfp_mask & GFP_RECLAIM_MASK, node);-------分配一個struct vm_struct來描述vmalloc區域 if (unlikely(!area)) return NULL; if (!(flags & VM_NO_GUARD)) size += PAGE_SIZE;-------------------------------------------------------加一頁做爲安全區間 va = alloc_vmap_area(size, align, start, end, node, gfp_mask);---------------申請一個vmap_area並將其插入vmap_area_root中。 if (IS_ERR(va)) { kfree(area); return NULL; } setup_vmalloc_vm(area, va, flags, caller);-----------------------------------填充vmalloc描述符vm_struct area。 return area; }
alloc_vmap_area在整個vmalloc空間中查找一塊大小合適而且每人使用的空間,即hole。空間範圍是VMALLOC_START~VMALLOC_END。
static struct vmap_area *alloc_vmap_area(unsigned long size, unsigned long align, unsigned long vstart, unsigned long vend, int node, gfp_t gfp_mask) { 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),--------------------分配一個vmap_area結構體 gfp_mask & GFP_RECLAIM_MASK, node); if (unlikely(!va)) return ERR_PTR(-ENOMEM); /* * Only scan the relevant parts containing pointers to other objects * to avoid false negatives. */ kmemleak_scan_area(&va->rb_node, SIZE_MAX, gfp_mask & GFP_RECLAIM_MASK); retry: spin_lock(&vmap_area_lock); /* * Invalidate cache if we have more permissive parameters. * cached_hole_size notes the largest hole noticed _below_ * the vmap_area cached in free_vmap_cache: if size fits * into that hole, we want to scan from vstart to reuse * the hole instead of allocating above free_vmap_cache. * Note that __free_vmap_area may update free_vmap_cache * without updating cached_hole_size or cached_align. */ 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; /* find starting point for our search */ 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 < addr) goto overflow; } else { addr = ALIGN(vstart, align); if (addr + size < addr) goto overflow; n = vmap_area_root.rb_node;--------------------------------------------vmap_area_root存放系統中正在使用的vmalloc塊,爲vmap_area結構。 first = NULL; while (n) {------------------------------------------------------------遍歷vmap_area_root左子葉節點找區間地址最小的區塊。 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;-----------------------------------------------------此時tmp->va_start<=addr<=tmp->va_end,找到起始地址最小的vmalloc區塊。 n = n->rb_left; } else n = n->rb_right; } if (!first) goto found; } /* from the starting point, walk areas until a suitable hole is found */ while (addr + size > first->va_start && addr + size <= vend) {-------------判斷申請空間addr+size的合法性。 if (addr + cached_hole_size < first->va_start) cached_hole_size = first->va_start - addr; addr = ALIGN(first->va_end, align); if (addr + size < addr) goto overflow; if (list_is_last(&first->list, &vmap_area_list)) goto found; first = list_entry(first->list.next,------------------------------------檢查下一個hole是否知足 struct vmap_area, list); } found: if (addr + size > vend) goto overflow; va->va_start = addr; va->va_end = addr + size; va->flags = 0; __insert_vmap_area(va);----------------------------------------------------將找到的新區塊插入到vmap_area_root中 free_vmap_cache = &va->rb_node; spin_unlock(&vmap_area_lock); BUG_ON(va->va_start & (align-1)); BUG_ON(va->va_start < vstart); BUG_ON(va->va_end > vend); return va; overflow: spin_unlock(&vmap_area_lock); if (!purged) { purge_vmap_area_lazy(); purged = 1; goto retry; } if (printk_ratelimit()) pr_warn("vmap allocation for size %lu failed: " "use vmalloc=<size> to increase size.\n", size); kfree(va); return ERR_PTR(-EBUSY); }
setup_vmalloc_vm主要用來設置vm_struct,同時將vm_struct和vmap_area關聯。
static void setup_vmalloc_vm(struct vm_struct *vm, struct vmap_area *va, unsigned long flags, const void *caller) { spin_lock(&vmap_area_lock); vm->flags = flags; vm->addr = (void *)va->va_start; vm->size = va->va_end - va->va_start; vm->caller = caller; va->vm = vm; va->flags |= VM_VM_AREA; spin_unlock(&vmap_area_lock); }
至此,已經在vmalloc找到合適大小的hole,而且將其插入到vmap_area_root中,更行了vmalloc描述符vm_struct。
__vmalloc_area_node則進行實際的頁面分配,並創建頁表映射,更新頁表cache。
static void *__vmalloc_area_node(struct vm_struct *area, gfp_t gfp_mask, pgprot_t prot, int node) { const int order = 0; struct page **pages; unsigned int nr_pages, array_size, i; const gfp_t nested_gfp = (gfp_mask & GFP_RECLAIM_MASK) | __GFP_ZERO; const gfp_t alloc_mask = gfp_mask | __GFP_NOWARN; nr_pages = get_vm_area_size(area) >> PAGE_SHIFT;---------------------------------計算vmalloc描述符vm_struct->size須要多少頁 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, area->caller); area->flags |= VM_VPAGES; } else { pages = kmalloc_node(array_size, nested_gfp, node); } area->pages = pages; if (!area->pages) { remove_vm_area(area->addr); kfree(area); return NULL; } for (i = 0; i < area->nr_pages; i++) {------------------------------------------逐頁分配頁框,這裏也能夠看出對vmalloc是沒法保證屋裏連續的,頁不是一塊兒分配,而是一頁一頁分配的。 struct page *page; if (node == NUMA_NO_NODE) page = alloc_page(alloc_mask);-----------------------------------------alloc_mask爲GFP_KERNEL|__GFP_HIGHMEM|__GFP_NOWARN,因此優先在vmalloc區域,容許睡眠。 else page = alloc_pages_node(node, alloc_mask, order); if (unlikely(!page)) { /* Successfully allocated i pages, free them in __vunmap() */ area->nr_pages = i; goto fail; } area->pages[i] = page; if (gfp_mask & __GFP_WAIT) cond_resched(); } if (map_vm_area(area, prot, pages))----------------------------------------------建議vmalloc區域的頁面映射關係 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; }
map_vm_area對分配的頁面進行了映射,map_vm_area-->vmap_page_range-->vmap_page_range_noflush。
int map_vm_area(struct vm_struct *area, pgprot_t prot, struct page **pages) { unsigned long addr = (unsigned long)area->addr; unsigned long end = addr + get_vm_area_size(area);----------------肯定映射的起始和結束地址 int err; err = vmap_page_range(addr, end, prot, pages); return err > 0 ? 0 : err; } static int vmap_page_range(unsigned long start, unsigned long end, pgprot_t prot, struct page **pages) { int ret; ret = vmap_page_range_noflush(start, end, prot, pages); flush_cache_vmap(start, end); return ret; }
vmap_page_range_noflush創建了映射關係,可是沒有刷新緩存。
static int vmap_page_range_noflush(unsigned long start, unsigned long end, pgprot_t prot, struct page **pages) { pgd_t *pgd; unsigned long next; unsigned long addr = start; int err = 0; int nr = 0; BUG_ON(addr >= end); pgd = pgd_offset_k(addr);--------------------------------獲得地址區域對應的PGD地址 do {-----------------------------------------------------遍歷地址空間中的全部對應PGD;若是end和start在同一PGD區域,則只須要一次。 next = pgd_addr_end(addr, end);----------------------addr和end在同一PGD的話,next即爲end;不然爲addr下一個PGD對應起始地址。 err = vmap_pud_range(pgd, addr, next, prot, pages, &nr); if (err) return err; } while (pgd++, addr = next, addr != end); return nr; } static int vmap_pte_range(pmd_t *pmd, unsigned long addr, unsigned long end, pgprot_t prot, struct page **pages, int *nr) { pte_t *pte; /* * nr is a running index into the array which helps higher level * callers keep track of where we're up to. */ pte = pte_alloc_kernel(pmd, addr);-------------------------------定位於addr對應的頁表項 if (!pte) return -ENOMEM; do { struct page *page = pages[*nr];------------------------------頁描述符 if (WARN_ON(!pte_none(*pte))) return -EBUSY; if (WARN_ON(!page)) return -ENOMEM; set_pte_at(&init_mm, addr, pte, mk_pte(page, prot));---------將頁描述符對應的頁框和頁表項進行關聯,映射關係被創建。 (*nr)++; } while (pte++, addr += PAGE_SIZE, addr != end); return 0; }
flush_cache_vmap則進行了相關操做: