天天看點

Linux vmalloc原理與實作前言一、vmalloc 原理二、資料結構三、vmalloc初始化四、vmalloc的應用五、總結參考資料

文章目錄

  • 前言
  • 一、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)
           
Linux vmalloc原理與實作前言一、vmalloc 原理二、資料結構三、vmalloc初始化四、vmalloc的應用五、總結參考資料

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(即記憶體條)無關。

Linux vmalloc原理與實作前言一、vmalloc 原理二、資料結構三、vmalloc初始化四、vmalloc的應用五、總結參考資料

其中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标志。

Linux vmalloc原理與實作前言一、vmalloc 原理二、資料結構三、vmalloc初始化四、vmalloc的應用五、總結參考資料

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。

Linux vmalloc原理與實作前言一、vmalloc 原理二、資料結構三、vmalloc初始化四、vmalloc的應用五、總結參考資料

三、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

繼續閱讀