Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

3.14 设备驱动

本章回答

一个字符设备从 insmod 到 /dev/mychar0 出现,内核里发生了什么?file_operations 的 read 怎么把数据安全地交给用户态?为什么驱动里不能直接解引用用户指针?中断来了,驱动怎么知道是自己的设备,又怎么唤醒等数据的进程?probe() 是谁调的?

读完你会

写出一个完整的字符设备驱动(cdev + sysfs + 中断 + copy_to_user);说清 dev_t、cdev、class、device 四者的注册顺序;用 ioctl 传结构与参数;解释 platform_driver 的匹配与 devm_* 的资源管理;在 QEMU 里完成加载、读写、观测的全流程。

3.12 与 3.13 讲的是「文件系统 → 块层」。但 Linux 的设备不止磁盘:键盘、串口、摄像头、GPIO、网卡,各自有一套驱动。这一章讲驱动怎么把自己接进 VFS,以及接进去之后怎么安全地读写数据、处理中断、管理资源。

驱动的本质是一张方法表:实现 file_operations 里的几个函数,注册一个设备号,用户态就能用 open/read/write/ioctl 操作它。复杂的是周边的纪律:用户指针必须 copy_*_user、中断上下文不能睡眠、资源要用 devm_* 管理、设备要用设备树/ACPI 描述。这些纪律在 3.3、3.8、3.11 都出现过,本章把它们组装起来。

统一背景:x86-64 QEMU + 6.8,模块在虚机里加载,不在宿主上 insmod。母本:李治军第 8 章(0.11 的设备驱动,从 out 指令到文件视图)、奔跑吧第 5 章(中断)、Kerrisk 第 64 章(pty 作为一种字符设备)。

3.14.1 设备模型:总线、设备、驱动

Linux 用一套统一的「设备模型」描述硬件,与具体总线无关:

  /sys/devices/                    ← 物理拓扑(PCI、platform、USB…)
      └── platform/demo@10000000/
              ├── driver -> ../../../bus/platform/drivers/demo
              ├── uevent
              └── ...
  /sys/bus/platform/
      ├── devices/demo@10000000/   ← 设备
      └── drivers/demo/            ← 驱动
              └── bind / unbind
  /sys/class/mychar/
      └── mychar0/                 ← 类设备(按功能分类,不是拓扑)
              ├── dev  (major:minor)
              └── uevent

三类对象的职责:

对象结构作用
总线struct bus_type匹配设备与驱动(match()),如 platform_bus_type、pci_bus_type
设备struct device硬件实例(地址、中断、DMA 能力),挂在总线上
驱动struct device_driver一组回调(probe/remove),声明能匹配哪些设备

匹配靠 ID 表:platform 总线用 of_match_table(设备树)或 acpi_match_table(ACPI),PCI 用 vendor/device ID。匹配成功后内核调用驱动的 probe(),由它初始化硬件、注册字符设备/网络设备/块设备。

驱动的注册顺序(字符设备):

alloc_chrdev_region()   申请设备号(major, minor)
cdev_init() + cdev_add()  把 file_operations 绑到设备号
class_create()          创建 /sys/class/mychar
device_create()         创建 /dev/mychar0 与 uevent

卸载时严格逆序:device_destroy → class_destroy → cdev_del → unregister_chrdev_region。

3.14.2 一个完整的字符设备驱动

下面的 mychar.c 实现了 open/read/write/ioctl/release,用 mutex 保护缓冲区、用等待队列支持阻塞读、用 copy_to_user/copy_from_user 与用户态交换数据。

// mychar.c — 最小但完整的字符设备(Linux 6.8)
#include <linux/cdev.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/ioctl.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/mutex.h>
#include <linux/slab.h>
#include <linux/uaccess.h>
#include <linux/wait.h>

#define MYCHAR_MAX 256
#define MYCHAR_MAGIC 'm'
#define MYCHAR_GET _IOR(MYCHAR_MAGIC, 1, int)
#define MYCHAR_SET _IOW(MYCHAR_MAGIC, 2, int)

static dev_t mychar_dev;
static struct cdev mychar_cdev;
static struct class *mychar_class;

static char mychar_buf[MYCHAR_MAX];
static int mychar_len;
static int mychar_value = 42;
static DEFINE_MUTEX(mychar_lock);
static DECLARE_WAIT_QUEUE_HEAD(mychar_wq);

static int mychar_open(struct inode *inode, struct file *file)
{
	pr_info("mychar: open by %s (pid %d)\n", current->comm, current->pid);
	return 0;
}

static int mychar_release(struct inode *inode, struct file *file)
{
	pr_info("mychar: release\n");
	return 0;
}

static ssize_t mychar_read(struct file *file, char __user *ubuf,
			   size_t count, loff_t *ppos)
{
	size_t len;

	if (*ppos != 0)
		return 0;                       /* 简易实现:一次读完 */

	/* 没有数据就睡眠,直到 write 唤醒;信号可打断 */
	if (wait_event_interruptible(mychar_wq, READ_ONCE(mychar_len) > 0))
		return -ERESTARTSYS;

	mutex_lock(&mychar_lock);
	len = min(count, (size_t)mychar_len);
	if (copy_to_user(ubuf, mychar_buf, len)) {
		mutex_unlock(&mychar_lock);
		return -EFAULT;
	}
	mutex_unlock(&mychar_lock);

	*ppos += len;
	return len;
}

static ssize_t mychar_write(struct file *file, const char __user *ubuf,
			    size_t count, loff_t *ppos)
{
	size_t len = min(count, (size_t)MYCHAR_MAX);

	if (copy_from_user(mychar_buf, ubuf, len))
		return -EFAULT;

	mutex_lock(&mychar_lock);
	mychar_len = len;
	mutex_unlock(&mychar_lock);

	wake_up_interruptible(&mychar_wq);      /* 唤醒阻塞的 read */
	return len;
}

static long mychar_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{
	int val;

	switch (cmd) {
	case MYCHAR_GET:
		return mychar_value;
	case MYCHAR_SET:
		if (get_user(val, (int __user *)arg))
			return -EFAULT;
		mychar_value = val;
		return 0;
	default:
		return -ENOTTY;                 /* 未知 ioctl 的标准错误 */
	}
}

static const struct file_operations mychar_fops = {
	.owner          = THIS_MODULE,
	.open           = mychar_open,
	.release        = mychar_release,
	.read           = mychar_read,
	.write          = mychar_write,
	.unlocked_ioctl = mychar_ioctl,
	.llseek         = no_llseek,
};

static int __init mychar_init(void)
{
	int ret;

	ret = alloc_chrdev_region(&mychar_dev, 0, 1, "mychar");
	if (ret)
		return ret;

	cdev_init(&mychar_cdev, &mychar_fops);
	mychar_cdev.owner = THIS_MODULE;
	ret = cdev_add(&mychar_cdev, mychar_dev, 1);
	if (ret)
		goto err_region;

	mychar_class = class_create("mychar");   /* 6.4+ 只有一个参数 */
	if (IS_ERR(mychar_class)) {
		ret = PTR_ERR(mychar_class);
		goto err_cdev;
	}
	if (IS_ERR(device_create(mychar_class, NULL, mychar_dev, NULL, "mychar0"))) {
		ret = -ENODEV;
		goto err_class;
	}

	pr_info("mychar: registered major=%d minor=%d\n",
		MAJOR(mychar_dev), MINOR(mychar_dev));
	return 0;

err_class:
	class_destroy(mychar_class);
err_cdev:
	cdev_del(&mychar_cdev);
err_region:
	unregister_chrdev_region(mychar_dev, 1);
	return ret;
}

static void __exit mychar_exit(void)
{
	device_destroy(mychar_class, mychar_dev);
	class_destroy(mychar_class);
	cdev_del(&mychar_cdev);
	unregister_chrdev_region(mychar_dev, 1);
	pr_info("mychar: unloaded\n");
}

module_init(mychar_init);
module_exit(mychar_exit);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("minimal char device with buffer, ioctl and blocking read");

配套 Makefile 与 3.1 的模板相同(obj-m += mychar.o),编译后在虚机里 insmod mychar.ko。dmesg 会打印主次设备号,/dev/mychar0 由 udev(或 devtmpfs)自动创建;如果没有,用 mknod /dev/mychar0 c <major> 0 手工建。

几个关键点:

  • copy_to_user 返回「未拷贝的字节数」,0 表示全部成功。它不能在内核态直接解引用用户指针,因为用户页可能不在内存(会触发缺页),而 SMAP 会阻止内核直接访问。
  • wait_event_interruptible 让 read 阻塞,write 里的 wake_up_interruptible 唤醒它。等待条件用 READ_ONCE(mychar_len) 读,避免编译器缓存。
  • llseek = no_llseek 表示不支持随机访问,lseek 返回 ESPIPE。不设置的话默认 no_llseek 也行,显式写出来更清楚。
  • class_create("mychar") 在 6.4 之后只有一个参数,旧代码的 class_create(THIS_MODULE, "mychar") 在 6.8 上编译不过。
  • 错误路径逆序清理:每个 goto err_* 只回滚已完成的步骤,这是内核驱动的基本功。

