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 释放资源 → 模块卸载。任何一步失败都必须按相反顺序回滚。下图展示了这一完整链路及关键资源节点。
四、驱动生命周期中的典型误区与复盘
很多驱动缺陷并非出现在功能逻辑,而是出现在生命周期边界。以下清单可作为代码评审和测试用例设计的检查表:
| 阶段 | 常见误区 | 后果 | 测试关注点 |
|---|---|---|---|
| 模块加载 | init 返回 0 但资源未就绪 | 后续 probe 空指针 | 异常参数、重复加载 |
| probe | 失败路径未释放已申请资源 | 内存/中断泄漏 | 注入分配失败、中断申请失败 |
| open/release | private_data 未隔离或未清空 | 并发访问串扰 | 多进程同时打开、异常关闭 |
| read/write | 未检查 copy_*_user 返回值 | 内核崩溃或数据错误 | 非法用户指针、越界长度 |
| remove | 释放顺序与申请顺序不一致 | use-after-free | 卸载时仍有用户打开 |