☰
Linux内核内存分配全解析:从kmalloc到vmalloc的选型与实战
2026/10/8 13:44:15 网站建设 项目流程

1. 内核内存分配到底在解决什么问题

很多做 Linux 开发的朋友,一开始接触内核内存分配,脑子里冒出来的第一个疑问往往是:用户态不是有malloc吗,为什么还要专门去研究内核里怎么分配内存?这个问题问得特别好,因为它恰恰点出了内核内存分配存在的根本理由。用户态的malloc背后其实也是靠内核给的页框撑起来的,只不过它多了一层用户态的内存池管理。而内核自己运行的时候,是没有这层“用户态管家”帮忙的,它必须自己直接跟物理内存打交道,还得在不能睡眠、不能缺页、不能随便阻塞的各种苛刻约束下把活干完。

我刚开始看内核代码那会儿,最不适应的就是同一个“分配内存”的动作,在内核里居然有几十个不同的函数名:kmalloc、vmalloc、kzalloc、alloc_pages、__get_free_pages、kmem_cache_alloc、get_zeroed_page……每个名字背后都对应着一套完全不同的语义、适用场景和限制条件。你要是选错了,轻则性能拉胯,重则直接在内核里触发 panic,连个调试信息都抓不到。所以这篇内容我想干的事,就是把“内核内存分配”这件事从底层原理到实操选型,掰开揉碎讲清楚,让你下次写驱动或者改内核代码的时候,能一眼看出该用哪个接口、为什么用它、以及用的时候要避开哪些坑。

这篇文章适合谁看?如果你正在学 Linux 内核、在写字符设备驱动、在做嵌入式 Linux 项目,或者面试时被问到“kmalloc 和 vmalloc 有什么区别”答不上来,那这篇内容就是给你准备的。我会尽量少堆术语,多用生活化的类比,把物理内存分配、页框管理、slab 缓存、vmalloc 虚拟连续这些概念讲明白,同时给出可以直接抄作业的代码示例和参数选择方法。整个过程我会按照“先讲清楚设计思路,再拆核心细节,然后走一遍实操,最后把常见坑列出来”的顺序来展开,你可以从头读,也可以直接跳到你现在最需要的那一节。

2. 内核内存分配的整体设计与思路拆解

2.1 为什么内核不能直接用 malloc 那套逻辑

要理解内核内存分配的设计,得先明白内核运行环境和用户态的根本差异。用户态程序申请内存,如果物理内存不够,内核可以把你的进程挂起,去把别的页换出到磁盘,等腾出空间再唤醒你。这个过程叫缺页异常处理,用户态程序对此毫无感知,malloc返回一个指针,你直接用就行。但内核自己不能这么干,因为内核代码运行在特权级,很多路径上根本不允许睡眠,比如中断处理程序、软中断上下文、持有自旋锁的临界区。你想想,如果中断处理程序里申请内存,结果内存不够,内核说“你等会儿,我去换出几页”,那整个系统就卡死了,因为中断上下文压根没有“等”这个选项。

所以内核内存分配的第一个设计约束就是:必须区分“可以睡眠”和“不可以睡眠”两种上下文。可以睡眠的路径,比如进程系统调用里,可以用GFP_KERNEL标志,允许内核在内存紧张时回收页缓存、换出匿名页,甚至直接触发直接回收。不可以睡眠的路径,比如中断里,只能用GFP_ATOMIC,这时候内核只能从紧急预留池里抠一点内存出来,抠不到就返回失败,绝不阻塞。这个区分是内核内存分配所有接口设计的基石,你后面看到的所有gfp_t标志,本质上都是在回答“当前上下文能不能等”这个问题。

第二个设计约束是物理连续和虚拟连续的取舍。有些硬件设备,比如 DMA 控制器,它不认虚拟地址,只认物理地址,而且往往要求一段物理上连续的内存。但物理内存经过长时间运行后,会变得非常碎片化,你想找一大块连续物理页框越来越难。于是内核提供了两条路:一条是直接向伙伴系统申请物理连续的页框,比如alloc_pages,但大块申请容易失败;另一条是先申请一堆离散的物理页,然后在内核虚拟地址空间里把它们映射成一段连续的虚拟地址,这就是vmalloc。前者物理连续、虚拟也连续,但受碎片影响大;后者虚拟连续、物理离散,几乎不会因为碎片失败,但映射开销大,而且不能用于 DMA。

