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 不执行”的现象。

Linux 驱动模型:总线—设备—驱动 匹配关系 设备树 / 板级信息 compatible = "vendor,demo" reg / interrupts / clocks platform_device 硬件资源描述对象 由内核解析生成 platform_driver of_match_table 匹配 probe / remove 总线匹配 bus_match compatible / name 比对 probe() 执行 申请资源、注册字符设备 用户态 /dev 节点 open / read / write / ioctl sysfs / udev 自动创建设备节点

四、迁移中的典型误区与复盘

误区现象根因排查手段
模块加载报 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 返回值未检查加日志并核对用户缓冲区长度
迁移时不要只改编译错误。很多问题在编译期不报错,却会在加载、匹配、probe 或用户态访问阶段暴露。建议按“模块加载 → 设备匹配 → probe 资源获取 → 字符设备注册 → 用户态访问”的顺序逐层验证。