设备驱动 (Device Drivers)


章节概述

本章进入硬件交互的世界。设备驱动是 Linux 内核中代码量最庞大的子系统(占总代码量的 60% 以上)。它连接了硬件和用户空间——将物理设备抽象为文件,使用户程序可以通过标准的 open/read/write/ioctl 操作硬件。

设备驱动是 C 语言”可移植汇编”特质的最佳展示:它需要操作硬件寄存器、处理中断、管理 DMA,同时向用户空间提供统一的文件接口。本章将带你从零编写一个完整的字符设备驱动,并深入理解内核的设备模型。

本章内容:

  • Linux 设备模型:bus、device、driver 的绑定机制
  • 字符设备驱动:完整的 file_operations 实现
  • 设备文件:major/minor 编号、/dev 目录、device_create
  • ioctl 接口:设备控制命令
  • 中断处理:request_irq、top half / bottom half
  • Rust 对比:同一驱动的 C 和 Rust 实现对比

前置要求


第一节:Linux 设备模型


1.1 三种设备类型

graph TD
 subgraph "Linux 设备分类"
 CHAR["字符设备 (cdev)<br/>● 按字节流访问<br/>● 通常不支持随机访问<br/>● 串口、键盘、鼠标<br/>● 实现 file_operations"]
 BLOCK["块设备 (bdev)<br/>● 按块访问 (512/4096 字节)<br/>● 支持随机访问<br/>● 硬盘、SSD、USB 存储<br/>● 有缓存层 (page cache)"]
 NET["网络设备 (netdev)<br/>● 不是通过文件访问!<br/>● 使用 socket API<br/>● 以太网、WiFi、loopback<br/>● 实现 net_device_ops"]
 
 CHAR -->|"在 /dev/ 下"| DEVFS["/dev/ttyS0, /dev/null..."]
 BLOCK -->|"在 /dev/ 下"| DEVFS
 NET -->|"ifconfig / ip 命令"| NETCMD["eth0, wlan0, lo..."]
 end

1.2 设备模型三要素:Bus、Device、Driver

graph TD
 BUS["Bus (总线)<br/>● USB bus, PCI bus, I2C bus, platform bus<br/>● 负责设备发现和驱动匹配"]
 
 DEV["Device (设备)<br/>● 描述一个物理/虚拟设备<br/>● 包含设备特定信息<br/>● struct device"]
 
 DRV["Driver (驱动)<br/>● 包含设备操作方法<br/>● probe(), remove() 回调<br/>● struct device_driver"]
 
 BUS -->|"注册设备"| DEV
 BUS -->|"注册驱动"| DRV
 BUS -->|"match() 匹配"| MATCH
 DEV -->|"设备信息"| MATCH
 DRV -->|"支持的 ID 表"| MATCH
 MATCH -->|"匹配成功"| BIND["调用 driver->probe()<br/>设备绑定到驱动"]
// ============================================
// 设备模型的注册与匹配
// ============================================
 
// 1. 平台总线 (platform bus) — 最常用的虚拟总线
// 用于 SoC 上集成的外设 (没有物理热插拔总线的设备)
// 如: 串口、GPIO、I2C 控制器等
 
#include <linux/platform_device.h>
 
// 平台设备: 描述一个 SoC 外设的资源 (MMIO 地址, IRQ 号等)
static struct resource my_device_resources[] = {
 [0] = DEFINE_RES_MEM(0xF0000000, 0x1000), // MMIO 寄存器区域
 [1] = DEFINE_RES_IRQ(42), // 中断号
};
 
static struct platform_device my_device = {
 .name = "my_device", // 用于与驱动匹配
 .id = -1, // -1 = 只有一个实例
 .num_resources = ARRAY_SIZE(my_device_resources),
 .resource = my_device_resources,
};
 
// 平台驱动: 实现设备操作
static int my_probe(struct platform_device *pdev)
{
 struct resource *res;
 void __iomem *regs;
 
 // 获取 MMIO 资源
 res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
 // 映射物理地址到内核虚拟地址
 regs = devm_ioremap_resource(&pdev->dev, res);
 if (IS_ERR(regs))
 return PTR_ERR(regs);
 
 // 获取 IRQ 资源
 int irq = platform_get_irq(pdev, 0);
 
 // 注册中断处理程序
 int ret = devm_request_irq(&pdev->dev, irq, my_isr,
 0, "my_device", pdev);
 
 dev_info(&pdev->dev, "Device probed successfully\n");
 return 0;
}
 
static int my_remove(struct platform_device *pdev)
{
 dev_info(&pdev->dev, "Device removed\n");
 return 0;
}
 
// 匹配表: 定义驱动支持的设备名称
static const struct of_device_id my_of_match[] = {
 { .compatible = "vendor,my-device" }, // 设备树匹配
 {},
};
MODULE_DEVICE_TABLE(of, my_of_match);
 
static struct platform_driver my_driver = {
 .probe = my_probe,
 .remove = my_remove,
 .driver = {
 .name = "my_device",
 .of_match_table = my_of_match,
 .owner = THIS_MODULE,
 },
};
 
module_platform_driver(my_driver); // 注册/注销 platform_driver

1.3 设备和驱动的 sysfs 表示

# sysfs 将设备模型暴露为用户可查看的文件系统
ls /sys/bus/ # 所有总线类型
ls /sys/bus/pci/devices/ # PCI 总线上的所有设备
ls /sys/bus/platform/drivers/ # 平台总线上的所有驱动
ls /sys/devices/ # 所有设备的层次结构
 
