Linux字符设备驱动的完整生命周期:从insmod到rmmod的状态管理与sysfs接口设计
Linux字符设备驱动的完整生命周期:从insmod到rmmod的状态管理与sysfs接口设计
一、字符设备驱动不是简单的读写函数
初学字符设备驱动时容易误解:写好open/read/write/release四个回调,注册一个设备号,就完成了。但生产级驱动必须处理完整的生命周期——模块加载时的资源初始化、设备创建与sysfs接口、引用计数防止热拔拔时模块被提前卸载、模块卸载时的资源释放顺序。忽略任何一个环节都会导致内核崩溃或资源泄漏。
二、生命周期状态机与关键节点
字符设备驱动的生命周期由五个状态组成:
| 状态 | 触发条件 | 关键动作 |
|---|---|---|
| UNINIT | 模块未加载 | 无 |
| INIT | insmod执行 | 注册设备号、创建cdev、初始化硬件 |
| ACTIVE | 设备节点被打开 | 引用计数+1 |
| BUSY | 有进程持有fd | 引用计数>0,禁止rmmod |
| CLEANUP | rmmod执行且引用计数=0 | 释放cdev、注销设备号、释放硬件资源 |
生命周期状态机
stateDiagram-v2 [*] --> UNINIT: 模块源码编译 UNINIT --> INIT: insmod执行 INIT --> ACTIVE: cdev_add成功 设备可用 ACTIVE --> BUSY: open() 引用计数+1 BUSY --> BUSY: 多次open() 计数递增 BUSY --> ACTIVE: release() 计数-1到0 ACTIVE --> CLEANUP: rmmod且计数=0 BUSY --> WAITING: rmmod但计数>0 等待释放 WAITING --> ACTIVE: 所有fd关闭 计数归零 自动卸载 CLEANUP --> UNINIT: 释放完成 note right of BUSY: try_module_get 增加模块引用计数 note right of ACTIVE: module_put 减少模块引用计数核心约束:try_module_get在open时增加模块引用计数,module_put在release时减少。引用计数>0时rmmod会被拒绝,保证正在使用的模块不会被卸载。
三、引用计数与热插拔的实现机制
模块引用计数
内核通过struct module的refcnt字段追踪模块引用。驱动必须手动维护:
/* open回调中增加引用计数 */ static int dev_open(struct inode *inode, struct file *filp) { if (!try_module_get(THIS_MODULE)) return -ENODEV; /* 模块正在卸载,拒绝打开 */ filp->private_data = &dev_data; return 0; } /* release回调中减少引用计数 */ static int dev_release(struct inode *inode, struct file *filp) { module_put(THIS_MODULE); return 0; }try_module_get而非module_get:如果模块正在卸载过程中,try_module_get返回0(安全失败),而module_get会直接增加计数导致卸载失败(不安全)。
热拔拔处理
热拔拔场景:设备物理移除时,驱动需要通知所有持有fd的进程,并等待它们关闭fd后再卸载模块。
static void handle_hot_unplug(void) { /* 通知用户空间设备已移除 */ sysfs_notify(&dev_data.dev.kobj, NULL, "status"); /* 标记设备不可用,后续open拒绝 */ dev_data.removed = true; /* 等待所有持有者释放 */ while (dev_data.open_count > 0) { msleep(100); } /* 所有fd已关闭,可以安全卸载 */ }四、sysfs接口设计与完整驱动代码
sysfs接口是驱动与用户空间的配置通道。每个属性文件对应一个show/store回调。设计原则:
- 只暴露配置参数和状态信息,不暴露内部实现细节
- store回调必须验证输入值的合法性
- 属性文件权限严格区分:配置文件只允许root写入,状态文件对所有人只读
完整驱动代码
#include <linux/module.h> #include <linux/kernel.h> #include <linux/fs.h> #include <linux/cdev.h> #include <linux/device.h> #include <linux/slab.h> #include <linux/uaccess.h> #include <linux/kobject.h> #define DEVICE_NAME "mydev" #define CLASS_NAME "mydev_class" #define BUF_SIZE 4096 static dev_t dev_num; static struct cdev my_cdev; static struct class *my_class; static struct device *my_device; struct dev_private { char buffer[BUF_SIZE]; size_t data_len; bool removed; int open_count; int max_buffer_size; /* 可配置参数 */ }; static struct dev_private dev_data = { .max_buffer_size = BUF_SIZE, }; /* ---- sysfs属性 ---- */ static ssize_t status_show( struct device *dev, struct device_attribute *attr, char *buf) { return sprintf(buf, "open_count: %d\n" "data_len: %zu\n" "removed: %d\n" "max_buffer_size: %d\n", dev_data.open_count, dev_data.data_len, dev_data.removed, dev_data.max_buffer_size); } static ssize_t max_buffer_size_store( struct device *dev, struct device_attribute *attr, const char *buf, size_t count) { int new_size; if (kstrtoint(buf, 10, &new_size)) return -EINVAL; if (new_size < 1 || new_size > BUF_SIZE) return -EINVAL; dev_data.max_buffer_size = new_size; return count; } static DEVICE_ATTR(status, 0444, status_show, NULL); static DEVICE_ATTR(max_buffer_size, 0644, NULL, max_buffer_size_store); /* ---- 文件操作 ---- */ static int dev_open(struct inode *inode, struct file *filp) { if (dev_data.removed) return -ENODEV; if (!try_module_get(THIS_MODULE)) return -ENODEV; dev_data.open_count++; filp->private_data = &dev_data; return 0; } static int dev_release(struct inode *inode, struct file *filp) { dev_data.open_count--; module_put(THIS_MODULE); return 0; } static ssize_t dev_read( struct file *filp, char __user *buf, size_t len, loff_t *off) { struct dev_private *priv = filp->private_data; if (*off >= priv->data_len) return 0; size_t to_copy = min(len, priv->data_len - *off); if (copy_to_user(buf, priv->buffer + *off, to_copy)) return -EFAULT; *off += to_copy; return to_copy; } static ssize_t dev_write( struct file *filp, const char __user *buf, size_t len, loff_t *off) { struct dev_private *priv = filp->private_data; size_t to_copy = min(len, (size_t)priv->max_buffer_size - *off); if (to_copy == 0) return -ENOSPC; if (copy_from_user(priv->buffer + *off, buf, to_copy)) return -EFAULT; *off += to_copy; priv->data_len = *off; return to_copy; } static struct file_operations fops = { .owner = THIS_MODULE, .open = dev_open, .release = dev_release, .read = dev_read, .write = dev_write, }; /* ---- 模块初始化与卸载 ---- */ static int __init mydev_init(void) { /* 动态分配设备号 */ if (alloc_chrdev_region(&dev_num, 0, 1, DEVICE_NAME) < 0) return -ENOMEM; /* 初始化并添加cdev */ cdev_init(&my_cdev, &fops); my_cdev.owner = THIS_MODULE; if (cdev_add(&my_cdev, dev_num, 1) < 0) goto fail_cdev; /* 创建设备类和设备节点 */ my_class = class_create(CLASS_NAME); if (IS_ERR(my_class)) goto fail_class; my_device = device_create(my_class, NULL, dev_num, NULL, DEVICE_NAME); if (IS_ERR(my_device)) goto fail_device; /* 创建sysfs属性文件 */ device_create_file(my_device, &dev_attr_status); device_create_file(my_device, &dev_attr_max_buffer_size); pr_info("mydev: 初始化完成 major=%d minor=%d\n", MAJOR(dev_num), MINOR(dev_num)); return 0; fail_device: class_destroy(my_class); fail_class: cdev_del(&my_cdev); fail_cdev: unregister_chrdev_region(dev_num, 1); return -1; } static void __exit mydev_exit(void) { /* 等待所有持有者释放 */ while (dev_data.open_count > 0) { pr_warn("mydev: 等待open_count归零 (%d)\n", dev_data.open_count); msleep(100); } /* 按注册逆序释放资源 */ device_remove_file(my_device, &dev_attr_status); device_remove_file(my_device, &dev_attr_max_buffer_size); device_destroy(my_class, dev_num); class_destroy(my_class); cdev_del(&my_cdev); unregister_chrdev_region(dev_num, 1); pr_info("mydev: 卸载完成\n"); } module_init(mydev_init); module_exit(mydev_exit); MODULE_LICENSE("GPL"); MODULE_AUTHOR("zhongyiren"); MODULE_DESCRIPTION("字符设备驱动完整生命周期示例");资源释放的逆序原则:sysfs属性→设备节点→设备类→cdev→设备号。每个资源的释放顺序必须与注册顺序严格相反,否则会产生 dangling reference。
五、总结
- 字符设备驱动的生命周期由五个状态构成:UNINIT→INIT→ACTIVE→BUSY→CLEANUP,每个状态转换有明确的触发条件和关键动作
- 引用计数通过try_module_get/module_put维护,open时增加、release时减少,计数>0时rmmod被拒绝保证安全
- 热拔拔处理需要标记设备不可用、通知用户空间、等待所有fd关闭后才执行卸载,不能强制卸载正在使用的模块
- sysfs接口设计原则:只暴露配置和状态、store回调必须验证输入、权限严格区分root写入与只读
- 资源释放必须按注册逆序执行:sysfs→device→class→cdev→chrdev_region,逆序释放避免dangling reference导致的内核崩溃