2. Linux驱动基础回顾

在进入驱动代码质量与测试工程之前,需要先把 Linux 驱动的基本骨架重新梳理一遍。驱动代码的质量问题,往往不是“写不出来”,而是对内核模块加载机制、设备模型、文件操作接口和生命周期理解不完整,导致资源泄漏、竞态、空指针、引用计数错误等缺陷。本节从六个关键对象出发:内核模块、字符设备、平台设备、设备树、文件操作接口与驱动生命周期,建立后续测试与质量分析所需的共同语言。

一、内核模块:驱动的装载单元

Linux 驱动通常以内核模块(.ko)形式存在,可以在系统运行时动态加载和卸载。模块的入口和出口由宏声明,内核负责调用。最基础的模块框架如下:

#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("course");
MODULE_DESCRIPTION("driver quality demo");

模块加载使用 insmod 或 modprobe,卸载使用 rmmod,查看信息使用 lsmod、modinfo。质量关注点在于:module_init 返回非 0 时内核会认为加载失败,必须回滚已申请资源;module_exit 必须释放所有资源,否则卸载后仍占用内存或设备。

模块参数通过 module_param 暴露,测试时可用不同参数组合验证边界行为,例如缓冲区大小、调试开关、超时时间。

二、字符设备与文件操作接口

字符设备是驱动中最常见的设备类型,用户空间通过 /dev 下的设备节点访问。驱动需要向内核注册设备号,并实现 file_operations 中的回调。核心接口包括 open、release、read、write、ioctl、mmap 等。

static int demo_open(struct inode *inode, struct file *filp)
{
    filp->private_data = &demo_dev;
    return 0;
}

static ssize_t demo_read(struct file *filp, char __user *buf,
                         size_t count, loff_t *ppos)
{
    return simple_read_from_buffer(buf, count, ppos,
                                   demo_buf, demo_len);
}

static const struct file_operations demo_fops = {
    .owner   = THIS_MODULE,
    .open    = demo_open,
    .release = demo_release,
    .read    = demo_read,
    .write   = demo_write,
    .unlocked_ioctl = demo_ioctl,
};

文件操作接口是用户态与内核态之间的契约。质量测试中要重点验证:返回值是否符合约定、copy_to_user/copy_from_user 是否检查失败、count 与偏移是否越界、并发打开时私有数据是否隔离。

三、平台设备、设备树与驱动生命周期

平台设备(platform device)用于描述 SoC 内部集成外设,不依赖可热插拔总线。设备树(Device Tree)用 .dts 描述硬件资源,如寄存器地址、中断号、时钟、GPIO。驱动通过 of_match_table 与设备树节点匹配,匹配成功后调用 probe,移除时调用 remove。

static const struct of_device_id demo_of_match[] = {
    { .compatible = "vendor,demo-device" },
    { }
};
MODULE_DEVICE_TABLE(of, demo_of_match);

static struct platform_driver demo_driver = {
    .probe  = demo_probe,
    .remove = demo_remove,
    .driver = {
        .name = "demo",
        .of_match_table = demo_of_match,
    },
};
module_platform_driver(demo_driver);

驱动生命周期可以概括为:模块加载 → 注册驱动 → 设备匹配 → probe 申请资源 → 用户空间访问 → remove 释放资源 → 模块卸载。任何一步失败都必须按相反顺序回滚。下图展示了这一完整链路及关键资源节点。

Linux 驱动生命周期与关键对象关系 内核模块加载 insmod / modprobe 注册 platform_driver of_match_table 匹配 设备树节点 compatible / reg / irq probe 申请资源 ioremap / irq / clk / gpio 注册字符设备 cdev / alloc_chrdev_region 用户空间访问 open/read/write/ioctl file_operations 回调 copy_to_user / 并发保护 remove 释放资源 iounmap / free_irq / clk_put 模块卸载 rmmod probe 失败回滚 质量检查点 • probe 每一步失败都要 goto 回滚,避免半初始化状态 • remove 与 probe 资源申请顺序严格对称,防止泄漏与重复释放

四、驱动生命周期中的典型误区与复盘

很多驱动缺陷并非出现在功能逻辑,而是出现在生命周期边界。以下清单可作为代码评审和测试用例设计的检查表:

阶段 常见误区 后果 测试关注点
模块加载 init 返回 0 但资源未就绪 后续 probe 空指针 异常参数、重复加载
probe 失败路径未释放已申请资源 内存/中断泄漏 注入分配失败、中断申请失败
open/release private_data 未隔离或未清空 并发访问串扰 多进程同时打开、异常关闭
read/write 未检查 copy_*_user 返回值 内核崩溃或数据错误 非法用户指针、越界长度
remove 释放顺序与申请顺序不一致 use-after-free 卸载时仍有用户打开
驱动生命周期测试不能只测“正常加载—正常卸载”一条路径。必须覆盖 probe 中途失败、remove 时设备仍被占用、模块重复加载、设备树匹配失败等异常分支,这些才是质量问题的高发区。

五、从基础回顾到质量工程