# 查看设备树信息 (如果有)
ls /sys/firmware/devicetree/
 
# 查看某个设备的信息
cat /sys/devices/system/cpu/cpu0/uevent
 
# 设备事件 (热插拔)
udevadm monitor --property # 监听设备热插拔事件

第二节:字符设备驱动完整示例


2.1 驱动概述

我们将实现一个”虚拟字符设备”——它在内存中模拟一个简单的环形缓冲区,支持常规的文件读写操作。这是一个完整的、可以直接编译加载的驱动。

graph TD
 subgraph "用户空间"
 APP["应用程序<br/>open(/dev/vdev)<br/>read/write/ioctl/close"]
 end
 
 subgraph "内核空间"
 VFS["VFS 层<br/>vfs_read / vfs_write"]
 CDEV["vdev 驱动<br/>struct cdev<br/>file_operations"]
 BUF["环形缓冲区<br/>(内存)"]
 end
 
 APP <--> VFS
 VFS <--> CDEV
 CDEV <--> BUF

2.2 完整驱动代码

// ============================================
// vdev.c —— 一个完整的内核字符设备驱动
// 功能: 环形缓冲, 支持 read/write/ioctl/llseek
// 编译: 作为独立内核模块
// ============================================
 
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/fs.h> // file_operations, struct cdev
#include <linux/cdev.h> // cdev_init, cdev_add
#include <linux/device.h> // device_create, class_create
#include <linux/slab.h> // kmalloc, kfree
#include <linux/uaccess.h> // copy_to_user, copy_from_user
#include <linux/mutex.h> // mutex (替代自旋锁用于可睡眠上下文)
#include <linux/sched.h>
 
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Tutorial");
MODULE_DESCRIPTION("A virtual character device with ring buffer");
 
// ============================================
// 设备参数
// ============================================
#define VDEV_NAME "vdev"
#define VDEV_CLASS "vdev_class"
#define VDEV_BUF_SIZE 4096 // 环形缓冲区大小 (4KB)
#define VDEV_DEV_MAJOR 0 // 0 = 动态分配主设备号
 
// ============================================
// 驱动私有数据结构
// 每个设备实例一个 (这里只有一个实例)
// ============================================
struct vdev_device {
 struct cdev cdev; // 字符设备核心结构
 struct device *device; // 设备节点
 dev_t devno; // 设备号 (major + minor)
 
 char *buf; // 环形缓冲区
 size_t size; // 缓冲区大小
 size_t head; // 写指针 (下一个写入位置)
 size_t tail; // 读指针 (下一个读取位置)
 size_t count; // 缓冲区中当前可读字节数
 
 struct mutex lock; // 互斥锁 (保护并发访问)
 wait_queue_head_t read_wq; // 读等待队列 (阻塞读)
 wait_queue_head_t write_wq; // 写等待队列 (阻塞写)
};
 
static struct vdev_device *vdev_dev;
 
// ============================================
// 辅助函数: 环形缓冲区操作
// ============================================
 
// 判断缓冲区是否为空/满
static inline bool buf_is_empty(struct vdev_device *d) {
 return d->count == 0;
}
static inline bool buf_is_full(struct vdev_device *d) {
 return d->count == d->size;
}
// 获取可写空间
static inline size_t buf_avail_write(struct vdev_device *d) {
 return d->size - d->count;
}
// 获取可读字节数
static inline size_t buf_avail_read(struct vdev_device *d) {
 return d->count;
}
 
// 写入一个字节到环形缓冲区
static int buf_write_byte(struct vdev_device *d, char byte)
{
 if (buf_is_full(d))
 return -ENOSPC;
 
 d->buf[d->head] = byte;
 d->head = (d->head + 1) % d->size;
 d->count++;
 return 1;
}
 
// 从环形缓冲区读取一个字节
static int buf_read_byte(struct vdev_device *d, char *byte)
{
 if (buf_is_empty(d))
 return -EAGAIN;
 
 *byte = d->buf[d->tail];
 d->tail = (d->tail + 1) % d->size;
 d->count--;
 return 1;
}
 
// ============================================
// file_operations 实现
// ============================================
 
// 打开设备
static int vdev_open(struct inode *inode, struct file *filp)
{
 struct vdev_device *d;
 
 // 从 inode 获取设备实例
 d = container_of(inode->i_cdev, struct vdev_device, cdev);
 filp->private_data = d; // 保存到 file, 供后续操作使用
 
 // 非阻塞模式?
 if (filp->f_flags & O_NONBLOCK)
 pr_debug("vdev: opened in non-blocking mode\n");
 
 pr_info("vdev: device opened\n");
 return 0;
}
 
// 释放设备 (对应 close)
static int vdev_release(struct inode *inode, struct file *filp)
{
 pr_info("vdev: device closed\n");
 return 0;
}
 
