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 内存分配标志与上下文关系图

内存分配标志与上下文选择流程图 分配内存请求 原子上下文? 是 否 GFP_ATOMIC 不可睡眠,使用紧急池 GFP_KERNEL 可睡眠,可回收页 DMA 需要? DMA 需要? GFP_ATOMIC | GFP_DMA 原子 + DMA 内存 GFP_KERNEL | GFP_DMA 可睡眠 + DMA 内存 保持 GFP_ATOMIC 保持 GFP_KERNEL 大块内存? 考虑 vmalloc

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 使用流程

  1. 启用内核配置:CONFIG_DEBUG_KMEMLEAK=y,重新编译内核。
  2. 挂载 debugfs:mount -t debugfs none /sys/kernel/debug
  3. 触发扫描:echo scan > /sys/kernel/debug/kmemleak
  4. 查看泄漏:cat /sys/kernel/debug/kmemleak
  5. 清除记录:echo clear > /sys/kernel/debug/kmemleak
最佳实践: 在驱动开发阶段始终开启 KASAN 和 kmemleak;提交代码前运行完整的 kmemleak 扫描,确保零泄漏。对于网络驱动,特别注意 skb 分配和 DMA 映射的配对释放。

4.6 内存分配与释放配对检查图

常见内存分配/释放配对检查 分配函数 kmalloc() vmalloc() alloc_pages() dma_alloc_coherent() 释放函数(必须配对) kfree() vfree() __free_pages() dma_free_coherent() 泄漏检查要点 • 每分配一次,必须释放一次 • 释放函数必须与分配函数 严格配对 • 错误路径也要释放 • 使用 kmemleak 定期扫描 • 驱动卸载时检查计数 • 网络驱动注意 skb 和 DMA 映射的释放

4.7 本章小结

  • kmalloc/kfree:物理连续、快速分配,适用于驱动核心数据结构和 DMA 缓冲区。
  • vmalloc/vfree:虚拟连续、物理不连续,适用于大块内存但不需要 DMA 的场景。
  • GFP_KERNEL:进程上下文、可睡眠,最常用。
  • GFP_ATOMIC:中断上下文、不可睡眠,必须使用。
  • 内存泄漏检测:kmemleak、slabinfo、KASAN、手动计数,开发阶段必须启用。
  • 配对原则:分配与释放函数必须严格配对,错误路径不能遗漏。