之前在编写字符设备的时候,我们使用过 kmalloc
和 kfree
来分配和释放内存,除了这个方法外,内核还提供了其他分配内存的方法。
本节主要说一下Linux中的内存分配问题。主要包括以下内容:
-
kmalloc
介绍; -
slab
介绍; -
vmalloc
介绍;
1. kmalloc详细介绍
使用 kmalloc
内存分配除非被阻塞,否则可以运行得很快,它分配的区域在物理内存中也是连续的,但它不会对所获取的内存空间清零,因此,需要对分配的内存进行显式清空,否则会有信息泄露的风险。
其函数原型如下:
#include <linux/slab.h>
void *kmalloc(size_t size, int flags);
第一个参数是需要分配内存的大小,关键是第二个参数 flag
,其表示分配内存的方式。
通常我们使用 GFP_KERNEL
这个标志(说明一下,GFP是get_free_page的缩写,方便识记),使用该标志可能会导致休眠。
除了 GFP_KERNEL
外,还有以下标志来控制内存分配方式:
GFP_ATOMIC:不会休眠,可用于中断上下文中;
GFP_KERNEL:通常使用的分配方法,可能引起休眠;
GFP_USER:为用户空间页分配内存,可能引起休眠;
GFP_HIGHUSER:类似GFP_USER,但会从高端内存(如果有)中进行分配;
GFP_NOIO、GFP_NOFS:类似GFP_KERNEL,但增加了一些限制,NOFS分配不运行执行任何文件系统调用,NOIO禁止任何IO的初始化。
基本上,使用 GFP_KERNEL
和 GFP_ATOMIC
即可以满足我们大多数的驱动开发要求。
上面的标志位可以与下面的标志进行或操作,进一步控制如何进行分配:
__GFP_DMA: 分配的内存位于DMA内存区段
__GFP_HIGHMEM: 分配的内存可位于高端内存
__GFP_COLD: 通常内存分配会返回“缓存热(cache warm)”页面,即在处理器缓存中找到的页面。该标志为请求使用尚未使用的“冷”页面,一般用于DMA页面分配中
__GFP_NOWARM: 分配内存时不产生警告信息
__GFP_HIGHT: 高优先级请求,紧急情况下允许消耗内核保留的最后一些页面
__GFP_REPEAT: 分配失败时重试一次
__GFP_NOFAIL: 不允许失败,未分配成功不会返回
__GFP_NORETRY : 分配失败后立刻返回
这里对Linux内核中的内存区段做一些说明。Linux内核把内存分为3个区段:
- DMA内存:范围是0~16M,该区域的物理页面专门供I/O设备的DMA使用
- 常规内存:范围是16~896M,该区域的物理页面是内核能够直接使用的
- 高端内存:围是896~结束,高端内存,内核不能直接使用
如果未指定上面的标志位,则DMA、常规内存都可能被搜索分配;如果指定 __GFP_DMA
,则只会有DMA内存被搜索分配;如果指定了 __GFP_HIGHMEM
,则三个区段都会被搜索分配。
使用kmalloc分配内存是有一个上限的,这个上限和架构、内核配置有关,不过,为了代码的可移植性,不要分配大于128KB的内存。
2. 后备高速缓存(slab分配器)
对于需要反复分配的大小相同的内存块,可以考虑将其保存在一个内存池中。Linux中实现了这种类型池,称为后备高速缓存,也被称为 slab 分配器。相关函数如下:
#include <linux/slab.h>
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)(voide *, kmem_cache_t *, unsgined long flags));
void *kmem_cache_alloc(kmem_cache_t *cache, int flags);
void kmem_cache_free(kmem_cache_t *cache, const void *obj);
int kmem_cache_destroy(kmem_cache_t *cache);
- slab分配器具有
kmem_cache_t
类型,通过kmem_cache_create()
函数进行创建,其可以容纳任意数目的内存区域,这些内存区域的大小等于参数size
。参数name
为该结构类型的名字,主要用于问题的追踪。参数offset
为页面中第一个对象的偏移量,用于确保某些特殊对齐方式,如果不需要可以设置为0表示默认。flags
控制如何完成分配,具体可以看mm/slab.c
中。最后两个构造和析构函数是可选的参数,必须同时存在或不存在,如果需要建议使用同一个函数实现,通过flags
(构造时会有SLAB_CTOR_CONSTRUCTOR
标志)区分。 - 上面创建了slab对象后,就可以调用
kmem_cache_alloc()
来分配内存空间。参数kmem_cache_t
是上面创建的slab对象,flags
和kmalloc
的flags
相同,用于需要分配更多内存时使用。 - 释放使用的内存使用
kmem_cache_free()
函数。 - 最后使用完成 slab 对象后需要销毁,调用
kmem_cache_destroy()
函数。
书中还介绍了内存池的概念和介绍,但其最后说应该尽量避免在驱动代码中使用mempool,因此这里也就不再过多介绍了。
3. get_free_page和相关函数
如果需要分配大块内存,使用面向页的分配技术会更好,相关函数如下:
get_zeroed_page(unsigned int flags); // 返回一页的空间,并将页面清0
__get_free_page(unsigned int flags); // 和上面类似,但不清页面
__get_free_pages(unsigned int flags, unsgined int order); // 返回若干连续物理页面,不清0
void free_page(unsigned long addr); // 释放页面
void free_page(unsgined long addr, unsigned long order); // 释放页面
-
flags
和kmalloc
中的是一样的。 -
order
为阶数,表示分配/释放的页数,其表示2的order
次方,例如order
为3则表示分配8页,order
为0表示分配1页
如果想知道每个内存区段下每个阶数可获得的数据块数目,可以查看 /proc/buddyinfo
节点。
4. vmalloc 及其相关函数
使用 vmalloc
分配的内存,其虚拟地址空间是连续的,但物理空间不一定连续。该函数的原型和相关函数如下:
#include <linux/vmalloc.h>
void *vmalloc(unsigned long size);
void vfree(void *addr);
void *ioremap(unsigned long offset, unsigned long size);
void iounmap(void *addr);
使用 vmalloc
和 kmalloc
分配的内存地址其实都是虚拟地址,不过 kmalloc
的地址和物理地址是一一对应的,可能是基于某个常量有一个固定的偏移,分配时不用修改页表。 vmalloc
分配的内存地址完全是虚拟的,每次分配都要修改页表来建立与物理地址的映射关系。
因此,vmalloc
分配的地址只有配合对一个的MMU才有意义,如果需要真正的物理地址时,不能使用 vmalloc
。
还有,vmalloc
适合用于分配大块的、只在软件中使用的、用于缓冲的内存区域,因此用于分配较小空间时是不划算的。
另外要注意的是,vmalloc
不能用于原子上下文中,因为其内部实现调用了 kmalloc(GFP_KERNEL)
来获取页表的存储空间,该调用可能产生休眠。
5. 使用示例
对上面说到的部分函数进行使用,也很简单,就是 init 的时候用不同的方法分配内存,在移除的时候释放内存。
需要说明的是,由于内核版本的不同,上面说的部分函数的参数和返回值有所不同。
以下代码是基于5.11.0-43-generic编写了,kmem_cache
部分的函数参数和返回值与上面所说的不同,不过基本过程和原理是一样的。
部分代码如下:
分配内存:
scull_mem_dev.kmalloc_data = (char *)kmalloc(DATA_SIZE, GFP_KERNEL); /** kmalloc 分配数据空间 */
if(scull_mem_dev.kmalloc_data == NULL){
Log("kmalloc alloc data failed!\n");
goto alloc_data_err;
}
Log("kmalloc mem addr: 0x%08lX", (unsigned long)scull_mem_dev.kmalloc_data);
scull_mem_dev.vmalloc_data = (char *)vmalloc(DATA_SIZE); /** vmalloc 分配数据空间 */
if(scull_mem_dev.vmalloc_data == NULL){
Log("vmalloc alloc data failed!\n");
goto alloc_data_err;
}
Log("vmalloc mem addr: 0x%08lX", (unsigned long)scull_mem_dev.vmalloc_data);
scull_mem_dev.kmem_cache = kmem_cache_create("test_scull_mem", DATA_SIZE, 0, 0, NULL);
if (scull_mem_dev.kmem_cache == NULL) {
Log("create kmem cache failed!");
goto alloc_data_err;
}
scull_mem_dev.kmem_cache_data = kmem_cache_alloc(scull_mem_dev.kmem_cache, GFP_KERNEL); /** kmem_cache 分配数据空间 */
if (scull_mem_dev.kmem_cache_data == NULL) {
Log("kmem cache alloc data failed!\n");
goto alloc_data_err;
}
Log("kmem cache mem addr: 0x%08lX", (unsigned long)scull_mem_dev.kmem_cache_data);
释放内存:
if (scull_mem_dev.kmem_cache_data != NULL) {
kmem_cache_free(scull_mem_dev.kmem_cache, scull_mem_dev.kmem_cache_data);
}
if (scull_mem_dev.kmem_cache != NULL) {
kmem_cache_destroy(scull_mem_dev.kmem_cache);
scull_mem_dev.kmem_cache_data = NULL;
}
if (scull_mem_dev.kmalloc_data != NULL) {
kfree(scull_mem_dev.kmalloc_data);
scull_mem_dev.kmalloc_data = NULL;
}
if (scull_mem_dev.vmalloc_data != NULL) {
vfree(scull_mem_dev.vmalloc_data);
scull_mem_dev.vmalloc_data = NULL;
}
完整代码位于https://gitee.com/Quehehe/LinuxDeviceDriver仓库的scull_mem目录下。