// 从设备读取数据
static ssize_t vdev_read(struct file *filp, char __user *buf,
 size_t count, loff_t *ppos)
{
 struct vdev_device *d = filp->private_data;
 ssize_t retval = 0;
 
 if (count == 0)
 return 0;
 
 // 获取互斥锁
 if (mutex_lock_interruptible(&d->lock))
 return -ERESTARTSYS; // 被信号中断
 
 // 等待数据可用 (非阻塞模式直接返回)
 while (buf_is_empty(d)) {
 mutex_unlock(&d->lock);
 if (filp->f_flags & O_NONBLOCK)
 return -EAGAIN;
 
 pr_debug("vdev: read blocking, no data\n");
 // 睡眠, 等待 write 方写入数据后唤醒
 if (wait_event_interruptible(d->read_wq,
 !buf_is_empty(d)))
 return -ERESTARTSYS; // 被信号唤醒
 
 if (mutex_lock_interruptible(&d->lock))
 return -ERESTARTSYS;
 }
 
 // 逐字节读取 (生产代码中应该批量拷贝)
 while (count > 0 && !buf_is_empty(d)) {
 char byte;
 buf_read_byte(d, &byte);
 
 if (copy_to_user(buf, &byte, 1)) {
 retval = retval ? : -EFAULT;
 break;
 }
 
 buf++;
 count--;
 retval++;
 }
 
 mutex_unlock(&d->lock);
 
 // 唤醒等待的写者 (因为现在有空间了)
 if (!buf_is_full(d))
 wake_up_interruptible(&d->write_wq);
 
 return retval;
}
 
// 向设备写入数据
static ssize_t vdev_write(struct file *filp, const char __user *buf,
 size_t count, loff_t *ppos)
{
 struct vdev_device *d = filp->private_data;
 ssize_t retval = 0;
 
 if (count == 0)
 return 0;
 
 if (mutex_lock_interruptible(&d->lock))
 return -ERESTARTSYS;
 
 // 等待空间可用
 while (buf_is_full(d)) {
 mutex_unlock(&d->lock);
 if (filp->f_flags & O_NONBLOCK)
 return -EAGAIN;
 
 pr_debug("vdev: write blocking, buffer full\n");
 if (wait_event_interruptible(d->write_wq,
 !buf_is_full(d)))
 return -ERESTARTSYS;
 
 if (mutex_lock_interruptible(&d->lock))
 return -ERESTARTSYS;
 }
 
 // 逐字节写入
 while (count > 0 && !buf_is_full(d)) {
 char byte;
 if (copy_from_user(&byte, buf, 1)) {
 retval = retval ? : -EFAULT;
 break;
 }
 
 buf_write_byte(d, byte);
 
 buf++;
 count--;
 retval++;
 }
 
 mutex_unlock(&d->lock);
 
 // 唤醒等待的读者 (因为现在有数据了)
 if (!buf_is_empty(d))
 wake_up_interruptible(&d->read_wq);
 
 return retval;
}
 
// ioctl —— 设备控制接口
#define VDEV_IOC_MAGIC 'V'
#define VDEV_IOC_RESET _IO(VDEV_IOC_MAGIC, 0) // 重置缓冲区
#define VDEV_IOC_GET_SIZE _IOR(VDEV_IOC_MAGIC, 1, int) // 获取缓冲区大小
#define VDEV_IOC_GET_COUNT _IOR(VDEV_IOC_MAGIC, 2, int) // 获取可读字节数
#define VDEV_IOC_MAXNR 2
 
static long vdev_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
 struct vdev_device *d = filp->private_data;
 int retval = 0;
 int val;
 
 // 检查魔数
 if (_IOC_TYPE(cmd) != VDEV_IOC_MAGIC)
 return -ENOTTY;
 if (_IOC_NR(cmd) > VDEV_IOC_MAXNR)
 return -ENOTTY;
 
 mutex_lock(&d->lock);
 
 switch (cmd) {
 case VDEV_IOC_RESET:
 // 清空缓冲区
 d->head = 0;
 d->tail = 0;
 d->count = 0;
 pr_info("vdev: buffer reset\n");
 break;
 
 case VDEV_IOC_GET_SIZE:
 val = (int)d->size;
 retval = put_user(val, (int __user *)arg);
 break;
 
 case VDEV_IOC_GET_COUNT:
 val = (int)d->count;
 retval = put_user(val, (int __user *)arg);
 break;
 
 default:
 retval = -ENOTTY;
 }
 
 mutex_unlock(&d->lock);
 return retval;
}
 
// llseek — 定位文件偏移
// 对于串行设备通常不支持 seek,但这里我们返回当前位置
static loff_t vdev_llseek(struct file *filp, loff_t off, int whence)
{
 loff_t newpos;
 
 switch (whence) {
 case SEEK_SET: newpos = off; break;
 case SEEK_CUR: newpos = filp->f_pos + off; break;
 case SEEK_END: return -EINVAL; // 无大小概念
 default: return -EINVAL;
 }
 
 if (newpos < 0)
 return -EINVAL;
 
 filp->f_pos = newpos;
 return newpos;
}
 
// ============================================
// file_operations 注册表
// ============================================
static const struct file_operations vdev_fops = {
 .owner = THIS_MODULE,
 .open = vdev_open,
 .release = vdev_release,
 .read = vdev_read,
 .write = vdev_write,
 .unlocked_ioctl = vdev_ioctl,
 .llseek = vdev_llseek,
};
 
// ============================================
// 驱动初始化和清理
// ============================================
 
static struct class *vdev_class;
 
