之前在编写字符设备的时候,我们使用过 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目录下。