用户态测试程序:

/* mychar_test.c — 读写与 ioctl
 * 编译: cc -std=c17 -D_GNU_SOURCE -Wall -Wextra -O2 -o mychar_test mychar_test.c
 */
#define _GNU_SOURCE
#include <fcntl.h>
#include <stdio.h>
#include <string.h>
#include <sys/ioctl.h>
#include <unistd.h>

#define MYCHAR_MAGIC 'm'
#define MYCHAR_GET _IOR(MYCHAR_MAGIC, 1, int)
#define MYCHAR_SET _IOW(MYCHAR_MAGIC, 2, int)

int main(void)
{
	char buf[64] = "hello, driver\n";
	char out[64] = {0};
	int fd, val;

	fd = open("/dev/mychar0", O_RDWR);
	if (fd == -1) { perror("open"); return 1; }

	val = ioctl(fd, MYCHAR_GET, 0);
	printf("ioctl GET -> %d\n", val);
	val = 7;
	if (ioctl(fd, MYCHAR_SET, &val) == -1) perror("ioctl SET");

	if (write(fd, buf, strlen(buf)) == -1) perror("write");
	ssize_t n = read(fd, out, sizeof out - 1);
	if (n < 0) { perror("read"); return 1; }
	printf("read %zd bytes: %s", n, out);

	close(fd);
	return 0;
}

预期输出 ioctl GET -> 42、ioctl SET 成功、read 14 bytes: hello, driver。第二次 read 会阻塞(因为 mychar_len 仍大于 0,*ppos 非 0 直接返回 0)——这正是「简易实现」的边界,真实驱动要维护读位置与环形缓冲。

字符设备的五个坑
  1. 直接解引用用户指针:*(int *)arg 会触发缺页或 SMAP 错误。必须 get_user/copy_from_user。安全模块(如 HARDENED_USERCOPY)会抓这类错误。
  2. copy_to_user 返回值判断反了:它返回「没拷贝成功的字节数」,if (copy_to_user(...)) 才是错误分支。
  3. 忘了 cdev_del/unregister_chrdev_region:模块卸载后设备号泄漏,再次加载会拿到新号,/dev 里的旧节点失效。
  4. ioctl 不检查 cmd:未知命令应返回 ENOTTY,否则用户态探测能力(如 hdparm)会误判。
  5. 在 open 里做耗时初始化:open 应尽量轻,重活放到 probe 或第一次 read;否则 open 阻塞会影响 udev 与 shell。

3.14.3 中断与 DMA:驱动与硬件的握手

字符设备的「数据到达」通常由中断通知。注册中断:

/* 上半部:确认中断、唤醒等待者,不做耗时工作 */
static irqreturn_t mychar_irq(int irq, void *dev_id)
{
	struct mychar_dev *dev = dev_id;

	/* 读设备状态寄存器,确认是自己的中断 */
	if (!(readl(dev->regs + REG_STATUS) & STATUS_RX_READY))
		return IRQ_NONE;                /* 共享中断:不是我的 */

	writel(STATUS_RX_READY, dev->regs + REG_STATUS);   /* 清中断 */
	dev->rx_len = readl(dev->regs + REG_RX_LEN);
	wake_up_interruptible(&dev->wq);    /* 唤醒 read */
	return IRQ_HANDLED;
}

/* 注册:devm_ 版本随设备自动释放 */
ret = devm_request_irq(&pdev->dev, irq, mychar_irq, IRQF_SHARED,
		       "mychar", dev);

中断处理的纪律(3.3 讲过,这里落地):

  • 上半部只做必须立刻做的:读状态、清中断、唤醒;耗时解析放到 workqueue/线程化中断。
  • 共享中断要判断「是不是我的」,不是就返回 IRQ_NONE,让内核继续找下一个 handler。
  • 不能睡眠:不能 mutex_lock、不能 copy_to_user、不能 kmalloc(GFP_KERNEL)。
  • dev_id 必须唯一且稳定,free_irq 时用它区分共享线上的 handler。

DMA 让设备直接读写内存,CPU 不参与拷贝。两种映射:

类型接口特点用途
一致性(coherent)dma_alloc_coherent()物理连续、CPU 与设备看到一致长期存在的环形缓冲、描述符
流式(streaming)dma_map_single()/dma_map_sg()临时映射,需手动同步单次传输的数据缓冲
/* 一致性 DMA:返回 CPU 虚拟地址与设备地址 */
void *cpu_addr = dma_alloc_coherent(dev, size, &dma_handle, GFP_KERNEL);
/* 流式 DMA:映射一段已有缓冲 */
dma_addr_t dma = dma_map_single(dev, buf, len, DMA_TO_DEVICE);
if (dma_mapping_error(dev, dma)) { ... }
/* 传输完成后解除映射(读方向要先同步) */
dma_unmap_single(dev, dma, len, DMA_FROM_DEVICE);

有 IOMMU 时,dma_map_* 会建立 I/O 页表,设备看到的是 IOVA 而不是物理地址——这既是隔离(设备不能访问任意内存),也是「物理不连续也能 DMA」的基础。

3.14.4 ioctl、mmap 与 poll

ioctl 是「非读写操作」的入口。命令号用 _IO/_IOR/_IOW/_IOWR 宏编码方向、大小与类型:

#define MYCHAR_MAGIC 'm'
#define MYCHAR_GET _IOR(MYCHAR_MAGIC, 1, int)      /* 用户态读 */
#define MYCHAR_SET _IOW(MYCHAR_MAGIC, 2, int)      /* 用户态写 */
#define MYCHAR_XFER _IOWR(MYCHAR_MAGIC, 3, struct mychar_req)  /* 双向 */

宏里编码了「幻数(8 位)+ 序号(8 位)+ 方向(2 位)+ 大小(14 位)」,所以内核可以用 _IOC_DIR(cmd)/_IOC_SIZE(cmd) 解出来,32 位兼容层也能据此转换。自定义幻数要避开已用的(Documentation/userspace-api/ioctl/ioctl-number.rst)。

mmap 让用户态直接访问设备内存(如帧缓冲)。驱动实现:

static int mychar_mmap(struct file *file, struct vm_area_struct *vma)
{
	struct mychar_dev *dev = file->private_data;
	unsigned long size = vma->vm_end - vma->vm_start;

	if (size > dev->buf_size)
		return -EINVAL;
	/* 把内核页的 pfn 映射到用户态;vma->vm_page_prot 决定权限 */
	if (remap_pfn_range(vma, vma->vm_start,
			    virt_to_phys(dev->buf) >> PAGE_SHIFT,
			    size, vma->vm_page_prot))
		return -EAGAIN;
	return 0;
}

remap_pfn_range() 只适合「物理页固定、不会被回收」的内存(dma_alloc_coherent、vmalloc 的页、预留内存)。普通 kmalloc 的内存在 CONFIG_DEBUG_VM 下可能被拒绝映射,因为页可能被迁移。

poll 支持 select/poll/epoll:

static __poll_t mychar_poll(struct file *file, poll_table *wait)
{
	struct mychar_dev *dev = file->private_data;
	__poll_t mask = 0;

	poll_wait(file, &dev->wq, wait);
	if (READ_ONCE(dev->rx_len) > 0)
		mask |= EPOLLIN | EPOLLRDNORM;
	if (dev->tx_ready)
		mask |= EPOLLOUT | EPOLLWRNORM;
	return mask;
}

poll_wait() 只注册等待队列,不阻塞;真正的睡眠由 VFS 的 do_poll() 完成。第 4 篇会讲 epoll 怎么利用这个回调(4.10)。

3.14.5 platform_driver:SoC 驱动的标准形态

x86 的 PCI 设备有枚举机制,ARM/嵌入式的大量设备没有「自描述」能力,靠设备树(DT)或 ACPI 描述。platform_driver 是这类「平台设备」的驱动模板:

static int demo_probe(struct platform_device *pdev)
{
	struct demo_dev *dev;
	struct resource *res;
	int irq, ret;

	dev = devm_kzalloc(&pdev->dev, sizeof(*dev), GFP_KERNEL);
	if (!dev)
		return -ENOMEM;
	platform_set_drvdata(pdev, dev);

	res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
	dev->regs = devm_ioremap_resource(&pdev->dev, res);
	if (IS_ERR(dev->regs))
		return PTR_ERR(dev->regs);

	irq = platform_get_irq(pdev, 0);
	if (irq < 0)
		return irq;
	ret = devm_request_irq(&pdev->dev, irq, demo_irq, 0, "demo", dev);
	if (ret)
		return ret;

	/* 读 DT/ACPI 属性(两种后端统一接口) */
	if (device_property_read_u32(&pdev->dev, "demo,clock-hz", &dev->clk_hz))
		dev->clk_hz = 1000000;          /* 默认值 */

	dev_info(&pdev->dev, "demo probed, regs=%pR irq=%d clk=%u\n",
		 res, irq, dev->clk_hz);
	return 0;
}

