kmalloc 函数内幕
kmalloc
是一个分配内存的函数,功能类似于 malloc
。其函数原型如下:
/**
* kmalloc - allocate memory
* @size: how many bytes of memory are required.
* @flags: the type of memory to allocate.
*
* kmalloc is the normal method of allocating memory
* for objects smaller than page size in the kernel.
*
* The @flags argument may be one of:
*
* %GFP_USER - Allocate memory on behalf of user. May sleep.
*
* %GFP_KERNEL - Allocate normal kernel ram. May sleep.
*
* %GFP_ATOMIC - Allocation will not sleep. May use emergency pools.
* For example, use this inside interrupt handlers.
*
* %GFP_HIGHUSER - Allocate pages from high memory.
*
* %GFP_NOIO - Do not do any I/O at all while trying to get memory.
*
* %GFP_NOFS - Do not make any fs calls while trying to get memory.
*
* %GFP_NOWAIT - Allocation will not sleep.
*
* %__GFP_THISNODE - Allocate node-local memory only.
*
* %GFP_DMA - Allocation suitable for DMA.
* Should only be used for kmalloc() caches. Otherwise, use a
* slab created with SLAB_DMA.
*
* Also it is possible to set different flags by OR'ing
* in one or more of the following additional @flags:
*
* %__GFP_COLD - Request cache-cold pages instead of
* trying to return cache-warm pages.
*
* %__GFP_HIGH - This allocation has high priority and may use emergency pools.
*
* %__GFP_NOFAIL - Indicate that this allocation is in no way allowed to fail
* (think twice before using).
*
* %__GFP_NORETRY - If memory is not immediately available,
* then give up at once.
*
* %__GFP_NOWARN - If allocation fails, don't issue any warnings.
*
* %__GFP_REPEAT - If allocation fails initially, try once more before failing.
*
* There are other flags available as well, but these are not intended
* for general use, and so are not documented here. For a full list of
* potential flags, always refer to linux/gfp.h.
*/
void *kmalloc(size_t size, gfp_t flags)
kmalloc
的第一个参数是要分配内存块的大小 size
,第二个参数是分配标志 flags
。
关于 flags
,最常用的就是 GFP_KERNEL
,它表示是内核空间的进程来执行这个内存分配的(最终是通过调用 get_free_pages
,所以其标志最前面才是 GFP
)。使用 GFP_KERNEL
允许 kmalloc
在空闲内存较少时把当前进程转入休眠以等待内存页。所以使用 GFP_KERNEL
的分配内存的函数必须是可重入的。
在其他情况下,GFP_KERNEL
就不适用了。对于在中断处理函数,tasklet
以及内核定时器函数,当前进程就不能进入休眠状态。对于这种情况应该使用 GFP_ATOMIC
。
相关的符号如下:
GFP_ATOMIC
:用于在中断处理例程或其他运行于原子上下文中分配内存,不会休眠。GFP_KERNEL
:内核分配内存的一般方法,可能会引起休眠。GFP_USER
:用于为用户空间分配内存,可能会引起休眠。GFP_HIGHUSER
:类似于GFP_USER
,如果还有空闲的高端内存,就从这里分配。GFP_NOIO
,GFP_NOFS
:这两个类似于GFP_KERNEL
。GFP_NOFS
分配内存不允许执行任何文件系统调用;而GFP_NOIO
禁止任何IO的初始化。__GFP_DMA
:该标志请求分配可以进行DMA
的内存段中。__GFP_HIGHMEM
:这个标志表示要分配的内存位于高端内存。__GFP_COLD
:这个标志请求尚未使用的 “冷” 页面。__GFP_NOWARN
:这个标志很少使用。它可以避免内核在无法满足分配请求时产生警告。__GFP_HIGH
:该标志标记了一个高优先级的请求。__GFP_REPEAT
,__GFP_NOFAIL
,__GFP_NORETRY
:这几个标志表示在请求分配内存遇到困难时的行为。__GFP_REPEAT
表示继续尝试,但是可能会再次失败;__GFP_NOFAIL
告诉分配器始终不返回失败,它会努力尝试满足内存分配请求。不推荐使用这个;__GFP_NORETRY
告诉分配器,如果请求的内存不能立刻获得,就马上返回。
内存区段
Linux内核把内存分为三个区段:
- 可用于
DMA
的内存; - 常规内存;
- 高端内存;
size参数
内核负责管理系统的物理内存,物理内存只能按照页面来进行分配。
Linux处理内存分配的方法是,创建一系列的内存对象池,每个池中内存块的大小是固定的。处理内存分配请求时,就直接在包含有足够大的内存块的对象池中传递一整块给请求者。
一般 kmalloc
能处理的最小内存块是 32
或者 64
。
如果我们希望代码能够有良好的可移植性,则不应该分配大于 128KB
的内存。但是,如果希望获取多于几千字节的内存,最好使用除 kmalloc
以外的方法。
后备高速缓存
Linux内核的高速缓存管理一般称作 slab分配器
。其在 <linux/slab.h>
头文件中声明。slab
分配的高速缓存类型为 kmem_cache_t
,其相关API如下:
struct kmem_cache *kmem_cache_create(const char *, size_t, size_t,
unsigned long,
void (*)(void *));
一旦某个对象的高速缓存被创建,就可以调用 kmem_cache_alloc
从中分配内存对象:
/**
* kmem_cache_alloc - Allocate an object
* @cachep: The cache to allocate from.
* @flags: See kmalloc().
*
* Allocate an object from this cache. The flags are only relevant
* if the cache has no available objects.
*/
void *kmem_cache_alloc(struct kmem_cache *cachep, gfp_t flags)
这个参数 cachep
就是之前分配的高速缓存,flags
参数和 kmalloc
的参数一致。释放一个内存对象就使用 kmem_cache_free
:
void kmem_cache_free(struct kmem_cache *cachep, void *objp);
如果驱动程序代码已经不需要使用这个高速缓存了(一般情况下是在模块卸载的时候),这时驱动程序就应该释放这个高速缓存:
void kmem_cache_destroy(struct kmem_cache *s);
这个销毁动作只有等缓存中分配的所有对象都被回收之后才能执行成功。所以在执行的时候还需要检查其返回状态。如果返回失败,说明发生了 内存泄漏。
使用后备高速缓存的好处就是——内核可以通过 /proc/slabinfo
显示高速缓存的使用情况。
基于slab高速缓存的scull——scullc
查看 /proc/slabinfo
结果如下:
内存池
内核中某些地方的内存分配是不允许失败的。为了保证这种特殊情况下也能成功的分配内存,内核提供了一种内存池 mempool
。内存池其实就是一种特殊的高速后备缓存,它始终保留一些空闲的内存,以便在这种特殊的紧急情况下使用。
内存池对象为 mempool_t
,定义在 <linux/mempool.h>
头文件中,通过 mempool_create
来创建内存池对象:
/**
* mempool_create - create a memory pool
* @min_nr: the minimum number of elements guaranteed to be
* allocated for this pool.
* @alloc_fn: user-defined element-allocation function.
* @free_fn: user-defined element-freeing function.
* @pool_data: optional private data available to the user-defined functions.
*
* this function creates and allocates a guaranteed size, preallocated
* memory pool. The pool can be used from the mempool_alloc() and mempool_free()
* functions. This function might sleep. Both the alloc_fn() and the free_fn()
* functions might sleep - as long as the mempool_alloc() function is not called
* from IRQ contexts.
*/
mempool_t *mempool_create(int min_nr, mempool_alloc_t *alloc_fn,
mempool_free_t *free_fn, void *pool_data)
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);
一般情况下,我们会使用内核的 slab分配器
来完成这个分配和释放的动作,通过 mempool_alloc_slab
和 mempool_free_slab
这两个函数来实现。
在建立内存池之后,通过以下的函数来分配和释放内存:
void *mempool_alloc(mempool_t *pool, int gfp_mask);
void mempool_free(void *element, mempool_t *pool);
可以通过下列函数来调整 mempool
的 size
:
int mempool_resize(mempool_t *pool, int new_min_nr, int gfp_mask);
通过下列函数销毁内存池:
void mempool_destroy(mempool_t *pool);
需要注意的是,使用 mempool
很容易大量浪费内存。在驱动程序的代码中应该尽量避免使用 mempool
这种方式分配内存。
get_free_page 和相关函数
如果模块需要分配大块内存,使用面向页的分配技术会更有优势。
分配页面可使用下面的函数:
/* 返回指向新页面的指针,并将页面清空*/
get_zeroed_page(gfp_t gfp_mask);
/* 返回指向新页面的指针,不将页面清空*/
__get_free_page(gfp_t gfp_mask);
/* 分配若干物理连续的页,返回指向第一个页的指针,不清空分配的页面*/
__get_free_pages(gfp_t gfp_mask, unsigned int order);
gfp_mask
通常使用 GFP_KERNEL
和 GFP_ATOMIC
,也可能加上 __GFP_DMA
,申请用于 DMA
操作的内存;__GFP_HIGHMEM
,申请高端内存。
order
就是要分配的页面数量,不过这里分配的是 2
的幂次,比如:
- o r d e r = 0 order = 0 order=0,就是 2 0 = 1 2^0=1 20=1,分配一个页面;
- o r d e r = 1 order = 1 order=1,就是 2 1 = 2 2^1=2 21=2,分配两个页面;
- o r d e r = 2 order = 2 order=2,就是 2 2 = 4 2^2=4 22=4,分配四个页面;
- o r d e r = 3 order = 3 order=3,就是 2 3 = 8 2^3=8 23=8,分配八个页面;
- 。。。
如果 order
的值太大,没有这么多的空闲内存,那么就会返回失败。
当程序不再需要使用页面时,可以使用下列的函数来释放页面:
void free_page(unsigned long addr);
void free_pages(unsigned long addr, unsigned int order);
如果释放页面的数量和之前分配的数量不一致的话,内存映射关系就会被破坏,随后系统就可能会出错。
使用整页的scull——scullp
scullp
分配的内存是一个或者多个完整页:scullp_order
变量默认是
0
0
0,但是可以在编译或者加载的时候修改。
alloc_pages接口
alloc_pages
也是用于内存分配的。Linux页分配器的核心代码是一个 alloc_pages_node
的函数:
static inline struct page *alloc_pages_node(int nid, gfp_t gfp_mask,
unsigned int order)
这个函数有两个相关的函数:
alloc_pages(gfp_mask, order)
alloc_page(gfp_mask)
通过以下的函数可以释放由上面申请的页:
void __free_page(struct page *page);
void __free_pages(struct page *page, unsigned int order);
void free_hot_page(struct page *page);
void free_cold_page(struct page *page);
vmalloc及其辅助函数
vmalloc
返回一个虚拟地址连续的内存空间,尽管这块区域可能物理地址不连续,内核却认为这块区域是连续的。vmalloc
分配失败返回 NULL
;分配成功返回一个执行一段逻辑地址连续的,大小为 size
的内存区域。
需要注意的是,在大多数的情况下都不推荐使用 vmalloc
;通过 vmalloc
获取的内存使用效率也不高。
vmalloc
的函数原型以及相关函数如下:
void *vmalloc(unsigned long size);
void vfree(const void *addr);
void *ioremap(unsigned long offset, unsigned long size);
void iounmap(void *addr);
需要强调的是,kmalloc
和 __get_free_pages
返回的也都是虚拟地址,其值仍然需要通过MMU(内存管理单元),将虚拟地址转为实际的物理地址。
kmalloc
和 __get_free_pages
使用的虚拟地址和物理地址是一一对应的,可能会有基于常量 PAGE_OFFSET
的偏移。这两个函数一般不需要修改页表。但是,vmalloc
和 ioremap
使用的地址范围是完全虚拟的,每次分配都需要通过对页面进行一些适当的设置来建立虚拟内存区域。
使用 vmalloc
分配的地址是不能在微处理器之外使用的,因为它们只在处理器的内存管理单元上才有意义。像驱动程序需要真正的物理地址时,比如要获得 DMA
的地址时,就不能使用 vmalloc
了。
vmalloc
真正的适用场景是分配一大块连续的,只在软件中存在的,用户缓冲区的内存区域的时候。vmalloc
是比 __get_free_pages
开销要大的,因为它不但要获取内存,还有建立新页表。
和 vmalloc
一样,ioremap
也会建立页表。但是 ioremap
并不实际的分配内存,它的返回值是一个特殊的虚拟地址,可以用来访问指定的内存区域,这个地址最后要用 iounmap
释放。
vmalloc
的一个缺点是他不能在原子上下文中使用,因为它内部实现使用了 kmalloc(GFP_KERNEL)
来获取内存,可能会进入休眠。
使用虚拟地址的scull——scullv
scullv
模块使用了 vmalloc
。
该模块每次分配16页的内存。这里的内存分配使用了较大的数据块以获取比scullp更好的性能。用 __get_free_pages
函数分配一页以上的内存容易出错,即使分配成功速度也比较慢。用 vmalloc
分配几个页时会比其他方式快一些,不过由于建立页表的开销,所以分配一个页时速度比较慢。order
指定每次要分配的内存的阶数,在 scullv
模块中默认值是
4
4
4。