|
1、为什么需要vmalloc
在分配内存时,总是希望能分配到连续的物理内存页,愿望是美好的,但系统中可能没有太多的连续内存可用(比如内存碎片严重时),此时就需要一种非连续内存的分配方式。于是乎,就产生了vmalloc,vmalloc用于分配不连续的物理内存页,但将其映射到内核虚拟地址空间中后,其虚拟地址是连续的,使用vmalloc分配内存有三个特点:
1)物理内存不一定连续
2)返回的虚拟内存是连续的。
3)优先分配高端内存,所以vmalloc也是在内核态使用高端内存的最主要的方式。
在用户态,应用程序直接看到的是虚拟地址空间,物理内存和虚拟内存间通过页表映射,用户太分配的内存在虚拟地址空间中总是连续的,而物理内存是否连续不必关心,所以这对用户态来说其实没啥影响(除了会稍影响性能和占用TLB)。
2、基本原理(针对IA32)
内核虚拟地址空间中,有一段专门的区间用于vmalloc,称之vmalloc区,位于线性映射区之后,准确的说是从892M+8M(VMALLOC_START)到VMALLOC_END之间,其中892M是线性映射区,用于线性映射低端内存,之后的8M是安全间隙,用于区间隔离,防止越界。
vmalloc区中包含一个个独立的子区域,每个子区域用于一次独立映射,各个子区域间通过一个内存页进行隔离,防止不正确的内存访问操作。内核中管理vmalloc区中的子区域,使用了vm_struct数据结构
/*
* vmalloc区域中的子内存区,ioremap也使用了该区域
* 所有的vm_struct组成一个链表,管理着vmalloc区域
* 中已经建立的各个子区域,该链表头保存于
* 全局变量vmlist中。
*/
struct vm_struct {
struct vm_struct *next;
//该内存区在虚拟地址空间中的起始地址
void *addr;
// 该内存区的大小
unsigned long size;
/*
* 与该内存区关联的标志集合,用于指定内存
* 区域类型,可选值有3:
* VM_ALLOC指定由vmalloc产生的子区域
* VM_MAP表示将现存pages集合映射到连续的虚拟
地址空间中。
* VM_IOREMAP表示将IO内存映射到vmalloc区域中。
*/
unsigned long flags;
/*
* 指向page指针的数组,每个数组成员表示一个
* 映射到虚拟地址空间中的物理内存页的实例。
*/
struct page **pages;
// pages数组项的数目,即该内存区对应的物理内存页数
unsigned int nr_pages;
/*
* ioremap时使用,用来保存该区域映射的物理
* 内存地址,在通常的vmalloc流程中不使用该
* 字段,因为vmalloc流程中会分配物理内存,并
* 通过修改内核页表来实现虚拟地址到物理
* 地址见的映射。
*/
phys_addr_t phys_addr;
const void *caller;
};
vmalloc基本流程如下:
vmalloc()
-->__vmalloc()
-->__vmalloc_node()
-->__vmalloc_node_range()
-->get_vm_area_node() //从vmalloc区中获取空闲的子区域
-->__vmalloc_area_node() //使用alloc_page分配物理内存,并修改页表,进行映射。
3、代码分析
__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, int node, const void *caller)
{
//vmalloc区域中的子内存区
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区中的子区域链表vmlist(全局变量)中
* 获取空闲的子区域vm_struct。
*/
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;
}
__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) {
/*
* 如果区域大小大于1页,则进行迭代,注意
* 此处传入的内存分配标记中有__GFP_HIGHMEM,
* 表示优先从高端内存中分配,这也是vmalloc
* 的用处所在。
*/
pages = __vmalloc_node(array_size, 1, nested_gfp|__GFP_HIGHMEM,
PAGE_KERNEL, node, caller);
area->flags |= VM_VPAGES;
// 迭代直到分配的区域大小小于1页,则使用kmalloc分配
} 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;
}
// 将分配的page填入vm_struct结构中
area->pages[i] = page;
}
/*
* 将新分配的区域进行映射,即修改内核页表
* (进程页表在page fault中更新),建立虚拟地址到
* 物理地址间的映射。
*/
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;
}
|
|