static const struct of_device_id demo_of_match[] = {
	{ .compatible = "demo,platform-v1" },
	{ /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, demo_of_match);

static struct platform_driver demo_driver = {
	.probe  = demo_probe,
	.remove = demo_remove,
	.driver = {
		.name           = "demo-platform",
		.of_match_table = demo_of_match,
	},
};
module_platform_driver(demo_driver);

三个要点:

  1. devm_* 自动释放:devm_kzalloc、devm_ioremap_resource、devm_request_irq 注册到 &pdev->dev 的资源链上,remove 或 probe 失败时自动回滚,不用手写 goto err 链。
  2. probe 返回 -EPROBE_DEFER 表示「依赖还没就绪」(如时钟、 regulator、GPIO),驱动核心会稍后重试。这比调整 initcall 顺序正确(3.2 例 3.2.1)。
  3. device_property_read_u32() 同时支持 DT 与 ACPI,比 of_property_read_u32() 更通用;新驱动优先用它。

MMIO 与端口 I/O 是 x86 的两套地址空间。 devm_ioremap_resource() 映射的是 IORESOURCE_MEM(内存映射,之后用 readl()/writel());老式设备用独立的 64 KiB 端口空间(IORESOURCE_IO),要用 inb()/outb(),且先 request_region() 占住端口:

方式资源类型映射/访问典型设备
MMIOIORESOURCE_MEMdevm_ioremap_resource() + readl/writelPCIe 设备、SoC 寄存器
端口 I/OIORESOURCE_IOdevm_request_region() + inb/outb串口、ACPI、老式 IDE

把 IORESOURCE_IO 交给 devm_ioremap_resource() 会失败;反过来对 MMIO 用 inb() 会读到无意义的值。0.11 的块设备驱动就是靠 outb 发命令、靠中断收完成,6.8 的块层把这些封装在 request_queue 后面(3.13)。

3.14.6 udev、sysfs 与热插拔

device_create() 会产生一个 uevent(KOBJ_ADD),用户态 udevd 收到后:

  • 读 /sys/class/mychar/mychar0/dev 得到 major:minor;
  • 按 /etc/udev/rules.d/ 的规则决定设备节点名、权限、属主;
  • mknod 创建 /dev/mychar0,或由 devtmpfs 提前建好。

观测命令:

# 看 uevent 流(插拔设备时)
udevadm monitor --udev --property

# 看设备的属性与父设备链
udevadm info -a -n /dev/mychar0

# 看 sysfs 里的类设备
ls -l /sys/class/mychar/mychar0/
cat /sys/class/mychar/mychar0/dev        # 主次设备号

# 看已注册的字符设备与主设备号
cat /proc/devices | grep mychar

驱动也可以暴露自定义 sysfs 属性:

static ssize_t value_show(struct device *dev, struct device_attribute *attr, char *buf)
{
	return sysfs_emit(buf, "%d\n", mychar_value);
}

static ssize_t value_store(struct device *dev, struct device_attribute *attr,
			   const char *buf, size_t count)
{
	int val, ret = kstrtoint(buf, 0, &val);
	if (ret)
		return ret;
	mychar_value = val;
	return count;
}
static DEVICE_ATTR_RW(value);

/* 在 device_create 之后 */
ret = device_create_file(&pdev->dev, &dev_attr_value);

DEVICE_ATTR_RW 生成 value_show/value_store 与权限 0644,用户态 echo 5 > /sys/.../value 就能改。sysfs 属性是「一个文件一个值」的约定,不要在里面做复杂解析。

3.14.7 实验:在 QEMU 里跑通全流程

# 1. 宿主上编译模块(针对 3.1 的源码树)
make -C ~/linux-6.8 M=$PWD modules
# 2. 把 mychar.ko 与 mychar_test 打进 initramfs,启动 QEMU
# 3. 虚机内:
insmod /mychar.ko
dmesg | tail -5
ls -l /dev/mychar0
cat /proc/devices | grep mychar
./mychar_test
# 4. 观测
cat /sys/class/mychar/mychar0/dev
udevadm info -a -n /dev/mychar0 | head -20
rmmod mychar
dmesg | tail -3

预期:insmod 后出现 registered major=240 minor=0(major 由内核动态分配,不同机器不同),/dev/mychar0 存在,mychar_test 输出 ioctl 与读写结果,rmmod 后 dmesg 有 unloaded。如果 /dev/mychar0 没出现,检查是否挂了 devtmpfs、udevd 是否在跑,或手工 mknod。

例 3.14.1 · 一个「read 永远阻塞」的驱动 bug

某驱动在 read 里用 wait_event_interruptible(wq, dev->len > 0),但中断处理程序只更新了 dev->len,忘了 wake_up_interruptible(&wq)。现象是:数据明明到了(用 cat /proc/interrupts 能看到中断计数增长),read 却一直不返回。

排查:cat /proc/PID/wchan 显示 wait_event_interruptible,/proc/PID/stack 显示卡在 mychar_read;在中断处理程序里加 pr_info 确认中断确实进了。结论是「唤醒丢失」。

正确写法是「先更新状态,再唤醒」,且两者之间不能有 return:

dev->len = n;
smp_wmb();                       /* 确保 len 对读者可见 */
wake_up_interruptible(&dev->wq); /* 必须在状态更新之后 */

顺序反了会丢唤醒:读者检查 len == 0 后、睡眠前,写者更新 len 并唤醒——唤醒发生在读者入队之前,读者随后睡下去,永远等不到。wait_event 的宏用「检查条件 + 设置等待」的原子序列避免这个窗口,但前提是写者「先改状态再唤醒」。

3.14.8 设备模型的三张表:device、driver、bus_type

这一节回答一个具体问题:/sys/devices 里那个目录、/sys/bus/platform/drivers/demo 里那个目录,以及驱动里的 struct platform_driver,三者到底靠哪些字段互相找到,又是谁在什么时候释放它们?

3.14.1 给的是鸟瞰图:总线匹配设备与驱动。真正读代码时,难点不在「匹配」这个动作,而在生命周期:struct device 是引用计数的对象,probe() 成功不等于设备永远存在,remove() 之后 struct device 还可能被 /sys 里的一个打开的 fd 钉住。把三张表的字段拆开看,很多「驱动卸载后 use-after-free」「probe 失败后资源泄漏」的 bug 都能提前避免。

struct device:硬件的软件代表

struct device 定义在 include/linux/device.h。6.8 的字段很多,下面只列与驱动作者直接相关的部分:

字段类型谁维护含义与陷阱
kobjstruct kobject驱动核心引用计数与 sysfs 入口;kobject 的 release 必须释放整个 device
parentstruct device *注册者父设备,决定 /sys/devices 的拓扑与电源管理顺序
busstruct bus_type *device_add()所在总线;为 NULL 时不会匹配任何驱动
driverstruct device_driver *驱动核心当前绑定的驱动;/sys/devices/.../driver 符号链接指向它
platform_datavoid *板级代码老式平台数据,DT 世界已基本淘汰
driver_datavoid *驱动dev_set_drvdata()/dev_get_drvdata() 的存储;platform_set_drvdata 是它的包装
of_nodestruct device_node *OF 核心设备树节点;ACPI 设备为 NULL,新代码用 fwnode
fwnodestruct fwnode_handle *固件层统一 DT/ACPI/软件节点的抽象,device_property_*() 走它
devtdev_t字符/块设备主次设备号;/sys/dev/char/ 下的编号来自它
classstruct class *驱动功能分类(/sys/class/...),与总线拓扑正交
groupsconst struct attribute_group **驱动设备级 sysfs 属性组,dev_groups 的运行时版本
releasevoid (*)(struct device *)驱动必须提供最后一个引用释放时调用;忘记设置会打印 Device ... does not have a release()
dma_masku64 *总线/驱动设备可寻址的物理地址位宽;dma_alloc_coherent() 用它判断是否需要 bounce
coherent_dma_masku64总线/驱动一致性 DMA 的地址掩码,常见 32 位设备为 DMA_BIT_MASK(32)
dma_opsconst struct dma_map_ops *架构/IOMMUDMA 映射实现;x86 上由 IOMMU 驱动替换
numa_nodeint总线设备就近的 NUMA 节点,-1 表示未知

这张表里最容易被忽视的是 release。struct device 不是栈对象,也不该由驱动自己 kfree();正确做法是内嵌在一个更大的结构里,在 release 回调里释放那个外层结构:

/* 常见模式:device 内嵌在驱动私有结构里 */
struct demo_dev {
	struct device dev;        /* 必须第一个或显式 container_of */
	struct cdev cdev;
	void __iomem *regs;
	int irq;
};

static void demo_dev_release(struct device *dev)
{
	struct demo_dev *d = container_of(dev, struct demo_dev, dev);

	/* 这里只释放「设备对象」本身,硬件资源应在 remove() 里先关掉 */
	kfree(d);
}

static struct demo_dev *demo_dev_alloc(struct device *parent)
{
	struct demo_dev *d = kzalloc(sizeof(*d), GFP_KERNEL);

	if (!d)
		return NULL;
	device_initialize(&d->dev);       /* 初始化 kobject,引用计数 = 1 */
	d->dev.parent = parent;
	d->dev.release = demo_dev_release;
	dev_set_name(&d->dev, "demo%d", 0);
	return d;
}

device_initialize() 与 device_register() 的区别是「初始化」与「发布」:前者只建立 kobject、设置引用计数为 1,后者把设备加到总线、创建 sysfs 目录、发 uevent。注册失败时要用 put_device() 而不是 kfree(),否则 release 不会走、引用计数也乱。

struct device_driver:一组回调,不是硬件

字段类型含义
nameconst char *驱动名;platform 总线在没有 DT 匹配表时用它和设备名比对
busstruct bus_type *所属总线,由 driver_register() 填写/校验
ownerstruct module *通常是 THIS_MODULE,防止驱动在用的时候被卸载
probeint (*)(struct device *)匹配成功后调用;返回 0 表示绑定成功
removevoid (*)(struct device *)解绑时调用;6.8 里还有返回 void 的 remove 与返回 int 的 remove_new 过渡
shutdownvoid (*)(struct device *)关机/重启前调用,用来把设备置于安全状态
of_match_tableconst struct of_device_id *DT 匹配表,compatible 字符串
acpi_match_tableconst struct acpi_device_id *ACPI 匹配表,_HID/_CID
pmstruct dev_pm_ops系统休眠/运行时电源管理回调
groupsconst struct attribute_group **驱动级 sysfs 属性(/sys/bus/.../drivers/demo/)
deferred_probestruct list_head内部使用:等待依赖就绪的设备链表

probe() 的返回值有明确语义,不能随便返回:

返回值含义驱动核心的动作
0绑定成功建立 device->driver 链接,发 KOBJ_BIND uevent
-ENODEV / -ENXIO这个设备我不支持不再重试;其他驱动还可以试
-EPROBE_DEFER依赖(时钟、 regulator、GPIO)还没就绪放回 deferred 链表,依赖注册后重试
-ENOMEM内存不足绑定失败,通常不自动重试
其他负值探测失败打印错误,解绑已建立的 devres 资源

-EPROBE_DEFER 是平台驱动最常见的「正确失败」。它和 -ENODEV 的区别不是错误码好看,而是会不会重试:返回 -ENODEV 会让内核认为「这个驱动永远不适合这个设备」,即使时钟驱动 100 ms 后才注册,也不会再试。

struct bus_type:匹配规则的拥有者

字段/回调作用
name总线名,决定 /sys/bus/<name>/
dev_name设备命名回调,未设置时用 dev_set_name() 的名字
matchint (*)(struct device *, struct device_driver *),返回 1 表示匹配
uevent向用户态补充环境变量(MODALIAS= 等)
probe总线级 probe,在驱动 probe 之前/之后做总线特有初始化
remove总线级 remove
shutdown总线级关机回调
online/offlineCPU 热插拔(CPU 总线专用)
bus_groups/dev_groups/drv_groups总线、设备、驱动的默认 sysfs 属性组

匹配过程不是「总线遍历所有设备」,而是双向的:

driver_register(drv)
  → bus_add_driver()
      → driver_attach(drv)
          → bus_for_each_dev(bus, NULL, drv, __driver_attach)
              → driver_match_device(drv, dev)
                  → bus->match(dev, drv)       /* 或 of/acpi 匹配表 */
              → driver_probe_device(drv, dev)
                  → really_probe(dev, drv)
                      → dev->bus->probe(dev)    /* 可选 */
                      → drv->probe(dev)         /* 你的 probe */

设备先注册时走对称路径:device_add() → bus_probe_device() → device_initial_probe() → __device_attach(),遍历总线上的驱动找匹配。两条路径最终都进 really_probe(),所以「先有设备还是先有驱动」不影响结果,只影响谁触发。

引用计数:为什么 remove 之后设备还活着

struct device 的 kobject 引用计数来自多个方向:总线、driver core、sysfs 打开的文件、devm_* 资源、用户态 open("/sys/...")。因此 device_del() 只做「从总线摘除、删 sysfs、发 uevent」,真正的内存释放要等最后一个引用 put_device()。

这带来一个常见现象:rmmod 返回后,/sys/devices/... 目录可能还短暂存在,或者 dmesg 里 remove() 已经打印,但 release() 还没打印。这不是泄漏,而是还有引用没放。排查时看:

# 设备的引用计数(kobject 的 refcount)
cat /sys/devices/platform/demo.0/uevent
# 谁打开了 sysfs 文件
lsof /sys/devices/platform/demo.0/uevent 2>/dev/null
# 内核里所有 device 的 kobject 名字与引用
bpftrace -e 'kprobe:kobject_get { @[kstack(3)] = count(); }'

反过来,如果 release() 一直不打印、/sys 目录已消失,常见原因是驱动在 probe() 里 device_register() 了子设备却没有在 remove() 里 device_unregister(),子设备持有父设备的引用,形成引用环。

设备模型字段的六个坑
  1. 以为 dev_set_drvdata() 会自动清零:它只是写 dev->driver_data,卸载/重新 probe 前要自己置 NULL,否则 remove() 里可能拿到上一次的野指针。
  2. device_register() 失败后直接 kfree(dev):必须先 put_device(),让 release 走完;直接 kfree 会让 kobject 的引用计数与内存生命周期脱节。
  3. probe() 里 devm_kzalloc() 后返回 -EPROBE_DEFER:devres 会在 probe 失败时自动释放,这是对的;但如果你把指针存进了全局变量,defer 之后全局变量就是野指针。
  4. remove() 里先 kfree 再 device_unregister():device_unregister() 还会访问 dev 的字段(名字、parent),顺序反了就是 UAF。
  5. platform 驱动只写 .name 不写 of_match_table:DT 系统上设备名是 demo@10000000,和驱动名 demo 不一定相等,匹配不上;要么写 compatible,要么用 MODULE_ALIAS。
  6. 把 struct device 放在栈上:device_register() 会把它的地址挂进总线链表,函数返回后链表里就是野指针。struct device 必须动态分配或内嵌在长生命周期对象里。

观测:一次 probe 到底走了哪些函数

不用改驱动,用 ftrace 的 function_graph 限定根函数,就能看到 really_probe() 的完整子树:

cd /sys/kernel/tracing
echo 0 > tracing_on
echo function_graph > current_tracer
echo 'really_probe' > set_graph_function
echo 1 > tracing_on
modprobe demo_platform
echo 0 > tracing_on
cat trace | sed -n '1,60p'

预期能看到 really_probe() → devres_open_group() → demo_probe() → devm_kzalloc() → devm_ioremap_resource() 的层次,以及每个 devm_* 的耗时。如果 demo_probe() 的耗时占了 90%,问题在驱动自己;如果时间花在 driver_deferred_probe_trigger() 或 wait_for_device_probe(),问题在依赖顺序。

用 bpftrace 统计「probe 失败原因」,比逐个看 dmesg 快:

bpftrace -e '
kretprobe:really_probe
{
	@rc[retval] = count();
}
interval:s:10 { exit(); }'

retval 为 0 是成功,-517 是 -EPROBE_DEFER,-19 是 -ENODEV。如果 -517 持续增长而设备一直没起来,说明某个依赖驱动永远没注册,deferred 链表会越积越长,/sys/kernel/debug/devices_deferred 里能看到等待中的设备名与原因。

3.14.9 从 open 到 read:VFS 怎样找到你的 fops

这一节回答:用户态 open("/dev/mychar0") 返回一个 fd 的过程中,内核在哪一步把 struct file_operations 换成你的实现?为什么 cdev_add() 之后还要有 device_create(),两者各自解决什么?

字符设备驱动最容易「看起来注册了但 open 返回 ENODEV」,原因往往是把「设备号注册」「cdev 注册」「inode 出现」三件事混为一谈。把 VFS 的调用链摊开,每一步失败的 errno 都很明确。

打开路径的七个站点

sys_openat(AT_FDCWD, "/dev/mychar0", O_RDWR, 0)
  → do_filp_open()
      → path_openat()
          → do_open()
              → vfs_open(path, file)
                  → do_dentry_open(file, path->dentry, open)
                      → f_op = file->f_inode->i_fop
                      → f_op->open(inode, file)        /* chrdev_open */
                          → kobj_lookup(cdev_map, inode->i_rdev, &idx)
                          → cdev_get(p)                 /* 引用计数 +1 */
                          → replace_fops(file, new_fops) /* 换成驱动的 fops */
                          → new_fops->open(inode, file)  /* 你的 mychar_open */

七个站点的失败语义:

站点失败原因用户态看到
path_openat()路径不存在、权限不足ENOENT / EACCES
do_dentry_open()inode 没有 i_fopENXIO
chrdev_open()i_rdev 没在 cdev_map 里ENXIO
cdev_get()cdev 正在被删除ENXIO
你的 open()你自己返回负数原样返回
open() 成功但 O_NONBLOCK 未处理后续 read 阻塞不是 open 的错
device_create() 没做/dev 节点不存在ENOENT(在路径阶段)

关键在 chrdev_open():VFS 先看 inode 的默认 i_fop(字符设备统一是 def_chr_fops),再由 chrdev_open() 用 inode->i_rdev 去 cdev_map 里查你 cdev_add() 时登记的 struct cdev。查到后 replace_fops(file, cdev->ops),把 file->f_op 换成你的表,然后调用你的 .open。所以:

  • alloc_chrdev_region() 只分配号,不建立映射;
  • cdev_add() 才把「号 → cdev → fops」写进 cdev_map;
  • device_create() 只负责在 sysfs/devtmpfs 里造节点与 uevent,不参与 open 的查找。

手工 mknod /dev/mychar0 c 240 0 能打开,就是因为 open 只看 i_rdev 与 cdev_map,不看 /sys。反过来,device_create() 成功但 cdev_add() 失败,节点存在、open 却返回 ENXIO——这是「节点在、驱动不在」的经典组合。

file_operations:每个回调的调用时机

6.8 的 struct file_operations 有二十多个成员,字符驱动常用的就十来个:

成员何时调用返回值/语义常见错误
open每次 open()0 或负 errno在 open 里做重初始化,拖慢 udev
release最后一个 fd 关闭0以为每次 close() 都会调(共享 fd 不会)
readread()/readv()字节数、0(EOF)、负 errno不更新 *ppos,死循环
writewrite()/writev()字节数、负 errno短写不返回实际字节数
read_iter/write_iterread() 的新路径(new_sync_read)iov_iter 语义同时实现 read 与 read_iter,后者优先
unlocked_ioctlioctl()0 或负 errno不检查 _IOC_TYPE,把别的命令当自己的
compat_ioctl32 位进程在 64 位内核0 或负 errno不实现时返回 -ENOTTY,32 位工具直接失败
pollselect/poll/epoll__poll_t 位掩码只 poll_wait() 不返回就绪位,永远超时
mmapmmap()0 或负 errno用 remap_pfn_range 映射可迁移页
llseeklseek()新偏移不设置时默认 no_llseek,返回 ESPIPE
fasyncfcntl(F_SETFL, O_ASYNC)0忘记 kill_fasync(),信号永远不发

read/write 与 read_iter/write_iter 的优先级:VFS 的 vfs_read() 先看 file->f_op->read,没有才走 read_iter;new_sync_read() 则相反。为了不踩坑,新驱动只实现 read_iter/write_iter,或只实现 read/write,不要两套都写。两套都写时,不同系统调用路径可能走不同实现,行为不一致。

open 里该做什么、不该做什么

open() 是「每次打开」的入口,不是「驱动初始化」的入口。它应该做:

  • 检查 O_NONBLOCK、O_CLOEXEC 等标志,存进 file->private_data;
  • 给每个 fd 分配独立的读位置/缓冲(如果需要);
  • 增加设备的使用计数(atomic_inc(&dev->open_count)),防止 rmmod 时还有 fd。

它不该做:

  • 申请大块内存、读固件、等硬件上电(放到 probe());
  • 拿一把全局互斥锁做耗时操作(会阻塞其他 open);
  • 把 file->private_data 指向一个栈变量(返回后失效)。

一个最小但正确的 open/release 组合:

struct mychar_file {
	loff_t pos;
	bool nonblock;
};

static int mychar_open(struct inode *inode, struct file *file)
{
	struct mychar_dev *dev = container_of(inode->i_cdev, struct mychar_dev, cdev);
	struct mychar_file *priv;

	priv = kzalloc(sizeof(*priv), GFP_KERNEL);
	if (!priv)
		return -ENOMEM;
	priv->nonblock = !!(file->f_flags & O_NONBLOCK);
	file->private_data = priv;

	atomic_inc(&dev->open_count);
	stream_open(inode, file);       /* 明确「不支持 llseek」,避免默认 offset 语义混乱 */
	return 0;
}

static int mychar_release(struct inode *inode, struct file *file)
{
	struct mychar_dev *dev = container_of(inode->i_cdev, struct mychar_dev, cdev);

	kfree(file->private_data);
	file->private_data = NULL;
	if (atomic_dec_and_test(&dev->open_count))
		wake_up(&dev->release_wq);  /* 让 rmmod 等待最后一个 fd 关闭 */
	return 0;
}

stream_open() 是新代码常用的辅助:它把 file->f_mode 标成 FMODE_STREAM,并把 f_pos 固定为 0,告诉 VFS「这个设备是字节流,不要按文件偏移处理」。对串口、字符流设备,它比手写 no_llseek 更清楚。

用 bpftrace 验证「谁调用了你的 open」

# 1. 看 chrdev_open 是否命中,以及 inode 的设备号
bpftrace -e '
kprobe:chrdev_open
{
	printf("chrdev_open inode=%p rdev=%u:%u comm=%s\n",
	       arg0, arg0->i_rdev >> 20, arg0->i_rdev & 0xfffff, comm);
}'

# 2. 看你的 open 返回值(模块已加载)
bpftrace -e 'kretprobe:mychar_open { @rc[retval] = count(); }'

# 3. 看 read 的调用者与返回长度
bpftrace -e '
kprobe:mychar_read { @bytes[comm] = sum(arg2); }
interval:s:5 { exit(); }'

第一段里 arg0->i_rdev >> 20 是主设备号、& 0xfffff 是次设备号(new_encode_dev() 的布局),只用于演示;内核里应使用 MAJOR()/MINOR() 宏。如果 chrdev_open 没命中,说明 open 根本没走到字符设备分支——先检查路径是不是指向了别的文件,或 i_fop 被文件系统换掉。

打开路径的五个坑
  1. 「device_create() 会注册 cdev」:不会。它只创建 class device 与 /dev 节点;没有 cdev_add(),open 返回 ENXIO。
  2. 「cdev_add() 之后就能 open」:还需要设备节点(mknod 或 device_create/devtmpfs)。节点是用户态路径,cdev 是内核查找表,两者缺一不可。
  3. 「release 等于 close」:dup()/fork() 共享同一个 struct file,release 只在最后一个引用关闭时调用;每次 close() 都调用的是 flush。
  4. 「O_NONBLOCK 只影响 open」:它存在 file->f_flags 里,read/write/poll 都要检查;只检查 open 会导致非阻塞读仍然睡眠。
  5. 「llseek = no_llseek 可以省掉」:不设置时 file->f_op->llseek 为 NULL,VFS 会返回 ESPIPE;但如果你实现了 read 又忘了 *ppos,cat 会反复读同一段。显式 stream_open() 或 no_llseek 更安全。

3.14.10 platform 与 PCI:两种枚举,同一套 probe

这一节回答:为什么 SoC 驱动写 platform_driver,而显卡/网卡写 pci_driver?probe() 拿到的 struct platform_device 与 struct pci_dev 各自能提供什么,资源(寄存器、中断、DMA)从哪里来?

两种总线的差别在设备怎么被发现:

维度platformPCI
设备来源设备树/ACPI/板级代码静态注册总线枚举(配置空间自描述)
设备标识compatible 字符串或设备名vendor/device ID、class code
寄存器资源reg 属性 → IORESOURCE_MEMBAR 0–5 → IORESOURCE_MEM/IO
中断interrupts 属性 → platform_get_irq()配置空间/MPTABLE/ACPI → pci_irq_vector()
热插拔基本没有(除 CPU/内存)原生支持(PCIe hotplug)
DMA 地址通常 32/64 位由 DT 描述dma_mask 由配置空间/ACPI 决定

platform_device 里到底有什么

struct platform_device 是「设备」而不是「驱动」:

字段含义
name设备名;无 DT 时与 driver->name 匹配
id同名设备的编号,-1 表示只有一个
dev内嵌的 struct device,所有设备模型操作都走它
num_resources / resource寄存器、中断、DMA 的资源数组
id_entry旧式 platform ID 表匹配结果
mfd_cell多功能设备(MFD)子设备描述
driver_override强制绑定到某个驱动名,调试用

资源用 platform_get_resource() 取,不要直接读 pdev->resource[];前者会处理 IORESOURCE_BY_INDEX 等标志,也能配合 devm_ 做生命周期管理:

static int demo_probe(struct platform_device *pdev)
{
	struct demo_dev *d;
	struct resource *res;
	int irq;

	d = devm_kzalloc(&pdev->dev, sizeof(*d), GFP_KERNEL);
	if (!d)
		return -ENOMEM;

	/* 寄存器:从 DT 的 reg 或 ACPI 的 _CRS 来 */
	res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
	if (!res)
		return -ENODEV;
	d->regs = devm_ioremap_resource(&pdev->dev, res);
	if (IS_ERR(d->regs))
		return PTR_ERR(d->regs);

	/* 中断:从 DT 的 interrupts 或 ACPI 来;返回负 errno 而不是 0 */
	irq = platform_get_irq(pdev, 0);
	if (irq < 0)
		return irq;

	d->clk = devm_clk_get_enabled(&pdev->dev, NULL);
	if (IS_ERR(d->clk))
		return dev_err_probe(&pdev->dev, PTR_ERR(d->clk), "clock\n");

	platform_set_drvdata(pdev, d);
	return devm_request_irq(&pdev->dev, irq, demo_irq, 0, dev_name(&pdev->dev), d);
}

dev_err_probe() 是 5.9 之后的好东西:它把 -EPROBE_DEFER 记录到 deferred 原因里,但不打印成错误(defer 是正常流程),其他错误才打印。用它替代 dev_err() 可以避免启动日志被大量 -517 刷屏。

PCI 设备的 probe 多两步

PCI 驱动在 probe() 之前,内核已经替它读了配置空间、分配了 BAR。驱动要做的是:

  1. pci_enable_device():打开 I/O 与 memory decode,否则访问 BAR 会返回全 1;
  2. pci_request_regions() / pcim_iomap_regions():声明 BAR 所有权并映射;
  3. dma_set_mask_and_coherent():告诉内核设备能寻址多少位;
  4. pci_alloc_irq_vectors():MSI/MSI-X 或 legacy INTx;
  5. 注册自己的子系统(register_netdev、cdev_add、misc_register……)。
static int pci_demo_probe(struct pci_dev *pdev, const struct pci_device_id *id)
{
	int ret;

	ret = pcim_enable_device(pdev);          /* devm 版本,remove 时自动 disable */
	if (ret)
		return ret;
	ret = pcim_iomap_regions(pdev, BIT(0), "pci_demo");
	if (ret)
		return ret;

	ret = dma_set_mask_and_coherent(&pdev->dev, DMA_BIT_MASK(64));
	if (ret)
		ret = dma_set_mask_and_coherent(&pdev->dev, DMA_BIT_MASK(32));
	if (ret)
		return ret;

	ret = pci_alloc_irq_vectors(pdev, 1, 1, PCI_IRQ_ALL_TYPES);
	if (ret < 0)
		return ret;

	return 0;
}

pcim_* 是 PCI 子系统的 devm 封装,和 devm_* 一样挂在 &pdev->dev 上。注意 pci_alloc_irq_vectors() 成功后要 pci_free_irq_vectors(),但 pcim_ 系列没有对应的自动释放,所以 remove() 里仍要手写。

probe 的并发与模块引用

probe() 可能并发执行:两个 CPU 同时注册两个同型号设备,驱动核心会分别调用 probe()。因此:

  • 驱动全局状态要用锁或 per-device 结构,不能假设「一次只有一个 probe」;
  • probe() 里创建 sysfs 属性、debugfs 文件时,名字要带设备编号,否则第二个设备注册失败;
  • probe() 成功返回后,驱动核心会 try_module_get(drv->owner),remove() 时 module_put()。所以模块不会被「正在 probe」时卸载,但 rmmod 会等待正在进行的 probe/remove 结束。

/sys/bus/.../drivers/demo/{bind,unbind} 可以手工触发绑定/解绑,是调试 probe 顺序的利器:

echo 0000:00:1f.2 > /sys/bus/pci/drivers/ahci/unbind
echo 0000:00:1f.2 > /sys/bus/pci/drivers/ahci/bind

bind 只对「设备已注册、驱动已注册、但未绑定」的状态有效;设备已经被别的驱动占用时返回 EBUSY,驱动不支持该设备时返回 ENODEV。

platform/PCI 的六个坑
  1. platform_get_irq() 返回 0 当成成功:中断号 0 在有些架构上是合法值,但函数失败返回负 errno。判断 < 0,不要 if (!irq)。
  2. devm_ioremap_resource() 与 devm_ioremap() 混用:前者会 request_mem_region() 声明资源,后者不会;用错会导致 /proc/iomem 里看不到你的区域,热插拔时冲突检测失效。
  3. PCI probe 里忘记 pci_set_master():设备能读写 MMIO,但 DMA 事务总线的 master 位没开,DMA 直接失败(读回全 0 或触发 parity error)。
  4. pci_alloc_irq_vectors() 后按 MSI-X 的数量索引:实际分配数量可能小于请求,必须用返回值作为向量数,不能假设等于 num_vectors。
  5. remove() 里先释放 devm 资源再关硬件:devm 的释放顺序是 LIFO,硬件可能还在 DMA 写入已释放的内存;正确顺序是「先停硬件/关中断,再让 devm 释放」。
  6. 在 probe() 里 msleep(1000) 等硬件稳定:会拖慢整个启动/热插拔线程;用 readl_poll_timeout() 带超时轮询,或把等待放到 workqueue。

3.14.11 DMA 与 IOMMU:谁负责「设备看到的内存」

这一节回答:dma_map_single() 返回的 dma_addr_t 和 virt_to_phys() 有什么区别?为什么有的机器上不 mapping 也能 DMA,有的机器上必须 mapping?dma_alloc_coherent() 的内存为什么不能随便 kfree()?

先分清三个地址:

地址谁用例子
CPU 虚拟地址(VA)CPU 指令dev->buf、kmalloc() 返回值
CPU 物理地址(PA)MMU、页表virt_to_phys()、/proc/iomem
设备地址(DMA/IOVA)设备 DMA 引擎dma_map_single() 返回值、BAR 里的描述符

在没有 IOMMU 的机器上,DMA 地址通常等于物理地址;有 IOMMU 时,dma_map_*() 会在 I/O 页表里建立「IOVA → PA」的映射,设备看到的是 IOVA。驱动不能把 virt_to_phys() 的结果直接写给设备,必须走 DMA API,否则在 IOMMU 机器上设备会访问到错误的地址(甚至触发 IOMMU fault)。

两种映射的语义

类型分配/映射一致性释放适用
一致性dma_alloc_coherent(dev, size, &handle, gfp)强一致(通常 uncached/写合并)dma_free_coherent()长期环形缓冲、描述符、固件共享内存
流式dma_map_single(dev, va, len, dir)需显式 dma_sync_*dma_unmap_single()单次收发、page cache 缓冲、skb
流式 scatterlistdma_map_sg()同上,按段同步dma_unmap_sg()分散/聚集 I/O、NVMe、网卡

流式映射的方向不是「可选的注释」,它决定 cache 维护动作:

  • DMA_TO_DEVICE:CPU 写、设备读;map 时 clean cache,unmap 时不需要 invalidate;
  • DMA_FROM_DEVICE:设备写、CPU 读;map 时 invalidate,unmap 时也要 invalidate;
  • DMA_BIDIRECTIONAL:两者都做,代价最高;
  • DMA_NONE:仅用于调试。

如果方向写反,常见现象是「设备收到了旧数据」或「CPU 读到了旧数据」,而且在 x86 上可能完全正常(x86 的 DMA 与 cache 一致性由硬件保证),到 ARM64 上才复现。这就是「驱动在开发机上好好的,上板子就坏」的经典原因。

/* 发送:CPU 填好缓冲,交给设备读 */
memcpy(buf, data, len);
dma = dma_map_single(dev, buf, len, DMA_TO_DEVICE);
if (dma_mapping_error(dev, dma))
	return -ENOMEM;
writel(dma, dev->regs + REG_TX_ADDR);
writel(len, dev->regs + REG_TX_LEN);
writel(CMD_TX, dev->regs + REG_CMD);
/* 设备完成后中断里 unmap */
dma_unmap_single(dev, dma, len, DMA_TO_DEVICE);

/* 接收:设备写,CPU 读;unmap 前不要碰缓冲 */
dma = dma_map_single(dev, buf, len, DMA_FROM_DEVICE);
writel(dma, dev->regs + REG_RX_ADDR);
/* ... 中断到来 ... */
dma_unmap_single(dev, dma, len, DMA_FROM_DEVICE);
/* 现在才能读 buf */

同步与「谁拥有缓冲」

流式映射的核心规则是所有权:dma_map_single() 之后,缓冲归设备;dma_unmap_single() 之后,归 CPU。在归设备期间,CPU 不能读写,否则:

  • CPU 写 → 与设备写冲突,结果未定义;
  • CPU 读 → 可能读到设备写了一半的数据。

如果必须在映射期间让 CPU 看一眼(例如轮询描述符的完成位),要用 dma_sync_single_for_cpu() / dma_sync_single_for_device() 成对切换所有权,而不是直接读。

dma_alloc_coherent() 的内存则没有这个问题:它保证 CPU 与设备看到一致的内容,代价是通常不能使用 cache(或使用写合并),CPU 访问比普通内存慢。它也不能用 kfree() 释放,必须 dma_free_coherent(),因为底层可能是 alloc_pages() + remap_pfn_range(),或者 IOMMU 的映射。

IOMMU 带来的三个变化

  1. 隔离:设备只能访问 I/O 页表里映射过的页,不能 DMA 到任意物理内存;这挡住了不少「恶意设备/固件 bug」。
  2. 合并:物理不连续的页可以映射成连续的 IOVA,dma_map_sg() 能把 scatterlist 合并成设备看得懂的一段。
  3. 故障可见:设备访问未映射地址会触发 IOMMU fault,dmesg 里有 DMAR: [DMA Read] Request device ... fault addr ...,比「数据静默错误」好查得多。

查看当前设备的 IOMMU 分组与映射:

# IOMMU 是否启用
dmesg | grep -i -E 'DMAR|IOMMU'
# 设备属于哪个 IOMMU group
ls -l /sys/kernel/iommu_groups/*/devices/
# 某设备的 DMA 映射(需要 debugfs)
cat /sys/kernel/debug/iommu/intel/dmar_translation_struct 2>/dev/null | head
DMA 的七个坑
  1. 用 virt_to_phys() 当代替 DMA 地址:无 IOMMU 时碰巧对,有 IOMMU 时必错;swiotlb 还会把缓冲 bounce 到低地址。
  2. dma_map_single() 后忘记检查 dma_mapping_error():映射失败返回 0 或全 1,直接写给设备会 DMA 到错误地址。
  3. 方向写反:x86 上难复现,ARM64 上表现为旧数据/数据损坏;按「谁写」选方向,不按「读还是写系统调用」选。
  4. 映射期间 CPU 读写缓冲:违反所有权,轻则数据错,重则 cache 行在 CPU 与设备之间反复弹跳。
  5. dma_alloc_coherent() 的指针用 kfree() 释放:底层不是 slab,kfree 会 panic;必须 dma_free_coherent()。
  6. 忘记 dma_set_mask_and_coherent():默认掩码可能是 32 位,64 位设备拿到高位地址后 DMA 失败;先设 64,失败再退回 32。
  7. 在 remove() 里先 dma_free_coherent() 再停设备:设备可能还在写这块内存;先关中断/停 DMA,再释放。

轮询、中断与 DMA 的取舍

方式CPU 占用延迟适用
轮询(polling)高(忙等)最低高频、低延迟、DPDK
中断低(空闲时)中(中断延迟)通用设备
DMA + 中断最低(CPU 不搬数据)中网卡/磁盘/高速设备

napi 是「中断 + 轮询」的混合:第一个包触发中断,随后关中断、批量轮询,预算用完再开中断。busy_poll/SO_BUSY_POLL 把轮询下放到 socket 层,用 CPU 换延迟;io_uring 的 IORING_SETUP_SQPOLL 用内核线程轮询 SQ,减少系统调用。

3.14.12 实验:一个 misc 驱动、一条 udev 规则与一次观测

这一节回答:能不能用不到 120 行代码,把「设备注册 → 节点创建 → 权限 → 读写/ioctl/poll → 卸载」整条链路跑通,并用 bpftrace 证明每一次用户态调用都进了你的回调?

misc 设备是字符设备的一个简化封装:它自动分配次设备号、自动创建 /dev 节点、自动挂在 /sys/class/misc/ 下,适合只需要一个设备节点的驱动(看门狗、LED、GPIO、自定义控制口)。相比 3.14.2 的 cdev 全流程,它省掉了 alloc_chrdev_region/cdev_add/class_create/device_create 四步。

模块:misc_demo.c

// misc_demo.c — misc 设备 + 阻塞/非阻塞读 + ioctl + poll + sysfs(Linux 6.8)
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/ioctl.h>
#include <linux/miscdevice.h>
#include <linux/module.h>
#include <linux/mutex.h>
#include <linux/poll.h>
#include <linux/slab.h>
#include <linux/uaccess.h>
#include <linux/wait.h>

#define DEMO_MAX 128
#define DEMO_MAGIC 'D'
#define DEMO_GET_VALUE _IOR(DEMO_MAGIC, 1, int)
#define DEMO_SET_VALUE _IOW(DEMO_MAGIC, 2, int)

static char demo_buf[DEMO_MAX];
static int demo_len;
static int demo_value = 7;
static DEFINE_MUTEX(demo_lock);
static DECLARE_WAIT_QUEUE_HEAD(demo_wq);

static int demo_open(struct inode *inode, struct file *file)
{
	pr_info("misc_demo: open by %s (pid %d)\n", current->comm, current->pid);
	stream_open(inode, file);       /* 字节流语义:忽略 f_pos */
	return 0;
}

