Linux网络驱动开发从入门到DMA数据收发 - 第4节
4.1 内核内存分配基础:kmalloc 与 kfree
在 Linux 内核模块与驱动开发中,不能直接使用用户空间的 malloc/free。内核提供了专用的内存分配与释放接口,其中 kmalloc 与 kfree 是最基础、最常用的配对函数。
4.1.1 kmalloc 函数原型与行为
#include <linux/slab.h>
void *kmalloc(size_t size, gfp_t flags);
void kfree(const void *objp);
- size:请求分配的字节数。最大受限于
KMALLOC_MAX_SIZE(通常为 4 MB,取决于体系结构)。 - flags:内存分配标志,决定分配行为(是否可睡眠、是否允许高优先级回收等)。
- 返回值:成功返回虚拟地址(物理连续),失败返回 NULL。
- 释放:
kfree释放由kmalloc分配的内存,传入 NULL 是安全的(无操作)。
关键区别: kmalloc 分配的内存物理地址连续,适合 DMA 设备;但分配速度比 vmalloc 快,且不会产生 TLB 压力。
4.1.2 kmalloc 与 kfree 使用示例
#include <linux/module.h>
#include <linux/slab.h>
#include <linux/printk.h>
static char *buf;
static int __init my_init(void)
{
buf = kmalloc(1024, GFP_KERNEL);
if (!buf) {
pr_err("kmalloc failed\n");
return -ENOMEM;
}
memset(buf, 0, 1024);
pr_info("kmalloc success: %px\n", buf);
return 0;
}
static void __exit my_exit(void)
{
kfree(buf);
pr_info("memory freed\n");
}
module_init(my_init);
module_exit(my_exit);
常见错误: 忘记检查 kmalloc 返回值直接使用;多次释放同一指针;释放非 kmalloc 分配的内存(如栈地址、vmalloc 地址)。
4.2 vmalloc:虚拟连续、物理不连续
vmalloc 分配在 虚拟地址空间连续,但物理页框可能分散。适用于需要大块内存但不需要 DMA 的场景(如内核模块的临时缓冲区、网络协议栈的某些数据结构)。
4.2.1 vmalloc 与 kmalloc 对比
| 特性 | kmalloc | vmalloc |
|---|---|---|
| 物理连续性 | 物理连续 | 物理不连续 |
| 分配速度 | 快(slab 缓存) | 慢(页表操作) |
| 最大大小 | ~4 MB(架构相关) | 大(受限于虚拟地址空间) |
| DMA 适用性 | 是 | 否(除非额外映射) |
| 内存开销 | 低 | 高(页表 + TLB) |
| 典型用途 | 驱动数据结构、DMA 缓冲区 | 大块临时缓冲区、模块加载 |
4.2.2 vmalloc 与 vfree 使用
#include <linux/vmalloc.h>
void *vmalloc(unsigned long size);
void vfree(const void *addr);
static char *big_buf;
big_buf = vmalloc(8 * 1024 * 1024); // 分配 8 MB
if (!big_buf) {
pr_err("vmalloc failed\n");
return -ENOMEM;
}
// 使用 big_buf ...
vfree(big_buf);
性能提示: vmalloc 分配时可能睡眠(需要建立页表),不能在原子上下文中使用。频繁 vmalloc/vfree 会导致严重的页表碎片,建议在驱动初始化时一次性分配,运行期间复用。
4.3 内存分配标志:GFP_KERNEL vs GFP_ATOMIC
分配标志 gfp_t 控制内核内存分配的行为,最重要的两个标志是 GFP_KERNEL 和 GFP_ATOMIC。
4.3.1 核心标志对比
| 标志 | 可睡眠 | 使用上下文 | 失败概率 | 典型场景 |
|---|---|---|---|---|
| GFP_KERNEL | 是 | 进程上下文(系统调用、ioctl、文件操作) | 低(可回收页、可等待) | 驱动初始化、数据路径(非中断) |
| GFP_ATOMIC | 否 | 中断上下文、软中断、自旋锁持有中 | 高(仅使用空闲页或紧急池) | 中断处理函数、tasklet、timer |
| GFP_DMA | 取决于组合 | 需要 DMA 内存时 | 中等 | DMA 缓冲区分配(常与 GFP_KERNEL 组合) |
| GFP_HIGHUSER | 是 | 用户空间映射 | 低 | mmap 后备内存 |
4.3.2 选择规则
- 进程上下文且允许睡眠 → 使用
GFP_KERNEL(最常用,效率最高)。 - 中断上下文、软中断、tasklet、持有自旋锁 → 必须使用
GFP_ATOMIC,否则会导致内核死锁或调度异常。 - 需要 DMA 缓冲区 → 组合使用
GFP_KERNEL | GFP_DMA(或GFP_ATOMIC | GFP_DMA在原子上下文中)。 - 分配大块内存(> 1 MB) → 考虑
vmalloc或alloc_pages高阶分配。
严重错误: 在中断处理函数中使用 GFP_KERNEL 会导致内核尝试睡眠,触发 "scheduling while atomic" 错误,系统可能崩溃或死锁。
4.4 内存分配标志与上下文关系图
4.5 内存泄漏检测
内核驱动中的内存泄漏比用户空间更危险:泄漏的内存无法被回收,长期运行会导致系统内存耗尽、性能下降甚至崩溃。Linux 内核提供了多种检测手段。
4.5.1 内核内存泄漏检测工具
| 工具/机制 | 原理 | 使用方法 | 适用场景 |
|---|---|---|---|
| kmemleak | 扫描内存,检测未引用的已分配对象 | 内核配置 CONFIG_DEBUG_KMEMLEAK=y,通过 /sys/kernel/debug/kmemleak 查看 |
长期运行检测、回归测试 |
| slabinfo | 查看 slab 缓存使用统计 | cat /proc/slabinfo 或 slabtop |
快速查看 kmalloc 缓存泄漏 |
| KASAN | 动态内存错误检测(越界、释放后使用) | 内核配置 CONFIG_KASAN=y |
开发阶段、单元测试 |
| 手动计数 | 驱动内维护分配/释放计数器 | 在模块代码中增加 atomic_t 变量 | 小型驱动、快速验证 |
4.5.2 手动计数检测示例
#include <linux/atomic.h>
static atomic_t alloc_count = ATOMIC_INIT(0);
void *my_kmalloc(size_t size, gfp_t flags)
{
void *ptr = kmalloc(size, flags);
if (ptr)
atomic_inc(&alloc_count);
return ptr;
}
void my_kfree(const void *ptr)
{
if (ptr) {
atomic_dec(&alloc_count);
kfree(ptr);
}
}
// 在模块退出时检查
static void __exit my_exit(void)
{
int remaining = atomic_read(&alloc_count);
if (remaining)
pr_warn("Memory leak detected: %d allocations not freed!\n", remaining);
else
pr_info("All memory freed cleanly.\n");
}
4.5.3 kmemleak 使用流程
- 启用内核配置:
CONFIG_DEBUG_KMEMLEAK=y,重新编译内核。 - 挂载 debugfs:
mount -t debugfs none /sys/kernel/debug - 触发扫描:
echo scan > /sys/kernel/debug/kmemleak - 查看泄漏:
cat /sys/kernel/debug/kmemleak - 清除记录:
echo clear > /sys/kernel/debug/kmemleak
最佳实践: 在驱动开发阶段始终开启 KASAN 和 kmemleak;提交代码前运行完整的 kmemleak 扫描,确保零泄漏。对于网络驱动,特别注意 skb 分配和 DMA 映射的配对释放。
4.6 内存分配与释放配对检查图
4.7 本章小结
- kmalloc/kfree:物理连续、快速分配,适用于驱动核心数据结构和 DMA 缓冲区。
- vmalloc/vfree:虚拟连续、物理不连续,适用于大块内存但不需要 DMA 的场景。
- GFP_KERNEL:进程上下文、可睡眠,最常用。
- GFP_ATOMIC:中断上下文、不可睡眠,必须使用。
- 内存泄漏检测:kmemleak、slabinfo、KASAN、手动计数,开发阶段必须启用。
- 配对原则:分配与释放函数必须严格配对,错误路径不能遗漏。