【Linux内核模块】模块的使用计数

你有没有想过,当多个设备或程序同时依赖一个内核模块时,内核是如何管理模块的加载和卸载的?答案就在模块的使用计数(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 兼容性处理建议


一、什么是模块使用计数?

用图书馆借书打比方。

1.1 图书馆的借阅计数器

想象一个图书馆有一本《Linux 内核开发秘籍》:

  • 当第一个读者借阅时,计数器加 1,表示有 1 人正在使用
  • 第二个读者借阅时,计数器变为 2
  • 当一个读者归还时,计数器减 1
  • 只有当计数器归 0 时,这本书才会被放回仓库(相当于模块卸载)

1.2 内核模块的使用计数

内核模块的使用计数原理完全一样:

  • 当有其他模块或设备使用当前模块时,计数加 1
  • 使用结束后,计数减 1
  • 只有当计数为 0 时,模块才能被卸载

这个机制确保了模块不会在被使用时被意外卸载,避免系统崩溃。

1.3 使用计数的本质

使用计数本质上是一个原子计数器(atomic_t类型),存放在模块结构体(struct module)中,内核通过操作这个计数器来控制模块的生命周期。

二、使用计数的核心操作:增、减、查

2.1 增加使用计数:try_module_get()

当模块 A 要使用模块 B 时,必须先调用:

if (!try_module_get(moduleB)) {
    // 获取失败,模块B已卸载或不可用
    return -ENODEV;
}
// 获取成功,现在可以安全使用模块B的导出符号
  • 检查模块 B 是否存在且未标记为卸载
  • 如果满足条件,将模块 B 的使用计数加 1
  • 返回 true(非 0)表示获取成功

2.2 减少使用计数:module_put()

当模块 A 使用完模块 B 后,必须调用: 

module_put(moduleB);  // 使用计数减1
  • 将模块 B 的使用计数减 1
  • 检查计数是否变为 0
  • 如果变为 0 且模块 B 已标记为卸载,则执行实际卸载操作

2.3 查看使用计数:lsmod命令

用户空间可以通过lsmod命令查看模块的使用计数: 

$ lsmod | grep usbcore
usbcore               311296  14 usb_storage,usbhid,btusb,...

这里的14表示usbcore模块当前的使用计数为 14,即有 14 个其他模块正在使用它。

三、使用计数的典型应用场景

3.1 驱动模块的依赖管理

USB 核心驱动(usbcore)被众多 USB 设备驱动依赖:

  • 当插入 U 盘时,usb-storage驱动加载,usbcore计数 + 1
  • 再插入 USB 鼠标,usbhid驱动加载,usbcore计数 + 1
  • 拔出 U 盘,usb-storage卸载,usbcore计数 - 1
  • 只有当所有 USB 设备都拔出后,usbcore计数为 0,才能被卸载

3.2 模块导出符号的安全使用

当模块 A 使用模块 B 导出的符号时: 

// 模块A使用模块B的导出函数前
if (!try_module_get(THIS_MODULE)) {
    return -EFAULT;
}
// 使用模块B的函数...
result = moduleB_function();
// 使用完毕后
module_put(THIS_MODULE);

这样确保在模块 A 使用模块 B 期间,模块 B 不会被卸载。

3.3 设备文件操作中的计数

字符设备驱动常在内核态文件操作函数中维护计数: 

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] 模块卸载成功

六、使用计数的常见问题与解决方案

6.1 模块无法卸载:"Resource busy"

原因:使用计数不为 0,可能有:

  • 其他模块仍在使用当前模块
  • 设备文件仍被打开(用户程序未关闭)
  • 内核线程仍在运行

解决

# 查看模块依赖
lsmod | grep 模块名

# 查看打开的文件
lsof | grep /dev/模块设备名

# 终止相关进程
kill -9 进程ID

6.2 使用计数异常:计数不减少

原因

  • 忘记调用module_put()
  • try_module_get()module_put()不配对
  • 代码中出现异常导致module_put()未执行

解决

  • 检查所有try_module_get()调用处,确保都有对应的module_put()
  • 使用goto语句确保异常处理路径也会减少计数:

6.3 卸载模块后系统崩溃

原因

  • 使用计数管理错误,模块卸载时仍有代码在执行
  • 未正确同步,其他 CPU 上还有使用该模块的任务在运行

解决

  • 在关键代码处增加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

八、版本演进与兼容性

8.1 2.6内核与5.x内核差异

特性 2.6内核 5.x内核
计数存储 结构体成员 分离的percpu变量
状态标记 MODULE_STATE_LIVE module_is_live()函数
依赖管理 双向链表 改进的模块使用跟踪

8.2 兼容性处理建议

#if LINUX_VERSION_CODE < KERNEL_VERSION(3,8,0)
    MOD_INC_USE_COUNT;
#else
    try_module_get(THIS_MODULE);
#endif

模块使用计数虽然只是一个简单的计数器,但它是内核模块安全管理的基石。通过合理管理这个计数器,我们可以:

  1. 防止模块在被使用时被意外卸载
  2. 实现模块间的安全依赖关系
  3. 精确控制模块的生命周期
  4. 避免内核崩溃和资源泄漏

记住:每一个try_module_get()都必须对应一个module_put(),就像每一次借书都要归还一样。掌握了使用计数,你就掌握了内核模块管理的关键技能,离写出高质量的内核代码又近了一步!


你可能感兴趣的:(#,嵌入式Linux驱动开发实战,linux,运维,服务器)