3. 内核内存分配与释放

任务目标

在Linux内核驱动模块中,实现kmalloc、kzalloc、vmalloc三种内存分配方式,演示各自的使用场景与释放方法,并处理内存分配失败的情况。通过内核自带的泄漏检测机制验证模块卸载后无内存泄漏。

环境与依赖

  • Linux内核源码树(需已配置好编译环境,支持模块编译)
  • 内核版本:建议4.x及以上(支持kmemleak)
  • 本案例代码基于内核模块实现,不依赖具体硬件平台

核心实现

以下代码实现了一个完整的内核模块,包含三种分配方式、失败处理逻辑以及通过/proc接口触发的分配与释放操作。模块加载时注册proc入口,卸载时自动释放所有未释放内存并触发泄漏检测。

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/slab.h>
#include <linux/vmalloc.h>
#include <linux/proc_fs.h>
#include <linux/uaccess.h>
#include <linux/string.h>

#define BUF_SIZE 4096
#define KMALLOC_SIZE 1024
#define VALLOC_SIZE (64 * 1024)

static char *kmem_buf;
static char *kzalloc_buf;
static char *vmalloc_buf;
static struct proc_dir_entry *proc_entry;
static char proc_buf[BUF_SIZE];

/* 分配所有内存 */
static int alloc_all(void)
{
    /* kmalloc: 物理连续,适合小内存 */
    kmem_buf = kmalloc(KMALLOC_SIZE, GFP_KERNEL);
    if (!kmem_buf) {
        pr_err("kmalloc failed\n");
        return -ENOMEM;
    }

    /* kzalloc: 分配并清零 */
    kzalloc_buf = kzalloc(KMALLOC_SIZE, GFP_KERNEL);
    if (!kzalloc_buf) {
        pr_err("kzalloc failed\n");
        kfree(kmem_buf);
        kmem_buf = NULL;
        return -ENOMEM;
    }

    /* vmalloc: 虚拟地址连续,适合大内存 */
    vmalloc_buf = vmalloc(VALLOC_SIZE);
    if (!vmalloc_buf) {
        pr_err("vmalloc failed\n");
        kfree(kmem_buf);
        kfree(kzalloc_buf);
        kmem_buf = NULL;
        kzalloc_buf = NULL;
        return -ENOMEM;
    }

    /* 写入测试数据 */
    memset(kmem_buf, 0xAA, KMALLOC_SIZE);
    memset(kzalloc_buf, 0, KMALLOC_SIZE);
    memset(vmalloc_buf, 0xBB, VALLOC_SIZE);
    return 0;
}

/* 释放所有内存 */
static void free_all(void)
{
    if (kmem_buf) {
        kfree(kmem_buf);
        kmem_buf = NULL;
    }
    if (kzalloc_buf) {
        kfree(kzalloc_buf);
        kzalloc_buf = NULL;
    }
    if (vmalloc_buf) {
        vfree(vmalloc_buf);
        vmalloc_buf = NULL;
    }
}

/* proc读操作:显示当前分配状态 */
static ssize_t proc_read(struct file *file, char __user *buf,
                         size_t count, loff_t *ppos)
{
    int len;

    len = snprintf(proc_buf, BUF_SIZE,
                   "kmem_buf: %s\nkzalloc_buf: %s\nvmalloc_buf: %s\n",
                   kmem_buf ? "allocated" : "NULL",
                   kzalloc_buf ? "allocated" : "NULL",
                   vmalloc_buf ? "allocated" : "NULL");
    return simple_read_from_buffer(buf, count, ppos, proc_buf, len);
}

/* proc写操作:接收命令 */
static ssize_t proc_write(struct file *file, const char __user *buf,
                          size_t count, loff_t *ppos)
{
    char cmd[16];

    if (count >= sizeof(cmd))
        return -EINVAL;

    if (copy_from_user(cmd, buf, count))
        return -EFAULT;
    cmd[count] = '\0';

    if (strncmp(cmd, "alloc", 5) == 0) {
        if (alloc_all() == 0)
            pr_info("All memory allocated\n");
        else
            pr_err("Allocation failed\n");
    } else if (strncmp(cmd, "free", 4) == 0) {
        free_all();
        pr_info("All memory freed\n");
    } else {
        pr_warn("Unknown command: %s\n", cmd);
    }
    return count;
}

static const struct proc_ops proc_fops = {
    .proc_read = proc_read,
    .proc_write = proc_write,
};

static int __init mem_demo_init(void)
{
    proc_entry = proc_create("mem_demo", 0666, NULL, &proc_fops);
    if (!proc_entry) {
        pr_err("Failed to create proc entry\n");
        return -ENOMEM;
    }
    pr_info("mem_demo module loaded\n");
    return 0;
}

static void __exit mem_demo_exit(void)
{
    /* 确保所有内存已释放 */
    free_all();
    proc_remove(proc_entry);
    pr_info("mem_demo module unloaded\n");
}

module_init(mem_demo_init);
module_exit(mem_demo_exit);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Memory allocation and release demo");

编译与加载

创建Makefile并编译模块:

obj-m := mem_demo.o
KDIR := /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)

all:
	$(MAKE) -C $(KDIR) M=$(PWD) modules

clean:
	$(MAKE) -C $(KDIR) M=$(PWD) clean

执行make编译,然后加载模块:

sudo insmod mem_demo.ko

验证方法

  1. 功能验证:加载模块后,通过proc接口触发分配与释放:
# 触发分配
echo "alloc" | sudo tee /proc/mem_demo
# 查看分配状态
cat /proc/mem_demo
# 预期输出:三个缓冲区均显示"allocated"

# 触发释放
echo "free" | sudo tee /proc/mem_demo
cat /proc/mem_demo
# 预期输出:三个缓冲区均显示"NULL"
  1. 内存泄漏检测:卸载模块前开启内核泄漏检测:
# 开启kmemleak
echo scan > /sys/kernel/debug/kmemleak
sudo rmmod mem_demo
# 等待扫描
sleep 5
echo scan > /sys/kernel/debug/kmemleak
cat /sys/kernel/debug/kmemleak

通过条件:kmemleak输出中无与mem_demo相关的未引用内存条目,且dmesg中无分配失败或释放异常信息。

关键说明

  • kmalloc分配物理连续内存,适合小规模、频繁访问的数据结构;kzalloc在kmalloc基础上自动清零;vmalloc分配虚拟地址连续内存,适合大块内存但需要页表映射,访问效率略低。
  • 分配失败处理采用逐级回滚策略:后续分配失败时释放前面已成功分配的内存,避免部分分配导致泄漏。
  • 模块卸载时调用free_all()确保所有内存释放,这是防止泄漏的最后防线。
  • proc接口仅用于教学演示,实际驱动中应通过文件操作或ioctl触发分配逻辑。

常见问题

  • 编译报错:确认内核头文件已安装,且Makefile中KDIR路径正确。
  • proc文件无法写入:确认模块加载成功且权限正确(0666)。
  • kmemleak无输出:需内核配置CONFIG_DEBUG_KMEMLEAK=y,且debugfs已挂载。