static int __init vdev_init(void)
{
 int ret;
 
 // === 1. 分配设备结构 ===
 vdev_dev = kzalloc(sizeof(*vdev_dev), GFP_KERNEL);
 if (!vdev_dev)
 return -ENOMEM;
 
 // === 2. 分配设备号 ===
 ret = alloc_chrdev_region(&vdev_dev->devno, 0, 1, VDEV_NAME);
 if (ret < 0) {
 pr_err("vdev: alloc_chrdev_region failed: %d\n", ret);
 goto err_alloc_dev;
 }
 
 pr_info("vdev: major=%d, minor=%d\n",
 MAJOR(vdev_dev->devno), MINOR(vdev_dev->devno));
 
 // === 3. 初始化字符设备 ===
 cdev_init(&vdev_dev->cdev, &vdev_fops);
 vdev_dev->cdev.owner = THIS_MODULE;
 ret = cdev_add(&vdev_dev->cdev, vdev_dev->devno, 1);
 if (ret) {
 pr_err("vdev: cdev_add failed: %d\n", ret);
 goto err_unreg_region;
 }
 
 // === 4. 创建设备类 (/sys/class/vdev_class/) ===
 vdev_class = class_create(VDEV_CLASS);
 if (IS_ERR(vdev_class)) {
 ret = PTR_ERR(vdev_class);
 pr_err("vdev: class_create failed: %d\n", ret);
 goto err_cdev_del;
 }
 
 // === 5. 创建设备文件 (/dev/vdev) ===
 vdev_dev->device = device_create(vdev_class, NULL,
 vdev_dev->devno, NULL,
 VDEV_NAME);
 if (IS_ERR(vdev_dev->device)) {
 ret = PTR_ERR(vdev_dev->device);
 pr_err("vdev: device_create failed: %d\n", ret);
 goto err_class_destroy;
 }
 
 // === 6. 初始化环形缓冲区 ===
 vdev_dev->buf = kzalloc(VDEV_BUF_SIZE, GFP_KERNEL);
 if (!vdev_dev->buf) {
 ret = -ENOMEM;
 goto err_device_destroy;
 }
 vdev_dev->size = VDEV_BUF_SIZE;
 vdev_dev->head = 0;
 vdev_dev->tail = 0;
 vdev_dev->count = 0;
 
 // === 7. 初始化同步原语 ===
 mutex_init(&vdev_dev->lock);
 init_waitqueue_head(&vdev_dev->read_wq);
 init_waitqueue_head(&vdev_dev->write_wq);
 
 pr_info("vdev: device initialized successfully\n");
 return 0;
 
err_device_destroy:
 device_destroy(vdev_class, vdev_dev->devno);
err_class_destroy:
 class_destroy(vdev_class);
err_cdev_del:
 cdev_del(&vdev_dev->cdev);
err_unreg_region:
 unregister_chrdev_region(vdev_dev->devno, 1);
err_alloc_dev:
 kfree(vdev_dev);
 return ret;
}
 
static void __exit vdev_exit(void)
{
 kfree(vdev_dev->buf);
 device_destroy(vdev_class, vdev_dev->devno);
 class_destroy(vdev_class);
 cdev_del(&vdev_dev->cdev);
 unregister_chrdev_region(vdev_dev->devno, 1);
 kfree(vdev_dev);
 pr_info("vdev: device removed\n");
}
 
module_init(vdev_init);
module_exit(vdev_exit);

2.3 测试驱动

// ============================================
// test_vdev.c —— 用户态测试程序
// ============================================
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <sys/ioctl.h>
 
// 与驱动中相同的 ioctl 定义
#define VDEV_IOC_MAGIC 'V'
#define VDEV_IOC_RESET _IO(VDEV_IOC_MAGIC, 0)
#define VDEV_IOC_GET_SIZE _IOR(VDEV_IOC_MAGIC, 1, int)
#define VDEV_IOC_GET_COUNT _IOR(VDEV_IOC_MAGIC, 2, int)
 
int main(void)
{
 int fd, val;
 char buf[128];
 
 fd = open("/dev/vdev", O_RDWR);
 if (fd < 0) {
 perror("open");
 return 1;
 }
 
 // 测试写入
 if (write(fd, "Hello, kernel device!", 21) != 21) {
 perror("write");
 }
 
 // 测试 ioctl: 获取可读字节数
 if (ioctl(fd, VDEV_IOC_GET_COUNT, &val) == 0) {
 printf("Bytes available to read: %d\n", val);
 }
 
 // 测试读取
 memset(buf, 0, sizeof(buf));
 if (read(fd, buf, sizeof(buf)) > 0) {
 printf("Read from device: '%s'\n", buf);
 }
 
 // 测试 ioctl: 重置
 ioctl(fd, VDEV_IOC_RESET, NULL);
 ioctl(fd, VDEV_IOC_GET_COUNT, &val);
 printf("After reset, count = %d\n", val);
 
 close(fd);
 return 0;
}
# 编译和测试步骤:
# 1. make (编译驱动模块)
# 2. sudo insmod vdev.ko
# 3. ls -l /dev/vdev (查看设备文件)
# crw------- 1 root root 242, 0 Jun 16 10:00 /dev/vdev
# ↑ c = 字符设备, 242 = 主设备号, 0 = 次设备号
# 4. sudo chmod 666 /dev/vdev (允许普通用户访问)
# 5. gcc -o test_vdev test_vdev.c
# 6. ./test_vdev
# 7. dmesg | tail (查看内核日志)
# 8. sudo rmmod vdev
// 练习 1: 调试与追踪
// 1. 观察 /proc/devices 中注册的字符设备
// cat /proc/devices | grep vdev
//
// 2. 观察 sysfs 中设备的属性
// ls /sys/class/vdev_class/vdev/
// cat /sys/class/vdev_class/vdev/dev
//
// 3. 使用 strace 观察测试程序对设备文件的系统调用
// strace ./test_vdev
//
// 4. 使用 perf 追踪驱动函数
// sudo perf record -e 'sched:*' -g -- ./test_vdev

