「Linux」- 设备驱动 | Device Drivers

构建

驱动开发方法

DeepSeek / Linux 驱动开发方法

现代 Linux 驱动开发是一个 “基于框架,填充回调函数” 的过程,核心在于理解内核提供的各种子系统、设备模型以及如何处理并发与硬件交互。

学习与实践路线图

  1. 基础准备:精通 C 语言,了解一些汇编和硬件知识。
  2. 从最简单的内核模块开始:编写一个 `Hello World` 模块,学习编译、加载、卸载。
  3. 学习字符设备驱动:创建一个虚拟的字符设备(如 mydev),实现 `open`, `read`, `write`,并在用户空间用 `cat`, `echo` 进行测试。
  4. 深入并发与同步:在你的虚拟设备中模拟资源竞争,并引入互斥锁来解决它。
  5. 学习中断处理:结合一个真实的硬件(如开发板上的一个按键)或模拟中断,实现顶半部和底半部。
  6. 掌握设备树:在一个嵌入式开发板上,将设备的硬件资源(如寄存器地址、中断号)从驱动代码中剥离,写入设备树文件(`.dts`)。
  7. 集成到特定框架:根据你的设备类型,选择一个合适的框架(如 I2C, SPI, Input 子系统等)进行实践。
  8. 阅读内核源码:大量阅读内核中已有驱动的源码,这是最好的学习资料。

推荐资源:

  • 书籍:《Linux Device Drivers》(LDD3,虽旧但经典),《Essential Linux Device Drivers》
  • 内核文档:`Documentation/driver-api/` 目录下的文档。
  • 源码:Linux 内核源码 `drivers/` 目录下的例子。

开发 Linux 驱动主要有以下几种模式,从易到难,从抽象到具体:

用户空间驱动

概念:并非真正的“内核驱动”,而是将驱动逻辑实现在用户空间的应用程序中,通过操作系统提供的接口(如 `/dev/mem`, `sysfs`, `userspace I/O`)来访问硬件。

性质:
  • 优点:开发调试简单(可以用 GDB),崩溃不会影响系统稳定性。
  • 缺点:性能差,无法处理硬件中断,访问硬件受限。

场景:
  • 对性能要求不高的简单设备。
  • 原型开发和学习阶段。
  • 不想或无法加载内核模块的环境。

内核模块

概念:将驱动编译成内核模块,其以 .ko 为扩展名。模块可以在系统运行时动态地加载到内核或从内核卸载,而无需重新编译整个内核或重启系统。这是最主流、最经典的驱动开发方式。

核心思想:驱动是内核的一部分

首先,必须理解,如果 Linux 驱动运行在内核空间,它与操作系统内核紧密绑定。这意味着:
  • 权限极高:驱动故障很可能导致整个系统崩溃(内核恐慌)。
  • 编程约束多:不能直接使用标准 C 库(如 `printf`,`malloc`),而必须使用内核提供的对应函数(如 `printk`, `kmalloc`)。
  • 并发考虑:内核是多任务环境,驱动必须设计为可重入的,并能正确处理多线程并发访问。

性质:
  • 优点:灵活、高效、功能完整,是生产环境的标准做法。
  • 缺点:开发调试复杂,错误可能导致内核崩溃。

开发流程:
  1. 编写模块代码:实现 `module_init` 和 `module_exit` 入口 / 出口函数。
  2. 编写 Makefile:使用内核的 Kbuild 系统来编译模块。
  3. 编译:`make` 命令生成 `.ko` 文件。
  4. 加载:`insmod module.ko` 或 `modprobe module`。
  5. 卸载:`rmmod module`。
  6. 查看:`lsmod` 查看已加载模块。

基于标准框架的开发

Linux 内核为各类设备提供了成熟的驱动框架和子系统。开发者不应直接“裸写”驱动,而应集成到这些框架中。这是现代 Linux 驱动开发最推荐和最主要的方法。

核心思想:“实现框架定义的接口,并向框架注册你的驱动”。

常见框架举例:
  • 字符设备框架:最常见的框架,用于不适合归类的设备(如传感器、LED、按键)。通过实现 `file_operations` 结构体,定义 `open`, `read`, `write`, `ioctl` 等操作。
  • 平台设备框架:用于片上系统(SoC)中的外设,这些设备通常没有传统意义上的总线(如 CPU 内部的 GPIO, I2C 控制器)。它分离了“驱动代码”和“设备资源(地址、中断号)”,通过设备树(Device Tree)进行描述。
  • PCI / USB 设备框架:用于标准的 PCI 和 USB 设备。驱动需要向 PCI/USB 子系统注册,并提供探测(`probe`)和断开(`disconnect`)函数。
  • 块设备框架:用于磁盘、SSD 等存储设备。
  • 网络设备框架:用于网卡。驱动需要填充 `net_device` 结构体,并处理数据包的发送和接收。

### 驱动开发关键技术要点

无论采用哪种框架,以下技术都是必须掌握的:

  1. 设备模型与 sysfs:理解 `kobject`, `kset`, `ktype` 如何在内核中组织成层次结构,并在用户空间的 /sys/ 下暴露设备信息,实现设备管理和热插拔。
  2. 设备树:在 ARM、PowerPC 等嵌入式系统中,用于取代冗长的硬件编码代码,以一种数据结构的形式来描述系统的硬件配置。驱动通过内核 API 从设备树中获取资源(内存地址、中断号)。
  3. 并发与同步:
    • 自旋锁:用于短时间内的锁定,特别在中断上下文中。
    • 互斥锁:用于可睡眠的上下文,保护较长时间的临界区。
    • 信号量 / 完成量:用于任务间的同步。
  4. 中断处理:
    • 顶半部:在中断上下文中快速执行,通常只做最紧急的工作(如读取状态寄存器),然后调度底半部。
    • 底半部:用于处理耗时的任务,通常通过任务队列、工作队列或软中断实现,可以在其中睡眠。
  5. 内存管理:
    • 使用 `kmalloc`, `vmalloc` 等内核函数分配内存。
    • 理解 DMA 和一致性内存映射,使用 `dma_alloc_coherent` 等函数。
  6. 内核调试:
    • `printk`:最基础的日志输出。
    • `dynamic debug`:动态调试。
    • `/proc/kcore` 和 `kgdb`:内核调试器(较复杂)。
    • `ftrace`, `perf`:性能分析工具。

应用

找到磁盘与 ATA 的对应关系

#!/bin/sh

ll /sys/class/ata_port/
ll /dev/disk/by-path

[WIP] Asking for cache data failed

What is a “Asking for cache data failed” warning?

[913914.442594] sd 7:0:0:0: [sdd] Asking for cache data failed
[913914.442597] sd 7:0:0:0: [sdd] Assuming drive cache: write through