第三个设计约束是小对象分配的效率。内核里大量存在几十字节到几KB的小结构体分配,比如struct file、struct task_struct、网络协议栈的sk_buff。如果每次分配这些小对象都走伙伴系统按页申请,那一个4KB页只放一个几十字节的结构体,浪费极其严重,而且伙伴系统的分配和释放路径相对较长。于是内核引入了 slab 分配器,后来演进成 slub,专门管理小对象缓存。它从伙伴系统批量拿页,然后切成固定大小的小块,用空闲链表串起来,分配和释放都是 O(1) 操作,还顺便支持了对象构造和析构。这个设计思路跟用户态的malloc内存池非常像,只不过内核的 slab 是全局共享的,而且针对每种对象类型做了专门缓存。

2.2 从物理页框到虚拟地址的完整链路

理解内核内存分配,脑子里要有一条清晰的链路:物理页框 → 页表映射 → 虚拟地址。伙伴系统管理的是物理页框,它把物理内存按 2 的幂次组织成不同阶的块,阶数从 0 到 10(对应 1 页到 1024 页,即 4KB 到 4MB)。当你调用alloc_pages(gfp, order)时,伙伴系统会去找一个 2^order 个连续页框的块,如果找不到就往上找更大的块,然后分裂。释放的时候反过来,如果相邻的伙伴块也空闲,就合并成更大的块。这个机制的核心目标是尽量维持大块连续物理内存的可用性,但实际运行中碎片化不可避免。

拿到物理页框后,内核需要把它们映射到虚拟地址空间才能访问。对于低端内存(x86 上的 ZONE_NORMAL),内核有一个直接映射区,物理地址加一个固定偏移就是虚拟地址,不需要额外页表,访问效率极高。对于高端内存(ZONE_HIGHMEM,32位系统才有),内核没法全部直接映射,只能临时映射或者永久映射。对于vmalloc分配的页,内核会在 vmalloc 区域找一段空闲虚拟地址,然后逐页建立页表映射,把离散的物理页拼成连续的虚拟地址。这个映射过程需要修改页表、刷新 TLB,所以vmalloc的开销比kmalloc大得多,而且分配大小必须是页的整数倍。

这里有个很容易混淆的点:kmalloc返回的虚拟地址,在物理上也是连续的。因为kmalloc底层是从 slab 分配器拿内存,而 slab 的页来自伙伴系统,伙伴系统保证物理连续。所以kmalloc的内存既物理连续又虚拟连续,可以直接用于 DMA。而vmalloc返回的虚拟地址连续,但物理页是离散的,不能直接用于 DMA,除非你逐页做 DMA 映射。这个区别是面试高频考点,也是实际驱动开发中选型的关键依据。

2.3 分配接口选型的决策树

面对这么多分配接口,怎么快速选?我总结了一个简单的决策树,你在写代码时按这个顺序问自己几个问题就行。

第一个问题:当前上下文能不能睡眠?如果在中段、软中断、tasklet、持有自旋锁的代码里,答案是“不能”,那你只能用GFP_ATOMIC或者GFP_NOWAIT,而且分配大小要尽量小,因为原子分配失败率不低。如果在进程上下文、可以睡眠,那就用GFP_KERNEL,这是最常用的标志。

第二个问题:需要多大内存?如果小于一个页(4KB),优先考虑kmalloc,它走 slab 缓存,快且物理连续。如果大于一个页,但你又需要物理连续(比如 DMA),那就用alloc_pages或者__get_free_pages,但要注意高阶分配容易失败,尽量用dma_alloc_coherent这类专门接口。如果大于一个页且不需要物理连续,那就用vmalloc,它几乎不会因为碎片失败,但开销大。

