不连续页分配器
在设备长时间运行后,内存碎片话,连续的物理页比较稀缺;伙伴分配器和slab块分配器,分配的内存物理上是连续的;在这种情况下,如果需要分配长度超过一页的内存块,可以使用不连续页分配器,分配虚拟地址连续但物理地址不连续的内存块;
编程接口
不连续页分配器提供以下编程接口
vmalloc
分配不连续的物理页并且把物理页映射到连续的虚拟地址空间;
/**
* 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);
vfree
释放vmalloc分配的物理页和虚拟地址空间;
/**
* vfree - release memory allocated by vmalloc()
* @addr: memory base address
*
* Free the virtually continuous memory area starting at @addr, as
* obtained from vmalloc(), vmalloc_32() or __vmalloc(). If @addr is
* NULL, no operation is performed.
*
* Must not be called in NMI context (strictly speaking, only if we don't
* have CONFIG_ARCH_HAVE_NMI_SAFE_CMPXCHG, but making the calling
* conventions for vfree() arch-depenedent would be a really bad idea)
*
* NOTE: assumes that the object at @addr has a size >= sizeof(llist_node)
*/
void vfree(const void *addr);
vmap
把已经分配的不连续物理页映射到连续的虚拟地址空间;
/**
* vmap - map an array of pages into virtually contiguous space
* @pages: array of page pointers
* @count: number of pages to map
* @flags: vm_area->flags
* @prot: page protection for the mapping
*
* Maps @count pages from @pages into contiguous kernel virtual
* space.
*/
void *vmap(struct page **pages, unsigned int count,
unsigned long flags, pgprot_t prot);
vunmap
释放vamp分配的虚拟地址空间;
/**
* vunmap - release virtual mapping obtained by vmap()
* @addr: memory base address
*
* Free the virtually contiguous memory area starting at @addr,
* which was created from the page array passed to vmap().
*
* Must not be called in interrupt context.
*/
void vunmap(const void *addr);
有了不连续页分配器,在连续物理内存不足的情况下,就可以使用虚拟地址连续、物理地址不连续的内存;所以内核还提供了以下接口:
kvmalloc
先尝试使用kmalloc分配内存块,如果失败,则使用vmalloc分配不连续的物理页;
/*attempt to allocate physically contiguous memory, but upon
* failure, fall back to non-contiguous (vmalloc) allocation.
*/
void *kvmalloc(size_t size, gfp_t flags);
kvfree
检查内存块是否是通过vmalloc分配的;如果是则使用vfree释放,否则使用kfree;
void kvfree(const void *addr)
{
if (is_vmalloc_addr(addr))
vfree(addr);
else
kfree(addr);
}
数据结构
不连续页分配器结构如下图:
每个虚拟内存区域对应一个vmap_area;每个vmap_area关联一个vm_struct;
struct vmap_area
vmap_area和vm_area_struct有什么区别?
vmap_area是内核虚拟地址空间,vm_area_struct是用户空间虚拟地址;
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 llist_node purge_list; /* "lazy purge" list */
struct vm_struct *vm;
struct rcu_head rcu_head;
};
va_start
,起始虚拟地址;va_end
,结束虚拟地址;flags
,如果flags设置了标志位VM_VM_AREA,表示成员vm指向一个vm_struct实例,即vm_area关联一个vm_struct实例;rb_node
,红黑树节点,用来把vmap_area实例加入到根节点为vmap_area_root的红黑树中,虚拟地址作为key,提高查找效率;list
,链表节点,用来把vmap_area实例加入到头节点为vmap_area_list的链表中,该链表按虚拟地址从小到大排序;
sturct vm_struct
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;
};
addr
,起始虚拟地址;
size
,虚拟内存区域的长度;
flags
,如果设置了VM_ALLOC,表示虚拟内存区域是使用vmalloc分配的;
pages
,表示page指针数组;
nr_pages
,页数;
next
,指向下一个vm_struct实例;仅在不连续页分配器初始化以前使用;
phys_addr
,起始物理地址;仅在不连续页分配器初始化以前使用;
如果虚拟内存区域是使用vmap分配的,vm_struct实例的差别是:成员flags没有设置VM_ALLOC,成员pages是NULL,成员nr_pages是0;
技术原理
vmalloc虚拟地址空间范围
vmalloc虚拟地址空间的范围是[VMALLOC_START,VMALLOC_END),每种处理器架构都需要定义这两个宏,例如ARM64架构定义如下:
/*
* VMALLOC range.
*
* VMALLOC_START: beginning of the kernel vmalloc space
* VMALLOC_END: extends to the available space below vmmemmap, PCI I/O space
* and fixed mappings
*/
#define VMALLOC_START (MODULES_END)
#define VMALLOC_END (PAGE_OFFSET - PUD_SIZE - VMEMMAP_SIZE - SZ_64K)
MODULES_END
,内核模块区域的结束地址;
PAGE_OFFSET
,线性映射区域的起始地址;
PUD_SIZE
,一个页上层目录表项映射的地址空间长度;在页表中进行介绍;
VMEMMAP_SIZE
,vmemmap区域的长度;
vmalloc虚拟地址空间的起始地址等于内核模块区域的结束地址;
vmalloc虚拟地址空间的结束地址,等于(线性映射区域的起始地址-一个页上层目录表项映射的地址空间长度-vmemmap区域的长度-64KB);
vmalloc函数执行步骤
函数vmalloc的执行过程分为3步:
- 分配虚拟内存区域;
分配vmap_area实例和vm_struct实例;之后遍历vmap_area_list,在两个相邻vmap_area之间找到一个足够大的空洞,如果找到了,把起始虚拟地址和结束虚拟地址保存在新的vmap_area实例中,将新的vmap_area加入到vmap_area_list中;并把它加入到红黑树中;最后把新的vmap_area实例关联到vm_struct实例中;
- 分配物理页;
vm_struct实例的成员nr_pages存放页数n;分配page指针数组,数组的长度是n,vm_struct实例的成员pages指向page指针数组;之后连续执行n次如下操作:从页分配器(伙伴分配器)分配一个物理页,把物理页对应的page实例的地址存放到page指针数组中;
- 在内核的页表中把虚拟页映射到物理页;
内核的页表就是0号线程的页表;0号内核线程的进程描述符是全局变量init_task,成员active_mm指向全局变量init_mm,init_mm的成员pgd指向页全局目录swapper_pg_dir;
函数vmap和函数vmalloc的区别仅仅在于不需要分配物理页;
linux中常用内存分配函数
用户空间
malloc/calloc/realloc/free
glibc提供的标准接口;申请的内存,虚拟地址连续,物理地址不一定连续;从堆中申请内存,使用系统调用为brk/mmap;
mmap/munmap
场景:将文件利用虚拟内存技术映射到内存当中,直接操作内存,提供文件访问速度;
brk/sbrk
brk也用于内存映射,通常用于调整堆内存的大小;
内核空间
_get_free_page/_get_free_pages
伙伴系统,页分配器,分配的内存物理页是连续的;
kmalloc/kcalloc/kfree
slab,块分配器,基于伙伴系统管理小块内存,分配的内存物理页是连续的;分配小块内存,大小有限,不如vmalloc大;
vmalloc/vfree
不连续页分配器,分配的内存虚拟地址连续,物理地址不连续;从vmalloc区分配内存;能分配大块内存;