4、迁移实战准备:现有字符设备驱动源码分析、功能清单梳理、迁移目标定义
好,咱们正式开始动手之前,得先把家底摸清楚。
我见过不少工程师,拿到一个旧驱动,二话不说就开始改。改到一半发现,哎?这个功能原来依赖了某个内核接口,在新版本里已经废弃了。或者,那个 ioctl 命令号跟杂项设备的注册方式有冲突。结果呢?返工,浪费时间。
所以,这一节咱们稳一点。先把现有的字符设备驱动源码翻个底朝天,把功能清单列出来,再明确迁移目标。说白了,就是「知己知彼,百战不殆」。
4.1 现有字符设备驱动源码分析
我习惯先看入口函数。也就是 module_init 和 module_exit 那两段。为什么?因为这里藏着驱动生命周期管理的核心逻辑。
咱们拿一个典型的虚拟字符设备驱动举例。它的入口大概长这样:
static int __init my_char_init(void)
{
int ret;
// 1. 分配设备号
ret = alloc_chrdev_region(&dev_num, 0, 1, "my_device");
if (ret < 0) {
pr_err("Failed to alloc chrdev region\n");
return ret;
}
// 2. 初始化 cdev
cdev_init(&my_cdev, &my_fops);
my_cdev.owner = THIS_MODULE;
// 3. 添加 cdev 到内核
ret = cdev_add(&my_cdev, dev_num, 1);
if (ret < 0) {
unregister_chrdev_region(dev_num, 1);
return ret;
}
// 4. 创建设备类
my_class = class_create(THIS_MODULE, "my_class");
// 5. 创建设备节点
device_create(my_class, NULL, dev_num, NULL, "my_device");
return 0;
}
你看,这里做了五件事:分配设备号、初始化 cdev、添加 cdev、创建类、创建设备节点。每一个步骤都可能出错,而且出错后的清理逻辑必须配套。
我在项目中遇到过一个问题:有个同事在 cdev_add 失败后,忘了调用 unregister_chrdev_region。结果设备号被占用了,模块卸载后重新加载就报错。这种小坑,排查起来特别费劲。
再看 file_operations 结构体。这是字符设备驱动的灵魂。咱们得把里面每个回调函数都过一遍:
static const struct file_operations my_fops = {
.owner = THIS_MODULE,
.open = my_open,
.release = my_release,
.read = my_read,
.write = my_write,
.unlocked_ioctl = my_ioctl,
.llseek = my_llseek,
};
嗯,这里要注意。如果你的驱动用了 .ioctl 而不是 .unlocked_ioctl,那说明它比较老了。内核 2.6.36 之后,.ioctl 就被移除了。迁移的时候,必须改成 .unlocked_ioctl。
4.2 功能清单梳理
源码看完了,咱们得把功能点一条条列出来。我建议用表格来整理,一目了然。
| 功能模块 | 具体实现 | 依赖的内核接口 | 迁移注意事项 |
|---|---|---|---|
| 设备注册 | alloc_chrdev_region + cdev_add | fs/char_dev.c | 杂项设备用 misc_register 替代,无需手动分配设备号 |
| 设备节点创建 | class_create + device_create | drivers/base/core.c | 杂项设备自动创建设备节点,无需手动操作 |
| 打开/关闭 | my_open / my_release | 无特殊依赖 | 逻辑可复用,注意引用计数 |
| 读写操作 | my_read / my_write | copy_to_user / copy_from_user | 逻辑可复用,注意缓冲区大小检查 |
| 控制命令 | my_ioctl (unlocked_ioctl) | cmd 定义在 ioctl.h | 命令号需要重新审查,避免与杂项设备冲突 |
| 定位操作 | my_llseek | 无特殊依赖 | 逻辑可复用 |
你看,表格一列出来,哪些功能可以原封不动搬过去,哪些需要改造,就清清楚楚了。
我个人习惯,还会在表格后面加一列「优先级」。比如「设备注册」是必须改的,优先级最高。「读写操作」逻辑不变,优先级低。这样排工期的时候心里有数。
4.3 迁移目标定义
功能清单有了,接下来就是定目标。目标要具体、可衡量。我见过有人写「把驱动迁移到杂项设备」,这太笼统了。什么叫迁移完成?代码能编译通过?还是功能全部验证通过?
我建议从三个维度来定义迁移目标:
- 功能维度:迁移后的驱动,必须实现与原驱动完全相同的用户态接口。也就是说,应用程序不需要改一行代码,就能正常工作。
- 性能维度:迁移后的驱动,在相同负载下,读写延迟、吞吐量不能有明显下降。我曾经遇到一个案例,迁移后 ioctl 路径变长了,导致某个实时任务超时。后来排查发现是杂项设备的锁机制跟原来不一样。
- 代码维度:迁移后的代码,要删除所有与字符设备注册相关的逻辑(alloc_chrdev_region、cdev_add、class_create 等),替换为 misc_register。同时,
file_operations结构体要精简,去掉不需要的回调。
核心迁移目标:用 misc_register 替换字符设备注册流程,保持 file_operations 和业务逻辑不变,确保用户态接口完全兼容。
你想想看,如果这三个维度都达标了,那迁移就算成功了。否则,只能算「代码搬家」,不是真正的迁移。
4.4 知识体系梳理
为了让你更直观地理解整个迁移准备阶段的工作,我画了一张流程图。它展示了从源码分析到目标定义的完整路径。
小提示:在梳理功能清单时,我建议你把每个功能的「输入-处理-输出」都写清楚。比如 ioctl 命令,输入是什么参数,内核怎么处理,输出什么结果。这样迁移后做功能验证时,直接对照测试就行,不用重新理解代码。
注意:千万不要忽略那些「看起来没用」的代码。比如某个 #ifdef 条件编译块,可能是为了兼容某个老内核版本。迁移到杂项设备后,这些条件编译可能需要重新评估。我曾经因为漏掉一个 #ifdef CONFIG_PROC_FS,导致迁移后的驱动在某个内核版本上无法创建 proc 文件,排查了两天才找到原因。
好了,准备工作就绪。源码分析完了,功能清单列好了,迁移目标也定下来了。接下来,咱们就可以动手写代码了。