14. 内存分配

之前在编写字符设备的时候,我们使用过 kmallockfree 来分配和释放内存,除了这个方法外,内核还提供了其他分配内存的方法。

本节主要说一下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_KERNELGFP_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对象,flagskmallocflags 相同,用于需要分配更多内存时使用。
  • 释放使用的内存使用 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); // 释放页面
  • flagskmalloc 中的是一样的。
  • 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);

使用 vmallockmalloc 分配的内存地址其实都是虚拟地址,不过 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目录下。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,776评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,527评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,361评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,430评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,511评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,544评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,561评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,315评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,763评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,070评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,235评论 1 343
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,911评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,554评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,173评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,424评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,106评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,103评论 2 352

推荐阅读更多精彩内容

  • 本篇开始总结内存问题的分析,在分析之前先简单梳理下内存的基础知识。 一、虚拟内存 在早期的计算机中,程序是直接运行...
    Stan_Z阅读 3,250评论 0 22
  • 在内核里分配内存可不像在其他地方分配内存那么容易。造成这种局面的因素很多。从根本上讲,是因为内核本身不能像用户空间...
    CodeDog阅读 356评论 0 2
  • 本文介绍Linux内核内存分配函数kmalloc。 一、定义 注: 1)__builtin_constant_p编...
    小田BSP阅读 2,398评论 0 2
  • Linux 内存管理 1 页的概念 linux 内核中把物理页作为内存分配的最小单位,32位CPU 页的大小通常为...
    赤兔欢阅读 3,272评论 0 5
  • 1 内存组织 1.1 体系结构 (1)非一致内存访问(NUMA):指内存被划分为多个节点的多处理器系统,访问一个内...
    CHCD阅读 1,825评论 0 0