Linux内存管理16(基于6.1内核)---vmalloc分配内存
一、内存中不连续的页的分配
根据上文的讲述, 物理上连续的映射对内核是最好的, 但并不总能成功地使用. 在分配一大块内存时, 可能竭尽全力也无法找到连续的内存块。
在用户空间中这不是问题,因为普通进程设计为使用处理器的分页机制, 当然这会降低速度并占用TLB。
在内核中也可以使用同样的技术. 内核分配了其内核虚拟地址空间的一部分, 用于建立连续映射。
在IA-32系统中, 前16M划分给DMA区域, 后面一直到第896M作为NORMAL直接映射区, 紧随直接映射的前896MB物理内存,在插入的8MB安全隙之后, 是一个用于管理不连续内存的区域. 这一段具有线性地址空间的所有性质. 分配到其中的页可能位于物理内存中的任何地方. 通过修改负责该区域的内核页表, 即可做到这一点。
Persistent mappings和Fixmaps地址空间都比较小, 这样只剩下直接地址映射和VMALLOC区, 这个划分应该是平衡两个需求的结果:
-
尽量增加DMA和Normal区大小,也就是直接映射地址空间大小,当前主流平台的内存,基本上都超过了512MB,很多都是标配1GB内存,因此注定有一部分内存无法进行线性映射。
-
保留一定数量的VMALLOC大小,这个值是应用平台特定的,如果应用平台某个驱动需要用vmalloc分配很大的地址空间,那么最好通过在kernel参数中指定vmalloc大小的方法,预留较多的vmalloc地址空间。
-
并不是Highmem没有或者越少越好,这个是我的个人理解,理由如下:高端内存就像个垃圾桶和缓冲区,防止来自用户空间或者vmalloc的映射破坏Normal zone和DMA zone的连续性,使得它们碎片化。当这个垃圾桶较大时,那么污染Normal 和DMA的机会自然就小。
通过这种方式, 将内核的内核虚拟地址空间划分为几个不同的区域。
下面的图是VMALLOC地址空间内部划分情况:
二、用vmalloc分配内存
vmalloc是一个接口函数, 内核代码使用它来分配在虚拟内存中连续但在物理内存中不一定连续的内存。mm/vmalloc.c
void *vmalloc(unsigned long size)
该函数只需要一个参数, 用于指定所需内存区的长度, 与此前讨论的函数不同, 其长度单位不是页而是字节, 这在用户空间程序设计中是很普遍的。
使用vmalloc的最著名的实例是内核对模块的实现. 因为模块可能在任何时候加载, 如果模块数据比较多, 那么无法保证有足够的连续内存可用, 特别是在系统已经运行了比较长时间的情况下。
如果能够用小块内存拼接出足够的内存, 那么使用vmalloc可以规避该问题。
因为用于vmalloc的内存页总是必须映射在内核地址空间中, 因此使用ZONE_HIGHMEM
内存域的页要优于其他内存域. 这使得内核可以节省更宝贵的较低端内存域, 而又不会带来额外的坏处. 因此, vmalloc
等映射函数是内核出于自身的目的(并非因为用户空间应用程序)使用高端内存页的少数情形之一.
所有有关vmalloc的数据结构和API结构声明include/linux/vmalloc.h
2.1 数据结构
内核在管理虚拟内存中的vmalloc
区域时, 内核必须跟踪哪些子区域被使用、哪些是空闲的。为此定义了一个数据结构vm_struct
, 将所有使用的部分保存在一个链表中。该结构提的定义include/linux/vmalloc.h
struct vm_struct {
struct vm_struct *next;
void *addr;
unsigned long size;
unsigned long flags;
struct page **pages;
#ifdef CONFIG_HAVE_ARCH_HUGE_VMALLOC
unsigned int page_order;
#endif
unsigned int nr_pages;
phys_addr_t phys_addr;
const void *caller;
};
内核使用了一个重要的数据结构称之为vm_area_struct
, 以管理用户空间进程的虚拟地址空间内容. 尽管名称和目的都是类似的, 虽然二者都是做虚拟地址空间映射的, 但不能混淆这两个结构。
-
前者是内核虚拟地址空间映射,而后者则是应用进程虚拟地址空间映射。
-
前者不会产生page fault,而后者一般不会提前分配页面,只有当访问的时候,产生page fault来分配页面。
对于每个用vmalloc分配的子区域, 都对应于内核内存中的一个该结构实例. 该结构各个成员的语义如下:
字段 | 描述 |
---|---|
next | 使得内核可以将vmalloc区域中的所有子区域保存在一个单链表上 |
addr | 定义了分配的子区域在虚拟地址空间中的起始地址。size表示该子区域的长度. 可以根据该信息来勾画出vmalloc区域的完整分配方案 |
flags | 存储了与该内存区关联的标志集合, 这几乎是不可避免的. 它只用于指定内存区类型 |
pages | 是一个指针,指向page指针的数组。每个数组成员都表示一个映射到虚拟地址空间中的物理内存页的page实例 |
nr_pages | 指定pages中数组项的数目,即涉及的内存页数目 |
phys_addr | 仅当用ioremap映射了由物理地址描述的物理内存区域时才需要。该信息保存在phys_addr中 |
caller |
其中flags
只用于指定内存区类型, 所有可能的flag标识以宏的形式定义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_DMA_COHERENT 0x00000010 /* dma_alloc_coherent */
#define VM_UNINITIALIZED 0x00000020 /* vm_struct is not fully initialized */
#define VM_NO_GUARD 0x00000040 /* ***DANGEROUS*** don't add guard page */
#define VM_KASAN 0x00000080 /* has allocated kasan shadow memory */
#define VM_FLUSH_RESET_PERMS 0x00000100 /* reset direct map and flush TLB on unmap, can't be freed in atomic context */
#define VM_MAP_PUT_PAGES 0x00000200 /* put pages and free array in vfree */
#define VM_ALLOW_HUGE_VMAP 0x00000400 /* Allow for huge pages on archs with HAVE_ARCH_HUGE_VMALLOC */
flag标识 | 描述 |
---|---|
VM_IOREMAP | 表示将几乎随机的物理内存区域映射到vmalloc区域中. 这是一个特定于体系结构的操作 |
VM_ALLOC | 指定由vmalloc产生的子区域 |
VM_MAP | 用于表示将现存pages集合映射到连续的虚拟地址空间中 |
VM_USERMAP | |
VM_UNINITIALIZED | |
VM_NO_GUARD | |
VM_KASAN |
下图给出了该结构使用方式的一个实例。 其中依次映射了3个(假想的)物理内存页, 在物理内存中的位置分别是1 023、725和7 311. 在虚拟的vmalloc区域中, 内核将其看作起始于VMALLOC_START + 100的一个连续内存区, 大小为3*PAGE_SIZE的内核地址空间,被映射到物理页面725, 1023和7311
2.2 创建vm_area
因为大部分体系结构都支持mmu,
这里我们只考虑有mmu的
情况. 实际上没有mmu
支持时, vmalloc
就无法实现非连续物理地址到连续内核地址空间的映射, vmalloc
退化为kmalloc
实现。
在创建一个新的虚拟内存区之前, 必须找到一个适当的位置. vm_area
实例组成的一个链表, 管理着vmalloc
区域中已经建立的各个子区域. 定义mm/vmalloc.c
static struct vm_struct *vmlist __initdata;
内核在mm/vmalloc中提供了辅助函数get_vm_area
和__get_vm_area
, 它们负责参数准备工作, 而实际的分配工作交给底层函数__get_vm_area_node
来完成, 这些函数定义mm/vmalloc.c
struct vm_struct *__get_vm_area(unsigned long size, unsigned long flags,
unsigned long start, unsigned long end)
{
return __get_vm_area_node(size, 1, flags, start, end, NUMA_NO_NODE,
GFP_KERNEL, __builtin_return_address(0));
}
EXPORT_SYMBOL_GPL(__get_vm_area);
struct vm_struct *__get_vm_area_caller(unsigned long size, unsigned long flags,
unsigned long start, unsigned long end,
const void *caller)
{
return __get_vm_area_node(size, 1, flags, start, end, NUMA_NO_NODE,
GFP_KERNEL, caller);
}
/**
* get_vm_area - reserve a contiguous kernel virtual area
* @size: size of the area
* @flags: %VM_IOREMAP for I/O mappings or VM_ALLOC
*
* Search an area of @size in the kernel virtual mapping area,
* and reserved it for out purposes. Returns the area descriptor
* on success or %NULL on failure.
*/
struct vm_struct *get_vm_area(unsigned long size, unsigned long flags)
{
return __get_vm_area_node(size, 1, flags, VMALLOC_START, VMALLOC_END,
NUMA_NO_NODE, GFP_KERNEL,
__builtin_return_address(0));
}
struct vm_struct *get_vm_area_caller(unsigned long size, unsigned long flags,
const void *caller)
{
return __get_vm_area_node(size, 1, flags, VMALLOC_START, VMALLOC_END,
NUMA_NO_NODE, GFP_KERNEL, caller);
}
函数定义mm/vmalloc.c 通过__vmalloc_node_range
来完成实际的工作。
// http://lxr.free-electrons.com/source/mm/vmalloc.c?v=4.7#L1719
/**
* __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, 0, node, caller);
}
__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, unsigned long vm_flags, int node,
const void *caller)
{
struct vm_struct *area;
void *ret;
kasan_vmalloc_flags_t kasan_flags = KASAN_VMALLOC_NONE;
unsigned long real_size = size;
unsigned long real_align = align;
unsigned int shift = PAGE_SHIFT;
if (WARN_ON_ONCE(!size))
return NULL;
if ((size >> PAGE_SHIFT) > totalram_pages()) {
warn_alloc(gfp_mask, NULL,
"vmalloc error: size %lu, exceeds total pages",
real_size);
return NULL;
}
if (vmap_allow_huge && (vm_flags & VM_ALLOW_HUGE_VMAP)) {
unsigned long size_per_node;
/*
* Try huge pages. Only try for PAGE_KERNEL allocations,
* others like modules don't yet expect huge pages in
* their allocations due to apply_to_page_range not
* supporting them.
*/
size_per_node = size;
if (node == NUMA_NO_NODE)
size_per_node /= num_online_nodes();
if (arch_vmap_pmd_supported(prot) && size_per_node >= PMD_SIZE)
shift = PMD_SHIFT;
else
shift = arch_vmap_pte_supported_shift(size_per_node);
align = max(real_align, 1UL << shift);
size = ALIGN(real_size, 1UL << shift);
}
again:
area = __get_vm_area_node(real_size, align, shift, VM_ALLOC |
VM_UNINITIALIZED | vm_flags, start, end, node,
gfp_mask, caller);
if (!area) {
bool nofail = gfp_mask & __GFP_NOFAIL;
warn_alloc(gfp_mask, NULL,
"vmalloc error: size %lu, vm_struct allocation failed%s",
real_size, (nofail) ? ". Retrying." : "");
if (nofail) {
schedule_timeout_uninterruptible(1);
goto again;
}
goto fail;
}
/*
* Prepare arguments for __vmalloc_area_node() and
* kasan_unpoison_vmalloc().
*/
if (pgprot_val(prot) == pgprot_val(PAGE_KERNEL)) {
if (kasan_hw_tags_enabled()) {
/*
* Modify protection bits to allow tagging.
* This must be done before mapping.
*/
prot = arch_vmap_pgprot_tagged(prot);
/*
* Skip page_alloc poisoning and zeroing for physical
* pages backing VM_ALLOC mapping. Memory is instead
* poisoned and zeroed by kasan_unpoison_vmalloc().
*/
gfp_mask |= __GFP_SKIP_KASAN | __GFP_SKIP_ZERO;
}
/* Take note that the mapping is PAGE_KERNEL. */
kasan_flags |= KASAN_VMALLOC_PROT_NORMAL;
}
/* Allocate physical pages and map them into vmalloc space. */
ret = __vmalloc_area_node(area, gfp_mask, prot, shift, node);
if (!ret)
goto fail;
/*
* Mark the pages as accessible, now that they are mapped.
* The condition for setting KASAN_VMALLOC_INIT should complement the
* one in post_alloc_hook() with regards to the __GFP_SKIP_ZERO check
* to make sure that memory is initialized under the same conditions.
* Tag-based KASAN modes only assign tags to normal non-executable
* allocations, see __kasan_unpoison_vmalloc().
*/
kasan_flags |= KASAN_VMALLOC_VM_ALLOC;
if (!want_init_on_free() && want_init_on_alloc(gfp_mask) &&
(gfp_mask & __GFP_SKIP_ZERO))
kasan_flags |= KASAN_VMALLOC_INIT;
/* KASAN_VMALLOC_PROT_NORMAL already set if required. */
area->addr = kasan_unpoison_vmalloc(area->addr, real_size, kasan_flags);
/*
* In this function, newly allocated vm_struct has VM_UNINITIALIZED
* flag. It means that vm_struct is not fully initialized.
* Now, it is fully initialized, so remove this flag here.
*/
clear_vm_uninitialized_flag(area);
size = PAGE_ALIGN(size);
if (!(vm_flags & VM_DEFER_KMEMLEAK))
kmemleak_vmalloc(area, size, gfp_mask);
return area->addr;
fail:
if (shift > PAGE_SHIFT) {
shift = PAGE_SHIFT;
align = real_align;
size = real_size;
goto again;
}
return NULL;
}
实现分为3部分
-
首先,
get_vm_area
在vmalloc
地址空间中找到一个适当的区域。 -
接下来从物理内存分配各个页。
-
最后将这些页连续地映射到
vmalloc
区域中, 分配虚拟内存的工作就完成了。
如果显式指定了分配页帧的结点, 则内核调用alloc_pages_node
, 否则,使用alloc_page
从当前结点分配页帧。
分配的页从相关结点的伙伴系统移除. 在调用时, vmalloc
将gfp_mask
设置为GFP_KERNEL
| __GFP_HIGHMEM
,内核通过该参数指示内存管理子系统尽可能从ZONE_HIGHMEM
内存域分配页帧. 理由已经在上文给出:低端内存域的页帧更为宝贵,因此不应该浪费到vmalloc的分配中,在此使用高端内存域的页帧完全可以满足要求。
内存取自伙伴系统,而gfp_mask设置为GFP_KERNEL
| __GFP_HIGHMEM
,因此内核指示内存管
理子系统尽可能从ZONE_HIGHMEM
分配页帧。
三、备选映射方法
除了vmalloc
之外,还有其他方法可以创建虚拟连续映射。这些都基于上文讨论的__vmalloc
函数或使用非常类似的机制:
-
vmalloc_32
的工作方式与vmalloc相同,但会确保所使用的物理内存总是可以用普通32位指针寻址。如果某种体系结构的寻址能力超出基于字长计算的范围, 那么这种保证就很重要。 -
vmap
使用一个page
数组作为起点,来创建虚拟连续内存区。与vmalloc相比,该函数所用的物理内存位置不是隐式分配的,而需要先行分配好,作为参数传递。此类映射可通过vm_map
实例中的VM_MAP
标志辨别。 -
不同于上述的所有映射方法,
ioremap
是一个特定于处理器的函数, 必须在所有体系结构上实现. 它可以将取自物理地址空间、由系统总线用于I/O操作的一个内存块,映射到内核的地址空间中。
该函数在设备驱动程序中使用很多, 可将用于与外设通信的地址区域暴露给内核的其他部分使用(当然也包括其本身)。
四、释放内存
有两个函数用于向内核释放内存, vfree
用于释放vmalloc
和vmalloc_32
分配的区域,而vunmap
用于释放由vmap
或ioremap
创建的映射。这两个函数都会归结到__vunmap。
void __vunmap(void *addr, int deallocate_pages)
addr
表示要释放的区域的起始地址, deallocate_pages
指定了是否将与该区域相关的物理内存页返回给伙伴系统. vfree
将后一个参数设置为1, 而vunmap
设置为0, 因为在这种情况下只删除映射, 而不将相关的物理内存页返回给伙伴系统。
不必明确给出需要释放的区域长度, 长度可以从vmlist
中的信息导出. 因此__vunmap
的第一个任务是在__remove_vm_area
(由remove_vm_area
在完成锁定之后调用)中扫描该链表, 以找到
相关项。
unmap_vm_area使用找到的vm_area实例,从页表删除不再需要的项。与分配内存时类似,该函
数需要操作各级页表,但这一次需要删除涉及的项。它还会更新CPU高速缓存。
如果__vunmap的参数deallocate_pages设置为1(在vfree中),内核会遍历area->pages的所
有元素,即指向所涉及的物理内存页的page实例的指针。然后对每一项调用__free_page,将页释放到伙伴系统。
+---------------------+
| Start (vunmap) |
+---------------------+
|
v
+-----------------------------+
| Check if addr is NULL |
+-----------------------------+
|
v
+-----------------------------+
| Compute start and end address|
+-----------------------------+
|
v
+-----------------------------+
| Get current process's mm |
+-----------------------------+
|
v
+-----------------------------+
| Find VMA that includes addr |
+-----------------------------+
|
v
+-----------------------------+
| If no VMA found, return |
+-----------------------------+
|
v
+-----------------------------+
| Call unmap_vmas to unmap |
+-----------------------------+
|
v
+---------------------+
| End |
+---------------------+