static int demo_release(struct inode *inode, struct file *file)
{
	pr_info("misc_demo: release\n");
	return 0;
}

static ssize_t demo_read(struct file *file, char __user *ubuf,
			 size_t count, loff_t *ppos)
{
	size_t len;

	if (!READ_ONCE(demo_len)) {
		if (file->f_flags & O_NONBLOCK)
			return -EAGAIN;
		if (wait_event_interruptible(demo_wq, READ_ONCE(demo_len) > 0))
			return -ERESTARTSYS;    /* 被信号打断,交给 VFS 重试/返回 EINTR */
	}

	mutex_lock(&demo_lock);
	len = min(count, (size_t)demo_len);
	if (copy_to_user(ubuf, demo_buf, len)) {
		mutex_unlock(&demo_lock);
		return -EFAULT;
	}
	mutex_unlock(&demo_lock);
	return len;
}

static ssize_t demo_write(struct file *file, const char __user *ubuf,
			  size_t count, loff_t *ppos)
{
	size_t len = min(count, (size_t)DEMO_MAX);

	mutex_lock(&demo_lock);
	if (copy_from_user(demo_buf, ubuf, len)) {
		mutex_unlock(&demo_lock);
		return -EFAULT;
	}
	demo_len = len;
	mutex_unlock(&demo_lock);

	wake_up_interruptible(&demo_wq);    /* 先更新状态,再唤醒 */
	return len;
}