第三个问题:这个对象会频繁分配释放吗?如果会,而且大小固定,那就用kmem_cache_create创建一个专用 slab 缓存,然后用kmem_cache_alloc分配。这样比通用kmalloc更快,因为省去了查找合适大小缓存的开销,而且可以自定义构造函数。内核里很多核心结构体都是这么干的,比如task_struct有task_struct_cachep,mm_struct有mm_cachep。

第四个问题:需要零初始化吗?如果需要,直接用kzalloc或者kmem_cache_alloc加memset,但kzalloc更简洁。注意kmalloc返回的内存是不清零的,里面可能有之前释放的敏感数据,所以对外可见的结构体一定要清零,避免信息泄露。

3. 核心细节解析与实操要点

3.1 GFP 标志位到底怎么选

gfp_t是内核内存分配里最重要的参数,没有之一。它本质上是一个位掩码,告诉分配器三件事:能不能睡眠、从哪个内存区分配、要不要特殊行为。最常见的几个标志我列在下面,你写代码时可以直接对照。

标志能否睡眠适用上下文典型场景
GFP_KERNEL可以进程上下文系统调用、驱动 probe
GFP_ATOMIC不可以中断、软中断、自旋锁内中断处理程序
GFP_NOWAIT不可以同上,但不使用紧急池不希望触发回收
GFP_NOIO可以块设备层递归避免递归到 IO
GFP_NOFS可以文件系统层避免递归到 FS
GFP_DMA可以需要 DMA 低端内存老式 ISA 设备
GFP_HIGHUSER可以用户态页分配进程地址空间

选标志的核心原则是:能用 GFP_KERNEL 就别用 GFP_ATOMIC。因为GFP_ATOMIC只能从紧急预留池里拿内存,这个池子很小,而且不会触发回收,失败率明显更高。我见过不少驱动代码,明明在 probe 函数里(进程上下文,可以睡眠),却图省事写了GFP_ATOMIC,结果在高负载下偶尔分配失败,查半天查不出来。所以每次写分配代码前,先确认当前上下文,这是基本功。

还有一个容易忽略的点:GFP_KERNEL在内存紧张时会触发直接回收,也就是当前进程自己去扫描 LRU 链表、回写脏页、甚至触发 OOM。这个过程可能耗时很长,如果你在持有某些锁的情况下用GFP_KERNEL,要小心死锁。比如你持有文件系统锁,然后GFP_KERNEL触发回收,回收路径又需要同一把锁,那就死锁了。所以文件系统里常用GFP_NOFS,块设备里常用GFP_NOIO,就是为了切断这种递归。

3.2 kmalloc 的大小限制和实际行为

kmalloc看起来简单,但它的行为比很多人想的要复杂。首先,kmalloc能分配的最大大小是有限的,这个限制跟架构和配置有关。在常见的 x86_64 上,KMALLOC_MAX_SIZE通常是 4MB(order 10),但实际能成功分配的大小往往远小于这个值,因为高阶页框很容易因为碎片分配失败。我实测下来,在运行了一段时间的系统上,kmalloc分配超过 128KB 就开始不稳定了,超过 1MB 基本靠运气。

所以一个重要的经验法则是:kmalloc 只用于小对象,最好不超过几KB。如果你需要几十KB以上的内存,优先考虑vmalloc或者alloc_pages。内核里有个kmalloc_size_roundup函数可以帮你查实际分配的大小,因为kmalloc是按 2 的幂次缓存分配的,你申请 100 字节,实际可能给你 128 字节的缓存块。这个行为跟用户态malloc类似,但内核的缓存大小是固定的几档:8、16、32、64、96、128、192、256……一直到 8KB 左右,再往上就走页分配了。

kmalloc还有一个变体kzalloc,它在分配后把内存清零。很多人觉得清零只是多一步memset,性能影响不大,但实际上kzalloc可以利用 slab 分配器的SLAB_ZERO特性,在某些情况下比kmalloc加memset更快。而且从安全角度,凡是会拷贝到用户态或者暴露给外部的结构体,都应该用kzalloc,避免内核栈或之前释放的数据泄露。我个人的习惯是,只要不是性能极度敏感的路径,一律用kzalloc,省心。

3.3 vmalloc 的适用场景和性能代价

