【摘要】本文叙述了在Linux内核中常见的几种内存分配函数及其异同,对理解linux底层内存分配机制有个较好理解。
kmalloc()函数类似与我们常见的malloc()函数,前者用于内核态的内存分配,后者用于用户态。
kmalloc()函数在物理内存中分配一块连续的存储空间,且和malloc()函数一样,不会清除里面的原始数据,如果内存充足,它的分配速度很快。其原型如下:
static inline void *kmalloc(size_t size, gfp_t flags); /*返回的是虚拟地址*/
kfree()
释放,这个函数的用法和用户空间的 free()类似。vmalloc()
一般用在为只存在于软件中(没有对应的硬件意义)的较大的顺序缓冲区分配内存,当内存没有足够大的连续物理空间可以分配时,可以用该函数来分配虚拟地址连续但物理地址不连续的内存。由于需要建立新的页表,所以它的开销要远远大于kmalloc及后面将要讲到的__get_free_pages()
函数。且vmalloc()
不能用在原子上下文中,因为它的内部实现使用了标志为 GFP_KERNEL 的kmalloc()
。其函数原型如下:
void *vmalloc(unsigned long size);
void vfree(const void *addr);
create_module()
系统调用,它利用 vmalloc()函数来获取被创建模块需要的内存空间。copy_from_user()
对内存进行使用。下面举一个使用vmalloc函数的示例:static int xxx(...)
{
...
cpuid_entries = vmalloc(sizeof(struct kvm_cpuid_entry) * cpuid->nent);
if(!cpuid_entries)
goto out;
if(copy_from_user(cpuid_entries, entries, cpuid->nent * sizeof(struct kvm_cpuid_entry)))
goto out_free;
for(i=0; i<cpuid->nent; i++){
vcpuid->arch.cpuid_entries[i].eax = cpuid_entries[i].eax;
...
vcpuid->arch.cpuid_entries[i].index = 0;
}
...
out_free:
vfree(cpuid_entries);
out:
return r;
}
在linux中,内存分配是以页为单位的,32位机中一页为4KB,64位机中,一页为8KB,但具体还有根据平台而定。
根据返回值类型的不同,页分配函数分为两类,一是返回物理页地址,二是返回虚拟地址。虚拟地址和物理地址起始相差一个固定的偏移量。
#define __pa(x) ((x) - PAGE_OFFSET)
static inline unsigned long virt_to_phys(volatile void *address)
{
return __pa((void *)address);
}
#define __va(x) ((x) + PAGE_OFFSET)
static inline void* phys_to_virt(unsigned long address)
{
return __va(address);
}
根据返回页面数目分类,分为仅返回单页面的函数和返回多页面的函数。
该函数定义在头文件/include/linux/gfp.h
中,它既可以在内核空间分配,也可以在用户空间分配,它返回分配的第一个页的描述符而非首地址,其原型为:
#define alloc_page(gfp_mask) alloc_pages(gfp_mask, 0)
#define alloc_pages(gfp_mask, order) alloc_pages_node(numa_node_id(), gfp_mask, order) //分配连续2^order个页面
static inline struct page *alloc_pages_node(int nid, gfp_t gfp_mask, unsigned int order)
{
if(unlikely(order >= MAX_ORDER))
return NULL;
if(nid < 0)
nid = numa_node_id();
return __alloc_pages(gfp_mask, order, noed_zonelist(nid, gfp_mask));
}
它是kmalloc函数实现的基础,返回一个或多个页面的虚拟地址。该系列函数/宏包括 get_zeroed_page()
、_ _get_free_page()
和_ _get_free_pages()
。在使用时,其申请标志的值及含义与 kmalloc()
完全一样,最常用的是 GFP_KERNEL 和 GFP_ATOMIC。
/*分配多个页并返回分配内存的首地址,分配的页数为2^order,分配的页不清零。
order 允许的最大值是 10(即 1024 页)或者 11(即 2048 页),依赖于具体
的硬件平台。*/
unsigned long __get_free_pages(gfp_t gfp_mask, unsigned int order)
{
struct page *page;
page = alloc_pages(gfp_mask, order);
if(!page)
return 0;
return (unsigned long)page_address(page);
}
#define __get_free_page(gfp_mask) __get_free_pages(gfp_mask, 0)
/*该函数返回一个指向新页的指针并且将该页清零*/
unsigned long get_zeroed_page(unsigned int flags);
_ _get_free_pages()
系列函数/宏申请的内存应使用free_page(addr)
或free_pages(addr, order)
函数释放:#define __free_page(page) __free_pages((page), 0)
#define free_page(addr) free_pages((addr), 0)
void free_pages(unsigned long addr, unsigned int order)
{
if(addr != 0){
VM_BUG_ON(!virt_addr_valid((void*)addr));
__free_pages(virt_to_page((void *)addr), order);
}
}
void __free_pages(struct page *page, unsigned int order)
{
if(put_page_testzero(page)){
if(order == 0)
free_hot_page(page);
else
__free_pages_ok(page, order);
}
}
free_pages()函数是调用__free_pages()函数完成内存释放的。
kmem_cache
的结构体。该函数创建一个slab缓存(后备高速缓冲区),它是一个可以驻留任意数目全部同样大小的后备缓存。其原型如下:
struct kmem_cache *kmem_cache_create(const char *name, size_t size, \
size_t align, unsigned long flags,\
void (*ctor)(void *, struct kmem_cache *, unsigned long),\
void (*dtor)(void *, struct kmem_cache *, unsigned ong)));
其中:
name:创建的缓存名;
size:可容纳的缓存块个数;
align:后备高速缓冲区中第一个内存块的偏移量(一般置为0);
flags:控制如何进行分配的位掩码,包括 SLAB_NO_REAP
(即使内存紧缺也不自动收缩这块缓存)、SLAB_HWCACHE_ALIGN
( 每 个 数 据 对 象 被 对 齐 到 一 个 缓 存 行 )、SLAB_CACHE_DMA
(要求数据对象在 DMA 内存区分配)等);
ctor:是可选的内存块对象构造函数(初始化函数);
dtor:是可选的内存对象块析构函数(释放函数)。
一旦创建完后备高速缓冲区后,就可以调用kmem_cache_alloc()
在缓存区分配一个内存块对象了,其原型如下:
void *kmem_cache_alloc(struct kmem_cache *cachep, gfp_t flags);
cachep指向开始分配的后备高速缓存,flags与传给kmalloc函数的参数相同,一般为GFP_KERNEL。
该函数释放一个内存块对象:
void *kmem_cache_free(struct kmem_cache *cachep, void *objp);
与kmem_cache_create
对应的是销毁函数,释放一个后备高速缓存:
int kmem_cache_destroy(struct kmem_cache *cachep);
它必须等待所有已经分配的内存块对象被释放后才能释放后备高速缓存区。
创建一个存放线程结构体(struct thread_info)的后备高速缓存,因为在linux中涉及频繁的线程创建与释放,如果使用__get_free_page()函数会造成内存的大量浪费,效率也不高。所以在linux内核的初始化阶段就创建了一个名为thread_info的后备高速缓存,代码如下:
/* 创建slab缓存 */
static struct kmem_cache *thread_info_cache;
thread_info_cache = kmem_cache_create("thread_info", sizeof(struct thread_info), \
SLAB_HWCACHE_ALIGN|SLAB_PANIC, NULL, NULL);
/* 分配slab缓存 */
struct thread_info *ti;
ti = kmem_cache_alloc(thread_info_cache, GFP_KERNEL);
/* 使用slab缓存 */
...
/* 释放slab缓存 */
kmem_cache_free(thread_info_cache, ti);
kmem_cache_destroy(thread_info_cache);
在 Linux 内核中还包含对内存池的支持,内存池技术也是一种非常经典的用于分配大量小对象的后备缓存技术。
mempool_t *mempool_create(int min_nr, mempool_alloc_t *alloc_fn, \
mempool_free_t *free_fn, void *pool_data);
mempool_create()函数用于创建一个内存池,min_nr 参数是需要预分配对象的数目,alloc_fn 和 free_fn 是指向内存池机制提供的标准对象分配和回收函数的指针,其原型分别为:
typedef void *(mempool_alloc_t)(int gfp_mask, void *pool_data);
typedef void (mempool_free_t)(void *element, void *pool_data);
pool_data 是分配和回收函数用到的指针,gfp_mask 是分配标记。只有当_ _GFP_WAIT
标记被指定时,分配函数才会休眠。
在内存池中分配和回收对象需由以下函数来完成:
void *mempool_alloc(mempool_t *pool, int gfp_mask);
void mempool_free(void *element, mempool_t *pool);
mempool_alloc()用来分配对象,如果内存池分配器无法提供内存,那么就可以用预分配的池。
void mempool_destroy(mempool_t *pool);
mempool_create()函数创建的内存池需由 mempool_destroy()来回收。