static long demo_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{
	int val;

	switch (cmd) {
	case DEMO_GET_VALUE:
		return demo_value;
	case DEMO_SET_VALUE:
		if (get_user(val, (int __user *)arg))
			return -EFAULT;
		demo_value = val;
		return 0;
	default:
		return -ENOTTY;
	}
}

static __poll_t demo_poll(struct file *file, poll_table *wait)
{
	__poll_t mask = 0;

	poll_wait(file, &demo_wq, wait);
	if (READ_ONCE(demo_len))
		mask |= EPOLLIN | EPOLLRDNORM;
	return mask;
}

static const struct file_operations demo_fops = {
	.owner          = THIS_MODULE,
	.open           = demo_open,
	.release        = demo_release,
	.read           = demo_read,
	.write          = demo_write,
	.unlocked_ioctl = demo_ioctl,
	.poll           = demo_poll,
	.llseek         = no_llseek,
};

static ssize_t value_show(struct device *dev, struct device_attribute *attr, char *buf)
{
	return sysfs_emit(buf, "%d\n", demo_value);
}

static ssize_t value_store(struct device *dev, struct device_attribute *attr,
			   const char *buf, size_t count)
{
	int val, ret = kstrtoint(buf, 0, &val);

	if (ret)
		return ret;
	demo_value = val;
	return count;
}
static DEVICE_ATTR_RW(value);