vmalloc是很多人又爱又恨的接口。爱它是因为它几乎不会因为物理碎片分配失败,你申请几MB甚至几十MB,只要虚拟地址空间够,基本都能成功。恨它是因为它的性能代价不小:每次分配都要找虚拟地址区间、建立页表映射、刷新 TLB,释放时还要拆除映射。而且vmalloc分配的内存不能直接用于 DMA,因为物理不连续。

那什么时候该用vmalloc?我总结了几种典型场景。第一种是模块加载时的代码和数据,模块的代码段通常用vmalloc分配,因为模块大小不固定,而且不需要物理连续。第二种是大块缓冲区,比如你做一个抓包工具,需要几MB的环形缓冲区,用vmalloc比kmalloc靠谱得多。第三种是只在初始化时分配、之后长期使用的大结构,因为vmalloc的分配开销是一次性的,后续访问虽然多一层页表,但现代 CPU 的 TLB 命中率很高,实际性能损失可以接受。

但要注意,vmalloc在原子上下文里不能用,因为它内部可能睡眠(找虚拟地址区间、分配页表页都可能睡眠)。而且vmalloc的虚拟地址空间是有限的,在 32 位系统上尤其紧张,64 位系统好很多但也有上限。我见过有人在循环里反复vmalloc和vfree,结果虚拟地址空间碎片化,最后分配失败。所以vmalloc适合长期持有的大块内存,不适合频繁分配释放的小对象。

3.4 slab 缓存的自定义与复用

当你需要频繁分配释放同一种结构体时,通用kmalloc就不是最优解了。因为kmalloc每次都要根据大小去查找对应的缓存,而且不同大小的对象混在同一个缓存里,对 CPU 缓存局部性也不友好。这时候就该用kmem_cache_create创建专用缓存。

创建专用缓存的流程大概是:先定义一个kmem_cache_t指针,在模块初始化时调用kmem_cache_create(name, size, align, flags, ctor),其中name是缓存名,会出现在/proc/slabinfo里方便调试;size是对象大小;align通常填 0 让内核自动对齐;flags常用SLAB_HWCACHE_ALIGN让对象按缓存行对齐,提升性能;ctor是构造函数,可以为 NULL。创建好之后,用kmem_cache_alloc(cache, gfp)分配,用kmem_cache_free(cache, ptr)释放。模块卸载时记得kmem_cache_destroy。

这里有个实操心得:专用缓存的对象大小最好接近 2 的幂次,因为 slab 分配器内部还是按页来切分的,如果对象大小是 100 字节,一个 4KB 页只能放 40 个,剩下 96 字节浪费;如果调整到 96 字节,就能放 42 个,利用率更高。当然这需要你权衡结构体字段,有时候为了对齐和性能,浪费一点也值得。另外,SLAB_HWCACHE_ALIGN会让对象按 L1 缓存行(通常 64 字节)对齐,对于频繁访问的热点对象,这个标志能明显减少伪共享,建议加上。

4. 实操过程与核心环节实现

4.1 从零写一个使用 kmalloc 的字符设备驱动

光讲理论不够,我们直接走一遍实操。假设你要写一个简单的字符设备驱动,在open时分配一个缓冲区,在read时把缓冲区内容拷贝给用户,在release时释放缓冲区。这个场景用kmalloc最合适,因为缓冲区不大(比如 4KB),而且open和release都在进程上下文,可以睡眠。

先看核心代码结构。定义设备结构体:

struct mydev { char *buf; size_t buf_size; struct cdev cdev; };

在open函数里分配:

static int mydev_open(struct inode *inode, struct file *filp) { struct mydev *dev; dev = container_of(inode->i_cdev, struct mydev, cdev); dev->buf_size = 4096; dev->buf = kzalloc(dev->buf_size, GFP_KERNEL); if (!dev->buf) return -ENOMEM; filp->private_data = dev; return 0; }

这里用kzalloc而不是kmalloc,因为缓冲区内容会拷贝给用户态,清零可以避免泄露内核数据。GFP_KERNEL是因为open在进程上下文,允许睡眠。如果分配失败返回-ENOMEM,用户态会收到错误码。

read函数里拷贝数据:

