内核驱动框架与性能基础

驱动开发这事儿,说白了就是让内核认识你的硬件。我刚开始接触Linux驱动时,总觉得框架这东西太抽象,不如直接写寄存器来得痛快。后来被坑了几次才明白——框架不是束缚,是给你铺好的路。

这一章我们就把内核模块、字符设备、platform模型、设备树、并发锁这几个基础骨架过一遍。嗯,都是后面调优和定位故障的根基。

1. Linux内核模块加载机制

内核模块就是一段可以在运行时动态加载到内核的代码。你想想看,如果每次加个驱动都要重新编译整个内核,那开发效率得多低?

核心流程:

  • module_init() — 模块入口,加载时调用
  • module_exit() — 模块出口,卸载时调用
  • MODULE_LICENSE() — 声明许可证,不写的话内核会报"tainted"
  • MODULE_AUTHOR() / MODULE_DESCRIPTION() — 元信息

我见过不少新手在模块里直接操作硬件寄存器,结果加载时系统直接panic。为什么?因为模块加载时内存映射还没准备好,你得先让内核知道你的设备在哪。

// 一个最简单的内核模块
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>

static int __init my_driver_init(void)
{
    printk(KERN_INFO "Hello, driver loaded!\n");
    return 0;
}

static void __exit my_driver_exit(void)
{
    printk(KERN_INFO "Goodbye, driver unloaded!\n");
}

