Kmalloc函数的内幕
kmalloc
内存分配引擎是一个功能强大的工具,由于和malloc
相似,所以学习它也很容易。除非被阻塞,否则这个函数可能运行的很快,而且不对所获取的内存空间清零,也就是说,分配给他的内存空间任然保持着原有的数据。它分配的内存空间在物理内存中也是连续的。
flags参数
kmalloc
的原型是:
#include <linux/slab.h>
void *kmalloc(size_t size,int flags);
kmalloc
的第一个参数是要分配的块的大小,第二个参数是分配标志,更有意思的是,它能以多种方式控制kmalloc
的行为。
最常用的标志就是GFP_KERNEL,
他表示内存分配(最终总是调用get_free_pages
来实现实际的分配,这就是GFP_
前缀的由来,就是那个函数每个单词的首字母组合)是代表运行在内核空间的进程执行的,换句话说,这意味着调用他的函数,正代表某个进程执行系统调用。使用GFP_KERNEL
允许kmalloc
在空闲内存较少时把当前进程转入休眠以等待一个页面。因此使用GFP_KERNEL
分配内存的函数必须是可重入的
。在当前进程休眠时,内核会采取适当的行动,或者是把缓冲区的内容刷写到硬盘,或者是从一个用户进程换出内存,以获取一个内存页面。
注: 可重入函数主要用于多任务环境中,一个可重入的函数简单来说就是可以被中断的函数,也就是说,可以在这个函数执行的任何时刻中断它,转入OS调度下去执行另外一段代码,而返回控制时不会出现什么错误;而不可重入的函数由于使用了一些系统资源,比如全局变量区,中断向量表等,所以它如果被中断的话,可能会出现问题,这类函数是不能运行在多任务环境下的。
GFP_KERNEL
分配标志并不是始终适用的,有时kmalloc
是在进程上下文之外被调用的,例如在中断处理函数、tasklet
以及内核定时器中调用。这种情况下current进程就不应该休眠,驱动程序则应该换用GFP_ATOMIC
标志。内核通常会为原子性的分配预留一些空闲页面。使用GFP_ATOMIC
标志时,kmalloc
甚至可以用掉最后一个空闲页面。不过如果连最后一页都没有了,分配就返回失败了。意思就是说,在上述情况下是不能进入休眠的,所以在上述情况下使用GFP_ATOMIC
标志的kmalloc
分配内存,即使只剩最后一个内存页面也会分配,如果没有空闲内存肯定就分配失败,返回错误了。
除了GFP_KERNEL
和GFP_ATOMIC
外,还有一些其他的标志可用于替换或者补充这两个标志。不过这两个标志已经可以满足大多数驱动程序的需要了。所有的标志都定义在<linux/gfp.h>
中。有个别的标志使用两个下划线作为前缀,比如:__GFP_DMA
。另外,还有一些符号表示这些标志的常用组合,他们没有这种前缀,并且有时称为“分配优先级”。
kmalloc标志
参数 | 描述 |
---|---|
GFP_ATOMIC | 用于在中断函数或者其他运行在进程上下文之外的代码中分配内存,不会休眠。 |
GFP_KERNEL | 内核内存通常的分配方式,可能引起休眠 |
GFP_USER | 用于为用户空间页分配内存,可能会休眠 |
GFP_HIGHUSER | 类似于GFP_USER ,不过如果有高端内存的话就从那里分配。 |
GFP_NOIO,GFP_NOFS | 这两个标志功能类似于GFP_KERNEL ,但是为内核分配内存的工作方式添加了一些限制。具有GFP_NOFS 标志的分配不允许执行任何文件系统调用,而GFP_NOIO 禁止任何I/O的初始化。这两个标志主要在文件系统和虚拟内存代码中使用,这些代码中的内存分配可休眠,但不应该发生递归的文件系统调用。 |
上面列出的分配标志可以和下面的标志"或"起来使用,下面这些标志控制如何进行分配:
参数 | 描述 |
---|---|
__GFP_DMA | 该标志请求分配发生在DMA 的内存区段中,具体的含义是平台相关的,我们在后面讲解。 |
__GFP_HIGHMEM | 表明要分配的内存可位于高端内存。 |
__GFP_CLOD | 通常,内存分配器会试图返回"缓存热(cache warm )"页面,即可在处理器缓存中找到页面。相反,这个标志请求尚未使用冷页面。对用于DMA 读取的页面分配可使用这个标志,因为这种情况下,页面存在于处理器缓存中没有多大帮助。 |
__GFP_NOWARN | 该标志很少使用。他可以避免内核在无法满足分配请求时产生警告(使用printk )。 |
__GFP_HIGH | 该标志标记了一个高优先级的请求,他允许为紧急状况而消耗有内核保留的最后一些页面。 |
__GFP_REPET,__GFP_NOFALL,__GFP_NORETRY | 上述标志告诉分配器在满足分配请求而遇到困难时应该采取何种行为。 |
注:
高端内存:高端内存
内存区段
__GFP_DMA
和__GFP_HIGHMEM
的使用与平台相关,尽管在所有平台上都可以使用这两个标志。
Linux内核将内存分为三个区段:可用于DMA
的内存,常规内存以及高端内存。通常的内存分配都市发生在常规内存区,但是通过上面介绍的标志,可以请求在其他区段中分配内存。其思路是每种计算平台都必须知道如何将自己特定的内存范围归类到这三个区段中,而不是认为所有的RAM都一样。
当一个新页面为了满足Kmalloc
的要求被分配时,内核会创建一个内存区段的列表以供搜索。如果指定了__GFP_DMA
标志,则只有DMA
区段会被搜索,如果该区段没有可用内存,分配就会失败。如果没有指定特定的的标志,则常规区段和DMA
区段都会被搜索;而如果设置了__GFP_HIGHMEM
标志在,则所有三个区段都会被搜索以获取一个空闲页。
size参数
内核负责管理系统物理内存,物理内存只能基于页面进行分配;linux处理内存分配的方法是,创建一系列的内存对象池,每个池中的内存块大小是固定一致的。处理分配请求时,就直接在包含有足够大的内存池的池中传递一个整块给请求者。
驱动开发人员应该记住一点,就是内核只能分配一些预定义的,固定大小的字节数组。如果申请任意数量的内存,那么得到的很可能会多一些。
后备高速缓存
设备驱动程序常常会反复的分配很多同一大小的内存块。既然内核已经维护了一组拥有同一大小的内存块的内存池,那么为什么不为这些反复使用的块增加某些特殊的内存池呢?实际上内核的确实现了这种形式的内存池,通常称为后备高速缓存(lookaside cache)。
linux
内核的高速缓存管理有时称为"slab分配器"。因此相关函数和类型在<linux/slab.h>
中声明。slab分配器实现的高速缓存具有kmem_cache_t
类型,可通过kmem_cache_create
创建:
kmem_cache_t *kmem_cache_create(const char*name,size_t size,size_t offset,unsigned long flags,void (*constructor)(void*,kmem_cache_t*,unsigned long flags),void(*destructor)(void*,kmem_cache_t*,unsigned long flags));
该函数创建一个新的高速缓存对象,其中可以容纳任意数目的内存区域,这些区域的大小都相同。
参数 | 描述 |
---|---|
name | 参数name与这个高速缓存相关联,通常设置为将要高速缓存的结构体类型的名字。 |
size | 高速缓存对象中容纳任意数目的内存区域,这些区域的大小都相同,由size参数指定。 |
offset | 页面中第一个对象的偏移量,可以用来确保已分配的对象进行某种特殊的对齐,但是最常用的就是0,表示使用默认值。 |
flag | 控制如何完成分配 |
SLAB_NO_REAP 设置这个标志可以保护高速缓存在系统寻找内存的时候不会减少。设置该标志通常不是好主意,因为我们不应该对内存分配自由的做一些人为的,不必要的限制。SLAB_HWCACHE_ALLGN 这个标志要求所有数据对象跟高速缓存行对齐;SLAB_CACHE_DMA 这个标志要求每个数据对象都从可用于DMA 的内存区段中分配。 | |
constructor | 用于初始化新分配的对象 |
destructor | 清除对象 |
一旦某个对象的高速缓存被创建,就可以调用kmem_cache_alloc
从中分配内存对象:
void *kmem_cache_alloc(kmem_cache_t *cache,int flags);
参数 | 描述 |
---|---|
cache | 之前创建的高速缓存 |
flag | 和kmalloc 的相同 |
释放一个内存对象时使用kmem_cache_free
:
void kmem_cache_free(kmem_cache_t *cache,const void *obj);
如果驱动程序代码中和高速缓存有关的部分已经处理完了(一个典型的情况就是模块要被卸载了的时候,这时驱动应该释放他的高速缓存)。
intkmem_cache_destroy(kmem_cache_t *cache);
这个释放操作只有在已经将从缓存中分配的所有对象都归还后才能成功。所以,模块应该检查kmem_cache_destroy
的返回状态;如果失败,说明模块发生了内存泄漏(因为有一些对象被漏掉了);
内存池
内核中有些地方的内存分配时不允许失败的,为了确保这种情况下的成功分配,内核开发者建立了一种称为内存池(mempool
)的抽象。内存池其实就是某种形式的后备高速缓存,他试图始终保存空闲的内存,以便能在紧急状态下使用。
内存池对象的类型为mempool_t
(在<linux/mempool.h>
中定义),可使用mempool_create
来建立内存池对象。
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);
如果有必要,我们可以为mempool
编写特定用途的函数来处理内存分配。但是通常我们仅会让内核的slab分配器
为我们来处理这个任务。内核中有两个函数(mempool_alloc_slab
和mempool_free_slab
),他们的原型和上述内存池分配原型匹配,并利用kmem_cache_alloc
和kmem_cache_free
处理内存分配和释放。
构造内存池:
cache = kmem_cache_create(...);
pool = mempool_create(MY_POOL_MINIMUM,mempool_alloc_slab,mempool_free_slab,cache);
在建立内存池以后,可用如下所示分配和释放对象:
//分配
void *mempool_alloc(mempool_t *pool,int gfp_mask);
//释放
void *mempool_free(void);*element,mempool_t* pool);
在创建mempool
时,就会多次调用分配函数为预先分配的对象创建内存池。之后,对mempool_alloc
的调用将首先通过分配函数获得该对象。如果分配失败,就会返回预先分配的对象(如果存在的话)。在使用mempool_free释放一个对象时,如果预先分配的对象数目小于要求的最低数目,就会将该对象保留在内存池中;否则,该对象会返回给系统。
调整mempool
的大小:
如果调用成功,将把内存池调整为至少有new_min_nr个预分配对象。
int mempool_resize(mempool_t *pool,int new_min_nr,int gfp_msk);
如果不在需要内存池,可使用下面的函数返回给系统:
void mempool_destroy(mempool_t *pool);
在销毁mempool之前,必须将所有已分配的对象返回到内存池中,否则会导致oops。