你有没有想过,当多个设备或程序同时依赖一个内核模块时,内核是如何管理模块的加载和卸载的?答案就在模块的使用计数(Usage Count)机制中。这个看似简单的计数器,其实是内核模块管理的核心组件,它就像模块的人气计数器,决定着模块的生死大权。今天咱们就来揭开这个神秘计数器的面纱。
目录
一、什么是模块使用计数?
1.1 图书馆的借阅计数器
1.2 内核模块的使用计数
1.3 使用计数的本质
二、使用计数的核心操作:增、减、查
2.1 增加使用计数:try_module_get()
2.2 减少使用计数:module_put()
2.3 查看使用计数:lsmod命令
三、使用计数的典型应用场景
3.1 驱动模块的依赖管理
3.2 模块导出符号的安全使用
3.3 设备文件操作中的计数
四、使用计数的工作原理:内核源码揭秘
五、实战示例:手动控制模块使用计数
六、使用计数的常见问题与解决方案
6.1 模块无法卸载:"Resource busy"
6.2 使用计数异常:计数不减少
6.3 卸载模块后系统崩溃
七、使用计数的高级技巧
八、版本演进与兼容性
8.1 2.6内核与5.x内核差异
8.2 兼容性处理建议
用图书馆借书打比方。
想象一个图书馆有一本《Linux 内核开发秘籍》:
- 当第一个读者借阅时,计数器加 1,表示有 1 人正在使用
- 第二个读者借阅时,计数器变为 2
- 当一个读者归还时,计数器减 1
- 只有当计数器归 0 时,这本书才会被放回仓库(相当于模块卸载)
内核模块的使用计数原理完全一样:
这个机制确保了模块不会在被使用时被意外卸载,避免系统崩溃。
使用计数本质上是一个原子计数器(atomic_t
类型),存放在模块结构体(struct module
)中,内核通过操作这个计数器来控制模块的生命周期。
try_module_get()
当模块 A 要使用模块 B 时,必须先调用:
if (!try_module_get(moduleB)) {
// 获取失败,模块B已卸载或不可用
return -ENODEV;
}
// 获取成功,现在可以安全使用模块B的导出符号
module_put()
当模块 A 使用完模块 B 后,必须调用:
module_put(moduleB); // 使用计数减1
lsmod
命令用户空间可以通过lsmod
命令查看模块的使用计数:
$ lsmod | grep usbcore
usbcore 311296 14 usb_storage,usbhid,btusb,...
这里的14
表示usbcore
模块当前的使用计数为 14,即有 14 个其他模块正在使用它。
USB 核心驱动(usbcore
)被众多 USB 设备驱动依赖:
usb-storage
驱动加载,usbcore
计数 + 1usbhid
驱动加载,usbcore
计数 + 1usb-storage
卸载,usbcore
计数 - 1usbcore
计数为 0,才能被卸载当模块 A 使用模块 B 导出的符号时:
// 模块A使用模块B的导出函数前
if (!try_module_get(THIS_MODULE)) {
return -EFAULT;
}
// 使用模块B的函数...
result = moduleB_function();
// 使用完毕后
module_put(THIS_MODULE);
这样确保在模块 A 使用模块 B 期间,模块 B 不会被卸载。
字符设备驱动常在内核态文件操作函数中维护计数:
static int my_device_open(struct inode *inode, struct file *file) {
if (!try_module_get(THIS_MODULE)) {
return -EBUSY;
}
// 设备初始化...
return 0;
}
static int my_device_release(struct inode *inode, struct file *file) {
// 设备清理...
module_put(THIS_MODULE);
return 0;
}
这样当有用户打开设备文件时,模块计数 + 1;关闭时计数 - 1。
1. 模块结构体中的计数器
在include/linux/module.h
中定义:
struct module {
// ...
atomic_t refcnt; // 使用计数
// ...
};
refcnt
就是核心的使用计数器,初始值为 1(模块加载时)。
2. try_module_get()
源码简化版
int try_module_get(struct module *mod) {
if (mod->state != MODULE_STATE_LIVE)
return 0; // 模块已卸载或正在卸载
if (atomic_inc_not_zero(&mod->refcnt))
return 1; // 计数成功加1
return 0; // 模块在检查后状态变化
}
3. module_put()
源码简化版
void module_put(struct module *mod) {
if (atomic_dec_and_test(&mod->refcnt)) {
// 计数变为0,且模块已标记为卸载
synchronize_sched(); // 等待所有CPU上的任务完成
free_module(mod); // 释放模块内存
}
}
4. 使用计数的原子性保障
由于内核是多任务环境,可能有多个 CPU 同时操作计数,因此使用了原子操作:
atomic_inc_not_zero()
:原子性地增加计数,且检查结果是否非零atomic_dec_and_test()
:原子性地减少计数,并检查结果是否为零
这些原子操作确保了计数在并发环境下的正确性。
下面通过一个简单示例,演示如何在模块中手动管理使用计数。
1. 模块代码(count_demo.c
)
#include
#include
#include
#include
#include
#define DEMO_MAJOR 240
#define DEMO_NAME "count_demo"
// 设备打开函数
static int demo_open(struct inode *inode, struct file *file) {
printk(KERN_INFO "设备打开,当前使用计数: %d\n",
module_refcount(THIS_MODULE));
// 增加使用计数
if (!try_module_get(THIS_MODULE)) {
printk(KERN_ERR "获取模块失败\n");
return -EBUSY;
}
printk(KERN_INFO "使用计数已增加: %d\n",
module_refcount(THIS_MODULE));
return 0;
}
// 设备关闭函数
static int demo_release(struct inode *inode, struct file *file) {
printk(KERN_INFO "设备关闭,当前使用计数: %d\n",
module_refcount(THIS_MODULE));
// 减少使用计数
module_put(THIS_MODULE);
printk(KERN_INFO "使用计数已减少: %d\n",
module_refcount(THIS_MODULE));
return 0;
}
// 文件操作结构体
static struct file_operations demo_fops = {
.owner = THIS_MODULE,
.open = demo_open,
.release = demo_release,
};
// 模块初始化
static int __init demo_init(void) {
int ret;
ret = register_chrdev(DEMO_MAJOR, DEMO_NAME, &demo_fops);
if (ret < 0) {
printk(KERN_ERR "注册字符设备失败\n");
return ret;
}
printk(KERN_INFO "模块加载成功,初始使用计数: %d\n",
module_refcount(THIS_MODULE));
return 0;
}
// 模块退出
static void __exit demo_exit(void) {
unregister_chrdev(DEMO_MAJOR, DEMO_NAME);
printk(KERN_INFO "模块卸载成功\n");
}
module_init(demo_init);
module_exit(demo_exit);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("使用计数演示模块");
2. 测试步骤
# 编译并加载模块
make
sudo insmod count_demo.ko
# 查看初始计数(应该为1)
lsmod | grep count_demo
count_demo 16384 0
# 创建设备节点
sudo mknod /dev/count_demo c 240 0
# 打开设备(模拟用户程序)
cat /dev/count_demo & # 后台运行
# 查看计数(应该为2)
lsmod | grep count_demo
count_demo 16384 1
# 关闭设备
kill %1 # 终止刚才的cat命令
# 查看计数(应该恢复为1)
lsmod | grep count_demo
count_demo 16384 0
# 卸载模块
sudo rmmod count_demo
3. 查看内核日志
dmesg | tail
[ 1234.567890] 模块加载成功,初始使用计数: 1
[ 1234.678901] 设备打开,当前使用计数: 1
[ 1234.678910] 使用计数已增加: 2
[ 1234.789012] 设备关闭,当前使用计数: 2
[ 1234.789020] 使用计数已减少: 1
[ 1234.890123] 模块卸载成功
原因:使用计数不为 0,可能有:
解决:
# 查看模块依赖
lsmod | grep 模块名
# 查看打开的文件
lsof | grep /dev/模块设备名
# 终止相关进程
kill -9 进程ID
原因:
module_put()
try_module_get()
和module_put()
不配对module_put()
未执行解决:
try_module_get()
调用处,确保都有对应的module_put()
goto
语句确保异常处理路径也会减少计数:原因:
解决:
try_module_get()
/module_put()
对synchronize_sched()
确保所有 CPU 上的任务完成: static void __exit demo_exit(void) {
// 等待所有CPU上的任务完成
synchronize_sched();
// 执行卸载操作
unregister_chrdev(DEMO_MAJOR, DEMO_NAME);
}
1. 临时增加计数:防止模块被卸载
在执行关键操作前临时增加计数:
void critical_operation(void) {
if (!try_module_get(THIS_MODULE)) {
return; // 模块已卸载,无法执行
}
// 执行关键操作(此时模块不会被卸载)
do_critical_work();
module_put(THIS_MODULE); // 操作完成,释放计数
}
2. 检查模块是否正在卸载
if (module_is_being_unloaded(THIS_MODULE)) {
// 模块正在卸载,不要执行操作
return -EBUSY;
}
3. 查看模块依赖关系
# 查看模块依赖树
modinfo -F depends 模块名
# 可视化依赖关系(需要graphviz)
modgraph /lib/modules/$(uname -r)/kernel | dot -Tpng -o modules.png
特性 | 2.6内核 | 5.x内核 |
---|---|---|
计数存储 | 结构体成员 | 分离的percpu变量 |
状态标记 | MODULE_STATE_LIVE | module_is_live()函数 |
依赖管理 | 双向链表 | 改进的模块使用跟踪 |
#if LINUX_VERSION_CODE < KERNEL_VERSION(3,8,0)
MOD_INC_USE_COUNT;
#else
try_module_get(THIS_MODULE);
#endif
模块使用计数虽然只是一个简单的计数器,但它是内核模块安全管理的基石。通过合理管理这个计数器,我们可以:
- 防止模块在被使用时被意外卸载
- 实现模块间的安全依赖关系
- 精确控制模块的生命周期
- 避免内核崩溃和资源泄漏
记住:每一个try_module_get()
都必须对应一个module_put()
,就像每一次借书都要归还一样。掌握了使用计数,你就掌握了内核模块管理的关键技能,离写出高质量的内核代码又近了一步!