第三节:主设备号与次设备号


3.1 设备号的本质

// Linux 的设备号是 32 位整数 (dev_t)
// - 高 12 位: 主设备号 (Major) —— 标识驱动程序
// - 低 20 位: 次设备号 (Minor) —— 标识设备实例
//
// 例如: 主=242, 次=0 → MKDEV(242,0) = 242 << 20 | 0
 
// 设备号相关宏和函数
dev_t devno;
 
// MKDEV(major, minor) —— 构造设备号
devno = MKDEV(242, 0);
 
// MAJOR(devno) —— 获取主设备号
unsigned int major = MAJOR(devno); // = 242
 
// MINOR(devno) —— 获取次设备号
unsigned int minor = MINOR(devno); // = 0
 
// 分配设备号 (动态)
int alloc_chrdev_region(&devno, baseminor, count, name);
// 释放设备号
void unregister_chrdev_region(devno, count);
 
// 注册设备号 (静态, 需要指定)
int register_chrdev_region(devno, count, name);

3.2 /dev 下的设备文件

# 查看已注册的字符/块设备
cat /proc/devices
 
# Character devices:
# 1 mem
# 4 tty
# 5 /dev/tty
# 7 vcs
# 10 misc
# 13 input
# 242 vdev ← 我们的设备!
 
# Block devices:
# 8 sd
# 9 md
# 259 blkext
 
# 手动创建节点 (mknod):
# mknod /tmp/mydev c 242 0
# c = 字符设备, 242 = 主设备号, 0 = 次设备号

第四节:中断处理


4.1 Top Half 与 Bottom Half

sequenceDiagram
 participant HW as 硬件设备
 participant IRQ as 中断控制器<br/>(APIC / GIC)
 participant CPU as CPU
 participant TH as Top Half<br/>(request_irq 注册)
 participant BH as Bottom Half<br/>(tasklet / workqueue)
 participant PROC as 用户进程
 
 HW->>IRQ: 触发中断信号
 IRQ->>CPU: 中断通知 (IRQ 向量)
 CPU->>CPU: 保存上下文<br/>跳转 ISR
 CPU->>TH: 执行 top half
 Note over TH: - 快速处理<br/>- 屏蔽同类型中断<br/>- 确认中断 (ack)<br/>- 调度 bottom half<br/>- 尽快返回
 
 TH->>BH: 调度 (schedule_work, raise_softirq)
 TH->>CPU: 中断返回 (iret)
 
 Note over BH: 稍后在安全上下文中执行
 BH->>BH: 处理实际数据<br/>耗时的操作
 BH->>PROC: 唤醒等待进程<br/>(wake_up_interruptible)

4.2 中断处理核心代码

// ============================================
// 中断处理示例: 假设一个数据采集设备
// ============================================
 
#include <linux/interrupt.h>
#include <linux/workqueue.h>
 
// 设备私有数据 (扩展版)
struct my_device {
 void __iomem *regs; // MMIO 寄存器
 int irq; // 中断号
 
 // Bottom half 选项 1: work_struct (进程上下文, 可睡眠)
 struct work_struct bh_work;
 struct workqueue_struct *wq;
 
 // Bottom half 选项 2: tasklet (中断上下文, 不可睡眠)
 struct tasklet_struct bh_tasklet;
 
 // 中断统计数据
 unsigned long irq_count;
 
 // 数据缓冲
 unsigned char *rx_buf;
 size_t rx_len;
 
 wait_queue_head_t data_wq; // 等待数据的进程队列
};
 
// ============================================
// Top Half: 中断服务例程 (ISR)
// ============================================
 
// 中断处理函数 (top half)
// 要求:
// - 快速执行 (不能做耗时操作)
// - 不能睡眠 (不能调用可能阻塞的函数)
// - 不能访问用户空间内存
// - 在此期间相同 IRQ 线被屏蔽
static irqreturn_t my_device_isr(int irq, void *dev_id)
{
 struct my_device *dev = (struct my_device *)dev_id;
 u32 status;
 
 // 1. 读取中断状态寄存器, 确认是否是我们的中断
 status = ioread32(dev->regs + IRQ_STATUS_REG);
 
 if (!(status & IRQ_DATA_READY))
 return IRQ_NONE; // 不是我们的中断 (共享 IRQ 线的情况)
 
 // 2. 确认 (ack) 中断 — 清除硬件中断标志
 iowrite32(IRQ_ACK, dev->regs + IRQ_ACK_REG);
 
 // 3. 快速读取数据到缓冲区 (少量)
 // (大多数数据拷贝留给 bottom half)
 dev->irq_count++;
 
 // 4. 调度 bottom half
 // 选项 A: 使用 work_struct (进程上下文)
 queue_work(dev->wq, &dev->bh_work);
 
 // 选项 B: 使用 tasklet (中断上下文, 更轻量)
 // tasklet_schedule(&dev->bh_tasklet);
 
 return IRQ_HANDLED;
}
 
// ============================================
// Bottom Half 选项 1: work_struct (进程上下文)
// ============================================
 
