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)——这正是「简易实现」的边界,真实驱动要维护读位置与环形缓冲。
- 直接解引用用户指针:
*(int *)arg会触发缺页或 SMAP 错误。必须get_user/copy_from_user。安全模块(如HARDENED_USERCOPY)会抓这类错误。 copy_to_user返回值判断反了:它返回「没拷贝成功的字节数」,if (copy_to_user(...))才是错误分支。- 忘了
cdev_del/unregister_chrdev_region:模块卸载后设备号泄漏,再次加载会拿到新号,/dev里的旧节点失效。 ioctl不检查cmd:未知命令应返回ENOTTY,否则用户态探测能力(如hdparm)会误判。- 在
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);
三个要点:
devm_*自动释放:devm_kzalloc、devm_ioremap_resource、devm_request_irq注册到&pdev->dev的资源链上,remove或probe失败时自动回滚,不用手写goto err链。probe返回-EPROBE_DEFER表示「依赖还没就绪」(如时钟、 regulator、GPIO),驱动核心会稍后重试。这比调整 initcall 顺序正确(3.2 例 3.2.1)。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() 占住端口:
| 方式 | 资源类型 | 映射/访问 | 典型设备 |
|---|---|---|---|
| MMIO | IORESOURCE_MEM | devm_ioremap_resource() + readl/writel | PCIe 设备、SoC 寄存器 |
| 端口 I/O | IORESOURCE_IO | devm_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。
某驱动在 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 的字段很多,下面只列与驱动作者直接相关的部分:
| 字段 | 类型 | 谁维护 | 含义与陷阱 |
|---|---|---|---|
kobj | struct kobject | 驱动核心 | 引用计数与 sysfs 入口;kobject 的 release 必须释放整个 device |
parent | struct device * | 注册者 | 父设备,决定 /sys/devices 的拓扑与电源管理顺序 |
bus | struct bus_type * | device_add() | 所在总线;为 NULL 时不会匹配任何驱动 |
driver | struct device_driver * | 驱动核心 | 当前绑定的驱动;/sys/devices/.../driver 符号链接指向它 |
platform_data | void * | 板级代码 | 老式平台数据,DT 世界已基本淘汰 |
driver_data | void * | 驱动 | dev_set_drvdata()/dev_get_drvdata() 的存储;platform_set_drvdata 是它的包装 |
of_node | struct device_node * | OF 核心 | 设备树节点;ACPI 设备为 NULL,新代码用 fwnode |
fwnode | struct fwnode_handle * | 固件层 | 统一 DT/ACPI/软件节点的抽象,device_property_*() 走它 |
devt | dev_t | 字符/块设备 | 主次设备号;/sys/dev/char/ 下的编号来自它 |
class | struct class * | 驱动 | 功能分类(/sys/class/...),与总线拓扑正交 |
groups | const struct attribute_group ** | 驱动 | 设备级 sysfs 属性组,dev_groups 的运行时版本 |
release | void (*)(struct device *) | 驱动必须提供 | 最后一个引用释放时调用;忘记设置会打印 Device ... does not have a release() |
dma_mask | u64 * | 总线/驱动 | 设备可寻址的物理地址位宽;dma_alloc_coherent() 用它判断是否需要 bounce |
coherent_dma_mask | u64 | 总线/驱动 | 一致性 DMA 的地址掩码,常见 32 位设备为 DMA_BIT_MASK(32) |
dma_ops | const struct dma_map_ops * | 架构/IOMMU | DMA 映射实现;x86 上由 IOMMU 驱动替换 |
numa_node | int | 总线 | 设备就近的 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:一组回调,不是硬件
| 字段 | 类型 | 含义 |
|---|---|---|
name | const char * | 驱动名;platform 总线在没有 DT 匹配表时用它和设备名比对 |
bus | struct bus_type * | 所属总线,由 driver_register() 填写/校验 |
owner | struct module * | 通常是 THIS_MODULE,防止驱动在用的时候被卸载 |
probe | int (*)(struct device *) | 匹配成功后调用;返回 0 表示绑定成功 |
remove | void (*)(struct device *) | 解绑时调用;6.8 里还有返回 void 的 remove 与返回 int 的 remove_new 过渡 |
shutdown | void (*)(struct device *) | 关机/重启前调用,用来把设备置于安全状态 |
of_match_table | const struct of_device_id * | DT 匹配表,compatible 字符串 |
acpi_match_table | const struct acpi_device_id * | ACPI 匹配表,_HID/_CID |
pm | struct dev_pm_ops | 系统休眠/运行时电源管理回调 |
groups | const struct attribute_group ** | 驱动级 sysfs 属性(/sys/bus/.../drivers/demo/) |
deferred_probe | struct 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() 的名字 |
match | int (*)(struct device *, struct device_driver *),返回 1 表示匹配 |
uevent | 向用户态补充环境变量(MODALIAS= 等) |
probe | 总线级 probe,在驱动 probe 之前/之后做总线特有初始化 |
remove | 总线级 remove |
shutdown | 总线级关机回调 |
online/offline | CPU 热插拔(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(),子设备持有父设备的引用,形成引用环。
- 以为
dev_set_drvdata()会自动清零:它只是写dev->driver_data,卸载/重新 probe 前要自己置NULL,否则remove()里可能拿到上一次的野指针。 device_register()失败后直接kfree(dev):必须先put_device(),让release走完;直接kfree会让 kobject 的引用计数与内存生命周期脱节。probe()里devm_kzalloc()后返回-EPROBE_DEFER:devres 会在 probe 失败时自动释放,这是对的;但如果你把指针存进了全局变量,defer 之后全局变量就是野指针。remove()里先kfree再device_unregister():device_unregister()还会访问dev的字段(名字、parent),顺序反了就是 UAF。- platform 驱动只写
.name不写of_match_table:DT 系统上设备名是demo@10000000,和驱动名demo不一定相等,匹配不上;要么写 compatible,要么用MODULE_ALIAS。 - 把
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_fop | ENXIO |
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 不会) |
read | read()/readv() | 字节数、0(EOF)、负 errno | 不更新 *ppos,死循环 |
write | write()/writev() | 字节数、负 errno | 短写不返回实际字节数 |
read_iter/write_iter | read() 的新路径(new_sync_read) | iov_iter 语义 | 同时实现 read 与 read_iter,后者优先 |
unlocked_ioctl | ioctl() | 0 或负 errno | 不检查 _IOC_TYPE,把别的命令当自己的 |
compat_ioctl | 32 位进程在 64 位内核 | 0 或负 errno | 不实现时返回 -ENOTTY,32 位工具直接失败 |
poll | select/poll/epoll | __poll_t 位掩码 | 只 poll_wait() 不返回就绪位,永远超时 |
mmap | mmap() | 0 或负 errno | 用 remap_pfn_range 映射可迁移页 |
llseek | lseek() | 新偏移 | 不设置时默认 no_llseek,返回 ESPIPE |
fasync | fcntl(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 被文件系统换掉。
- 「
device_create()会注册 cdev」:不会。它只创建 class device 与/dev节点;没有cdev_add(),open 返回ENXIO。 - 「
cdev_add()之后就能 open」:还需要设备节点(mknod或device_create/devtmpfs)。节点是用户态路径,cdev 是内核查找表,两者缺一不可。 - 「
release等于close」:dup()/fork()共享同一个struct file,release只在最后一个引用关闭时调用;每次close()都调用的是flush。 - 「
O_NONBLOCK只影响 open」:它存在file->f_flags里,read/write/poll都要检查;只检查 open 会导致非阻塞读仍然睡眠。 - 「
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)从哪里来?
两种总线的差别在设备怎么被发现:
| 维度 | platform | PCI |
|---|---|---|
| 设备来源 | 设备树/ACPI/板级代码静态注册 | 总线枚举(配置空间自描述) |
| 设备标识 | compatible 字符串或设备名 | vendor/device ID、class code |
| 寄存器资源 | reg 属性 → IORESOURCE_MEM | BAR 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。驱动要做的是:
pci_enable_device():打开 I/O 与 memory decode,否则访问 BAR 会返回全 1;pci_request_regions()/pcim_iomap_regions():声明 BAR 所有权并映射;dma_set_mask_and_coherent():告诉内核设备能寻址多少位;pci_alloc_irq_vectors():MSI/MSI-X 或 legacy INTx;- 注册自己的子系统(
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_get_irq()返回 0 当成成功:中断号 0 在有些架构上是合法值,但函数失败返回负 errno。判断< 0,不要if (!irq)。devm_ioremap_resource()与devm_ioremap()混用:前者会request_mem_region()声明资源,后者不会;用错会导致/proc/iomem里看不到你的区域,热插拔时冲突检测失效。- PCI probe 里忘记
pci_set_master():设备能读写 MMIO,但 DMA 事务总线的 master 位没开,DMA 直接失败(读回全 0 或触发 parity error)。 pci_alloc_irq_vectors()后按 MSI-X 的数量索引:实际分配数量可能小于请求,必须用返回值作为向量数,不能假设等于num_vectors。remove()里先释放devm资源再关硬件:devm 的释放顺序是 LIFO,硬件可能还在 DMA 写入已释放的内存;正确顺序是「先停硬件/关中断,再让 devm 释放」。- 在
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 |
| 流式 scatterlist | dma_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 带来的三个变化
- 隔离:设备只能访问 I/O 页表里映射过的页,不能 DMA 到任意物理内存;这挡住了不少「恶意设备/固件 bug」。
- 合并:物理不连续的页可以映射成连续的 IOVA,
dma_map_sg()能把 scatterlist 合并成设备看得懂的一段。 - 故障可见:设备访问未映射地址会触发 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
- 用
virt_to_phys()当代替 DMA 地址:无 IOMMU 时碰巧对,有 IOMMU 时必错;swiotlb还会把缓冲 bounce 到低地址。 dma_map_single()后忘记检查dma_mapping_error():映射失败返回 0 或全 1,直接写给设备会 DMA 到错误地址。- 方向写反:x86 上难复现,ARM64 上表现为旧数据/数据损坏;按「谁写」选方向,不按「读还是写系统调用」选。
- 映射期间 CPU 读写缓冲:违反所有权,轻则数据错,重则 cache 行在 CPU 与设备之间反复弹跳。
dma_alloc_coherent()的指针用kfree()释放:底层不是 slab,kfree会 panic;必须dma_free_coherent()。- 忘记
dma_set_mask_and_coherent():默认掩码可能是 32 位,64 位设备拿到高位地址后 DMA 失败;先设 64,失败再退回 32。 - 在
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 过滤。
某驱动在 poll 里检查 demo_len > 0 返回 EPOLLIN,但 read 在拿锁后又检查了一次 demo_len,此时另一个线程已经把数据读走,于是返回 EAGAIN。用户态看到「poll 说可读,read 却失败」。
这不是 bug,而是 poll 的就绪是「曾经就绪」:从 poll 返回到 read 执行之间,状态可能变化。正确做法是用户态把 EAGAIN 当作「可以再 poll 一次」,而不是当成错误;驱动侧则要保证「检查条件」与「取数据」在同一把锁下,避免无谓的假就绪。对多读者场景,poll 只能保证「至少有一个读者能成功」,不能保证「你一定成功」。
3.14.13 本章出口检查
| 检查项 | 达标线 | 没达标去哪 |
|---|---|---|
| 说出字符设备注册四步 | chrdev → cdev → class → device | 3.14.1 |
写出完整 file_operations | open/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_map | 3.14.9 |
| 区分 platform 与 PCI 枚举 | 资源从哪来、probe 多几步 | 3.14.10 |
| 说清 DMA 三种地址与映射方向 | IOMMU 下不能用物理地址 | 3.14.11 |
| 跑通 misc 驱动与 udev 规则 | 读写/ioctl/poll 均有输出 | 3.14.12 |
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怎么在出错前拦住你。