module_init(my_driver_init);
module_exit(my_driver_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A simple example module");

避坑指南:我曾经在模块初始化里申请了DMA缓冲区但忘了释放,结果卸载模块后系统内存泄漏。记住:init里申请的资源,exit里一定要还回去。

2. 字符设备驱动框架

字符设备是Linux驱动里最基础的一类。它按字节流读写数据,典型的像串口、GPIO、I2C设备都属于字符设备。

核心结构体就两个:struct cdev 和 struct file_operations。前者代表设备,后者代表操作函数表。

file_operations成员 说明 典型场景
.open 打开设备 初始化硬件、申请资源
.release 关闭设备 释放资源、关闭硬件
.read 从设备读取数据 读取传感器值、接收数据
.write 向设备写入数据 发送命令、配置参数
.unlocked_ioctl 设备控制命令 设置波特率、切换模式

我个人习惯在注册字符设备时,先用 alloc_chrdev_region() 动态分配设备号,而不是写死主设备号。为什么?因为静态分配容易跟其他驱动冲突,动态分配省心多了。

// 字符设备注册核心步骤
dev_t dev_num;
struct cdev my_cdev;

// 1. 分配设备号
alloc_chrdev_region(&dev_num, 0, 1, "my_device");

// 2. 初始化cdev
cdev_init(&my_cdev, &my_fops);
my_cdev.owner = THIS_MODULE;

// 3. 添加设备到系统
cdev_add(&my_cdev, dev_num, 1);

// 4. 创建设备节点(通常由udev自动完成)
// class_create() + device_create()

注意:cdev_add()之后,内核随时可能调用你的操作函数。所以在add之前,必须确保所有资源都已就绪。我曾经在add之后才初始化自旋锁,结果并发访问直接死锁。

3. platform驱动模型

platform驱动是Linux设备模型里最常用的框架之一。它专门用于那些不挂在PCIe、USB、I2C等标准总线上的设备——说白了就是"片上设备"。

核心是两个结构体:struct platform_driver 和 struct platform_device。驱动和设备通过名字匹配,匹配成功后调用probe函数。

// platform驱动模板
static struct platform_driver my_pdrv = {
    .probe  = my_pdrv_probe,
    .remove = my_pdrv_remove,
    .driver = {
        .name = "my_platform_device",
        .of_match_table = my_of_match,
    },
};

module_platform_driver(my_pdrv);

我在项目中遇到过一个问题:probe函数里读取设备树属性时,忘了检查返回值。结果设备树里漏配了一个属性,驱动直接访问了无效内存。从那以后,我每次读取设备树属性都会加一句:

if (of_property_read_u32(np, "reg", &base_addr)) {
    dev_err(&pdev->dev, "missing reg property\n");
    return -EINVAL;
}

经验之谈:probe函数里尽量只做资源申请和初始化,不要把业务逻辑塞进去。probe失败时,要确保所有已申请的资源都能正确回滚。我习惯用goto标签做统一错误处理,虽然老派但可靠。

4. 设备树基础

设备树(Device Tree)是ARM Linux里描述硬件信息的标准方式。它解决了以前arch/arm目录下大量板级文件混乱的问题。

设备树文件以.dts为后缀,编译后生成.dtb二进制文件。内核在启动时解析dtb,构建出设备树结构体。

// 一个简单的设备树节点
my_device: my-device@ff100000 {
    compatible = "vendor,my-device";
    reg = <0x0 0xff100000 0x0 0x1000>;
    interrupts = <0 31 4>;
    clocks = <&clkc 15>;
    status = "okay";
};

你想想看,如果没有设备树,每次换一个板子都要改内核代码。有了设备树,只需要改dts文件,重新编译dtb就行。内核本身不用动。

设备树属性 含义 驱动中如何获取
compatible 设备兼容性字符串 of_match_table匹配
reg 寄存器地址和大小 platform_get_resource()
interrupts 中断号、触发类型 platform_get_irq()
clocks 时钟源引用 clk_get() / devm_clk_get()

常见坑:设备树里的地址和大小都是无符号整数,别用int去读。我曾经因为用了int读取reg属性,结果地址高位被截断,驱动访问了错误的物理地址,系统直接挂掉。

5. 驱动中的并发与锁机制

驱动开发里,并发问题是最隐蔽、最难定位的。你想想看,一个驱动可能同时被多个进程打开,中断随时可能触发,SMP多核上两个CPU可能同时访问同一个变量。

Linux内核提供了多种锁机制,我按使用频率排个序:

  • 自旋锁(spinlock) — 轻量级,适合短临界区,不能在持有锁时睡眠
  • 互斥锁(mutex) — 重量级,允许睡眠,适合长临界区
  • 读写锁(rwlock / rwsem) — 读多写少场景优化
  • 原子操作(atomic_t) — 简单的计数器、标志位
// 自旋锁使用示例
spinlock_t my_lock;
spin_lock_init(&my_lock);

spin_lock(&my_lock);
// 临界区代码,不能睡眠
shared_data++;
spin_unlock(&my_lock);

// 互斥锁使用示例
struct mutex my_mutex;
mutex_init(&my_mutex);

mutex_lock(&my_mutex);
// 临界区代码,可以睡眠
copy_to_user(buf, &shared_data, sizeof(shared_data));
mutex_unlock(&my_mutex);

核心原则:

  • 中断上下文只能用自旋锁,不能用互斥锁
  • 持有自旋锁时不能调用任何可能睡眠的函数(kmalloc(GFP_KERNEL)、copy_to_user等)
  • 锁的粒度要适中——太粗影响性能,太细容易死锁

我在项目中遇到过最典型的死锁场景:驱动A持有锁L1,然后调用驱动B的函数,驱动B又试图获取锁L1。两个驱动互相等待,系统卡死。解决方法是明确锁的层级关系,或者使用锁的嵌套顺序约定。

本章知识体系总览

下面这张图把本章的核心知识点串起来了。你可以把它当作一个思维导图,方便后面查阅。

内核驱动框架与性能基础 - 知识体系 内核驱动框架与性能基础 - 知识体系 Linux驱动框架 模块加载机制 字符设备驱动 platform驱动模型 设备树基础 并发与锁机制 module_init module_exit MODULE_LICENSE insmod/rmmod struct cdev file_operations alloc_chrdev_region cdev_add platform_driver platform_device probe/remove of_match_table .dts / .dtb compatible reg / interrupts of_property_read 自旋锁 互斥锁 原子操作 读写锁 提示:每个分支都是后续性能调优和故障定位的基础,建议动手写代码验证

嗯,这一章的内容就到这里。框架是骨架,性能调优是血肉,故障定位是经验。后面我们会一步步把这些基础用在实际的驱动开发中。