你有没有想过,当多个设备或程序同时依赖一个内核模块时,内核是如何管理模块的加载和卸载的?答案就在模块的使用计数(Usage Count)机制中。这个看似简单的计数器,其实是内核模块管理的核心组件,它就像模块的人气计数器,决定着模块的生死大权。今天咱们就来揭开这个神秘计数器的面纱。
用图书馆借书打比方。
想象一个图书馆有一本《Linux 内核开发秘籍》:
内核模块的使用计数原理完全一样:
这个机制确保了模块不会在被使用时被意外卸载,避免系统崩溃。
使用计数本质上是一个原子计数器(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); // 使用计数减1lsmod命令用户空间可以通过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 <linux/module.h>
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#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_demo3. 查看内核日志
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(),就像每一次借书都要归还一样。掌握了使用计数,你就掌握了内核模块管理的关键技能,离写出高质量的内核代码又近了一步!