static void my_device_work_handler(struct work_struct *work)
{
 // container_of 从 work_struct 反推设备结构
 struct my_device *dev = container_of(work, struct my_device, bh_work);
 u32 data;
 
 // 可以在进程上下文中执行耗时操作:
 // - 读取大量数据
 // - 进行复杂计算
 // - 调用可能睡眠的函数
 // - 复制数据到用户空间 (但不能直接这样做)
 
 // 读取硬件数据
 data = ioread32(dev->regs + DATA_REG);
 
 // 存储到缓冲区
 if (dev->rx_len < BUFFER_SIZE) {
 // (简化) 存储数据
 dev->rx_len++;
 }
 
 // 唤醒等待数据的进程
 wake_up_interruptible(&dev->data_wq);
}
 
// ============================================
// Bottom Half 选项 2: tasklet (中断上下文)
// ============================================
 
static void my_device_tasklet_handler(unsigned long data)
{
 struct my_device *dev = (struct my_device *)data;
 u32 val;
 
 // tasklet 运行在中断上下文:
 // - 不能睡眠
 // - 不能调用 schedule()
 // - 可以使用 GFP_ATOMIC 分配内存
 // - 比 work_struct 轻量 (调度开销更小)
 
 val = ioread32(dev->regs + DATA_REG);
 // 快速处理...
}
 
// ============================================
// 初始化: 注册中断
// ============================================
 
static int my_device_init(struct my_device *dev, int irq)
{
 int ret;
 
 dev->irq = irq;
 
 // 初始化 work_struct
 INIT_WORK(&dev->bh_work, my_device_work_handler);
 
 // 创建专用工作队列 (也可以使用系统默认的 system_wq)
 dev->wq = alloc_workqueue("my_device_wq", WQ_UNBOUND, 1);
 if (!dev->wq)
 return -ENOMEM;
 
 // 初始化 tasklet
 tasklet_init(&dev->bh_tasklet, my_device_tasklet_handler,
 (unsigned long)dev);
 
 // 初始化等待队列
 init_waitqueue_head(&dev->data_wq);
 
 // 注册中断处理
 // flags:
 // IRQF_SHARED — 允许多个设备共享此 IRQ 线
 // IRQF_ONESHOT — 中断处理期间保持屏蔽
 // IRQF_TRIGGER_RISING — 上升沿触发
 ret = request_irq(irq, // IRQ 号
 my_device_isr, // ISR 函数
 IRQF_SHARED, // 标志
 "my_device", // 名称 (显示在 /proc/interrupts)
 dev); // dev_id (传给 ISR, 用于区分共享设备)
 if (ret) {
 destroy_workqueue(dev->wq);
 return ret;
 }
 
 return 0;
}
 
// ============================================
// 清理: 释放中断
// ============================================
static void my_device_cleanup(struct my_device *dev)
{
 // 释放 IRQ (确保没有正在执行的 ISR)
 free_irq(dev->irq, dev);
 
 // 等待 work 执行完毕并清理
 cancel_work_sync(&dev->bh_work);
 destroy_workqueue(dev->wq);
 
 // tasklet 在 module_exit 时会自动清理
 tasklet_kill(&dev->bh_tasklet);
}

4.3 中断统计与调试

# 查看中断统计
cat /proc/interrupts
# CPU0 CPU1 CPU2 CPU3
# 0: 34 0 0 0 IO-APIC 2-edge timer
# 1: 9 0 0 0 IO-APIC 1-edge i8042
# 8: 0 1 0 0 IO-APIC 8-edge rtc0
# 9: 0 0 0 0 IO-APIC 9-fasteoi acpi
# 16: 33 0 1 0 IO-APIC 16-fasteoi ehci_hcd:usb1
# 42: 123 0 0 0 IO-APIC 42-fasteoi my_device
# ↑ ↑ ↑
# IRQ号 各CPU中断次数 设备名
 
# 查看中断中消耗的 CPU 时间
cat /proc/stat | grep -E "^intr|^softirq"
 
# 查看 bottom half (softirq) 统计
cat /proc/softirqs
# CPU0 CPU1
# HI: 0 0
# TIMER: 1234567 987654
# NET_TX: 1234 5678
# NET_RX: 56789 12345
# BLOCK: 1234 2345
# TASKLET: 100 50
# SCHED: 1234 2345
# HRTIMER: 56789 12345
# RCU: 123456 654321
 
# 使用 perf 追踪中断
sudo perf record -e irq:irq_handler_entry -e irq:irq_handler_exit -a -- sleep 10
sudo perf script
// 练习 2: 中断实验
// 1. 编写一个简单的内核模块, 使用 timer (非硬件中断) 模拟中断:
// - 使用 setup_timer 每秒触发一次定时器
// - 在定时器回调中模拟"硬件中断" → 调度 work_struct
// - 观察 /proc/interrupts 和 /proc/softirqs 的变化
//
// 2. 测量 bottom half 的调度延迟:
// 在 top half 中记录 ktime_get()
// 在 bottom half 中计算差值
//
// 3. 对比 tasklet 和 work_struct 的性能:
// 分别测量两者从调度到执行的平均延迟

第五节:C 驱动 vs Rust 驱动对比


5.1 为什么对比

关于 Rust 与 C 驱动开发的对比,Rust 正在进入 Linux 内核驱动程序开发。以下是一个精简的对比:

// ============================================
// C 驱动中常见的错误模式
// ============================================
 