static ssize_t mydev_read(struct file *filp, char __user *user_buf, size_t count, loff_t *ppos) { struct mydev *dev = filp->private_data; size_t remaining = dev->buf_size - *ppos; if (*ppos >= dev->buf_size) return 0; if (count > remaining) count = remaining; if (copy_to_user(user_buf, dev->buf + *ppos, count)) return -EFAULT; *ppos += count; return count; }

注意copy_to_user可能睡眠,但read本身就在进程上下文,没问题。这里没有用GFP_ATOMIC的必要。

release函数里释放:

static int mydev_release(struct inode *inode, struct file *filp) { struct mydev *dev = filp->private_data; kfree(dev->buf); dev->buf = NULL; return 0; }

释放后把指针置 NULL,防止悬空指针。这个习惯很重要,内核里 use-after-free 是最常见的 bug 之一。

4.2 用 alloc_pages 做 DMA 缓冲区分配

如果你的设备需要 DMA,而且要求物理连续,那就不能用kmalloc了(虽然kmalloc物理连续,但大块分配容易失败),应该用dma_alloc_coherent。这个接口会帮你分配物理连续的内存,并返回虚拟地址和总线地址,同时处理好缓存一致性。

dma_addr_t dma_handle; void *cpu_addr; size_t size = 64 * 1024; cpu_addr = dma_alloc_coherent(dev, size, &dma_handle, GFP_KERNEL); if (!cpu_addr) return -ENOMEM;

dma_alloc_coherent的dev参数是设备结构体指针,size是分配大小,dma_handle返回总线地址,驱动把这个地址写给硬件寄存器。GFP_KERNEL表示可以睡眠,所以这个调用不能在中断里做。释放用dma_free_coherent(dev, size, cpu_addr, dma_handle)。

如果你不需要缓存一致性,或者想自己管理缓存刷新,可以用dma_alloc_attrs加DMA_ATTR_NON_CONSISTENT标志,性能更好但需要手动dma_sync_single_for_cpu和dma_sync_single_for_device。这个进阶用法在网卡、存储驱动里很常见,但新手建议先用dma_alloc_coherent,省心。

这里有个参数计算过程值得说一下。假设你的 DMA 缓冲区大小是 64KB,那么dma_alloc_coherent内部会按页对齐分配,实际可能分配 64KB 加上一些对齐填充。总线地址的位宽取决于你的设备,比如 32 位设备只能访问 4GB 以下的总线地址,所以你可能需要加GFP_DMA标志限制分配区域。在 64 位系统上,如果设备支持 64 位寻址,就不需要这个限制。

4.3 自定义 slab 缓存的完整实现

假设你要实现一个内核模块,频繁创建和销毁一种固定大小的消息结构体,大小是 256 字节。用kmalloc每次分配 256 字节也可以,但用专用缓存更快。完整实现如下。

定义结构体和缓存指针:

struct my_msg { struct list_head list; u32 id; char data[240]; }; static struct kmem_cache *msg_cache;

模块初始化时创建缓存:

static int __init mymod_init(void) { msg_cache = kmem_cache_create("my_msg", sizeof(struct my_msg), 0, SLAB_HWCACHE_ALIGN, NULL); if (!msg_cache) return -ENOMEM; return 0; }

kmem_cache_create的第二个参数是对象大小,第三个是对齐(0 表示默认),第四个是标志,SLAB_HWCACHE_ALIGN让对象按缓存行对齐,第五个是构造函数,这里不需要就填 NULL。

分配和释放:

struct my_msg *msg = kmem_cache_alloc(msg_cache, GFP_KERNEL); if (!msg) return -ENOMEM; /* 使用 msg */ kmem_cache_free(msg_cache, msg);

模块卸载时销毁缓存:

static void __exit mymod_exit(void) { kmem_cache_destroy(msg_cache); }

注意kmem_cache_destroy之前必须确保所有对象都已释放,否则内核会报警告。我踩过一次坑:模块卸载时还有对象没释放,结果kmem_cache_destroy卡住,系统日志里一堆 “slab cache leak” 警告。所以最好在模块里维护一个引用计数,确保没有泄漏再销毁。

