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图展示了驱动模块从加载到卸载的完整生命周期,以及每个阶段对应的内核回调。

驱动模块生命周期与内核回调 驱动模块生命周期与内核回调 insmod 加载 module_init() 设备注册成功 正常运行 rmmod 卸载 module_exit() 资源释放 加载失败,自动回滚 说明: • 加载路径:insmod → module_init → 设备注册 → 正常运行 • 卸载路径:rmmod → module_exit → 资源释放 → 模块移除 • 失败路径:module_init返回非0 → 内核自动回滚,不留下半初始化状态

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 标准实现流程

  1. 分配设备号:使用 alloc_chrdev_region() 动态分配,或 register_chrdev_region() 静态指定。
  2. 初始化cdev:cdev_init(&cdev, &fops) 将操作函数表绑定到字符设备对象。
  3. 添加设备到内核:cdev_add(&cdev, dev, count) 使设备号与cdev关联。
  4. 创建设备类:class_create(THIS_MODULE, "class_name") 在sysfs中创建类。
  5. 创建设备节点:device_create(class, NULL, dev, NULL, "dev_name") 自动生成 /dev/dev_name。
  6. 实现操作函数:填充 open, read, write, release, unlocked_ioctl 等。
最佳实践: 在 module_init 中按顺序执行上述步骤,并在每一步失败时执行对应的回滚清理。推荐使用 goto 错误处理模式,确保资源不泄漏。

下面的SVG图展示了字符设备驱动从用户态到内核态的完整调用链路。

字符设备驱动调用链路 字符设备驱动调用链路 用户态应用程序 open() / read() / write() / ioctl() VFS 系统调用层 struct file_operations 分发 xxx_open() xxx_read() xxx_write() xxx_ioctl() 硬件寄存器 / DMA / 中断 用户态通过标准文件I/O接口操作设备,VFS根据设备号找到对应的file_operations表,分发到驱动实现的具体函数。

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 加载卸载测试是验证这些路径的有效工具。