// 错误 1: 使用后释放 (Use-After-Free)
static void c_use_after_free(struct my_dev *dev)
{
 char *buf = kmalloc(1024, GFP_KERNEL);
 // ... 使用 buf ...
 kfree(buf);
 // BUG! buf 已释放
 printk(KERN_INFO "data: %c\n", buf[0]); // UAF! C 编译器不检查
}
 
// 错误 2: 忘记检查返回值
static int c_forget_check(void *ptr)
{
 // 如果 ptr == NULL, 下面的访问导致内核崩溃
 void *new_ptr = krealloc(ptr, 1024, GFP_KERNEL);
 memcpy(new_ptr, data, 1024); // BUG! new_ptr 可能为 NULL
 return 0;
}
 
// 错误 3: 并发数据竞争
static int c_race_condition(struct my_dev *dev)
{
 // 多个 CPU 可能同时执行这里
 dev->counter++; // BUG! 非原子操作, 需要加锁或使用 atomic_t
 return dev->counter;
}
 
// 错误 4: 忘记释放资源 (goto 错误处理)
static int c_resource_leak(void)
{
 void *a = kmalloc(256, GFP_KERNEL);
 if (!a) return -ENOMEM;
 
 void *b = kmalloc(512, GFP_KERNEL);
 if (!b) {
 // BUG! 忘记 kfree(a)
 return -ENOMEM;
 }
 // ...
 kfree(b);
 kfree(a);
 return 0;
}
// ============================================
// C 驱动中的正确模式
// ============================================
 
// 模式: 使用 goto 进行统一错误处理
static int c_correct_error_handling(void)
{
 void *a = NULL, *b = NULL;
 int ret = 0;
 
 a = kmalloc(256, GFP_KERNEL);
 if (!a) {
 ret = -ENOMEM;
 goto out;
 }
 
 b = kmalloc(512, GFP_KERNEL);
 if (!b) {
 ret = -ENOMEM;
 goto out_free_a;
 }
 
 // ... 正常操作 ...
 
out_free_a:
 kfree(b);
out:
 kfree(a);
 return ret;
}
 
// 模式: 使用 devm_* 系列函数 (managed resources)
// 当设备从系统中移除时, 内核自动释放这些资源
static int c_managed_resources(struct platform_device *pdev)
{
 void __iomem *regs;
 
 // devm_ioremap_resource 会自动管理映射
 regs = devm_platform_ioremap_resource(pdev, 0);
 if (IS_ERR(regs))
 return PTR_ERR(regs);
 
 // devm_kzalloc 在设备移除时自动释放
 struct my_dev *dev = devm_kzalloc(&pdev->dev, sizeof(*dev), GFP_KERNEL);
 if (!dev)
 return -ENOMEM;
 
 return 0;
 // 不需要显式清理! 设备移除时内核自动分析 devm_* 链表并释放
}
 
// 模式: 使用 RCU 实现无锁读
// (详见 [[06_并发与同步|并发与同步]])
// ============================================
// Rust 驱动对应实现 (关键优势对比)
 
// ============================================
 
// 在 Rust 中:
// 
// 1. 使用后释放 → 编译时拒绝
// let data = kmalloc(1024)?;
// kfree(data); // data 的所有权被转移
// printk!("{}", data[0]); // 编译错误! data 已被移动
//
// 2. 空指针 → 使用 Option<T> 和 Result<T>
// let ptr = krealloc(ptr, 1024)?; // ? 操作符在失败时传播错误
// memcpy(ptr, data, 1024); // ptr 保证非空
//
// 3. 数据竞争 → 所有权和借用检查
// let mut guard = dev.lock.lock();
// guard.counter += 1; // 互斥锁保证独占访问
//
// 4. 资源泄漏 → RAII (Drop trait)
// 资源在离开作用域时自动释放
//
// 这意味着约 70% 的 C 安全漏洞 (根据 Google/MS 统计)
// 在 Rust 中根本无法编译通过!
 
// C 和 Rust 的关系: 互补而非替代
//
// C 的优势:
// - 30 年内核代码积累
// - 所有架构的完整支持
// - 开发者生态成熟
// - 更简单的编译模型
//
// Rust 的优势:
// - 编译时的内存安全保证
// - 现代化类型系统 (泛型、trait)
// - 更好的错误处理
// - 新驱动代码更少的 CVE
//
// 未来: C 负责核心和现有驱动, Rust 负责新驱动
// 练习 3: 驱动对比分析
// 1. 阅读 fs/nvme/ 下的 NVMe 驱动 (C 版本)
// 2. 比较 Rust 版本 (如果可用)
// 3. 找出 C 版本中可能需要人工审查的内存安全点 (5 处以上)
// 4. 阅读内核邮件列表, 了解最近因内存问题引入的 CVE
// 搜索: git log --grep="CVE" --oneline drivers/

章节测试


判断题 1

字符设备驱动通过实现 file_operations 结构体来向用户空间提供文件操作接口。 ( )

  • 正确

  • 错误

判断题 2

中断处理程序(top half)中可以安全地调用 kmalloc(..., GFP_KERNEL)。 ( )

  • 正确

  • 错误

判断题 3

Linux 内核使用 32 位 dev_t,其中高 12 位为主设备号,低 20 位为次设备号。 ( )

  • 正确

  • 错误

判断题 4