4.4 用 vmalloc 分配大块内存的实操

假设你要做一个调试功能,需要分配 8MB 的环形缓冲区来记录内核事件。这个大小用kmalloc基本没戏,用vmalloc正合适。

void *buf; size_t size = 8 * 1024 * 1024; buf = vmalloc(size); if (!buf) return -ENOMEM; /* 使用 buf */ vfree(buf);

vmalloc返回的虚拟地址是连续的,你可以像访问普通内存一样访问它。但要注意,vmalloc分配的内存不能直接传给 DMA,也不能在原子上下文里分配。如果你需要在中断里往这个缓冲区写数据,那分配要在进程上下文提前做好,中断里只做写入操作,写入本身不涉及分配,所以没问题。

还有一个细节:vmalloc分配的内存默认是不清零的,如果你需要清零,用vzalloc。但vzalloc会逐页清零,8MB 的话开销不小,如果只是环形缓冲区,不清零也行,反正会被覆盖。

5. 常见问题与排查技巧实录

5.1 分配失败到底该怪谁

内核内存分配失败,原因通常有三类:上下文用错标志、大小超过限制、内存真的不够。排查的时候按这个顺序来。

先看上下文。如果你在中断里用了GFP_KERNEL,内核会打印 “BUG: sleeping function called from invalid context”,这个错误很明确,直接改成GFP_ATOMIC就行。但如果你在中断里用了GFP_ATOMIC还是失败,那可能是紧急池太小,或者分配大小太大。GFP_ATOMIC能分配的最大大小通常比GFP_KERNEL小,因为紧急池只保留了一小部分内存。

再看大小。如果你用kmalloc分配 1MB,失败很正常,因为高阶页框碎片化。这时候应该改用vmalloc或者alloc_pages加__GFP_COMP。我见过有人用kmalloc分配 4MB,然后在insmod时偶尔失败,查了半天以为是内存不够,其实是碎片问题。

最后看内存。用free -m看系统内存,用cat /proc/buddyinfo看伙伴系统各阶空闲页框数量。如果高阶(order 8 以上)全是 0,那大块连续物理内存确实没有了,只能重启或者用vmalloc。/proc/buddyinfo的输出格式是每个内存区一行,从左到右是 order 0 到 order 10 的空闲块数量,你可以直观看到碎片程度。

5.2 内存泄漏怎么定位

内核内存泄漏比用户态难查,因为内核没有valgrind那么方便的工具。但有几个手段可以用。第一是看/proc/slabinfo,如果你用了自定义 slab 缓存,可以看active_objs和num_objs的差值,如果active_objs持续增长不下降,那大概率有泄漏。第二是用kmemleak,这是内核自带的泄漏检测工具,需要在编译时开启CONFIG_DEBUG_KMEMLEAK,启动后echo scan > /sys/kernel/debug/kmemleak触发扫描,然后cat /sys/kernel/debug/kmemleak看报告。kmemleak会记录每次分配的调用栈,能直接定位到泄漏点。

但kmemleak有性能开销,生产环境一般不开。日常调试我推荐用slub_debug,在启动参数里加slub_debug=FZPU,其中 F 是 sanity check,Z 是 red zoning(在对象前后加保护区,越界写会触发),P 是 poisoning(释放后填充特定值,use-after-free 会读到毒值),U 是 user tracking(记录分配释放调用栈)。开启后如果发生越界或 use-after-free,内核会打印详细报告,包括调用栈和内存内容。

5.3 常见问题速查表

问题现象可能原因排查方法解决方案
中断里分配失败用了 GFP_KERNEL 或 GFP_ATOMIC 池耗尽dmesg 看 sleeping function 警告改用 GFP_ATOMIC,减小分配大小
kmalloc 大块失败物理碎片,高阶页框不足cat /proc/buddyinfo改用 vmalloc 或 dma_alloc_coherent
系统变慢,slab 增长内存泄漏/proc/slabinfo,kmemleak检查释放路径,加 kmemleak
use-after-free 崩溃释放后继续访问slub_debug=FZPU释放后置 NULL,检查并发
vmalloc 分配失败虚拟地址空间碎片cat /proc/vmallocinfo减少频繁 vmalloc/vfree,改用缓存
DMA 缓冲区数据错乱缓存不一致检查是否用 dma_alloc_coherent用一致性分配或手动 sync

