2. Linux内核驱动基础回顾
在深入LTP测试集验证驱动稳定性之前,必须牢固掌握Linux内核驱动的基础骨架。本章将系统回顾驱动模块的物理结构、字符设备驱动的标准实现流程,以及驱动与内核核心子系统之间的关键接口。这些知识是后续编写可测试、可验证驱动代码的基石。
2.1 驱动模块的物理结构与生命周期
Linux内核驱动以可加载内核模块(Loadable Kernel Module, LKM)的形式存在。一个标准的驱动模块包含两个强制入口点和两个强制出口点,以及可选的许可证声明和参数传递机制。
| 组件 | 宏/函数 | 说明 |
|---|---|---|
| 模块入口 | module_init(init_func) |
模块加载时调用,完成硬件初始化、数据结构分配、设备注册 |
| 模块出口 | module_exit(exit_func) |
模块卸载时调用,释放资源、注销设备、撤销映射 |
| 许可证声明 | MODULE_LICENSE("GPL") |
必须声明,非GPL可能导致内核符号不可用 |
| 作者描述 | MODULE_AUTHOR("name") |
可选,用于标识驱动维护者 |
| 参数传递 | module_param(name, type, perm) |
允许在insmod时传递运行时参数 |
关键点:
module_init 和 module_exit 是驱动与内核加载器之间的契约。如果入口函数失败(返回非0),内核将自动回滚加载过程,不会留下半初始化的设备。
下面的SVG图展示了驱动模块从加载到卸载的完整生命周期,以及每个阶段对应的内核回调。
2.2 字符设备驱动框架
字符设备是Linux驱动中最基础、最常用的设备类型。其核心框架围绕struct file_operations展开,驱动开发者通过填充该结构体中的函数指针,向VFS(虚拟文件系统)暴露设备操作接口。
2.2.1 核心数据结构
| 数据结构 | 作用 | 关键字段 |
|---|---|---|
struct cdev |
内核中字符设备的抽象 | ops (file_operations指针), dev (设备号) |
struct file_operations |
设备操作函数表 | open, release, read, write, unlocked_ioctl |
dev_t |
设备号(主设备号+次设备号) | MAJOR(dev), MINOR(dev) |
struct class |
设备类,用于自动创建设备节点 | name, devnode回调 |
2.2.2 标准实现流程
- 分配设备号:使用
alloc_chrdev_region()动态分配,或register_chrdev_region()静态指定。 - 初始化cdev:
cdev_init(&cdev, &fops)将操作函数表绑定到字符设备对象。 - 添加设备到内核:
cdev_add(&cdev, dev, count)使设备号与cdev关联。 - 创建设备类:
class_create(THIS_MODULE, "class_name")在sysfs中创建类。 - 创建设备节点:
device_create(class, NULL, dev, NULL, "dev_name")自动生成/dev/dev_name。 - 实现操作函数:填充
open,read,write,release,unlocked_ioctl等。
最佳实践: 在
module_init 中按顺序执行上述步骤,并在每一步失败时执行对应的回滚清理。推荐使用 goto 错误处理模式,确保资源不泄漏。
下面的SVG图展示了字符设备驱动从用户态到内核态的完整调用链路。
2.3 驱动与内核的核心接口
驱动与内核之间的交互远不止文件操作接口。一个成熟的驱动需要与内核的多个子系统协作,包括内存管理、并发控制、中断处理、定时器、设备模型等。
2.3.1 关键内核接口分类
| 接口类别 | 典型函数/宏 | 用途 |
|---|---|---|
| 内存分配 | kmalloc(), kzalloc(), vmalloc() |
在内核空间分配连续或非连续内存 |
| 并发控制 | spin_lock(), mutex_lock(), atomic_t |
保护共享数据结构,防止竞态 |
| 中断注册 | request_irq(), free_irq() |
注册和释放中断处理程序 |
| 定时器 | timer_setup(), mod_timer(), del_timer() |
实现延迟操作和周期性任务 |
| IO内存访问 | ioremap(), iowrite32(), ioread32() |
将物理寄存器映射到内核虚拟地址空间 |
| 设备模型 | device_create(), class_create() |
在sysfs中注册设备,实现udev自动节点创建 |
| 等待队列 | wait_event_interruptible(), wake_up() |
实现阻塞I/O,进程睡眠与唤醒 |
2.3.2 典型驱动代码骨架
以下是一个完整的字符设备驱动骨架,展示了上述接口的典型用法:
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/slab.h>
#include <linux/uaccess.h>
#include <linux/mutex.h>
#define DEVICE_NAME "demo_drv"
#define CLASS_NAME "demo_class"
static int major;
static struct class *demo_class;
static struct cdev demo_cdev;
static DEFINE_MUTEX(demo_mutex);
static int demo_open(struct inode *inode, struct file *file) {
pr_info("demo_drv: device opened\n");
return 0;
}
static ssize_t demo_read(struct file *file, char __user *buf,
size_t count, loff_t *ppos) {
char kbuf[64];
int ret;
mutex_lock(&demo_mutex);
snprintf(kbuf, sizeof(kbuf), "Hello from kernel!\n");
ret = copy_to_user(buf, kbuf, strlen(kbuf));
mutex_unlock(&demo_mutex);
return ret ? -EFAULT : strlen(kbuf);
}
static ssize_t demo_write(struct file *file, const char __user *buf,
size_t count, loff_t *ppos) {
char kbuf[256];
int ret;
if (count > sizeof(kbuf))
return -EINVAL;
mutex_lock(&demo_mutex);
ret = copy_from_user(kbuf, buf, count);
mutex_unlock(&demo_mutex);
if (ret)
return -EFAULT;
pr_info("demo_drv: received %zu bytes\n", count);
return count;
}
static int demo_release(struct inode *inode, struct file *file) {
pr_info("demo_drv: device released\n");
return 0;
}
static struct file_operations demo_fops = {
.owner = THIS_MODULE,
.open = demo_open,
.read = demo_read,
.write = demo_write,
.release = demo_release,
};
static int __init demo_init(void) {
dev_t dev;
/* 1. 分配设备号 */
if (alloc_chrdev_region(&dev, 0, 1, DEVICE_NAME) < 0) {
pr_err("Failed to allocate device number\n");
return -EIO;
}
major = MAJOR(dev);
/* 2. 初始化cdev */
cdev_init(&demo_cdev, &demo_fops);
demo_cdev.owner = THIS_MODULE;
/* 3. 添加cdev到内核 */
if (cdev_add(&demo_cdev, dev, 1) < 0) {
pr_err("Failed to add cdev\n");
goto err_cdev_add;
}
/* 4. 创建设备类 */
demo_class = class_create(THIS_MODULE, CLASS_NAME);
if (IS_ERR(demo_class)) {
pr_err("Failed to create class\n");
goto err_class_create;
}
/* 5. 创建设备节点 */
if (IS_ERR(device_create(demo_class, NULL, dev, NULL, DEVICE_NAME))) {
pr_err("Failed to create device\n");
goto err_device_create;
}
pr_info("demo_drv: loaded, major=%d\n", major);
return 0;
err_device_create:
class_destroy(demo_class);
err_class_create:
cdev_del(&demo_cdev);
err_cdev_add:
unregister_chrdev_region(dev, 1);
return -EIO;
}
static void __exit demo_exit(void) {
dev_t dev = MKDEV(major, 0);
device_destroy(demo_class, dev);
class_destroy(demo_class);
cdev_del(&demo_cdev);
unregister_chrdev_region(dev, 1);
pr_info("demo_drv: unloaded\n");
}
module_init(demo_init);
module_exit(demo_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Kernel Test Engineer");
MODULE_DESCRIPTION("Demo driver for LTP testing");
常见陷阱:
copy_to_user/copy_from_user必须在进程上下文中调用,不能在中断上下文使用。- 忘记使用
mutex或spinlock保护共享数据会导致竞态条件,在SMP系统上尤其危险。 module_init中的错误处理必须完整,否则残留的设备号或cdev会导致后续加载失败。
2.4 驱动与内核接口的测试关注点
从测试角度看,驱动与内核的每个接口都是潜在的故障点。LTP测试集在验证驱动稳定性时,会重点覆盖以下场景:
- 并发压力:多进程同时
open/read/write/ioctl,验证锁机制是否正确。 - 边界条件:传递空指针、超大缓冲区、非法命令码,验证驱动是否优雅处理错误。
- 资源泄漏:反复加载/卸载模块,监控
/proc/slabinfo和/proc/meminfo是否持续增长。 - 中断路径:在中断上下文中调用可能睡眠的函数(如
kmalloc(GFP_KERNEL)),会导致内核崩溃。 - 设备节点权限:验证
udev创建的设备节点权限是否正确,非root用户能否正常访问。
测试驱动设计原则: 驱动代码中每个
if 分支、每个错误返回路径都应该有对应的测试用例。LTP的 ioctl 测试、read/write 压力测试、module 加载卸载测试是验证这些路径的有效工具。