内核模块KO全解:加载卸载+生命周期
1. 什么是Linux内核模块(.ko)
内核模块(Kernel Module) 后缀为 .ko,是Linux内核的「动态插件」。
Linux内核本身是一个庞大的核心镜像vmlinux,如果把所有驱动、文件系统、网络协议都编译进去,内核会变得无比臃肿,启动极慢。而内核模块机制解决了这个问题:
- 不需要的功能不编译进内核
- 需要用时动态加载 ,不用时动态卸载
- 全程无需重启系统、无需重新编译整个内核
两种内核代码形式对比
| 代码形式 | 特点 | 适用场景 |
|---|---|---|
| 静态编译 | 直接编译进内核镜像,开机常驻,无法卸载 | 核心功能(进程调度、内存管理) |
| 动态模块(.ko) | 按需加载卸载,独立编译 | 绝大多数硬件驱动、扩展功能 |
2. 内核模块完整生命周期(重中之重)
module_init()和module_exit()是内核模块生命周期的两个核心入口。
一个内核模块从加载到卸载,会完整经历以下4个阶段:
(1)加载阶段
执行sudo insmod xxx.ko时触发:
- 系统读取
.ko文件内容到内核态内存 - 内核校验模块信息:内核版本、许可证、依赖关系、函数符号
- 分配内核地址空间,重定位内核API地址(如
printk、device_create) - 执行
module_init()注册的初始化函数
(2)初始化执行阶段
对应驱动里的drv_init()函数:
- 申请设备号、创建设备类、生成
/dev/xxx设备文件 - 注册文件操作结构体、中断处理函数、硬件资源
- 初始化完成后,模块进入常驻运行状态
🔔 重要细节:标记
__init的函数,初始化完成后内存会被自动释放,节省内核空间。
(3)常驻运行阶段
模块加载完成后:
- 驻留在内核态全局地址空间
- 接收用户态通过系统调用下发的
open/read/write/ioctl请求 - 响应硬件中断、处理定时任务、管理设备状态
- 无主动退出逻辑,一直运行直到被卸载
(4)卸载销毁阶段
执行sudo rmmod xxx时触发:
- 检查模块引用计数,若被占用则卸载失败
- 执行
module_exit()注册的卸载函数 - 反向释放所有申请的资源:销毁设备、注销设备号、释放内存
- 从内核模块链表中移除该模块
- 回收模块占用的所有内核内存
⚠️ 致命禁忌:卸载时不释放资源会导致内核内存泄漏、设备占用,长期运行会造成系统卡顿甚至崩溃。
3. 模块加载与卸载底层原理
insmod加载底层做了什么
- 校验模块与当前内核的版本兼容性
- 将模块的代码段和数据段拷贝到内核态内存
- 解析模块的未定义符号,链接到内核全局符号表
- 执行模块的初始化函数
- 将模块添加到内核的全局模块链表中(
lsmod可查)
rmmod卸载底层做了什么
- 检查模块的引用计数,不为0则返回错误
- 执行模块的退出函数
- 注销模块在内核中注册的所有资源
- 从内核模块链表中移除该模块
- 释放模块占用的代码段、数据段内存
4. insmod与modprobe核心区别(面试必背)
这是驱动开发面试最高频的考点之一,必须背下来:
| 对比项 | insmod | modprobe |
|---|---|---|
| 依赖处理 | ❌ 不处理任何依赖,缺少依赖直接报错 | ✅ 自动解析并加载所有依赖模块 |
| 模块路径 | 必须写绝对/相对路径 ,如insmod ./chr_dev.ko |
自动搜索系统模块路径/lib/modules/$(uname -r)/ |
| 用法 | 只能加载单个本地ko文件 | 直接用模块名加载,如modprobe e1000e |
| 错误处理 | 简单报错,无自动修复 | 依赖缺失时会提示并尝试自动处理 |
| 常用场景 | 本地开发调试驱动 | 系统开机自动加载、生产环境 |
通俗理解
insmod:只干一件事,强行安装你指定的这一个插件,缺任何东西就直接报错摆烂modprobe:智能安装管家,先查清楚需要哪些配套插件,全部自动装好,最后再装目标模块
💡 补充:
modprobe -r 模块名也可以卸载模块,会反向卸载所有不再被使用的依赖模块 ,比rmmod更安全。
5. 内核模块常用全套命令
bash
# 1. 查看所有已加载的内核模块
lsmod
# 2. 加载本地开发的驱动模块(开发调试首选)
sudo insmod ./xxx.ko
# 3. 加载系统模块(自动处理依赖)
sudo modprobe 模块名
# 4. 卸载模块
sudo rmmod 模块名
sudo modprobe -r 模块名 # 安全卸载,同时卸载无用依赖
# 5. 查看模块详细信息(作者、版本、依赖、许可证)
modinfo xxx.ko
# 6. 更新模块依赖缓存(modprobe依赖此文件)
sudo depmod -a
# 7. 查看模块打印日志
dmesg | tail -20
6. 结合Day1驱动的连贯实操
bash
# 进入Day1的驱动目录
cd chr_demo
# 1. 编译驱动
make
# 2. 加载模块
sudo insmod chr_dev.ko
# 3. 查看模块是否加载成功
lsmod | grep chr_dev
# 4. 查看内核日志
dmesg | tail
# 5. 测试驱动
cat /dev/mychrdev
# 6. 卸载模块
sudo rmmod chr_dev
# 7. 清理编译产物
make clean
7. 模块开发两个重要细节
(1)必须声明GPL许可证
c
MODULE_LICENSE("GPL");
- 这行代码必须添加,否则内核会标记为"污染内核"
- 非GPL协议的模块无法使用部分内核导出函数
- 会导致驱动编译失败或运行异常
(2)模块引用计数机制
内核会自动维护每个模块的引用计数:
- 当设备被用户进程打开时,引用计数+1
- 当设备被关闭时,引用计数-1
- 引用计数不为0时,
rmmod会直接报错,防止正在使用的驱动被卸载
内核内存分配全解:kmalloc/kzalloc/vmalloc
1. 为什么驱动绝对不能用malloc/free?
作为Linux应用开发者,你天天用malloc/free,但在内核驱动中完全不能使用,核心原因有4个:
- 编译不通过 :
malloc是C标准库函数,内核编译时不链接libc.so,会直接报"未定义引用"错误 - 环境不匹配 :
malloc依赖用户态堆管理器,内核态没有这个基础设施 - 上下文不安全 :
malloc可能因内存不足而阻塞睡眠,而内核的中断上下文绝对禁止睡眠 - 内存隔离 :
malloc分配的是用户态虚拟内存,驱动需要的是内核态内存
因此,Linux内核专门提供了三套内存分配API,这是驱动开发每天都要用的核心知识点。
2. 一句话记住三者核心定位
- kmalloc :内核最常用、最快,分配物理地址连续的小内存
- kzalloc := kmalloc + 自动清零(驱动开发首选推荐)
- vmalloc :分配虚拟地址连续、物理地址不连续的大内存
3. 核心区别对比表(开发+面试必背)
| 对比项 | kmalloc | kzalloc | vmalloc |
|---|---|---|---|
| 物理地址连续性 | ✅ 连续 | ✅ 连续 | ❌ 不连续 |
| 是否自动清零 | ❌ 内存是脏数据(随机值) | ✅ 自动全部清零 | ❌ 不清零 |
| 分配大小上限 | 小内存(一般128KB~4MB以内) | 同kmalloc | 大内存(MB/GB级别) |
| 分配速度 | ⚡ 极快(最佳性能) | ⚡ 极快 | 🐌 较慢(需要建立页表映射) |
| 能否睡眠 | 由flags参数控制 | 同kmalloc | 可以睡眠 |
| 适用场景 | 高性能场景、DMA传输、硬件交互 | 绝大多数驱动常规场景 | 大内存分配、网络/文件系统缓存 |
| 对应的释放函数 | kfree() |
kfree() |
vfree() |
| 所需头文件 | linux/slab.h |
linux/slab.h |
linux/vmalloc.h |
4. 逐个详解+极简代码示例
(1)kmalloc
- 内核最基础的内存分配函数
- 分配的内存物理地址连续,这对DMA传输和硬件交互至关重要
- 缺点:不会清零内存,里面是之前残留的随机值,容易引发bug
c
#include <linux/slab.h>
// 分配1024字节内核内存,允许睡眠
char *buf = kmalloc(1024, GFP_KERNEL);
if (buf == NULL) {
printk(KERN_ERR "kmalloc分配失败\n");
return -ENOMEM;
}
// 使用内存...
// 释放内存
kfree(buf);
buf = NULL; // 好习惯,防止野指针
(2)kzalloc(驱动开发首选)
k代表kernel,z代表zero- 功能完全等同于
kmalloc,但会自动将分配的内存全部清零 - 最安全,避免了脏数据导致的各种难以排查的bug
- 驱动开发中90%的场景都应该优先使用
kzalloc
c
#include <linux/slab.h>
// 分配1024字节,自动清零
char *buf = kzalloc(1024, GFP_KERNEL);
if (buf == NULL) {
printk(KERN_ERR "kzalloc分配失败\n");
return -ENOMEM;
}
// 使用内存...
// 释放方式和kmalloc完全一样
kfree(buf);
buf = NULL;
(3)vmalloc
- 专门用于分配大内存
- 虚拟地址连续,但物理地址不连续
- 速度较慢,因为需要为不连续的物理页建立页表映射
- 不能用于DMA传输和硬件交互(这些场景要求物理地址连续)
- 只能在进程上下文使用,不能在中断上下文使用
c
#include <linux/vmalloc.h>
// 分配1MB大内存
char *buf = vmalloc(1024 * 1024);
if (buf == NULL) {
printk(KERN_ERR "vmalloc分配失败\n");
return -ENOMEM;
}
// 使用内存...
// ⚠️ 注意:必须用vfree释放,绝对不能用kfree!
vfree(buf);
buf = NULL;
5. 最重要的GFP分配标志
kmalloc和kzalloc的第二个参数是GFP标志,用于控制内存分配的行为,最常用的有两个:
(1)GFP_KERNEL
- 最常用的标志
- 允许内存分配器在内存不足时阻塞睡眠,等待可用内存
- 只能在进程上下文使用 (如驱动的
open、read、write函数)
(2)GFP_ATOMIC
- 原子分配标志
- 绝对不允许睡眠
- 必须在中断上下文、定时器回调等不能睡眠的场景使用
- 分配失败会直接返回NULL
6. 完整可运行的内存测试驱动
我把三种内存分配方式整合到一个完整的内核模块中,你可以直接编译运行测试:
mem_demo.c
c
#include <linux/init.h>
#include <linux/module.h>
#include <linux/slab.h>
#include <linux/vmalloc.h>
#include <linux/kernel.h>
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("内核内存分配测试驱动");
#define SMALL_SIZE 1024
#define LARGE_SIZE 1024 * 1024 // 1MB
static int __init mem_test_init(void)
{
char *km_buf = NULL;
char *kz_buf = NULL;
char *vm_buf = NULL;
printk(KERN_INFO "=== 开始内核内存分配测试 ===\n");
// 1. 测试kmalloc
km_buf = kmalloc(SMALL_SIZE, GFP_KERNEL);
if (km_buf) {
printk(KERN_INFO "kmalloc分配%d字节成功\n", SMALL_SIZE);
// 验证kmalloc分配的内存是脏数据
printk(KERN_INFO "kmalloc内存第一个字节: 0x%02x\n", (unsigned char)km_buf[0]);
} else {
printk(KERN_ERR "kmalloc分配失败\n");
}
// 2. 测试kzalloc
kz_buf = kzalloc(SMALL_SIZE, GFP_KERNEL);
if (kz_buf) {
printk(KERN_INFO "kzalloc分配%d字节成功(自动清零)\n", SMALL_SIZE);
// 验证kzalloc分配的内存已清零
printk(KERN_INFO "kzalloc内存第一个字节: 0x%02x\n", (unsigned char)kz_buf[0]);
} else {
printk(KERN_ERR "kzalloc分配失败\n");
}
// 3. 测试vmalloc
vm_buf = vmalloc(LARGE_SIZE);
if (vm_buf) {
printk(KERN_INFO "vmalloc分配%d字节成功\n", LARGE_SIZE);
} else {
printk(KERN_ERR "vmalloc分配失败\n");
}
// 释放所有内存
kfree(km_buf);
kfree(kz_buf);
vfree(vm_buf);
printk(KERN_INFO "=== 所有内存已成功释放 ===\n");
return 0;
}
static void __exit mem_test_exit(void)
{
printk(KERN_INFO "内存测试模块卸载\n");
}
module_init(mem_test_init);
module_exit(mem_test_exit);
Makefile
makefile
obj-m += mem_demo.o
KERNELDIR ?= /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)
all:
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules
clean:
$(MAKE) -C $(KERNELDIR) M=$(PWD) clean
运行命令
bash
make
sudo insmod mem_demo.ko
dmesg | tail
sudo rmmod mem_demo
make clean
7. 驱动开发内存使用铁律
- 优先使用kzalloc:自动清零,最安全,避免脏数据bug
- 小内存用kmalloc/kzalloc,大内存用vmalloc
- DMA和硬件交互必须用kmalloc/kzalloc:要求物理地址连续
- 释放函数绝对不能混用 :
kfree对应kmalloc/kzalloc,vfree对应vmalloc - 分配后必须检查返回值:内核内存分配可能失败,不检查会导致空指针崩溃
- 谁分配谁释放:在同一个函数中完成分配和释放,避免内存泄漏
- 中断上下文只能用GFP_ATOMIC:绝对不能使用可能睡眠的分配方式
核心知识点总结
- 内核模块是Linux驱动的基本存在形式,实现了功能的按需加载卸载
- 内核模块生命周期:加载 → 初始化 → 常驻运行 → 卸载释放
module_init和module_exit是模块的唯一入口和出口insmod适合本地开发调试,modprobe适合生产环境自动处理依赖- 内核不能用
malloc,必须使用kmalloc/kzalloc/vmalloc - kzalloc是驱动开发首选,自动清零,安全可靠
- kmalloc分配物理连续的小内存,vmalloc分配物理不连续的大内存
- 释放函数必须配对使用,绝对不能混用