5.4 几个我踩过的坑和独家心得

第一个坑:在持有自旋锁时用 GFP_KERNEL。自旋锁临界区不允许睡眠,而GFP_KERNEL可能睡眠,所以内核会报 “sleeping function called from invalid context”。正确做法是用GFP_ATOMIC,或者把分配移到锁外面提前做好。我当时的做法是在进入临界区前先分配好缓冲区,临界区里只做数据操作,这样既安全又高效。

第二个坑:kmalloc 返回的内存没清零就拷贝给用户态。这个坑很隐蔽,因为功能上没问题,但会泄露内核数据。攻击者可以通过读取未初始化内存获取内核指针、密钥等敏感信息。所以凡是copy_to_user的源缓冲区,一定要用kzalloc或者手动memset。我现在养成的习惯是,只要这个缓冲区会离开内核,一律kzalloc。

第三个坑:vmalloc 在原子上下文调用导致崩溃。vmalloc内部会分配页表页,可能睡眠,所以在中断里调用会触发 “BUG: scheduling while atomic”。如果你需要在中断里往大缓冲区写数据,正确做法是在模块初始化时vmalloc好,中断里只写不分配。这个思路其实就是“预分配”模式,内核里很多高性能路径都这么干。

第四个心得:用kmalloc_size_roundup查实际分配大小。有时候你申请 100 字节,实际拿到 128 字节的缓存块,如果你想知道真实开销,可以用这个函数。反过来,如果你要分配的对象大小是 130 字节,那实际会走 192 字节的缓存,浪费 62 字节。这时候可以考虑把结构体调整到 128 字节以内,或者干脆用自定义 slab 缓存精确控制。

第五个心得:释放后立即置 NULL。内核里 use-after-free 是最难查的 bug 之一,因为崩溃点往往离释放点很远。养成释放后置 NULL 的习惯,虽然不能完全避免并发场景下的问题,但至少能减少单线程路径上的误用。配合slub_debug=P的 poisoning 功能,释放后内存会被填充成 0x6b,如果你访问到 0x6b6b6b6b 这样的值,基本就能确定是 use-after-free。

6. 内核内存分配的进阶方向

如果你已经把上面这些基础接口用熟了,接下来可以往几个方向深入。第一个方向是per-CPU 分配器,内核为每个 CPU 维护了独立的页框缓存和 slab 缓存,分配时优先从当前 CPU 的缓存拿,避免锁竞争。你可以用alloc_pages加GFP_THISNODE或者直接用this_cpu_ptr访问 per-CPU 变量。第二个方向是内存压缩和迁移,内核有compaction机制,可以把分散的空闲页框迁移合并成大块,缓解碎片问题,你可以通过/proc/sys/vm/compact_memory手动触发。第三个方向是CMA(连续内存分配器),专门为需要大块连续物理内存的设备预留一段区域,启动时预留,运行时按需分配,适合摄像头、GPU 这类需要大块 DMA 缓冲的设备。

这些进阶内容每一个都够写一篇长文,但核心思路是一致的:理解物理内存的组织方式,理解虚拟地址的映射机制,理解不同上下文的约束条件。你把这三件事想明白了,再看任何新的分配接口,都能快速判断它适合什么场景、有什么代价。我在实际工作中遇到新接口时,第一反应就是查它的gfp参数怎么传、能不能睡眠、物理连不连续,这三个问题一回答,选型基本就定了。

最后分享一个我常用的调试命令组合,当你怀疑内存分配有问题时,按顺序执行:

cat /proc/buddyinfo # 看物理页框碎片 cat /proc/slabinfo # 看 slab 缓存使用 cat /proc/vmallocinfo # 看 vmalloc 区域 dmesg | tail -50 # 看内核日志

这四个命令基本能覆盖大部分内存分配问题的初步排查。如果还定位不到,再上kmemleak和slub_debug。这套流程我用了好几年,从嵌入式设备到服务器都管用。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询