static struct miscdevice demo_misc = {
	.minor = MISC_DYNAMIC_MINOR,
	.name  = "misc_demo",
	.fops  = &demo_fops,
	.mode  = 0666,
};

static int __init demo_init(void)
{
	int ret;

	ret = misc_register(&demo_misc);
	if (ret)
		return ret;

	ret = device_create_file(demo_misc.this_device, &dev_attr_value);
	if (ret) {
		misc_deregister(&demo_misc);
		return ret;
	}

	pr_info("misc_demo: registered as /dev/%s minor=%d\n",
		demo_misc.name, demo_misc.minor);
	return 0;
}

static void __exit demo_exit(void)
{
	device_remove_file(demo_misc.this_device, &dev_attr_value);
	misc_deregister(&demo_misc);
	pr_info("misc_demo: unloaded\n");
}

module_init(demo_init);
module_exit(demo_exit);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("misc char device with blocking read, ioctl, poll and sysfs");

编译仍用 3.1 的树外模块模板:

obj-m += misc_demo.o
KDIR ?= $(HOME)/linux-6.8
all:
	$(MAKE) -C $(KDIR) M=$(PWD) modules
clean:
	$(MAKE) -C $(KDIR) M=$(PWD) clean

预期 insmod misc_demo.ko 后 dmesg 出现 registered as /dev/misc_demo minor=...,/dev/misc_demo 由 devtmpfs 自动创建(即使没有 udevd)。/sys/class/misc/misc_demo/value 可读写。

