文章目錄
- 前言
- 一、vmalloc 原理
-
- 1.1 vmalloc space
- 1.2 vmalloc實作
- 1.3 __vmalloc_node_range
-
- 1.3.1 __get_vm_area_node
- 1.3.2 __vmalloc_area_node
- 1.3.3 map_vm_area
- 二、資料結構
-
- 2.1 struct vm_struct
- 2.2 struct vmap_area
- 三、vmalloc初始化
- 四、vmalloc的應用
- 五、總結
- 參考資料
前言
實體上連續的記憶體映射對核心是最高效的,這樣會充分利用高速緩存獲得較低的平均通路時間,但是伴随着系統的運作,會出現大量實體記憶體碎片,系統可能在配置設定大塊記憶體時,無法找到連續的實體記憶體頁。
vmalloc用于在核心中配置設定實體上不連續但虛拟位址連續的記憶體空間。主要用于核心子產品的配置設定和I/O驅動程式配置設定緩沖區。vmalloc配置設定的記憶體通過頁表映射而非直接映射。
一、vmalloc 原理
1.1 vmalloc space
// linux-3.10/Documentation/x86/x86_64/mm.txt
Virtual memory map with 4 level page tables:
ffffc90000000000 - ffffe8ffffffffff (=45 bits) vmalloc/ioremap space
// linux-3.10/arch/x86/include/asm/pgtable_64_types.h
#define VMALLOC_START _AC(0xffffc90000000000, UL)
#define VMALLOC_END _AC(0xffffe8ffffffffff, UL)
1.2 vmalloc實作
// linux-3.10/mm/vmalloc.c
/**
* vmalloc - allocate virtually contiguous memory
* @size: allocation size
* Allocate enough pages to cover @size from the page level
* allocator and map them into contiguous kernel virtual space.
*
* For tight control over page level allocator and protection flags
* use __vmalloc() instead.
*/
void *vmalloc(unsigned long size)
{
return __vmalloc_node_flags(size, NUMA_NO_NODE,
GFP_KERNEL | __GFP_HIGHMEM);
}
EXPORT_SYMBOL(vmalloc);
vmalloc函數隻有一個參數就是指定需要的核心虛拟位址空間的大小size,但是size的不能是位元組隻能是頁。我已1頁4K為例,該函數配置設定的記憶體大小是4K的整數倍。
GFP_KERNEL是核心中配置設定記憶體最常用的标志,這種配置設定可能會引起睡眠,阻塞,使用的普通優先級,用在可以重新安全排程的程序上下文中。是以不能在中斷上下文中調用vmalloc,也不允許在不允許阻塞的地方調用。
__GFP_HIGHMEM表示盡量從實體記憶體區的高端記憶體區配置設定記憶體,x86_64沒有高端記憶體區。
vmalloc在核心中最普遍的用處便是子產品的實作,因為子產品可以在任何時候加載,如果子產品的資料比較多,那麼無法保證有連續的實體記憶體,使用vmalloc便可以使用多塊小的實體頁(通常用多個1頁實體頁進行拼接)拼接出連續的核心虛拟位址空間。
// linux-3.10/mm/vmalloc.c
static inline void *__vmalloc_node_flags(unsigned long size,
int node, gfp_t flags)
{
return __vmalloc_node(size, 1, flags, PAGE_KERNEL,
node, __builtin_return_address(0));
}
// linux-3.10/mm/vmalloc.c
/**
* __vmalloc_node - allocate virtually contiguous memory
* @size: allocation size
* @align: desired alignment
* @gfp_mask: flags for the page level allocator
* @prot: protection mask for the allocated pages
* @node: node to use for allocation or NUMA_NO_NODE
* @caller: caller's return address
*
* Allocate enough pages to cover @size from the page level
* allocator with @gfp_mask flags. Map them into contiguous
* kernel virtual space, using a pagetable protection of @prot.
*/
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, node, caller);
}
vmalloc的核心函數就是__vmalloc_node_range:
其中的一些參數:
// linux-3.10/include/linux/numa.h
#define NUMA_NO_NODE (-1)
// linux-3.10/arch/x86/include/asm/pgtable_types.h
#define __PAGE_KERNEL (__PAGE_KERNEL_EXEC | _PAGE_NX)
#define PAGE_KERNEL __pgprot(__PAGE_KERNEL)
node = NUMA_NO_NODE
align = 1
gfp_mask = GFP_KERNEL | __GFP_HIGHMEM
start = VMALLOC_START
end = VMALLOC_END
prot = PAGE_KERNEL
caller = __builtin_return_address(0)
// linux-3.10/include/linux/vmalloc.h
/* 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_UNLIST 0x00000020 /* vm_struct is not listed in vmlist */
/* bits [20..32] reserved for arch specific ioremap internals */
// linux-3.10/mm/vmalloc.c
/**
* __vmalloc_node_range - allocate virtually contiguous memory
* @size: allocation size
* @align: desired alignment
* @start: vm area range start
* @end: vm area range end
* @gfp_mask: flags for the page level allocator
* @prot: protection mask for the allocated pages
* @node: node to use for allocation or NUMA_NO_NODE
* @caller: caller's return address
*
* Allocate enough pages to cover @size from the page level
* allocator with @gfp_mask flags. Map them into contiguous
* kernel virtual space, using a pagetable protection of @prot.
*/
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;
//尋找核心虛拟位址空間vmalloc區域
//VM_ALLOC表示使用vmlloc配置設定得到的頁
//VM_UNLIST表示vm_struct不在vmlist全局連結清單中
area = __get_vm_area_node(size, align, VM_ALLOC | VM_UNLIST,
start, end, node, gfp_mask, caller);
if (!area)
goto fail;
//一頁一頁的配置設定實體頁,建立與虛拟位址空間vmalloc區域之間的映射,
//為不連續的實體頁建立頁表,更新核心頁表
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;
}
1.3 __vmalloc_node_range
1.3.1 __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;
......
size = PAGE_ALIGN(size);
if (unlikely(!size))
return NULL;
//調用 kmalloc 配置設定 struct vm_struct 結構體
area = kzalloc_node(sizeof(*area), gfp_mask & GFP_RECLAIM_MASK, node);
if (unlikely(!area))
return NULL;
/*
* We always allocate a guard page.
*/
//配置設定一個guard page作為安全間隙
size += PAGE_SIZE;
//調用 kmalloc 配置設定 struct vmap_area 結構體
//将struct vmap_area 結構體添加到紅黑樹和連結清單中
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.
*/
//将struct vmap_area *va和struct vm_struct *area建立關聯
if (flags & VM_UNLIST)
setup_vmalloc_vm(area, va, flags, caller);
else
insert_vmalloc_vm(area, va, flags, caller);
return area;
}
該函數主要用來在vmalloc區域(VMALLOC_START,VMALLOC_END)找到一個合适的位置,建立一個新的虛拟區域。
其中alloc_vmap_area函數比較重要:
static DEFINE_SPINLOCK(vmap_area_lock);
/* Export for kexec only */
LIST_HEAD(vmap_area_list);
static struct rb_root vmap_area_root = RB_ROOT;
/* The vmap cache globals are protected by vmap_area_lock */
static struct rb_node *free_vmap_cache;
/*
* Allocate a region of KVA of the specified size and alignment, within the
* vstart and vend.
*/
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;
......
//調用 kmalloc 配置設定 一個 struct vmap_area結構體
va = kmalloc_node(sizeof(struct vmap_area),
gfp_mask & GFP_RECLAIM_MASK, node);
......
spin_lock(&vmap_area_lock);
/* 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 - 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;
}
/* from the starting point, walk areas until a suitable hole is found */
// 在vmlloc區找到一個合适的核心虛拟位址空間vmlloc區域
while (addr + size > first->va_start && addr + size <= vend) {
if (addr + cached_hole_size < first->va_start)
cached_hole_size = first->va_start - addr;
addr = ALIGN(first->va_end, align);
if (addr + size - 1 < addr)
goto overflow;
if (list_is_last(&first->list, &vmap_area_list))
goto found;
first = list_entry(first->list.next,
struct vmap_area, list);
}
//在vmlloc區找到一個合适的核心虛拟位址空間vmlloc區域後,調用__insert_vmap_area
found:
if (addr + size > vend)
goto overflow;
va->va_start = addr;
va->va_end = addr + size;
va->flags = 0;
__insert_vmap_area(va);
free_vmap_cache = &va->rb_node;
spin_unlock(&vmap_area_lock);
return va;
......
}
從VMALLOC_START開始,從vmap_area_root這棵紅黑樹上查找,這個紅黑樹裡存放着系統中正在使用的vmalloc區塊。周遊左子葉節點找區間位址最小的區塊。如果區塊的開始位址等于VMALLOC_START,說明這區塊是第一塊vmalloc區域。如果紅黑樹沒有一個節點,說明整個vmalloc區間都是空的。
查找每個存在的vmalloc區塊的縫隙hole能否容納目前要配置設定記憶體的大小。如果在已有vmalloc區塊的縫隙中沒能找到合适的hole。那麼從最後一塊vmalloc區塊的結束位址開始一個新的vmalloc區域。
找到新的區塊hole後,調用__insert_vmap_area函數把這個hole注冊到紅黑樹中。
__insert_vmap_area将struct vmap_area *va加入全局的紅黑樹vmap_area_root和全局的連結清單vmap_area_list中:
/* Export for kexec only */
LIST_HEAD(vmap_area_list);
static struct rb_root vmap_area_root = RB_ROOT;
static void __insert_vmap_area(struct vmap_area *va)
{
struct rb_node **p = &vmap_area_root.rb_node;
struct rb_node *parent = NULL;
struct rb_node *tmp;
while (*p) {
struct vmap_area *tmp_va;
parent = *p;
tmp_va = rb_entry(parent, struct vmap_area, rb_node);
if (va->va_start < tmp_va->va_end)
p = &(*p)->rb_left;
else if (va->va_end > tmp_va->va_start)
p = &(*p)->rb_right;
else
BUG();
}
rb_link_node(&va->rb_node, parent, p);
// 将struct vmap_area *va加入全局的紅黑樹vmap_area_root
rb_insert_color(&va->rb_node, &vmap_area_root);
/* address-sort this list */
tmp = rb_prev(&va->rb_node);
if (tmp) {
struct vmap_area *prev;
prev = rb_entry(tmp, struct vmap_area, rb_node);
list_add_rcu(&va->list, &prev->list);
} else
//将struct vmap_area *va加入全局的連結清單vmap_area_list中
list_add_rcu(&va->list, &vmap_area_list);
}
1.3.2 __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;
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;
}
//調用alloc_page從目前node配置設定實體頁(夥伴系統),是一頁一頁的配置設定不連續的實體頁
for (i = 0; i < area->nr_pages; i++) {
struct page *page;
gfp_t tmp_mask = gfp_mask | __GFP_NOWARN;
//node = NUMA_NO_NODE = -1
if (node < 0)
//alloc_page():buddy allocator接口,配置設定一頁實體頁
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;
}
//将配置設定的實體頁依次映射到連續的核心虛拟位址空間vmalloc區域
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;
}
order = 0,代表從夥伴系統接口配置設定一頁實體頁。
該函數調用alloc_page從目前node配置設定實體頁,實體頁來自于夥伴系統。vmalloc是為了配置設定大塊實體記憶體,但是由于記憶體碎片的緣故,記憶體塊的頁幀可能不是連續的,為了配置設定大塊的實體記憶體,是以配置設定單元盡可能的小,也就是一頁實體頁。vmalloc是一頁一頁的配置設定實體頁,這樣就可以保證即使有很多實體頁碎片,vmalloc配置設定大塊記憶體仍然可以成功。
調用buddy allocator接口alloc_page()每次擷取一個實體頁框填入到vm_struct中的struct page* 數組。
然後調用map_vm_area将配置設定的實體頁依次映射到連續的核心虛拟位址空間vmalloc區域。
1.3.3 map_vm_area
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 + area->size - PAGE_SIZE;
int err;
err = vmap_page_range(addr, end, prot, *pages);
if (err > 0) {
*pages += err;
err = 0;
}
return err;
}
EXPORT_SYMBOL_GPL(map_vm_area);
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;
}
/*
* Set up page tables in kva (addr, end). The ptes shall have prot "prot", and
* will have pfns corresponding to the "pages" array.
*
* Ie. pte at addr+N*PAGE_SIZE shall point to pfn corresponding to pages[N]
*/
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);
//擷取核心頁表init_mm.pgd = swapper_pg_dir
pgd = pgd_offset_k(addr);
do {
next = pgd_addr_end(addr, end);
//填充核心頁表的頁上級目錄pud,将其實體位址寫入頁上級目錄項中
err = vmap_pud_range(pgd, addr, next, prot, pages, &nr);
if (err)
return err;
} while (pgd++, addr = next, addr != end);
return nr;
}
// linux-3.10/arch/x86/include/asm/pgtable.h
/*
* a shortcut which implies the use of the kernel's pgd, instead
* of a process's
*/
#define pgd_offset_k(address) pgd_offset(&init_mm, (address))
調用pgd_offset_k擷取核心頁表init_mm.pgd ( swapper_pg_dir),建立實體頁與核心虛拟位址空間之間的映射,配置設定PUD,PMD,PTE更新核心頁表。但并不會将更新的核心頁表同步到使用者程序的頁表中,這就是lazy機制。隻有發生了對應的缺頁異常後,才會将更新的核心頁表同步到使用者程序的頁表。
vmalloc缺頁異常請參考:Linux vmalloc缺頁異常處理
調用pud_alloc、pmd_alloc、pte_alloc_kernel配置設定PUD,PMD,PTE。
以下為上面配置設定的不連續的實體頁建立核心頁表映射,填充核心頁表,擷取到核心全局目錄項後,一直循環,為不連續的實體頁填充核心頁表。
循環結束的條件:指向非連續實體記憶體區的所有頁表項全被建立。
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_t *pte
pte = pte_alloc_kernel(pmd, addr);
if (!pte)
return -ENOMEM;
//循環結束的條件:指向非連續實體記憶體區的所有頁表項全被建立
do {
//pages[*nr]為之前配置設定的實體頁數組
struct page *page = pages[*nr];
if (WARN_ON(!pte_none(*pte)))
return -EBUSY;
if (WARN_ON(!page))
return -ENOMEM;
//填充核心頁表的頁表項pte,将其實體位址寫入頁表項中
//填充頁表項,即:更新核心頁表項,把實體頁的實體位址寫入頁表項中
set_pte_at(&init_mm, addr, pte, mk_pte(page, prot));
(*nr)++;
} while (pte++, addr += PAGE_SIZE, addr != end);
return 0;
}
static int vmap_pmd_range(pud_t *pud, unsigned long addr,
unsigned long end, pgprot_t prot, struct page **pages, int *nr)
{
pmd_t *pmd;
unsigned long next;
pmd = pmd_alloc(&init_mm, pud, addr);
if (!pmd)
return -ENOMEM;
//循環結束的條件:指向非連續實體記憶體區的所有頁表項全被建立
do {
next = pmd_addr_end(addr, end);
填充核心頁表的頁中級目錄pmd,将其實體位址寫入頁中級目錄項中
if (vmap_pte_range(pmd, addr, next, prot, pages, nr))
return -ENOMEM;
} while (pmd++, addr = next, addr != end);
return 0;
}
static int vmap_pud_range(pgd_t *pgd, unsigned long addr,
unsigned long end, pgprot_t prot, struct page **pages, int *nr)
{
pud_t *pud;
unsigned long next;
//配置設定一個頁上級目錄
pud = pud_alloc(&init_mm, pgd, addr);
if (!pud)
return -ENOMEM;
//循環結束的條件:指向非連續實體記憶體區的所有頁表項全被建立
do {
next = pud_addr_end(addr, end);
填充核心頁表的頁中級目錄pmd,将其實體位址寫入頁中級目錄項中
if (vmap_pmd_range(pud, addr, next, prot, pages, nr))
return -ENOMEM;
} while (pud++, addr = next, addr != end);
return 0;
}
調用map_vm_area時,傳入配置設定的實體頁數組struct page **pages:将其最終傳入到vmap_pte_range函數中,配置設定PTE,調用set_pte_at(&init_mm, addr, pte, mk_pte(page, prot)),建立新的頁表項,将新頁框的實體位址寫入頁表,建立pte_t *pte與struct page *page之間的關聯。也就是建立新建立的頁表項與實體頁之間的關聯。
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);
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;
}
與上述相反的一系列函數操作集(vunmap,釋放相關的區域,從核心頁表中删除不再需要的項,與配置設定記憶體時相似,也要操作各級頁表):
/*** Page table manipulation functions ***/
static void vunmap_pte_range(pmd_t *pmd, unsigned long addr, unsigned long end)
{
pte_t *pte;
pte = pte_offset_kernel(pmd, addr);
do {
pte_t ptent = ptep_get_and_clear(&init_mm, addr, pte);
WARN_ON(!pte_none(ptent) && !pte_present(ptent));
} while (pte++, addr += PAGE_SIZE, addr != end);
}
static void vunmap_pmd_range(pud_t *pud, unsigned long addr, unsigned long end)
{
pmd_t *pmd;
unsigned long next;
pmd = pmd_offset(pud, addr);
do {
next = pmd_addr_end(addr, end);
if (pmd_none_or_clear_bad(pmd))
continue;
vunmap_pte_range(pmd, addr, next);
} while (pmd++, addr = next, addr != end);
}
static void vunmap_pud_range(pgd_t *pgd, unsigned long addr, unsigned long end)
{
pud_t *pud;
unsigned long next;
pud = pud_offset(pgd, addr);
do {
next = pud_addr_end(addr, end);
if (pud_none_or_clear_bad(pud))
continue;
vunmap_pmd_range(pud, addr, next);
} while (pud++, addr = next, addr != end);
}
static void vunmap_page_range(unsigned long addr, unsigned long end)
{
pgd_t *pgd;
unsigned long next;
BUG_ON(addr >= end);
pgd = pgd_offset_k(addr);
do {
next = pgd_addr_end(addr, end);
if (pgd_none_or_clear_bad(pgd))
continue;
vunmap_pud_range(pgd, addr, next);
} while (pgd++, addr = next, addr != end);
}
二、資料結構
vmalloc函數實作過程,涉及到最重要的兩個資料結構就是struct vm_struct和struct vmap_area。
這兩個結構提配置設定都是調用kmalloc進行記憶體配置設定,也就是調用的通用的sla(u)配置設定器。
2.1 struct vm_struct
對于核心用vmalloc配置設定的一段連續的虛拟位址空間區域,都用一個 struct vm_struct 結構體來管理。
// linux-3.10/include/linuxvmalloc.h
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;
};
成員 | 描述 |
---|---|
next | 使得核心可以将vmalloc區域中的所有子區域儲存在一個單連結清單上 |
addr | 定義了配置設定的子區域在虛拟位址空間中的起始位址。size表示該子區域的長度. 可以根據該資訊來勾畫出vmalloc區域的完整配置設定方案 |
size | 虛拟位址空間區域大小,該子區域的長度 |
flags | 存儲了與該記憶體區關聯的标志集合, 這幾乎是不可避免的. 它隻用于指定記憶體區類型 |
pages | 是一個指針,指向page指針的數組。每個數組成員都表示一個映射到虛拟位址空間中的實體記憶體頁的page執行個體 |
nr_pages | 指定pages中數組項的數目,即涉及的記憶體頁數目 |
phys_addr | 僅當用ioremap映射了由實體位址描述的實體記憶體區域時才需要。該資訊儲存在phys_addr中 |
從/proc/vmallocinfo檔案中我們可以看到vmalloc子區域都是頁的整數倍大小,pages表示使用了多少個實體頁,顯示的數目要加+1,比如pages = 4 表示占用5個實體頁,page = 2 表示占用3個實體頁。
當用ioremap映射了由實體位址描述的實體記憶體區域時,會将實體位址儲存再phys_addr成員中,ioremap是把IO位址映射到核心空間,也就是不會使用系統RAM(即記憶體條)中的記憶體,也就不會調用夥伴接口的函數alloc_page,這部分記憶體不屬于夥伴系統用管理,是以我們在/proc/vmallocinfo檔案中也看不到pages成員。也就是ioremap映射的實體位址與系統RAM(即記憶體條)無關。
其中flags字段可以的值為:
// linux-3.10/include/linux/vmalloc.h
/* 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_UNLIST 0x00000020 /* vm_struct is not listed in vmlist */
/* bits [20..32] reserved for arch specific ioremap internals */
列印的時候也會根據 flags字段 來顯示vmlloc 區域的類型:
// linux-3.10/mm/vmalloc.c
static int s_show(struct seq_file *m, void *p)
{
......
if (v->flags & VM_IOREMAP)
seq_printf(m, " ioremap");
if (v->flags & VM_ALLOC)
seq_printf(m, " vmalloc");
if (v->flags & VM_MAP)
seq_printf(m, " vmap");
if (v->flags & VM_USERMAP)
seq_printf(m, " user");
if (v->flags & VM_VPAGES)
seq_printf(m, " vpages");
......
}
static int __init proc_vmalloc_init(void)
{
proc_create("vmallocinfo", S_IRUSR, NULL, &proc_vmalloc_operations);
return 0;
}
module_init(proc_vmalloc_init);
使用vmalloc時通常為VM_ALLOC标志。
2.2 struct vmap_area
在建立一個新的虛拟記憶體區之前,必須要找到一個适合的虛拟位址空間區域。struct vmap_area用于描述這塊虛拟位址空間區域:vmalloc子區域。
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;
};
成員 | 描述 |
---|---|
va_start | vmalloc子區域的虛拟區間起始位址 |
va_end | vmalloc子區域的虛拟區間結束位址 |
flags | 類型辨別 |
rb_node | 插入紅黑樹vmap_area_root的節點 |
list | 用于加傳入連結表vmap_area_list的節點 |
purge_list | 用于加入到全局連結清單vmap_purge_list中 |
vm | 指向對應的vm_struct |
flags類型,通常為VM_VM_AREA
/*** Global kva allocator ***/
#define VM_LAZY_FREE 0x01
#define VM_LAZY_FREEING 0x02
#define VM_VM_AREA 0x04
struct vmap_area用于描述一段虛拟位址的區域,從結構體中va_start/va_end也能看出來。同時該結構體會通過rb_node挂在紅黑樹上,通過list挂在連結清單上。
struct vmap_area中vm字段是struct vm_struct結構,用于管理虛拟位址和實體頁之間的映射關系,可以将struct vm_struct構成一個連結清單,維護多段映射。
檢視/proc/vmallocinfo,每個區間就是一個vmap_area,這些區間其實是由vmap_area結構體組成的連結清單維護的,位址範圍為vmap_area.va_start~vmap_area.va_end。
三、vmalloc初始化
asmlinkage void __init start_kernel(void)
{
mm_init();
}
/*
* Set up kernel memory allocators
*/
static void __init mm_init(void)
{
/*
* page_cgroup requires contiguous pages,
* bigger than MAX_ORDER unless SPARSEMEM.
*/
page_cgroup_init_flatmem();
mem_init();
kmem_cache_init();
percpu_init_late();
pgtable_cache_init();
vmalloc_init();
}
夥伴系統和sla(u)b配置設定器初始化完成後再調用vmalloc_init,是以vmalloc中調用了kmalloc接口(主要用來為struct vm_struct和struct vmap_area配置設定記憶體)。
static struct vm_struct *vmlist __initdata;
void __init vmalloc_init(void)
{
struct vmap_area *va;
struct vm_struct *tmp;
int i;
for_each_possible_cpu(i) {
struct vmap_block_queue *vbq;
struct vfree_deferred *p;
vbq = &per_cpu(vmap_block_queue, i);
spin_lock_init(&vbq->lock);
INIT_LIST_HEAD(&vbq->free);
p = &per_cpu(vfree_deferred, i);
init_llist_head(&p->list);
INIT_WORK(&p->wq, free_work);
}
/* Import existing vmlist entries. */
for (tmp = vmlist; tmp; tmp = tmp->next) {
//為已經存在的struct vm_struct配置設定對應的struct vmap_area
va = kzalloc(sizeof(struct vmap_area), GFP_NOWAIT);
//初始化struct vmap_area
va->flags = VM_VM_AREA;
va->va_start = (unsigned long)tmp->addr;
va->va_end = va->va_start + tmp->size;
va->vm = tmp;
__insert_vmap_area(va);
}
vmap_area_pcpu_hole = VMALLOC_END;
vmap_initialized = true;
}
vmlist是一個全局的vm_struct資料結構,代碼主要就是周遊vmlist連結清單,為每一個struct vm_struct建立對應的struct vmap_area,并初始化,添加到全局的紅黑樹vmap_area_root和連結清單vmap_area_list中。所有通過vmap、vmlianalloc等接口配置設定的虛拟位址空間vm_area都會挂在這個紅黑樹vmap_area_root和連結清單vmap_area_list中。
四、vmalloc的應用
前面說到vmalloc在核心中一個廣泛的用途就是給子產品配置設定記憶體。module_alloc是一個與體系架構有關的函數,用于配置設定子產品記憶體。可以看到module_alloc的底層實作就是調用vmalloc函數配置設定記憶體。是以子產品在核心中的記憶體區域是通過頁表映射而非直接映射。
x86_64架構:
// linux-3.10/arch/x86/kernel/module.c
void *module_alloc(unsigned long size)
{
if (PAGE_ALIGN(size) > MODULES_LEN)
return NULL;
return __vmalloc_node_range(size, 1, MODULES_VADDR, MODULES_END,
GFP_KERNEL | __GFP_HIGHMEM, PAGE_KERNEL_EXEC,
-1, __builtin_return_address(0));
}
ARM64架構:
// linux-3.10/arch/arm64/kernel/module.c
void *module_alloc(unsigned long size)
{
return __vmalloc_node_range(size, 1, MODULES_VADDR, MODULES_END,
GFP_KERNEL, PAGE_KERNEL_EXEC, -1,
__builtin_return_address(0));
}
// linux-3.10/kernel/module.c
static void *module_alloc_update_bounds(unsigned long size)
{
void *ret = module_alloc(size);
if (ret) {
mutex_lock(&module_mutex);
/* Update module bounds. */
if ((unsigned long)ret < module_addr_min)
module_addr_min = (unsigned long)ret;
if ((unsigned long)ret + size > module_addr_max)
module_addr_max = (unsigned long)ret + size;
mutex_unlock(&module_mutex);
}
return ret;
}
static int move_module(struct module *mod, struct load_info *info)
{
......
void *ptr;
/* Do the allocs. */
ptr = module_alloc_update_bounds(mod->core_size);
memset(ptr, 0, mod->core_size);
mod->module_core = ptr;
......
ptr = module_alloc_update_bounds(mod->init_size);
memset(ptr, 0, mod->init_size);
mod->module_init = ptr;
......
}
其中struct module 的 module_core成員和struct vmap_area的va_start成員都表示核心子產品的起始位址,即核心子產品代碼段的起始位址:
mod -> module_core = vmap_area.va_start
五、總結
(1)在vmalloc區域(VMALLOC_START,VMALLOC_END)找到一個合适的位置,建立一個新的虛拟區域。
(2)調用buddy allocator接口:alloc_page(),配置設定一頁一頁的實體頁。
(3)建立核心虛拟位址空間vmalloc區域于實體頁之間的映射,配置設定核心頁表的PUD,PMD,PTE,更新核心頁表。
(4)由于是一頁一頁配置設定的實體頁,是以建立映射時也要一一建立映射,會導緻比直接記憶體映射大的多TLB抖動,這樣就不能充分利用緩存了,影響查詢頁表效率。
參考資料
Linux 3.10.0
https://www.cnblogs.com/LoyenWang/p/11965787.html
https://blog.csdn.net/u012489236/article/details/108313150
https://blog.csdn.net/weixin_42262944/article/details/119272588
https://kernel.blog.csdn.net/article/details/52705111