tasklet 和 work_struct 的 bottom half 都可以睡眠。 ( )

  • 正确

  • 错误

判断题 5

request_irq() 注册的中断处理函数可以通过 dev_id 参数在共享中断线时区分设备。 ( )

  • 正确

  • 错误

判断题 6

copy_to_user()copy_from_user() 可以在中断上下文中安全使用。 ( )

  • 正确

  • 错误

判断题 7

平台总线(platform bus)用于连接 USB 和 PCI 等热插拔设备。 ( )

  • 正确

  • 错误

判断题 8

devm_* 系列函数(如 devm_kzalloc)分配的资源在模块卸载时由模块作者手动释放。 ( )

  • 正确

  • 错误

判断题 9

网络设备驱动程序像字符设备一样通过 /dev/ 下的文件节点访问。 ( )

  • 正确

  • 错误

判断题 10

Rust 内核驱动的一个关键优势是其所有权系统在编译时防止了 UAF 和内存泄漏漏洞。 ( )

  • 正确

  • 错误


选择题 1

以下哪个函数用于向内核注册一个字符设备?

  • A. register_blkdev()

  • B. alloc_chrdev_region() + cdev_init() + cdev_add()

  • C. register_netdev()

  • D. device_create()

选择题 2

设备驱动中对硬件寄存器的访问应该使用哪个关键字修饰指针?

  • A. restrict

  • B. const

  • C. volatile

  • D. static

选择题 3

以下哪种 bottom half 机制可以睡眠?

  • A. tasklet

  • B. softirq

  • C. work_struct (workqueue)

  • D. timer callback

选择题 4

container_of 宏在内核驱动中的主要用途是什么?

  • A. 检查内存是否在容器范围内

  • B. 从内嵌的结构体成员指针反推出包含它的父结构体指针

  • C. 计算结构体的内存占用大小

  • D. 比较两个结构体是否相等

选择题 5

ioctl 命令中,_IO, _IOR, _IOW, _IOWR 宏的区别是什么?

  • A. 分别对应 0/1/2/3 号命令

  • B. 指示数据传输方向:无数据/读/写/读写

  • C. 指定不同的中断优先级

  • D. 区分不同的设备类型(字符/块/网络)

选择题 6

以下哪种情况适合使用 GFP_ATOMIC

  • A. 在文件系统 write() 路径中分配内存

  • B. 在中断处理程序(ISR)中分配内存

  • C. 在用户进程上下文中分配大块内存

  • D. 在模块初始化函数中分配内存

选择题 7

如果驱动需要注册一个中断处理函数,应该使用哪个函数?

  • A. register_irq()

  • B. request_irq()

  • C. add_interrupt()

  • D. setup_irq()

选择题 8

关于设备树(Device Tree)在 Linux 驱动中的作用,以下哪项正确?

  • A. 设备树只用于 x86 架构

  • B. 设备树描述硬件拓扑(设备、中断、寄存器地址),驱动通过 of_* 函数读取

  • C. 设备树是编译进内核的二进制代码

  • D. 设备树存储用户空间应用程序的配置

选择题 9

以下关于 Rust 内核驱动的说法,哪个是正确的?

  • A. Rust 驱动不需要实现 file_operations,Rust 有自己的文件抽象

  • B. Rust 驱动通过 FFI 调用 C 函数,最终挂接到内核的 C 设备模型上

  • C. Rust 驱动运行在独立的 Rust 内核中,与 C 内核通过 IPC 通信

  • D. Rust 驱动不能使用 request_irq,必须用 Rust 的重写版本

选择题 10

内核中 wake_up_interruptible(&wq) 的作用是什么?

  • A. 唤醒一个中断处理程序

  • B. 唤醒等待在等待队列 wq 上的所有进程(且只唤醒 TASK_INTERRUPTIBLE 的进程)

  • C. 触发一个 IRQ

  • D. 使 CPU 进入中断睡眠状态


编程练习题

练习题 1:增强 vdev 驱动()

难度: 简单

为 vdev 驱动添加以下功能:

  1. 支持多个设备实例(使用 minor 号区分)
  2. 支持 epoll/select/poll(实现 .poll 回调)
  3. 添加 /sys/class/vdev_class/vdev/count 属性(使用 DEVICE_ATTR
  4. 编写 bash 脚本自动化测试(读写 + ioctl + 并发)

练习题 2:编写按键中断模拟驱动()

难度: 简单

编写一个字符设备驱动,使用内核定时器模拟按键中断:

  1. 每隔 1-3 秒随机触发”中断”(timer callback)
  2. Timer callback 中调度 work_struct
  3. work_struct 生成一个随机按键码,写入环形缓冲区
  4. 用户空间程序读取按键事件
  5. 支持非阻塞读和 epoll

提示: 使用 mod_timer(), get_random_bytes(), wake_up_interruptible()

练习题 3:C vs Rust 安全检查清单制作()

难度: 简单

阅读一个真实的 Linux 驱动(如 drivers/tty/serial/8250/8250_core.c),制作一个安全检查清单:

  1. 列出所有 kmalloc/kfree 配对,检查是否有潜在的 double-free 或 leak
  2. 列出所有 copy_from_user 调用,检查是否有缓冲区溢出风险
  3. 列出所有锁的获取/释放,检查是否有潜在的死锁
  4. 分析每个 container_of 的使用是否安全

对比:如果这个驱动用 Rust 重写,以上哪些问题会被编译器自动发现?


知识网络