用户态:misc_test.c

/* misc_test.c — 覆盖 ioctl、阻塞/非阻塞读、poll
 * 编译: cc -std=c17 -D_GNU_SOURCE -Wall -Wextra -Werror -O2 -o misc_test misc_test.c
 */
#define _GNU_SOURCE
#include <errno.h>
#include <fcntl.h>
#include <poll.h>
#include <stdio.h>
#include <string.h>
#include <sys/ioctl.h>
#include <unistd.h>

#define DEMO_MAGIC 'D'
#define DEMO_GET_VALUE _IOR(DEMO_MAGIC, 1, int)
#define DEMO_SET_VALUE _IOW(DEMO_MAGIC, 2, int)

int main(void)
{
	char out[64] = {0};
	int fd, val, ret;
	struct pollfd pfd;

	fd = open("/dev/misc_demo", O_RDWR | O_NONBLOCK);
	if (fd < 0) { perror("open"); return 1; }

	/* 1. 空缓冲的非阻塞读:应返回 EAGAIN */
	ret = (int)read(fd, out, sizeof out);
	printf("empty read -> %d (%s)\n", ret, ret < 0 ? strerror(errno) : "ok");

	/* 2. ioctl:GET 返回默认值,SET 后 GET 应回读 */
	val = ioctl(fd, DEMO_GET_VALUE, 0);
	printf("GET -> %d\n", val);
	val = 99;
	if (ioctl(fd, DEMO_SET_VALUE, &val) < 0) perror("SET");
	printf("GET after SET -> %d\n", ioctl(fd, DEMO_GET_VALUE, 0));

	/* 3. 写入后 poll 应立刻就绪,read 返回写入长度 */
	if (write(fd, "hello-misc", 10) != 10) perror("write");
	pfd.fd = fd;
	pfd.events = POLLIN;
	ret = poll(&pfd, 1, 1000);
	printf("poll -> %d revents=%#x\n", ret, pfd.revents);
	ret = (int)read(fd, out, sizeof out - 1);
	printf("read -> %d: %s\n", ret, out);

	/* 4. 未知 ioctl 应返回 ENOTTY,而不是被当成合法命令 */
	errno = 0;
	ret = ioctl(fd, _IO('Z', 9), 0);
	printf("bad ioctl -> %d (%s)\n", ret, strerror(errno));

	close(fd);
	return 0;
}

