2. Linux内核与驱动基础
做驱动版本迁移,第一步不是改代码,而是把 Linux 驱动的“骨架”重新认清。内核模块机制决定代码如何进出内核,字符设备决定用户态如何访问设备,平台设备与设备树决定硬件信息如何被描述和匹配,驱动模型则把总线、设备、驱动三者组织成统一的关系网。理解这四层,迁移时才能判断哪些代码可以保留、哪些接口必须替换。
一、内核模块机制:驱动的装载单元
Linux 驱动绝大多数以模块形式存在,通过 insmod、modprobe 动态加载,通过 rmmod 卸载。模块入口和出口由固定宏声明,迁移时最常遇到的是入口返回值、许可证声明和模块参数接口的变化。
#include <linux/module.h>
#include <linux/init.h>
static int __init demo_init(void)
{
pr_info("demo module init\n");
return 0;
}
static void __exit demo_exit(void)
{
pr_info("demo module exit\n");
}
module_init(demo_init);
module_exit(demo_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("driver-team");
模块与内核之间通过符号表交互,EXPORT_SYMBOL 导出的符号才能被其他模块引用。迁移到新内核时,若依赖的内部符号被移除或改名,模块会直接加载失败,典型日志是 Unknown symbol 或 disagrees about version of symbol。因此迁移前应先确认依赖符号在新内核中的可见性。
dmesg 最后几行,再执行 modinfo xxx.ko 查看 vermagic、依赖和参数,能快速区分是版本不匹配还是符号缺失。二、字符设备:用户态访问的入口
字符设备以字节流方式访问,核心是设备号(主设备号 + 次设备号)与 file_operations 回调集合。用户态通过 open/read/write/ioctl 进入内核,最终落到驱动实现的对应函数。迁移中常见变化包括 register_chrdev 被 alloc_chrdev_region + cdev_add 取代,以及 copy_to_user/copy_from_user 的检查方式调整。
static int demo_open(struct inode *inode, struct file *filp) { return 0; }
static ssize_t demo_read(struct file *filp, char __user *buf,
size_t len, loff_t *off) { return 0; }
static const struct file_operations demo_fops = {
.owner = THIS_MODULE,
.open = demo_open,
.read = demo_read,
};
dev_t dev;
alloc_chrdev_region(&dev, 0, 1, "demo");
cdev_init(&demo_cdev, &demo_fops);
cdev_add(&demo_cdev, dev, 1);
设备节点通常由 udev 根据 sysfs 信息自动创建,迁移后若 /dev 下节点缺失,应检查 class_create/device_create 是否成功,而不是手工 mknod 掩盖问题。
三、平台设备、设备树与驱动模型
平台设备(platform device)用于描述那些不挂在可枚举总线上的片上外设。传统写法在板级文件中静态注册 platform_device,现代内核则改由设备树(Device Tree)描述硬件资源,驱动通过 of_match_table 与设备树节点匹配。匹配成功后,probe 被调用,驱动从设备树读取寄存器基址、中断号、时钟、GPIO 等参数。
static const struct of_device_id demo_of_match[] = {
{ .compatible = "vendor,demo-uart" },
{ }
};
MODULE_DEVICE_TABLE(of, demo_of_match);
static struct platform_driver demo_driver = {
.probe = demo_probe,
.remove = demo_remove,
.driver = {
.name = "demo-uart",
.of_match_table = demo_of_match,
},
};
module_platform_driver(demo_driver);
设备树节点中的 compatible 是匹配的关键,reg、interrupts、clocks、pinctrl 等属性决定驱动能否正确拿到资源。迁移时若硬件描述从板级文件改为设备树,驱动必须同步改为 of_* 接口读取,否则会出现“驱动加载了但 probe 不执行”的现象。
四、迁移中的典型误区与复盘
| 误区现象 | 根因 | 排查手段 |
|---|---|---|
| 模块加载报 Unknown symbol | 依赖的内部符号被移除或未导出 | dmesg + modinfo + 符号表比对 |
| 驱动加载成功但 probe 不执行 | compatible 与设备树不匹配 | 检查 /sys/bus/platform/devices 与 of_match_table |
| /dev 下无设备节点 | class_create/device_create 失败 | 查看 sysfs class 目录与 udev 日志 |
| ioctl 返回 -ENOTTY | 命令码或回调未注册 | 核对 unlocked_ioctl 与命令定义 |
| 读写越界或崩溃 | copy_to_user 返回值未检查 | 加日志并核对用户缓冲区长度 |