预期输出:

empty read -> -1 (Resource temporarily unavailable)
GET -> 7
GET after SET -> 99
poll -> 1 revents=0x1
read -> 10: hello-misc
bad ioctl -> -1 (Inappropriate ioctl for device)

四个结果分别验证了:O_NONBLOCK 被 read 尊重、ioctl 方向位与参数传递正确、poll 的等待队列真的被唤醒、未知命令返回 ENOTTY。

udev 规则:把权限与别名交给用户态

misc_register() 会发 KOBJ_ADD,udevd 按规则创建节点。写一条规则:

# /etc/udev/rules.d/99-misc-demo.rules
SUBSYSTEM=="misc", KERNEL=="misc_demo", MODE="0660", GROUP="plugdev", SYMLINK+="demo0"

重载并触发:

sudo udevadm control --reload-rules
sudo udevadm trigger --subsystem-match=misc
ls -l /dev/misc_demo /dev/demo0
udevadm info -q all -n /dev/misc_demo | head -20

SYMLINK+="demo0" 会创建 /dev/demo0 指向 /dev/misc_demo;MODE/GROUP 决定权限。不要在驱动里用 chmod 改 /dev 节点权限,节点归 udev/devtmpfs 管,驱动改权限会被下一次 uevent 覆盖。

观测:证明调用进了你的回调

# 1. 每次 read/write/ioctl 的返回值与调用者
bpftrace -e '
kretprobe:demo_read  { @read_rc[retval] = count(); }
kretprobe:demo_write { @write_rc[retval] = count(); }
kretprobe:demo_ioctl { @ioctl_rc[retval] = count(); }
interval:s:5 { exit(); }'

# 2. 谁在打开这个设备(含进程名)
bpftrace -e 'kprobe:demo_open { printf("%s pid=%d\n", comm, pid); }'

# 3. ioctl 命令号分布(用户态视角,不依赖模块符号)
perf trace -e ioctl -p $(pgrep misc_test) 2>&1 | head -20

如果模块符号被优化成静态函数,kprobe 可能找不到;把 demo_read 等改成 noinline 或保留符号(KBUILD_CFLAGS += -fno-inline)即可。生产内核通常没有模块符号,此时用 tracepoint:syscalls:sys_enter_ioctl 从系统调用侧观测,再配合 args->cmd 过滤。

例 3.14.2 · 为什么 `poll` 返回了就绪,`read` 却返回 EAGAIN

某驱动在 poll 里检查 demo_len > 0 返回 EPOLLIN,但 read 在拿锁后又检查了一次 demo_len,此时另一个线程已经把数据读走,于是返回 EAGAIN。用户态看到「poll 说可读,read 却失败」。

这不是 bug,而是 poll 的就绪是「曾经就绪」:从 poll 返回到 read 执行之间,状态可能变化。正确做法是用户态把 EAGAIN 当作「可以再 poll 一次」,而不是当成错误;驱动侧则要保证「检查条件」与「取数据」在同一把锁下,避免无谓的假就绪。对多读者场景,poll 只能保证「至少有一个读者能成功」,不能保证「你一定成功」。

3.14.13 本章出口检查

检查项达标线没达标去哪
说出字符设备注册四步chrdev → cdev → class → device3.14.1
写出完整 file_operationsopen/read/write/ioctl/release 齐全3.14.2
正确使用 copy_to_user返回值语义、错误分支3.14.2
说清中断上下文限制不睡眠、判断共享中断3.14.3
区分 coherent 与 streaming DMA何时用哪个、如何同步3.14.3
解释 devm_* 的价值自动回滚、减少 goto 链3.14.5
在 QEMU 跑通加载/读写/卸载全流程有输出3.14.7
说出 device/driver/bus 的关键字段release、match、probe 返回值3.14.8
画出 open 到 fops 的路径chrdev_open 与 cdev_map3.14.9
区分 platform 与 PCI 枚举资源从哪来、probe 多几步3.14.10
说清 DMA 三种地址与映射方向IOMMU 下不能用物理地址3.14.11
跑通 misc 驱动与 udev 规则读写/ioctl/poll 均有输出3.14.12
即时自测 · 3.14

Q1 alloc_chrdev_region()、cdev_add()、device_create() 三者的顺序能不能换?换了会怎样?

Q2 为什么 read 里不能直接 memcpy(ubuf, kbuf, len)?copy_to_user 多做了什么?

Q3 共享中断线上有两个设备,中断来了,驱动怎么判断是不是自己的?判断错了会怎样?

Q4 dma_alloc_coherent() 与 dma_map_single() 的本质区别是什么?什么时候必须用前者?

Q5 probe() 返回 -EPROBE_DEFER 之后会发生什么?为什么不直接 return -ENODEV?

答案

A1 不能。必须先有设备号(alloc_chrdev_region),cdev_add 才能把 file_operations 绑到号上;device_create 依赖 class 与设备号来生成 /dev 节点与 uevent。顺序错了会出现「节点存在但 open 返回 ENODEV」或「cdev 绑到无效号」。

A2 用户指针可能指向未映射/已换出的页,直接解引用会触发内核态缺页(可能睡眠、可能 Oops);SMAP 还会阻止内核访问用户页。copy_to_user 做三件事:access_ok() 检查范围、在 stac/clac 之间拷贝、处理缺页(返回未拷贝字节数而不是崩溃)。

A3 读设备的状态寄存器,看中断标志是否置位;不是自己的就返回 IRQ_NONE,内核会继续调用同一中断线上的下一个 handler。判断错了(该返回 IRQ_NONE 却返回 IRQ_HANDLED)会让内核以为中断已处理,另一个设备永远收不到中断(中断丢失)。

A4 dma_alloc_coherent() 分配「CPU 与设备看到一致」的内存(通常物理连续、uncached 或写合并),适合长期存在的环形缓冲/描述符;dma_map_single() 把一段已有内存临时映射给设备,适合单次传输,用完要 dma_unmap_single()。设备需要长期持有同一块物理内存时必须用前者。

A5 驱动核心把设备放回「待探测」链表,等依赖驱动注册后重试(driver_deferred_probe_trigger())。返回 -ENODEV 表示「这个设备我永远不支持」,不会再重试;-EPROBE_DEFER 表示「现在不行,稍后再试」。

母本对应

母本章节本书落点程度
李治军第 8 章 设备驱动(实践项目 8 设备驱动)3.14.1、3.14.3对照(0.11 的 out 指令与文件视图)
奔跑吧 Linux 内核第 5 章 5.1 中断管理机制3.14.3完整(注册、上半部、共享中断)
奔跑吧 Linux 内核第 5 章 5.3 workqueue 工作队列3.14.3精选(下半部选型)
Kerrisk第 64 章 伪终端3.14.1、3.14.4对照(pty 作为字符设备的用户态视角)
Kerrisk第 13 章 文件 I/O 缓冲3.14.2背景(read/write 语义)
鸟哥第 19 章 开机流程与主要服务3.14.6背景(udev 与设备节点)
OSTEP第 36 章 I/O 设备3.14.11、3.14.11.1完整(轮询/中断/DMA 取舍表、NAPI/busy_poll)

下一站:3.15 内核调试、观测与性能分析 把前面所有子系统的「观测点」收拢成一套方法论:printk/ftrace/perf/bpftrace 各自解决什么问题、crash 怎么读 panic、KASAN/lockdep 怎么在出错前拦住你。