--- title: 面试-Linux驱动开发核心 tags: [ 面试, Linux驱动, 字符设备, 设备树, pinctrl, gpio, 并发, 自旋锁, 互斥体, 中断, 下半部, platform, IO模型, poll, 嵌入式, ] created: 2026-09-17 updated: 2026-09-17 --- # 面试-Linux驱动开发核心 > 本文档涵盖 Linux 驱动开发核心的综合面试题,覆盖字符设备驱动、设备树、pinctrl 与 gpio 子系统、并发与同步、中断处理、platform 总线模型、IO 模型七大核心领域,共 35 道深度面试题。题目类型包括概念理解、对比分析、实战应用、问题排查与方案设计,适合 I.MX6ULL / RK3568 等嵌入式 Linux 平台求职复习。 > 💡 **关联知识**: [[03-Linux驱动开发核心/01-字符设备驱动基础]] | [[03-Linux驱动开发核心/01-字符设备驱动框架]] | [[03-Linux驱动开发核心/02-设备树语法与实战]] | [[03-Linux驱动开发核心/03-pinctrl与gpio子系统]] | [[03-Linux驱动开发核心/04-并发同步与原子操作]] | [[03-Linux驱动开发核心/05-中断下半部处理]] | [[03-Linux驱动开发核心/06-阻塞IO与poll机制]] | [[03-Linux驱动开发核心/07-platform总线模型]] | [[03-Linux驱动开发核心/08-misc与input子系统]] --- ## 目录 | 板块 | 题号 | 核心考点 | | --------------- | ------- | ----------------------------------------------------- | | 字符设备驱动 | Q1-Q5 | 注册流程、file/inode、数据拷贝、设备节点、多实例设计 | | 设备树 | Q6-Q10 | DT 由来、DTS/DTB、属性语法、OF API、调试排查 | | pinctrl 与 gpio | Q11-Q15 | 子系统分工、DTS 写法、新旧 API、电平极性、冲突排查 | | 并发与同步 | Q16-Q20 | 竞态来源、原子操作、锁对比、irqsave、死锁 | | 中断处理 | Q21-Q25 | 上下半部、tasklet/workqueue、中断号、上下文限制、消抖 | | platform 总线 | Q26-Q30 | 模型思想、匹配机制、probe 流程、DT 化、框架改造 | | IO 模型 | Q31-Q35 | 五种模型、等待队列、poll/epoll、异步通知、综合设计 | --- ## 一、字符设备驱动 > 对应笔记:[[03-Linux驱动开发核心/01-字符设备驱动基础]]、[[03-Linux驱动开发核心/01-字符设备驱动框架]] ### Q1: 字符设备驱动的完整注册流程是什么?register_chrdev 与 register_chrdev_region + cdev 两种方式有什么区别? **答案要点**: 1. 字符设备驱动注册的本质是:把设备号与一组文件操作 `file_operations` 绑定,并告诉内核 2. 老式接口 `register_chrdev` 一次性完成"设备号 + fops"注册,但会占用一个主设备号下的所有次设备号 3. 新式接口拆成三步:`register_chrdev_region/alloc_chrdev_region` 申请设备号、`cdev_init` 初始化 cdev、`cdev_add` 添加到内核 4. 卸载时对称调用 `cdev_del` + `unregister_chrdev_region` 5. 新式接口按需申请连续次设备号,更利于多设备管理和设备号规划 **详细解答**: 字符设备是 Linux 中最基础的一类设备,用户空间通过 `/dev/xxx` 对设备进行 `open/read/write/ioctl` 操作,最终被内核转发到驱动注册的 `file_operations`。 **老式方式(register_chrdev)**——正点原子《第四十章 字符设备驱动开发》中的 `chrdevbase` 示例: ```c #define CHRDEVBASE_MAJOR 200 #define CHRDEVBASE_NAME "chrdevbase" static int __init chrdevbase_init(void) { int retvalue = 0; retvalue = register_chrdev(CHRDEVBASE_MAJOR, CHRDEVBASE_NAME, &chrdevbase_fops); if (retvalue < 0) { printk("chrdevbase driver register failed\r\n"); } return 0; } static void __exit chrdevbase_exit(void) { unregister_chrdev(CHRDEVBASE_MAJOR, CHRDEVBASE_NAME); } module_init(chrdevbase_init); module_exit(chrdevbase_exit); MODULE_LICENSE("GPL"); ``` 它的缺点很明确:**只要注册了主设备号 200,就意味着 200 这个主设备号下的 0~255 全部次设备号都被这一个驱动占用了**,其他驱动无法再使用主设备号 200 的任何一个次设备号。 **新式方式(推荐)**——申请设备号、注册 cdev、创建设备节点三段式: ```c struct newchrled_dev { dev_t devid; /* 设备号 */ struct cdev cdev; /* cdev */ struct class *class; /* 类 */ struct device *device; /* 设备 */ int major; /* 主设备号 */ int minor; /* 次设备号 */ }; static int __init newchrled_init(void) { int ret = 0; /* 1、申请设备号 */ if (newchrled.major) { /* 定义了主设备号 */ newchrled.devid = MKDEV(newchrled.major, 0); ret = register_chrdev_region(newchrled.devid, NEWCHRLED_CNT, NEWCHRLED_NAME); } else { /* 未定义主设备号,动态申请 */ ret = alloc_chrdev_region(&newchrled.devid, 0, NEWCHRLED_CNT, NEWCHRLED_NAME); newchrled.major = MAJOR(newchrled.devid); newchrled.minor = MINOR(newchrled.devid); } if (ret < 0) { printk("newchrled chrdev_region err!\r\n"); return ret; } /* 2、初始化 cdev 并添加到内核 */ newchrled.cdev.owner = THIS_MODULE; cdev_init(&newchrled.cdev, &newchrled_fops); cdev_add(&newchrled.cdev, newchrled.devid, NEWCHRLED_CNT); /* 3、创建类与设备(见 Q4) */ return 0; } ``` 两者对比: | 对比项 | register_chrdev | register_chrdev_region + cdev | | ------------- | ---------------------------------- | ----------------------------- | | 占用设备号 | 占用整个主设备号(256 个次设备号) | 只占用申请的 cnt 个次设备号 | | 是否绑定 fops | 一次绑定 | cdev_init 时绑定 | | 多设备支持 | 差 | 好,可精确分配次设备号 | | 内核推荐度 | 已过时 | 推荐 | | 底层实现 | 内部同样是封装 cdev | 直接操作 cdev | > **设备号基础**(原书 40.3.1 节):`dev_t` 本质是 `unsigned int`(32 位),**高 12 位为主设备号(范围 0~4095),低 20 位为次设备号**。相关宏定义在 `include/linux/kdev_t.h`: > > ```c > #define MINORBITS 20 > #define MINORMASK ((1U << MINORBITS) - 1) > #define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS)) > #define MINOR(dev) ((unsigned int) ((dev) & MINORMASK)) > #define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi)) > ``` > > 因此选择主设备号时不要超过 4095;`register_chrdev` 会一次性占用一个主设备号下全部 256 个次设备号。 **追问**: - 追问1:`alloc_chrdev_region` 和 `register_chrdev_region` 该如何选择?动态申请后如何让用户知道主设备号?(`/proc/devices`) - 追问2:`cdev` 在内核里是如何与 `inode->i_cdev` 关联的?为什么 `open` 时能找回我们的设备结构体? - 追问3:一个驱动想管理 4 个同类设备,设备号应该怎么申请?`cdev_add` 的 count 应传多少? --- ### Q2: struct file、struct inode、struct file_operations 分别代表什么?三者是什么关系? **答案要点**: 1. `struct inode` 代表文件系统层面的一个文件节点(含设备号、cdev 指针),一个文件在磁盘/内存中只有一份 2. `struct file` 代表进程打开文件后的一条"打开实例",每 open 一次就有一个,含私有数据 `private_data` 和打开标志 `f_flags` 3. `struct file_operations` 是驱动的"方法表",把系统调用映射到具体函数指针 4. 关系:用户 `open` → VFS 创建 `struct file` → 通过 `inode->i_rdev` 找到设备号 → 通过 `inode->i_cdev` 找到 `cdev` → `cdev->ops` 指向驱动的 `file_operations` 5. 因此 `open()` 里常把设备结构体挂到 `filp->private_data`,后续 `read/write` 通过它取回设备对象 **详细解答**: 理解这三个结构体是写字符设备驱动的基本功,很多"为什么 open 里能拿到设备、read 里怎么找到设备"的问题都源于此。 `struct inode`(`include/linux/fs.h`): ```c struct inode { umode_t i_mode; /* 权限 */ kuid_t i_uid; kgid_t i_gid; dev_t i_rdev; /* 设备号,字符设备关键字段 */ const struct file_operations *i_fop; struct cdev *i_cdev; /* 指向内核中的 cdev */ /* ... */ }; ``` `struct file`(`include/linux/fs.h`): ```c struct file { struct path f_path; struct inode *f_inode; /* 指向对应的 inode */ const struct file_operations *f_op; unsigned int f_flags; /* 打开标志,O_NONBLOCK 等 */ fmode_t f_mode; loff_t f_pos; /* 文件偏移 */ void *private_data; /* 驱动私有数据 */ /* ... */ }; ``` `struct file_operations`(节选,签名以原书第四十章示例代码 40.1.1 为准): ```c struct file_operations { struct module *owner; /* 通常为 THIS_MODULE */ loff_t (*llseek)(struct file *, loff_t, int); ssize_t (*read)(struct file *, char __user *, size_t, loff_t *); ssize_t (*write)(struct file *, const char __user *, size_t, loff_t *); unsigned int (*poll)(struct file *, struct poll_table_struct *); long (*unlocked_ioctl)(struct file *, unsigned int, unsigned long); long (*compat_ioctl)(struct file *, unsigned int, unsigned long); int (*open)(struct inode *, struct file *); int (*release)(struct inode *, struct file *); int (*fasync)(int, struct file *, int); /* ... */ }; ``` > 注意:老资料里常见 `int (*ioctl)(...)` 的写法,那是更老内核的成员;本书使用的 4.1.15 内核里对应成员为 `unlocked_ioctl`(32 位应用在 64 位系统上则走 `compat_ioctl`),`poll` 的返回类型为 `unsigned int`。 三者的关系可用下图理解: ``` 用户 open("/dev/newchrled") │ ▼ VFS 查找路径 → 得到 inode(唯一,含 i_rdev 设备号、i_cdev 指针) │ ▼ 为本次打开分配一个 struct file(每次 open 一份) │ ▼ inode->i_cdev->ops = 驱动的 file_operations │ ▼ 调用 fops->open(inode, filp) │ ▼ 驱动: filp->private_data = &my_dev; // 后续 read/write 用它取回设备 ``` 关键区别: | 结构体 | 生命周期 | 数量 | 关键字段 | | --------------- | -------------------------- | ------------ | ---------------------------- | | inode | 随文件存在而存在 | 每个文件一份 | i_rdev、i_cdev | | file | 每次 open 创建,close 销毁 | 每次打开一份 | private_data、f_flags、f_pos | | file_operations | 驱动加载时静态定义 | 每个驱动一份 | 各种操作函数指针 | **追问**: - 追问1:`inode` 和 `file` 是一对一吗?多个进程同时打开同一个设备会创建几个 `file`?(多个,各自独立) - 追问2:`private_data` 为什么在 `open` 里赋值、在 `release` 里清理?如果忘记会怎样? - 追问3:`f_pos` 出现在 `read/write` 的 `loff_t *offt` 参数里,它和 `filp->f_pos` 是什么关系? --- ### Q3: 内核空间与用户空间为什么要用 copy_to_user / copy_from_user?直接用 memcpy 会有什么后果? **答案要点**: 1. 用户空间与内核空间属于不同的地址空间/保护级别,用户指针不能直接被内核解引用 2. 直接用 `memcpy` 访问用户指针,在用户地址未映射、非法或换页时会触发 oops/kernel panic 3. `copy_to_user`/`copy_from_user` 会做地址合法性检查、缺页处理,并能正确检查访问权限 4. 返回值是**未能拷贝的字节数**,返回 0 表示成功(这一点极易记反) 5. 参数里的用户指针必须用 `__user` 标注,便于 sparse 静态检查 **详细解答**: 这是字符设备驱动 `read/write` 实现里最容易出错、也是面试高频的点。 ```c static ssize_t chrdevbase_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt) { int retvalue = 0; unsigned char readbuf[100]; /* 把内核数据拷贝到用户空间 */ retvalue = copy_to_user(buf, readbuf, cnt); if (retvalue == 0) printk("kernel senddata ok!\r\n"); else printk("kernel senddata failed!\r\n"); return 0; } static ssize_t chrdevbase_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt) { int retvalue = 0; unsigned char writebuf[100]; /* 把用户数据拷贝到内核空间 */ retvalue = copy_from_user(writebuf, buf, cnt); if (retvalue == 0) printk("kernel recevdata:%s\r\n", writebuf); else printk("kernel recevdata failed!\r\n"); return 0; } ``` 两个函数的原型(原书第四十章给出 `copy_to_user` 原型,`copy_from_user` 与之对称): ```c /* 文件: include/linux/uaccess.h */ static inline long copy_to_user(void __user *to, const void *from, unsigned long n); static inline long copy_from_user(void *to, const void __user *from, unsigned long n); ``` 为什么不能用 memcpy: | 场景 | memcpy 直接访问用户指针 | copy_*_user | | ---------------- | ----------------------- | -------------------------------- | | 用户地址未映射 | 触发页错误,内核 oops | 内部异常表处理,返回未拷贝字节数 | | 用户传入非法指针 | kernel panic | 安全失败 | | 用户空间发生换页 | 无法处理 | 自动处理缺页 | | 权限校验 | 无 | 检查范围合法性 | | 返回值语义 | 无 | 返回失败字节数 | 正确的返回值处理范式应该是: ```c if (copy_to_user(buf, &data, sizeof(data))) return -EFAULT; /* 严格判定:非 0 即失败 */ if (copy_from_user(&data, buf, sizeof(data))) return -EFAULT; ``` 注意一个正点原子教程里的常见写法问题:`if (retvalue < 0)` 判断是**不严谨的**,因为 `copy_*_user` 返回的是 `unsigned long` 类型的未拷贝字节数,它不会为负。很多老代码沿用这个写法是因为 `int` 接收时行为上恰好能进入分支,但严格判断应该是 `!= 0`。 > **原书表述不准确**:正点原子第四十章原文写的是"如果复制成功,返回值为 0,如果复制失败则返回负数"。按内核实际实现,`copy_to_user/copy_from_user` 返回的是**未拷贝的字节数**(`unsigned long`),失败时该值大于 0 而非负数,正确写法是判断 `!= 0`。 **追问**: - 追问1:`copy_to_user` 返回值到底是"成功拷贝的字节数"还是"未拷贝的字节数"?如何据此返回正确的系统调用结果? - 追问2:如果驱动要在 `read` 里拷贝 100 字节,用户只给了 10 字节缓冲,会发生什么?`cnt` 应该信任用户传进来的值吗? - 追问3:`__user`、`__iomem` 这些标注对编译产物有影响吗?(无,仅用于 sparse 检查) --- ### Q4: 设备节点 /dev/xxx 是怎么来的?class_create / device_create 与 mdev / udev 是什么关系? **答案要点**: 1. 设备节点有两条产生路径:手动 `mknod`,或由内核通过 class/device 机制自动创建 2. `class_create` 在 `/sys/class/` 下创建类目录,`device_create` 在类目录下创建具体设备并发出 uevent 3. 用户空间的 mdev(BusyBox)或 udev(systemd)监听 uevent,自动在 `/dev` 下创建带正确主次设备号的节点 4. 手动 `mknod /dev/xxx c major minor` 需要知道主次设备号,适合调试 5. 自动创建要求根文件系统有 mdev/udev 支持且 `/proc`、`/sys` 已挂载 **详细解答**: 在用动态设备号(`alloc_chrdev_region`)时,主设备号不固定,如果每次都要查 `/proc/devices` 再手动 `mknod` 就非常低效,因此内核提供了 class/device 自动建节点机制。 驱动侧代码: ```c /* 创建类 */ newchrled.class = class_create(THIS_MODULE, NEWCHRLED_NAME); if (IS_ERR(newchrled.class)) { return PTR_ERR(newchrled.class); } /* 创建设备 */ newchrled.device = device_create(newchrled.class, NULL, newchrled.devid, NULL, NEWCHRLED_NAME); if (IS_ERR(newchrled.device)) { return PTR_ERR(newchrled.device); } ``` 卸载时对称销毁: ```c device_destroy(newchrled.class, newchrled.devid); class_destroy(newchrled.class); ``` 内核侧的流程: ``` device_create() │ ├─ 在 /sys/class/newchrled/ 下创建 newchrled 目录 │ └─ dev 文件内容为 "major:minor" │ └─ 通过 kobject_uevent 向用户空间广播 "add" 事件 │ ▼ mdev / udev 收到 uevent │ ▼ 读取 /sys/class/newchrled/newchrled/dev 里的 major:minor │ ▼ 执行 mknod /dev/newchrled c major minor ``` 手动创建(调试常用): ```bash # 查看内核分配的主设备号 cat /proc/devices | grep newchrled # c 表示字符设备,200 是主设备号,0 是次设备号 mknod /dev/newchrled c 200 0 # 权限:设备节点默认可能只有 root 可访问 chmod 666 /dev/newchrled ``` 两种方式对比: | 方式 | 优点 | 缺点 | 适用 | | ----------------- | ---------------------------------- | -------------------------------- | ------------------ | | mknod 手动 | 不依赖用户空间守护进程 | 主设备号不固定时麻烦、权限需手改 | 调试、动态设备号前 | | class/device 自动 | 主设备号任意、权限符合用户空间规则 | 依赖 mdev/udev 和 sysfs | 产品、推荐 | **追问**: - 追问1:`device_create` 之后 `/dev` 下没有节点,可能是什么原因?(mdev/udev 未启动、没有热插拔、`/sys` 未挂载、uevent 被禁用) - 追问2:`class_create` 在新内核里参数变成了什么?(旧版需传 `THIS_MODULE`,新内核 `class_create` 只接受 name,owner 自动设置) - 追问3:为什么说"设备节点是用户空间创建的,不是内核直接创建的"?内核只负责通知,对不对? --- ### Q5: 设计题:如果让你设计一个支持 4 路同类设备的字符设备驱动,设备号、cdev、私有数据应该如何组织? **答案要点**: 1. 主设备号只需一个,次设备号起始为 0,一次性申请连续 4 个次设备号(count = 4) 2. 用全局数组 `struct mydev g_mydev[4]`,每个元素含 `cdev`、`dev_t`、`device *`,但 `class` 只需一个 3. `file_operations` 只定义一份,`open` 里通过 `inode->i_rdev` 求次设备号 `MINOR`,定位到具体实例 4. 把实例指针挂到 `filp->private_data`,`read/write` 通过它访问对应硬件 5. 卸载时循环 `cdev_del`、`device_destroy`,最后统一 `unregister_chrdev_region(count=4)` **详细解答**: 这是一个典型的"驱动框架设计"题,考察你对设备号、cdev 与实例关系的理解。多实例的关键在于**一个 fops、一个 class、N 个 cdev 与 N 个 device**。 数据结构设计: ```c #define MYDEV_CNT 4 #define MYDEV_NAME "mydev" struct mydev_instance { dev_t devid; /* 本实例的设备号 */ struct cdev cdev; /* 本实例的 cdev */ struct device *device; /* 本实例的设备 */ int id; /* 实例编号 0~3 */ void __iomem *base; /* 本实例的寄存器基址(ioremap 后) */ int irq; /* 中断号 */ atomic_t opened; }; static struct mydev_instance g_mydev[MYDEV_CNT]; static struct class *mydev_class; static int mydev_major; ``` open 里通过次设备号定位实例: ```c static int mydev_open(struct inode *inode, struct file *filp) { unsigned int minor = MINOR(inode->i_rdev); struct mydev_instance *inst; if (minor >= MYDEV_CNT) return -ENODEV; inst = &g_mydev[minor]; if (!atomic_dec_and_test(&inst->opened)) { atomic_inc(&inst->opened); return -EBUSY; } filp->private_data = inst; /* 关键:后续 read/write 用它 */ return 0; } ``` 初始化时统一申请设备号、循环创建 cdev 与 device: ```c static int __init mydev_init(void) { int i, ret; dev_t devid; /* 一次申请 4 个连续次设备号 */ ret = alloc_chrdev_region(&devid, 0, MYDEV_CNT, MYDEV_NAME); if (ret < 0) return ret; mydev_major = MAJOR(devid); mydev_class = class_create(THIS_MODULE, MYDEV_NAME); if (IS_ERR(mydev_class)) { ret = PTR_ERR(mydev_class); goto err_class; } for (i = 0; i < MYDEV_CNT; i++) { g_mydev[i].id = i; g_mydev[i].devid = MKDEV(mydev_major, i); atomic_set(&g_mydev[i].opened, 1); cdev_init(&g_mydev[i].cdev, &mydev_fops); g_mydev[i].cdev.owner = THIS_MODULE; cdev_add(&g_mydev[i].cdev, g_mydev[i].devid, 1); g_mydev[i].device = device_create(mydev_class, NULL, g_mydev[i].devid, NULL, "%s%d", MYDEV_NAME, i); /* mydev0~3 */ if (IS_ERR(g_mydev[i].device)) { ret = PTR_ERR(g_mydev[i].device); goto err_dev; } } return 0; err_dev: while (--i >= 0) { device_destroy(mydev_class, g_mydev[i].devid); cdev_del(&g_mydev[i].cdev); } class_destroy(mydev_class); err_class: unregister_chrdev_region(MKDEV(mydev_major, 0), MYDEV_CNT); return ret; } ``` 设计要点总结: | 设计点 | 做法 | 原因 | | -------- | ---------------------- | ---------------------------------- | | 设备号 | 1 主 + 4 连续次 | 便于统一管理与释放 | | class | 只建一个 | 同类设备的集合 | | device | 每实例一个 | 对应 /dev/mydev0~3 | | fops | 只定义一份 | 行为相同,靠 private_data 区分实例 | | 实例定位 | `MINOR(inode->i_rdev)` | open 时唯一能拿到设备号的地方 | | 错误回滚 | goto 分级清理 | 避免卸载不干净导致设备号泄漏 | **追问**: - 追问1:如果不把实例指针放进 `private_data`,`read/write` 还能找到当前操作的是哪一路设备吗? - 追问2:`cdev_add` 的 count 参数传 1 还是 4?传 4 会怎样? - 追问3:用 `devm_*` 系列(如 `device_create` 没有 devm 版本,但 `class_create` 也没有)能否简化这种回滚逻辑?为什么内核对字符设备这部分 devm 支持有限? --- ## 二、设备树 > 对应笔记:[[03-Linux驱动开发核心/02-设备树语法与实战]] ### Q6: 为什么要引入设备树?它解决了 board-xxx.c 硬编码的哪些问题? **答案要点**: 1. ARM 早期用 `arch/arm/mach-xxx/board-xxx.c` 用 C 代码硬编码板级信息,导致内核里充斥大量板级垃圾代码 2. 每换一块板子就要改内核源码、重新编译,同 SoC 不同主板无法共用一份内核 3. 设备树把"硬件描述"从内核 C 代码中剥离成独立的 `.dts` 数据文件,内核只负责解析 4. 一份 SoC 内核镜像 + 不同 DTB,即可支持多块板子(同 SoC 系列) 5. 设备树还统一了各架构的硬件描述方式,Linus 曾明确要求 ARM 用 DT 替换板级文件 **详细解答**: 在设备树出现之前,ARM Linux 的板级信息是写死在 C 文件里的,例如: ```c /* 老式 board-xxx.c 里的示意代码 */ static struct resource mydev_res[] = { [0] = { .start = 0x0209C000, .end = 0x0209C3FF, .flags = IORESOURCE_MEM, }, [1] = { .start = 100, .end = 100, .flags = IORESOURCE_IRQ, }, }; static struct platform_device mydev_device = { .name = "mydev", .id = -1, .num_resources = ARRAY_SIZE(mydev_res), .resource = mydev_res, }; ``` 存在的问题: | 问题 | 说明 | | ---------- | ----------------------------------------- | | 内核臃肿 | 每块板子一份 board-xxx.c,多达几千个文件 | | 无法复用 | 相同 SoC 的不同板子要各自维护,改动易冲突 | | 修改成本高 | 改个 GPIO 都要重编内核 | | 不可逆 | 硬件信息编译进二进制,运行时不可调整 | | 描述能力弱 | 用 C 结构体难以表达复杂的总线拓扑与属性 | 设备树方案把这些内容抽成数据文件: ```dts /* alientek_alpha.dts 片段 */ / { model = "Freescale i.MX6 ULL 14x14 EVK Board"; compatible = "fsl,imx6ull-14x14-evk", "fsl,imx6ull"; mydev@0209c000 { compatible = "atkalpha-mydev"; reg = <0x0209c000 0x4000>; interrupt-parent = <&gpio1>; interrupts = <18 IRQ_TYPE_EDGE_BOTH>; status = "okay"; }; }; ``` 内核启动时由 U-Boot 把 DTB 的地址通过寄存器传给内核,内核解析 DTB 并据此生成 `platform_device` 等设备对象。 对比总结: | 维度 | board-xxx.c | 设备树 | | ------------ | ------------- | ------------------- | | 硬件描述位置 | 内核源码 | 独立 .dts/.dtb | | 换主板 | 重新编译内核 | 只换 DTB | | 内核树整洁度 | 差 | 好 | | 运行时可变 | 否 | 可(改 DTB) | | 驱动匹配 | name 匹配为主 | compatible 匹配为主 | **追问**: - 追问1:设备树是"取代驱动"还是"取代板级信息"?驱动逻辑还需不需要写 C 代码? - 追问2:DTB 是内核自己加载的吗?U-Boot 在其中扮演什么角色? - 追问3:设备树能描述时序、寄存器位域这种复杂信息吗?不能的话应该放在哪里?(驱动代码/驱动私有数据) --- ### Q7: DTS、DTSI、DTB、DTBO 分别是什么?设备树是如何编译和查看的? **答案要点**: 1. DTS(Device Tree Source)是设备树源文件,描述某一款具体单板的硬件 2. DTSI 是被包含的公共源文件,通常描述 SoC 级公共信息,用 `#include` 被 DTS 引用 3. DTB(Device Tree Blob)是 DTS 经 DTC 编译后的二进制,供内核解析 4. DTBO 是设备树 Overlay 的二进制,用于运行时给基设备树动态增加/修改节点 5. 编译命令 `make dtbs`;可用 DTC 反编译;运行时通过 `/proc/device-tree` 查看 **详细解答**: 四者的关系: ``` SoC 级公共描述 板级差异描述 imx6ull.dtsi ──include──► imx6ull-alientek-emmc.dts │ │ └────────── DTC 编译 ──────────┘ │ ▼ imx6ull-alientek-emmc.dtb │ 内核启动时解析 │ ▼ /proc/device-tree/ 下的节点树 ``` 编译命令(在 Linux 内核源码根目录): ```bash # 编译所有设备树 make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- dtbs # 只编译指定的 dtb make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- imx6ull-alientek-emmc.dtb ``` DTC(Device Tree Compiler)手动编译与反编译: ```bash # DTS 编译成 DTB dtc -I dts -O dtb -o myboard.dtb myboard.dts # DTB 反编译回 DTS(调试神器) dtc -I dtb -O dts -o myboard.dts myboard.dtb ``` Overlay 与 DTBO: ```bash # 编译 overlay 需要基础 DTB 带 -@(symbols)编译 dtc -@ -I dts -O dtb -o myoverlay.dtbo myoverlay.dts # 在 U-Boot 中应用 overlay fdt apply 0x83000000 myoverlay.dtbo ``` 运行时查看设备树: ```bash # 查看根节点 model 属性 cat /proc/device-tree/model # 输出: Freescale i.MX6 ULL 14x14 EVK Board # 查看根节点 compatible cat /proc/device-tree/compatible # 输出: fsl,imx6ull-14x14-evkfsl,imx6ull # 查看 soc 节点下的子节点 ls /proc/device-tree/soc/ # 查看某个节点的属性(属性文件内容可能是二进制/大端) ls /proc/device-tree/gpioled/ hexdump -C /proc/device-tree/gpioled/led-gpio ``` 四者对比: | 类型 | 全称 | 是否可读 | 用途 | | ---- | -------------------------- | -------- | ------------------------ | | DTS | Device Tree Source | 文本 | 单板硬件描述 | | DTSI | Device Tree Source Include | 文本 | SoC 公共信息,被 include | | DTB | Device Tree Blob | 二进制 | 内核解析的最终产物 | | DTBO | Device Tree Blob Overlay | 二进制 | 运行时动态打补丁 | **追问**: - 追问1:DTSI 和头文件的区别是什么?`#include` 和 `/include/` 两种语法有何不同用途? - 追问2:DTB 为什么不能直接用文本?内核为什么不直接解析 DTS? - 追问3:`/proc/device-tree` 里读到的 int 属性为什么是"反的"?(大端存储,需转换成 CPU 字节序) --- ### Q8: 设备树的常用属性 compatible、reg、#address-cells、#size-cells、ranges、status、phandle 分别是什么含义? **答案要点**: 1. `compatible` 是"兼容性字符串列表",驱动靠它匹配设备(最重要) 2. `reg` 描述设备地址范围,格式由父节点的 `#address-cells`、`#size-cells` 决定 3. `#address-cells` / `#size-cells` 声明子节点 reg 中"地址"和"长度"各占几个 32 位单元 4. `ranges` 描述子总线地址到父总线地址的映射;空 ranges 表示 1:1 映射 5. `status` 为 `"okay"` 才使能设备,`"disabled"` 表示禁用 6. `phandle` 是节点唯一编号,用于其他节点引用(`&label` 最终编译成 phandle) **详细解答**: `compatible`——匹配的核心: ```dts gpioled { compatible = "atkalpha-gpioled"; /* 驱动 of_match_table 匹配这一项 */ status = "okay"; }; ``` 驱动侧匹配: ```c static const struct of_device_id gpioled_of_match[] = { { .compatible = "atkalpha-gpioled" }, { /* sentinel */ } }; MODULE_DEVICE_TABLE(of, gpioled_of_match); ``` `#address-cells` / `#size-cells` / `reg`——地址描述: ```dts soc { #address-cells = <1>; /* 子节点 reg 的地址占 1 个 cell */ #size-cells = <1>; /* 子节点 reg 的长度占 1 个 cell */ aips-bus@02000000 { #address-cells = <1>; #size-cells = <1>; iomuxc: iomuxc@020e0000 { compatible = "fsl,imx6ul-iomuxc"; reg = <0x020e0000 0x4000>; /* 起始地址 长度 */ }; }; }; ``` 如果 `#address-cells = <2>`,则 `reg` 的地址需要 2 个 cell: ```dts reg = <0x00000000 0x020e0000 0x4000>; /* 高位 低位 长度 */ ``` `ranges`——地址映射: ```dts /* 空 ranges 表示子节点地址与父节点 1:1 映射 */ ranges; /* 非空表示映射:子地址 0x0 映射到父地址 0x02000000,长度 0x100000 */ ranges = <0x0 0x02000000 0x100000>; ``` `status`——设备开关: ```dts status = "okay"; /* 使能 */ status = "disabled"; /* 禁用,驱动 probe 不会被调用 */ ``` `phandle` 与 `&label` 引用: ```dts gpio1: gpio@0209c000 { gpio-controller; #gpio-cells = <2>; }; /* 通过 label 引用,编译后变成 phandle */ led-gpio = <&gpio1 3 GPIO_ACTIVE_LOW>; ``` 常见属性速查: | 属性 | 作用 | 驱动 API | | ---------- | --------- | --------------------------------- | | compatible | 匹配驱动 | of_match_table | | reg | 地址范围 | of_address_to_resource / of_iomap | | interrupts | 中断信息 | irq_of_parse_and_map | | status | 使能开关 | 内核自动跳过 disabled | | pinctrl-0 | 引脚配置 | 内核自动应用 | | clocks | 时钟 | clk_get | | xxx-gpio | GPIO 引用 | of_get_named_gpio | **追问**: - 追问1:`#address-cells` 是写在父节点还是子节点?为什么容易写错? - 追问2:`compatible` 为什么要写多个字符串?匹配顺序是怎样的? - 追问3:`reg` 和 `ranges` 一起用时,`of_iomap` 拿到的是映射前还是映射后的地址? --- ### Q9: 驱动如何从设备树读取资源?of_ 函数族有哪些常用接口?of_iomap 和 ioremap 有什么区别? **答案要点**: 1. 节点查找:`of_find_node_by_path`、`of_find_node_by_name`、`of_get_next_child`、`of_get_parent` 2. 属性读取:`of_find_property`、`of_property_read_u32`、`of_property_read_string`、`of_property_read_u32_array` 3. GPIO:`of_get_named_gpio`;中断:`irq_of_parse_and_map` 4. 地址映射:`of_iomap` 直接由节点+索引映射寄存器,`ioremap` 需要自己先拿到物理地址 5. 设备树方式下驱动通常不自己解析地址,而是由内核把 `reg` 转成 `struct resource` 交给 `platform_get_resource` **详细解答**: 正点原子《第四十三章 Linux设备树》给出的完整读取流程,通常分四步: ```c static int __init dtsof_init(void) { int ret; struct device_node *nd; const char *str; u32 regdata[10]; struct property *comppro; struct resource res; /* 1、查找节点 */ nd = of_find_node_by_path("/atkalientek"); if (nd == NULL) { ret = -EINVAL; goto fail_findnd; } /* 2、读取 compatible 属性(字符串列表) */ comppro = of_find_property(nd, "compatible", NULL); if (comppro == NULL) goto fail; /* 3、读取 status 属性(单字符串) */ ret = of_property_read_string(nd, "status", &str); /* 4、读取 reg 属性(u32 数组) */ ret = of_property_read_u32_array(nd, "reg", regdata, 10); /* 5、把 reg 转成 resource 并映射寄存器 */ of_address_to_resource(nd, 0, &res); /* ioremap(res.start, resource_size(&res)); */ return 0; } ``` 常用 OF API 一览: ```c /* 节点查找 */ struct device_node *of_find_node_by_path(const char *path); struct device_node *of_find_node_by_name(struct device_node *from, const char *name); struct device_node *of_find_compatible_node(struct device_node *from, const char *type, const char *compatible); struct device_node *of_get_parent(const struct device_node *node); struct device_node *of_get_next_child(const struct device_node *node, struct device_node *prev); /* 属性查找 */ struct property *of_find_property(const struct device_node *np, const char *name, int *lenp); int of_property_read_u32(const struct device_node *np, const char *propname, u32 *out_value); int of_property_read_string(const struct device_node *np, const char *propname, const char **out_string); int of_property_read_u32_array(const struct device_node *np, const char *propname, u32 *out_values, size_t sz); /* 统计属性元素数量(原书 4.1.15 内核用此函数;elem_size 为单个元素字节数) */ int of_property_count_elems_of_size(const struct device_node *np, const char *propname, int elem_size); int of_property_read_u32_index(const struct device_node *np, const char *propname, u32 index, u32 *out_value); int of_n_addr_cells(struct device_node *np); /* 读 #address-cells */ int of_n_size_cells(struct device_node *np); /* 读 #size-cells */ /* GPIO / 中断 */ int of_get_named_gpio(struct device_node *np, const char *propname, int index); unsigned int irq_of_parse_and_map(struct device_node *dev, int index); /* 地址映射 */ void __iomem *of_iomap(struct device_node *node, int index); ``` `of_iomap` 与 `ioremap` 对比: | 对比项 | of_iomap | ioremap | | ---------------- | --------------- | ----------------------- | | 输入 | 设备节点 + 索引 | 物理地址 + 长度 | | 是否需解析设备树 | 自动解析 reg | 不需要 | | 适用场景 | 设备树驱动 | 无设备树 / 已知物理地址 | | 释放 | iounmap | iounmap | | 推荐度 | 设备树平台推荐 | 传统 / 裸地址场景 | ```c /* of_iomap 方式:一步到位 */ base = of_iomap(nd, 0); /* ioremap 方式:先拿物理地址,再映射 */ of_address_to_resource(nd, 0, &res); base = ioremap(res.start, resource_size(&res)); ``` **追问**: - 追问1:`of_property_read_u32_array` 返回负值说明什么?如何先用 `of_property_count_elems_of_size` 探测长度?(原书 4.1.15 内核没有 `of_property_count_u32_elems`,该函数是后续内核新增的便利接口) - 追问2:`of_find_node_by_path` 里的路径是 `/atkalientek`,为什么不用 `/soc/atkalientek`?(取决于节点实际层级,DT 是树状路径) - 追问3:`ioremap` 得到的指针必须用 `readl/writel` 访问,为什么不能直接 `*base = x`?(内存屏障与端序,`__iomem` 标注) --- ### Q10: 问题排查题:设备树节点明明加了,驱动 probe 却不执行 / 属性读出来是错的,如何排查? **答案要点**: 1. 确认节点是否真的进入内核:`ls /proc/device-tree/...` 或反编译 DTB 2. 确认 `status` 是否为 `"okay"`(默认缺省是 okay,但被 dtsi 设成 disabled 时不会 probe) 3. 确认 `compatible` 与驱动 `of_match_table` 完全一致(大小写、连字符、厂商前缀) 4. 确认使用的是新 DTB:重新 `make dtbs` 并替换到开发板(最常见坑) 5. 属性读取错误多因 `#address-cells`/`#size-cells` 不匹配、属性名拼写错、字节序未转换 **详细解答**: 这是实战中最耗时的一类问题,按"节点在不在 → 使能没使能 → 匹配没匹配 → DTB 新不新"的顺序排查效率最高。 **第一步:确认节点进了内核** ```bash # 方式一:运行时查看 ls /proc/device-tree/ ls /proc/device-tree/gpioled/ cat /proc/device-tree/gpioled/compatible; echo cat /proc/device-tree/gpioled/status; echo # 方式二:把板子上的 DTB 拉回来反编译(最可靠) dtc -I dtb -O dts -o check.dts /boot/imx6ull-alientek-emmc.dtb grep -n "gpioled" check.dts ``` **第二步:确认 status** ```dts /* 父节点或自身被 disabled,都不会 probe */ status = "disabled"; /* 改成 "okay" */ ``` 注意:某些 SoC 的 `.dtsi` 里默认把外设设成 `disabled`,如果只在板级 dts 里追加节点却没写 `status = "okay"`,节点会继承/被显式禁用。 **第三步:确认 compatible 匹配** ```c static const struct of_device_id gpioled_of_match[] = { { .compatible = "atkalpha-gpioled" }, /* 必须与 dts 一字不差 */ { } }; MODULE_DEVICE_TABLE(of, gpioled_of_match); ``` ```dts compatible = "atkalpha-gpioled"; /* 大小写、连字符都要一致 */ ``` 常见错误:驱动写 `atkalpha,gpioled`,dts 写 `atkalpha-gpioled`;或厂商前缀写了但 dts 没写。 **第四步:确认 DTB 是最新的** ```bash # 内核源码目录 make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- dtbs # 把新的 dtb 拷到开发板对应位置 cp arch/arm/boot/dts/imx6ull-alientek-emmc.dtb /tftpboot/ # 或写进 emmc / sd 的 boot 分区 ``` **第五步:属性读取出错排查** | 现象 | 原因 | 解决 | | --------------------------------- | --------------------------- | ----------------------------------- | | of_find_node_by_path 返回 NULL | 路径错、节点未使能、DTB 旧 | 用 `/proc/device-tree` 核对完整路径 | | of_property_read_u32 返回 -EINVAL | 属性名拼写错 / 属性不是 u32 | `hexdump -C` 看属性文件 | | of_get_named_gpio 返回负值 | 属性名错 / 缺 `#gpio-cells` | 检查 GPIO 控制器定义 | | reg 解析出的地址不对 | cells 数不匹配 | 核对父节点 cells | 一个实用的调试手段是在 probe 里打印节点与属性: ```c static int mydev_probe(struct platform_device *pdev) { struct device_node *nd = pdev->dev.of_node; int gpio; dev_info(&pdev->dev, "device node: %pOF\n", nd); /* 打印节点全路径 */ gpio = of_get_named_gpio(nd, "led-gpio", 0); dev_info(&pdev->dev, "gpio = %d\n", gpio); return 0; } ``` **追问**: - 追问1:如果 `compatible` 正确但 probe 还是不执行,还可能是什么原因?(驱动没加载 `depmod/modprobe`、probe 返回 `-EPROBE_DEFER` 依赖未就绪) - 追问2:`status = "okay"` 写在了错误层级会怎样? - 追问3:如何确认内核实际解析的设备树是哪一个?(U-Boot 打印 `fdt addr`、`/sys/firmware/devicetree/base`) --- ## 三、pinctrl 与 gpio 子系统 > 对应笔记:[[03-Linux驱动开发核心/03-pinctrl与gpio子系统]] ### Q11: pinctrl 子系统和 gpio 子系统有什么区别和联系?为什么不能只用一个? **答案要点**: 1. pinctrl 负责"引脚复用 + 电气属性"(把引脚配成 GPIO/UART/SPI,设置上下拉/驱动能力),操作的是 IOMUXC 寄存器 2. gpio 子系统负责"电平与方向"(输入/输出、读值/写值、中断号),操作的是 GPIO 数据寄存器 3. 二者是"先配置功能,后操作电平"的先后关系:pinctrl 把引脚设为 GPIO 功能,gpio 子系统才能控制它 4. 早期直接在设备树 `reg` 里硬编码 IOMUXC 寄存器地址,可移植性差、无法检测冲突 5. 分离后,SoC 差异被 pinctrl 驱动吸收,设备驱动只调用通用 API **详细解答**: 职责分工: | 方面 | pinctrl 子系统 | gpio 子系统 | | -------- | ----------------------- | ------------------------- | | 职责 | 引脚复用 + 电气属性 | 电平/方向控制 | | 寄存器 | IOMUXC_SW_MUX/PAD_CTL | GPIO_DR/GDIR/PSR | | 时机 | 设备初始化时配置 | 运行时读写 | | 设备树 | pinctrl-names/pinctrl-0 | xxx-gpio = <&gpio1 3 ...> | | 冲突管理 | 检测引脚复用冲突 | 检测 GPIO 被重复请求 | **联系**:一个 LED 引脚的完整使用链路: ``` 设备树 ├─ pinctrl-0 = <&pinctrl_led>; // 把引脚复用为 GPIO、设置电气属性 └─ led-gpio = <&gpio1 3 GPIO_ACTIVE_LOW>; // 声明使用 GPIO1_IO03 内核 pinctrl 子系统 → 配置 IOMUXC(MUX 设为 GPIO 模式、PAD 设为推挽等) │ ▼ gpio 子系统 → gpio_direction_output() / gpio_set_value() │ ▼ 实际寄存器 GPIO1_GDIR / GPIO1_DR ``` **为什么不能只用一个**: 如果只有 gpio 子系统,引脚可能还停留在 UART 复用功能上,无论怎么设置 `gpio_set_value` 都不会有电平输出——因为引脚的 MUX 没有切到 GPIO。反之,如果只有 pinctrl,能配置功能却无法在运行时读写电平。所以二者是互补关系。 ```c /* 正确顺序:内核会自动应用 pinctrl-0,然后驱动操作 gpio */ static int led_probe(struct platform_device *pdev) { struct gpio_desc *led; /* pinctrl-0 已由内核在 probe 前自动应用 */ led = devm_gpiod_get(&pdev->dev, "led", GPIOD_OUT_HIGH); if (IS_ERR(led)) return PTR_ERR(led); gpiod_set_value(led, 0); /* 此时引脚已是 GPIO 功能,操作有效 */ return 0; } ``` **追问**: - 追问1:内核是在什么时候自动应用 `pinctrl-0` 的?(驱动 probe 前,`really_probe` → `pinctrl_bind_pins`) - 追问2:如果驱动想在第 20 行才切换引脚配置,可以怎么做?(`pinctrl_select_state`) - 追问3:同一个引脚能不能既做 GPIO 又做 UART?(不能,MUX 只有一种功能有效) --- ### Q12: pinctrl 在设备树里是怎么写的?以 I.MX6ULL 的 `fsl,pins` 五元组为例说明。 **答案要点**: 1. 在 `&iomuxc` 节点下定义 pinctrl 子节点(配置组),用 `fsl,pins` 描述引脚 2. 设备节点用 `pinctrl-names = "default"` 和 `pinctrl-0 = <&配置组>` 引用 3. I.MX6ULL 的 `MX6UL_PAD_xxx__yyy` 宏展开为 5 个值:mux_reg、conf_reg、input_reg、mux_mode、input_val 4. `fsl,pins` 里每一行是"宏 + 电气属性值",电气属性值(如 0x17059)编码上下拉、驱动强度等 5. 内核 pinctrl 驱动解析这些值并写入 IOMUXC 寄存器 **详细解答**: `&iomuxc` 下定义配置组: ```dts &iomuxc { pinctrl-names = "default"; /* 父级默认状态 */ pinctrl-0 = <&pinctrl_hog_1>; imx6ul-evk { /* LED 使用的引脚配置组 */ pinctrl_led: ledgrp { fsl,pins = < MX6UL_PAD_GPIO1_IO03__GPIO1_IO03 0x10B0 >; }; /* 按键配置组 */ pinctrl_key: keygrp { fsl,pins = < MX6UL_PAD_UART1_CTS_B__GPIO1_IO18 0xF080 >; }; }; }; ``` 设备节点引用配置组并声明 GPIO: ```dts gpioled { compatible = "atkalpha-gpioled"; pinctrl-names = "default"; pinctrl-0 = <&pinctrl_led>; led-gpio = <&gpio1 3 GPIO_ACTIVE_LOW>; status = "okay"; }; ``` `MX6UL_PAD_UART1_CTS_B__GPIO1_IO18`(定义在 `imx6ull-pinfunc.h`)展开为 5 个值: ```c #define MX6UL_PAD_UART1_CTS_B__GPIO1_IO18 0x008C 0x0318 0x0000 0x5 0x0 /* │ │ │ │ │ * mux_reg ─────────┘ │ │ │ └ input_val * conf_reg ────────────────┘ │ └ mux_mode(5=GPIO) * input_reg ───────────────────────┘ */ ``` 含义: | 顺序 | 名称 | 示例值 | 含义 | | ---- | --------- | ------ | ---------------------------------- | | 1 | mux_reg | 0x008C | MUX 控制寄存器相对 IOMUXC 基址偏移 | | 2 | conf_reg | 0x0318 | PAD 电气属性寄存器偏移 | | 3 | input_reg | 0x0000 | 输入选择寄存器偏移 | | 4 | mux_mode | 0x5 | 复用模式,5 = ALT5 = GPIO | | 5 | input_val | 0x0 | 输入选择值 | 地址计算: ``` IOMUXC 基址 = 0x020E0000 mux_reg = 0x020E0000 + 0x008C = 0x020E008C (IOMUXC_SW_MUX_CTL_PAD_...) conf_reg = 0x020E0000 + 0x0318 = 0x020E0318 (IOMUXC_SW_PAD_CTL_PAD_...) ``` 电气属性值(`fsl,pins` 每行最后那个值,即 `IOMUXC_SW_PAD_CTL_PAD_xxx` 寄存器值)的位定义(据原书第八章 GPIO 寄存器讲解): ```c bit 0: SRE 压摆率 (0:低压摆率 1:高压摆率) bit 5:3: DSE 驱动能力 (当IO用作输出时设置驱动能力) bit 7:6: SPEED 速度等级 (2'b11 = 200MHz) bit 11: ODE 开漏输出使能 (0:禁止 1:使能) bit 12: PKE 上下拉/状态保持器使能 bit 13: PUE 上下拉选择 (0:下拉 1:上拉/状态保持器) bit 15:14: PUS 上下拉电阻值 (4档) bit 16: HYS 迟滞比较器使能 (IO作输入时有用) ``` 多状态 pinctrl(低功耗场景): ```dts usdhc1 { pinctrl-names = "default", "state_100mhz", "state_200mhz"; pinctrl-0 = <&pinctrl_usdhc1>; pinctrl-1 = <&pinctrl_usdhc1_100mhz>; pinctrl-2 = <&pinctrl_usdhc1_200mhz>; status = "okay"; }; ``` **追问**: - 追问1:`fsl,pins` 里一行的"宏 + 一个值"和宏展开的 5 个值是什么关系?(宏展开 5 个,加电气属性那个值,实际是 6 个 32 位数) - 追问2:`pinctrl-names` 的第一个必须是 `"default"` 吗?为什么?(内核默认选 default) - 追问3:电气属性值是从哪里得到的?(SoC 参考手册 / NXP 官方 dts / 正点原子提供) --- ### Q13: gpio 子系统的旧 API(gpio__)和新 API(gpiod__)有什么区别?实际开发该用哪个? **答案要点**: 1. 旧 API 面向"全局 GPIO 编号",需手动 `gpio_request/free`,从设备树解析要自己来 2. 新 API 面向 `struct gpio_desc *`,通常用 `devm_gpiod_get` 直接从设备树获取,自动管理生命周期 3. 新 API 天然支持 `GPIO_ACTIVE_LOW` 极性处理、GPIO 数组、name 映射 4. `devm_` 前缀意味着随设备卸载自动释放,避免资源泄漏 5. 新驱动推荐 gpiod;但很多老教程/老代码仍是 gpio_*,要能读懂 **详细解答**: 旧 API 的典型流程(正点原子教程常用): ```c static int led_probe(struct platform_device *pdev) { struct device_node *nd = pdev->dev.of_node; int led_gpio, ret; /* 1、从设备树获取 GPIO 编号 */ led_gpio = of_get_named_gpio(nd, "led-gpio", 0); if (led_gpio < 0) return -EINVAL; /* 2、申请 GPIO */ ret = gpio_request(led_gpio, "LED_PIN"); if (ret < 0) return -EINVAL; /* 3、设为输出,默认关闭 */ gpio_direction_output(led_gpio, 1); /* 使用时 */ gpio_set_value(led_gpio, 0); /* 点亮 */ gpio_get_value(led_gpio); /* 读电平 */ /* 4、卸载时 */ gpio_free(led_gpio); return 0; } ``` 新 API 的典型流程: ```c static int led_probe(struct platform_device *pdev) { struct gpio_desc *led; /* 从设备树 "led-gpios"/"led" 属性获取,自动 request */ led = devm_gpiod_get(&pdev->dev, "led", GPIOD_OUT_HIGH); if (IS_ERR(led)) return PTR_ERR(led); /* 逻辑值操作,内核自动处理 active-low */ gpiod_set_value(led, 1); gpiod_get_value(led); /* devm 自动释放,无需 gpiod_put(也可显式调用) */ return 0; } ``` 新旧对比: | 特性 | 旧 API gpio_* | 新 API gpiod_* | | ---------- | ---------------------- | ----------------------- | | 资源管理 | 手动 request/free | devm 自动管理 | | 设备树集成 | of_get_named_gpio 手动 | devm_gpiod_get 直接支持 | | 错误处理 | 返回负值 | ERR_PTR/IS_ERR | | 极性处理 | 需自己处理 active-low | 自动处理 | | 多 GPIO | 逐个获取 | devm_gpiod_get_array | | 推荐程度 | 已不推荐 | 推荐 | 注意点: - 新 API 的设备树属性名有约定:单个用 `led-gpios` 或 `led-gpio`,`devm_gpiod_get(dev, "led", ...)` 会去匹配 `-gpios`/`-gpio`。 - 旧 API 在现代内核仍然可用,但新提交的驱动基本都用 gpiod。 - 面试中常被问:"为什么 gpiod 更安全?"核心就是 **devm 自动释放 + 极性自动处理 + 错误用 ERR_PTR 表达**。 **追问**: - 追问1:`devm_gpiod_get` 里的 con_id="led" 对应设备树哪个属性? - 追问2:`GPIOD_OUT_HIGH` 和 `GPIOD_OUT_LOW` 是设置物理电平还是逻辑电平?(逻辑电平,会按 active-low 反转) - 追问3:如果设备树既没写 `led-gpios` 也没写 `led-gpio`,`devm_gpiod_get` 会怎样?(返回 `ERR_PTR(-ENOENT)`) --- ### Q14: 设备树写 `GPIO_ACTIVE_LOW` 后,驱动写 `gpio_set_value(gpio, 1)` 到底输出高还是低? **答案要点**: 1. `GPIO_ACTIVE_LOW` 表示"逻辑有效电平为低",即逻辑 1 对应物理低电平 2. 使用 **gpiod 新 API** 时,内核会自动做电平反转,写 1 实际输出物理低电平 3. 使用 _*旧 gpio_* API_* 时,`GPIO_ACTIVE_LOW` 不会被自动处理,通常仍需按实际物理电平调用 4. 这正是新 API 相比旧 API 更"语义化"的体现:驱动只关心"开/关",不关心硬件极性 5. 点不亮的常见原因是:极性理解错误、pinctrl 没配、GPIO 没设输出 **详细解答**: 以正点原子 ALPHA 开发板的 LED 为例,硬件上 LED 阳极接 3.3V,阴极经限流电阻接 GPIO1_IO03,因此**输出低电平点亮**。设备树这样描述: ```dts gpioled { compatible = "atkalpha-gpioled"; pinctrl-names = "default"; pinctrl-0 = <&pinctrl_led>; led-gpio = <&gpio1 3 GPIO_ACTIVE_LOW>; /* 低电平有效 */ status = "okay"; }; ``` 使用旧 API 时(正点原子教程写法),需要按物理电平来,所以代码里出现"反直觉"的取值: ```c /* 旧 API:直接写物理电平,低电平点亮 */ gpio_set_value(dev->led_gpio, 0); /* 点亮 LED */ gpio_set_value(dev->led_gpio, 1); /* 熄灭 LED */ ``` 使用新 gpiod API 时,内核读到了 `GPIO_ACTIVE_LOW`,会自动反转: ```c led = devm_gpiod_get(&pdev->dev, "led", GPIOD_OUT_HIGH); /* 注意:GPIOD_OUT_HIGH 表示逻辑高=灭(active-low 下物理为高) */ gpiod_set_value(led, 1); /* 逻辑 1 = 有效 = 物理低 = 点亮 */ gpiod_set_value(led, 0); /* 逻辑 0 = 无效 = 物理高 = 熄灭 */ ``` 内核内部的处理逻辑等价于: ```c if (gpiod_is_active_low(led)) value = !value; /* 逻辑值取反成物理值 */ gpio_set_value(gpio_num, value); ``` 三种情况总结: | 场景 | 设备树极性 | 驱动调用 | 物理输出 | 结果 | | ------ | ----------- | -------------------- | -------- | ---- | | 旧 API | ACTIVE_LOW | gpio_set_value(g,1) | 高 | 灭 | | 旧 API | ACTIVE_LOW | gpio_set_value(g,0) | 低 | 亮 | | 新 API | ACTIVE_LOW | gpiod_set_value(d,1) | 低 | 亮 | | 新 API | ACTIVE_HIGH | gpiod_set_value(d,1) | 高 | 亮 | 排查点不亮问题: 1. 确认 pinctrl 已把引脚配成 GPIO 功能(MUX mode) 2. 确认 `gpio_direction_output` 成功(返回 0) 3. 确认极性:ACTIVE_LOW 是否匹配硬件(LED 接法) 4. 测量实际电平:万用表 / 示波器 / `devmem2` 读 GPIO1_DR **追问**: - 追问1:如果硬件是"高电平点亮"但设备树误写成 `GPIO_ACTIVE_LOW`,用 gpiod 会出现什么现象?(逻辑反了,写 1 灭、写 0 亮) - 追问2:旧 API 为什么不自动处理 active-low?新 API 是在哪一层处理的? - 追问3:`gpiod_set_raw_value` 和 `gpiod_set_value` 区别是什么?(前者绕过极性反转,操作物理值) --- ### Q15: 问题排查题:驱动报 `gpio_request: already requested` 或 GPIO 输出异常,怎么定位和解决? **答案要点**: 1. GPIO 被其他驱动/节点提前 request,多数是同引脚在设备树里被多个节点引用 2. 排查手段:`cat /sys/kernel/debug/gpio` 看占用者;`/proc/device-tree` 搜索引脚;`devmem2` 读寄存器 3. 解决:注释掉冲突节点的 pinctrl/GPIO 声明,或其驱动里释放 4. 输出异常的常见原因:pinctrl 未配置、方向未设为输出、active-low 弄反、引脚被复用为其他功能 5. 预防:引脚使用前全局搜索、用独占性 GPIO 请求、注意 SoC 默认功能 **详细解答**: **排查`already requested`**: ```bash # 1. 查看所有 GPIO 占用情况(需要内核开启 DEBUG_FS) cat /sys/kernel/debug/gpio # 典型输出: # gpiochip0: GPIOs 0-31, parent: platform/20a0000.gpio, gpio0: # gpio-3 ( |led ) out lo ``` 如果 `gpio-3` 已经被 `led` 占用,而你的驱动还想 request,就会失败。 ```bash # 2. 在设备树里搜索使用同一引脚的节点 grep -rn "GPIO1_IO03\|<&gpio1 3" /proc/device-tree/ ``` 很多时候是历史遗留:某个默认设备(如某个未用的外设)在 `.dtsi` 里引用了这个引脚。 **排查输出异常**: ```bash # 直接读 GPIO 数据寄存器,确认物理电平 devmem2 0x0209C000 w # GPIO1_DR # 0x0209C000 是 GPIO1 数据寄存器,bit3 对应 GPIO1_IO03 # 读方向寄存器 devmem2 0x0209C004 w # GPIO1_GDIR # 读 MUX 寄存器,确认引脚功能是否被配成 GPIO devmem2 0x020E0068 w # GPIO1_IO03 的 IOMUXC_SW_MUX_CTL_PAD 寄存器(0x020E0000+0x0068) devmem2 0x020E02F4 w # GPIO1_IO03 的 IOMUXC_SW_PAD_CTL_PAD 寄存器(0x020E0000+0x02F4) ``` 排查清单: | 现象 | 可能原因 | 处理 | | ------------------------- | ------------------------------ | ----------------------------- | | gpio_request 返回 -EBUSY | 被占用 | 注释冲突节点 / 查 debugfs | | gpio_request 返回 -EINVAL | GPIO 编号非法 | 检查 of_get_named_gpio 返回值 | | 输出电平不跟随 | 方向没设输出 | gpio_direction_output | | 输出电平不跟随 | pinctrl 没配(仍复用其他功能) | 检查 pinctrl-0 | | 输出反过来 | active-low 理解错 | 修正极性或改用 gpiod | | 读输入恒为 0/1 | 上下拉/外部电路 | 检查 PAD 配置与硬件 | 调试示例: ```c /* 打印 GPIO 控制器与占用信息 */ int gpio_num = of_get_named_gpio(nd, "led-gpio", 0); struct gpio_chip *chip = gpio_to_chip(gpio_num); dev_info(&pdev->dev, "gpio %d on chip %s\n", gpio_num, chip->label); ``` **追问**: - 追问1:`/sys/kernel/debug/gpio` 需要内核打开哪些配置?(`CONFIG_DEBUG_FS`、`CONFIG_GPIOLIB`) - 追问2:同一个 GPIO 被 request 两次一定失败吗?`gpio_request` 有无共享机制? - 追问3:如果是复用冲突(引脚被 UART 占用),`gpio_request` 会失败吗?(不一定,MUX 冲突更隐蔽,需查 pinctrl) --- ## 四、并发与同步 > 对应笔记:[[03-Linux驱动开发核心/04-并发同步与原子操作]] ### Q16: Linux 驱动中并发(竞态)的来源有哪些?为什么单核也要考虑并发? **答案要点**: 1. 多进程/多线程同时访问同一设备(如两个进程同时 open/read 同一驱动) 2. 中断随时打断进程上下文,中断处理函数与进程可能访问同一共享数据 3. 内核抢占(CONFIG_PREEMPT)使进程在内核态也可能被更高优先级任务抢占 4. SMP 多核下多个 CPU 真正并行执行内核代码 5. 即使单核,中断 + 抢占也会造成并发,所以任何驱动都要考虑保护 **详细解答**: 竞态条件(Race Condition)指多个执行单元同时访问共享资源,结果依赖执行时序。 ``` 理想执行: 实际可能执行: 进程A 读 count(=1) 进程A 读 count(=1) 进程A 写 count(=2) 进程B 读 count(=1) 进程B 读 count(=2) 进程A 写 count(=2) 进程B 写 count(=3) 进程B 写 count(=2) ← 丢失一次自增 ``` 并发来源: | 场景 | 说明 | 危害 | | -------------- | -------------------------- | ---- | | 多进程/多线程 | 多进程打开同一设备 | 中 | | 中断与进程 | 中断打断持有共享资源的进程 | 高 | | 内核抢占 | 内核态被高优先级进程抢占 | 中 | | SMP 多核 | 多核同时执行内核代码 | 最高 | | 软中断/tasklet | 下半部与进程/中断并发 | 中 | 一个经典例子是"设备只能被一个进程打开": ```c /* 错误写法:check-then-act 存在竞态 */ static int led_open(struct inode *inode, struct file *filp) { if (led_dev.in_use) /* 进程A判断为未使用 */ return -EBUSY; /* ← 此刻进程B也可能判断为未使用 */ led_dev.in_use = 1; /* 两个进程都置1,都打开成功 */ filp->private_data = &led_dev; return 0; } ``` 正确做法是使用原子操作或加锁,把"判断 + 占有"变成一个不可分割的整体(见 Q17)。 **追问**: - 追问1:单核无抢占无中断的极端情况下还需要锁吗?(理论不需要,但现实中不存在这种环境) - 追问2:`volatile` 能解决竞态吗?(不能,它只阻止编译器优化,不保证原子性) - 追问3:SMP 和单核在锁的选择上有什么差异?(SMP 必须用带内存屏障的锁,单核更多是防中断) --- ### Q17: 原子操作适合什么场景?它的底层是如何保证原子性的? **答案要点**: 1. 原子操作适用于对**单个变量**的加减/位操作,如引用计数、状态标志、设备占用标志 2. `atomic_t` 是封装了 `int counter` 的结构体,配合 `ATOMIC_INIT`、`atomic_read/set/inc/dec`、`atomic_dec_and_test` 使用 3. 底层依赖 CPU 的原子指令:ARM 的 `LDREX/STREX` 独占访问,x86 的 `lock` 前缀 4. 对多位共享数据、复杂结构体的操作不能用原子操作,必须用锁 5. 位操作 `set_bit/clear_bit/test_bit` 也是原子的 **详细解答**: `atomic_t` 定义(`include/linux/types.h`): ```c typedef struct { int counter; } atomic_t; ``` 常用 API: | 函数 | 说明 | | --------------------------------------- | ------------------ | | `ATOMIC_INIT(int i)` | 静态定义并初始化 | | `atomic_read(v)` | 读值 | | `atomic_set(v, i)` | 设值 | | `atomic_inc(v)` / `atomic_dec(v)` | 自增 / 自减 | | `atomic_add(i, v)` / `atomic_sub(i, v)` | 加 / 减 | | `atomic_dec_return(v)` | 自减并返回 v 的值 | | `atomic_inc_return(v)` | 自增并返回 v 的值 | | `atomic_dec_and_test(v)` | 自减并测试是否为 0 | | `atomic_inc_and_test(v)` | 自增并测试是否为 0 | | `atomic_sub_and_test(i, v)` | 减并测试是否为 0 | | `atomic_add_negative(i, v)` | 加 i 后结果为负返回真 | 对应 64 位版本为 `atomic64_t`(`typedef struct { long long counter; } atomic64_t;`),API 前缀换为 `atomic64_`、类型换为 `long long`。本书 Cortex-A7 为 32 位架构,只用 32 位原子操作。 设备互斥访问的经典用法: ```c struct gpioled_dev { /* ... */ atomic_t lock; }; static int led_open(struct inode *inode, struct file *filp) { /* 原子减 1 并测试是否为 0:为 0 说明之前是 1(设备可用) */ if (!atomic_dec_and_test(&gpioled.lock)) { atomic_inc(&gpioled.lock); /* 恢复原值 */ return -EBUSY; /* 设备已被打开 */ } filp->private_data = &gpioled; return 0; } static int led_release(struct inode *inode, struct file *filp) { struct gpioled_dev *dev = filp->private_data; atomic_inc(&dev->lock); /* 释放设备 */ return 0; } /* 初始化 */ atomic_set(&gpioled.lock, 1); ``` 位操作: ```c void set_bit(int nr, void *p); /* 置位 */ void clear_bit(int nr, void *p); /* 清位 */ void change_bit(int nr, void *p); /* 翻转 */ int test_bit(int nr, void *p); /* 测试,返回该位原值 */ int test_and_set_bit(int nr, void *p); /* 测试并置位,返回原值 */ int test_and_clear_bit(int nr, void *p);/* 测试并清位,返回原值 */ int test_and_change_bit(int nr, void *p);/* 测试并翻转,返回原值 */ ``` 底层原理: ``` ARM (Cortex-A7) 的 LDREX/STREX: LDREX R1, [R0] ; 独占读取 ADD R1, R1, #1 STREX R2, R1, [R0] ; 独占写入;若期间有其他访问,R2 != 0 失败重试 x86 的 lock 前缀: lock incl (mem) ; 锁总线 / 锁缓存行,保证读改写原子 本质:要么用"独占监视器",要么用"总线锁",保证 读-改-写 三步不可分割。 ``` 适用与不适用: | 适用 | 不适用 | | --------------- | -------------------- | | 单个整数的增减 | 多个变量需要一起改 | | 引用计数 | 复杂数据结构保护 | | 状态标志(0/1) | 需要睡眠的临界区 | | 位标志 | 涉及外部设备的临界区 | **追问**: - 追问1:`atomic_t` 为什么不能用于 64 位计数?(32 位原子在多数架构上才有保证,见 `atomic64_t`) - 追问2:`atomic_dec_and_test` 相比"先 read 再判断再 dec"好在哪? - 追问3:位操作适用于位图的哪类场景?(如 LED 状态位图、设备就绪位图) --- ### Q18: 自旋锁、信号量、互斥体有什么区别?各自适用什么场景? **答案要点**: 1. 自旋锁忙等待、不可睡眠、可用于中断上下文、临界区必须极短 2. 信号量可睡眠、可计数(可允许多个并发)、只能在进程上下文使用 3. 互斥体是"计数为 1 且带优先级继承"的特殊信号量,语义更严格,是最常用的互斥锁 4. 选择原则:能睡眠用 mutex,不能睡眠用 spinlock;需要计数并发用 semaphore 5. 中断上下文只能用自旋锁(或原子操作),绝不能用可能睡眠的锁 **详细解答**: 三者核心 API: ```c /* 自旋锁 */ DEFINE_SPINLOCK(lock); /* 静态定义并初始化 */ spin_lock_init(&lock); /* 动态初始化 */ spin_lock(&lock); spin_unlock(&lock); spin_trylock(&lock); /* 尝试获取,失败返回 0 */ spin_is_locked(&lock); /* 已被持有返回非 0 */ spin_lock_irq(&lock); /* 禁止本地中断 + 加锁 */ spin_unlock_irq(&lock); spin_lock_irqsave(&lock, flags); /* 保存中断状态 + 关本地中断 + 加锁 */ spin_unlock_irqrestore(&lock, flags); spin_lock_bh(&lock); /* 关闭下半部 + 加锁 */ spin_unlock_bh(&lock); /* 信号量 */ struct semaphore sem; DEFINE_SEMAPHORE(name); /* 定义并初始化为 1 */ sema_init(&sem, 1); /* 计数为 1 即互斥 */ down(&sem); /* 不可被信号打断 */ down_interruptible(&sem); /* 可被信号打断 */ down_trylock(&sem); /* 非阻塞,成功返回 0 */ up(&sem); /* 互斥体 */ struct mutex lock; DEFINE_MUTEX(name); /* 静态定义并初始化 */ mutex_init(&lock); mutex_lock(&lock); mutex_unlock(&lock); mutex_trylock(&lock); /* 成功返回 1,失败返回 0 */ mutex_is_locked(&lock); /* 被持有返回 1 */ mutex_lock_interruptible(&lock); /* 可被信号打断 */ ``` 使用示例: ```c /* 自旋锁保护短临界区 */ static irqreturn_t key_irq(int irq, void *dev_id) { unsigned long flags; spin_lock_irqsave(&dev->lock, flags); dev->key_count++; spin_unlock_irqrestore(&dev->lock, flags); return IRQ_HANDLED; } /* 互斥体保护可能睡眠的临界区 */ static ssize_t led_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt) { struct mydev *dev = filp->private_data; if (mutex_lock_interruptible(&dev->lock)) return -ERESTARTSYS; gpiod_set_value(dev->led, value); /* 可能触发的复杂操作 */ msleep(10); /* 允许睡眠 */ mutex_unlock(&dev->lock); return 0; } ``` 对比表: | 特性 | 自旋锁 spinlock | 信号量 semaphore | 互斥体 mutex | | ---------- | --------------- | ---------------- | ----------------- | | 等待方式 | 忙等待(自旋) | 睡眠等待 | 睡眠等待 | | 可睡眠 | 否 | 是 | 是 | | 中断上下文 | 可用 | 不可用 | 不可用 | | 计数 | 0/1 | 可 >1 | 0/1 | | 优先级继承 | 无 | 无 | 有 | | 持有者 | 无限制 | 无限制 | 必须解锁者=加锁者 | | 递归 | 否 | - | 否 | | 开销 | 低(忙等) | 中(上下文切换) | 中 | | 临界区时长 | 极短(μs) | 可长(ms) | 可长 | | 典型场景 | 中断、短临界区 | 计数并发控制 | 通用互斥 | **其他衍生锁**(原书 47.3.3 节):在自旋锁基础上还衍生出两类锁,多在 Linux 内核内部使用,普通驱动用得不多—— - **读写自旋锁 `rwlock_t`**:适用于"读多写少"或"生产者/消费者"模型。无写操作时允许多个线程同时持有读锁并发读;写锁独占,持有写锁时不能读。API:`read_lock/read_unlock`、`write_lock/write_unlock`(均有 `_irq/_irqsave/_bh` 变体)。 - **顺序锁 `seqlock_t`**:允许"读和写同时进行",但不允许并发写。读用 `read_seqbegin()` 取序号、读完用 `read_seqretry()` 校验期间是否发生过写,若有则重读。注意:顺序锁保护的资源**不能是指针**,否则写时指针可能失效导致读方崩溃。 选择决策: ``` 需要保护临界区 │ ├─ 会在中断上下文访问? ──是──► 自旋锁(irqsave) │ ├─ 需要允许多个并发? ──是──► 信号量(计数 >1) │ └─ 进程上下文、可能睡眠 ──► 互斥体(首选) ``` **追问**: - 追问1:为什么自旋锁不能睡眠?自旋锁持有期间睡眠会导致什么? - 追问2:互斥体为什么必须由"加锁的同一上下文"解锁?信号量可以跨上下文 up 吗? - 追问3:自旋锁的临界区里如果调用了 `copy_to_user`(可能缺页睡眠),会有什么后果? --- ### Q19: 为什么在可能被中断访问的临界区要用 spin_lock_irqsave?它和 spin_lock_irq 有什么区别? **答案要点**: 1. 如果进程 A 持有自旋锁时被中断打断,中断处理函数又去获取同一把锁,就会在**同一个 CPU 上死锁** 2. `spin_lock_irqsave` 在加锁前禁止本地中断并保存中断状态,解锁时恢复 3. `spin_lock_irq` 也禁止中断,但它假设调用前中断是开启的,解锁时无条件开启中断 4. `irqsave/irqrestore` 保存并恢复原状态,在中断上下文/已关中断的场景下更安全 5. 只在确定临界区不会被中断访问时,才用最小的 `spin_lock` **详细解答**: 死锁场景: ``` CPU0: 进程A: spin_lock(&lock) ← 获得锁 |(此时发生中断) 中断处理: spin_lock(&lock) ← 锁已被自己持有,本 CPU 自旋 → 死锁! (中断不返回,进程A永远无法 spin_unlock) ``` `spin_lock_irqsave` 的做法: ```c static DEFINE_SPINLOCK(lock); static irqreturn_t handler(int irq, void *dev_id) { unsigned long flags; spin_lock_irqsave(&lock, flags); /* 关本地中断 + 保存之前中断状态 + 加锁 */ /* 临界区 */ spin_unlock_irqrestore(&lock, flags); /* 解锁 + 恢复中断状态 */ return IRQ_HANDLED; } ``` `spin_lock_irqsave` vs `spin_lock_irq`: | 对比项 | spin_lock_irqsave | spin_lock_irq | | ------------ | ---------------------- | ------------------------------- | | 保存中断状态 | 是(flags) | 否 | | 解锁行为 | 恢复到加锁前状态 | 无条件开中断 | | 适用 | 不确定中断是否已关闭时 | 确定中断当前是开的 | | 风险 | 低 | 在中断开/已关中断时可能误开中断 | | 推荐度 | 推荐 | 特定场景 | 为什么不直接 `spin_lock`: - `spin_lock` 在 SMP 下能防止其他 CPU 并发,但在**单核**上只禁止内核抢占,不禁止中断。 - 若临界区数据会被中断处理函数访问,单核下 `spin_lock` 无法阻止中断,仍可能死锁或数据错乱。 - 因此"可能被中断访问"的临界区必须用 `irqsave` 变体。 其他变体: ```c spin_lock_bh(&lock); /* 禁止下半部(软中断/tasklet),不禁止硬中断 */ spin_unlock_bh(&lock); /* 与上半部并发保护的场景 */ ``` **追问**: - 追问1:`spin_lock_irqsave` 里的 `flags` 变量为什么必须传同一个?(恢复的是加锁前的中断状态) - 追问2:在中断处理函数里加锁,还需要 `irqsave` 吗?(**原书用法**:线程中使用 `spin_lock_irqsave/spin_unlock_irqrestore`、中断中使用 `spin_lock/spin_unlock`——线程侧持锁时已禁止本地中断,中断侧才不会在本 CPU 上自旋死锁;若线程侧只用 `spin_lock` 而中断也访问同一数据,则两边都必须关中断。在中断里用 `irqsave` 变体同样安全) - 追问3:什么时候用 `spin_lock_bh`?(与软中断/tasklet 共享数据时) --- ### Q20: 实战题:驱动里如何避免死锁?加锁的位置和粒度怎样设计? **答案要点**: 1. 死锁四大来源:同锁递归获取、锁顺序不一致(AB-BA)、持锁睡眠、中断/进程交叉 2. 规避:统一加锁顺序、临界区尽量短、绝不在持锁时睡眠、不在中断里用会睡眠的锁 3. 加锁粒度:只保护真正的共享数据,不要把整个函数体都包进锁 4. 用 `trylock` 做容错路径,用 `mutex_lock_interruptible` 响应信号 5. 加锁/解锁必须成对,错误分支也要解锁(goto 统一出口) **详细解答**: **死锁四种典型场景**: ``` 1. 同锁递归: spin_lock(&lock); ... spin_lock(&lock); // 自己等自己 2. 锁顺序不一致(AB-BA): 进程A: lock(a) → lock(b) 进程B: lock(b) → lock(a) // 互相等待 3. 持锁睡眠: spin_lock(&lock); copy_to_user(...); // 可能缺页睡眠 → 与中断死锁 4. 中断/进程交叉: 进程持锁 → 中断打断 → 中断抢同一把锁(见 Q19) ``` **规避原则**: | 原则 | 说明 | | ------------ | ------------------------------- | | 统一锁顺序 | 多个锁按全局固定顺序获取 | | 临界区最小化 | 只包住共享数据操作,不含耗时/IO | | 持锁不睡眠 | 自旋锁临界区禁用可能睡眠的调用 | | 中断安全 | 与中断共享的数据用 irqsave | | 成对释放 | 错误分支也解锁,用统一出口 | | 超时/尝试 | trylock 避免无限等待 | **加锁位置与粒度设计示例**: ```c struct mydev { struct mutex lock; /* 保护共享数据 */ int count; /* 共享计数 */ struct gpio_desc *led; /* 硬件资源,操作可能慢 */ }; static ssize_t mydev_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt) { struct mydev *dev = filp->private_data; u8 val; int ret; /* 锁外做费时/可能睡眠的用户拷贝 */ if (copy_from_user(&val, buf, 1)) return -EFAULT; if (mutex_lock_interruptible(&dev->lock)) return -ERESTARTSYS; dev->count++; /* 只保护共享数据 */ ret = 0; mutex_unlock(&dev->lock); /* 尽快解锁 */ /* 硬件操作放锁外,避免持锁期间阻塞他人 */ gpiod_set_value(dev->led, val ? 1 : 0); return ret; } ``` 注意:上例中 `val` 是局部变量,`count` 才是共享数据,因此拷贝和硬件操作都放在锁外,只有 `count++` 在锁内,这就是"细粒度"。 **统一出口写法**: ```c static int mydev_ioctl(struct file *filp, unsigned int cmd, unsigned long arg) { struct mydev *dev = filp->private_data; int ret = 0; if (mutex_lock_interruptible(&dev->lock)) return -ERESTARTSYS; switch (cmd) { case MY_CMD_A: ret = do_a(dev); if (ret) goto out; /* 错误也走统一出口 */ break; default: ret = -EINVAL; goto out; } out: mutex_unlock(&dev->lock); /* 唯一解锁点 */ return ret; } ``` **追问**: - 追问1:`mutex_lock_interruptible` 返回非 0 时为什么可以直接 return 而不解锁?(它没拿到锁) - 追问2:如果临界区必须调用一个可能睡眠的函数,应该换成哪种锁? - 追问3:如何用 lockdep 内核配置检测死锁?(`CONFIG_PROVE_LOCKING`) --- ## 五、中断处理 > 对应笔记:[[03-Linux驱动开发核心/05-中断下半部处理]]、[[03-Linux驱动开发核心/05-中断下半部机制]] ### Q21: 中断上半部和下半部为什么要分开?划分原则是什么? **答案要点**: 1. 硬中断处理期间通常会屏蔽同级甚至所有中断,执行时间过长会导致丢中断、系统卡顿 2. 上半部(top half):做最紧急的事(应答硬件、清中断标志、保存关键数据),必须极快、不可睡眠 3. 下半部(bottom half):做耗时的后续处理(数据加工、唤醒进程),可以被延迟、可以被调度 4. 划分原则:"必须立刻做、不能等"的放上半部,其余一律放下半部 5. 下半部机制有软中断、tasklet、工作队列、线程化中断,按能否睡眠选择 **详细解答**: 中断处理的总原则是**快进快出**。一个中断处理函数如果执行 10ms,就意味着这个 CPU 在这 10ms 内无法响应其他中断。 ``` 硬件中断触发 │ ▼ ┌─────────────────────────┐ │ 上半部(硬中断上下文) │ → 读取硬件状态、清中断标志、 │ 极短、不可睡眠 │ 保存数据、调度下半部 └─────────────────────────┘ │ ▼(延迟) ┌─────────────────────────┐ │ 下半部 │ → 数据加工、唤醒等待进程、 │ 软中断/进程/内核线程 │ 耗时操作 └─────────────────────────┘ ``` 划分原则: | 归属 | 特征 | 例子 | | ------ | -------------------------- | ------------------------------ | | 上半部 | 硬件相关、必须立即、极短 | 清中断标志、ACK 中断控制器 | | 下半部 | 数据处理、可延迟、可能耗时 | 解析报文、唤醒 read 阻塞的进程 | 正点原子中断实验的经典划分: ```c /* 上半部:只做最小工作——启动定时器消抖 */ static irqreturn_t key0_handler(int irq, void *dev_id) { struct imx6uirq_dev *dev = (struct imx6uirq_dev *)dev_id; dev->curkeynum = 0; dev->timer.data = (volatile long)dev_id; mod_timer(&dev->timer, jiffies + msecs_to_jiffies(10)); return IRQ_RETVAL(IRQ_HANDLED); } /* 下半部效果:定时器回调里读 GPIO、置位、唤醒 */ void timer_function(unsigned long arg) { /* 读电平、判断按下/松开、atomic_set、wake_up_interruptible... */ } ``` 对比表: | 特性 | 上半部 | 下半部 | | ------------ | ---------- | ------------------- | | 上下文 | 硬中断 | 软中断/进程/线程 | | 时间要求 | 极短(μs) | 可较长(ms) | | 可否睡眠 | 否 | 工作队列/线程化:可 | | 是否屏蔽中断 | 会 | 一般不 | | 优先级 | 最高 | 较低 | **追问**: - 追问1:如果中断处理里必须读一个慢速 I2C 设备,应该怎么设计?(上半部调度下半部,I2C 读放下半部) - 追问2:下半部会被中断打断吗?(软中断/tasklet 可能被硬中断打断,但不会被同 CPU 的软中断嵌套) - 追问3:所有中断都一定要有下半部吗?(不一定,简单中断可以只在上半部完成) --- ### Q22: tasklet、工作队列、线程化中断有什么区别?实际项目怎么选? **答案要点**: 1. 软中断(softirq):内核静态定义、并发性最高,普通驱动不要直接使用 2. tasklet:基于软中断、同一 tasklet 不会并发、运行在软中断上下文、**不可睡眠**、开销小 3. 工作队列(workqueue):运行在进程上下文/内核线程、**可以睡眠**、可并发、开销较大 4. 线程化中断:上半部极小 + 下半部在内核线程,可睡眠、可与互斥体配合 5. 选择:不需要睡眠→tasklet;需要睡眠→workqueue;需要互斥/复杂→threaded irq **详细解答**: **tasklet**: ```c /* 定义 + 处理函数 */ struct tasklet_struct my_tasklet; void my_tasklet_func(unsigned long data) { /* 软中断上下文,不能睡眠 */ } static irqreturn_t my_handler(int irq, void *dev_id) { tasklet_schedule(&my_tasklet); /* 调度 tasklet */ return IRQ_HANDLED; } static int __init my_init(void) { tasklet_init(&my_tasklet, my_tasklet_func, 0); request_irq(irq, my_handler, 0, "my", NULL); return 0; } ``` 也可静态定义:`DECLARE_TASKLET(name, func, data)`。 **工作队列**: ```c struct work_struct my_work; void my_work_func(struct work_struct *work) { /* 进程上下文,可以睡眠 */ msleep(10); } static irqreturn_t my_handler(int irq, void *dev_id) { schedule_work(&my_work); /* 丢到系统工作队列 */ return IRQ_HANDLED; } static int __init my_init(void) { INIT_WORK(&my_work, my_work_func); request_irq(irq, my_handler, 0, "my", NULL); return 0; } ``` 自定义工作队列: ```c struct workqueue_struct *wq = alloc_workqueue("mywq", WQ_UNBOUND, 1); queue_work(wq, &my_work); destroy_workqueue(wq); ``` 延迟工作: ```c struct delayed_work dwork; INIT_DELAYED_WORK(&dwork, my_work_func); schedule_delayed_work(&dwork, msecs_to_jiffies(100)); cancel_delayed_work_sync(&dwork); /* 卸载时必须取消 */ ``` **线程化中断**: ```c static irqreturn_t key_hard(int irq, void *dev_id) { return IRQ_WAKE_THREAD; /* 唤醒线程处理 */ } static irqreturn_t key_thread(int irq, void *dev_id) { msleep(5); /* 内核线程上下文,可睡眠 */ return IRQ_HANDLED; } request_threaded_irq(irq, key_hard, key_thread, IRQF_TRIGGER_FALLING | IRQF_ONESHOT, "key", dev); ``` 三者对比: | 特性 | tasklet | 工作队列 | 线程化中断 | | ---------- | ----------------- | ---------------- | ------------- | | 执行上下文 | 软中断 | 进程(内核线程) | 内核线程 | | 可睡眠 | 否 | 是 | 是 | | 并发性 | 同 tasklet 不并发 | 可并发 | 可并发 | | 开销 | 小 | 中 | 较大 | | 延迟 | 低 | 中 | 较高 | | 典型用途 | 快速小任务 | 耗时/需睡眠 | 复杂中断+互斥 | 选择决策: ``` 需要下半部 │ ├─ 需要睡眠 / 调用可能阻塞的接口? ──是──► 工作队列 or 线程化中断 │ ├─ 需要与进程互斥 → 线程化中断 │ └─ 只是耗时 → 工作队列 │ └─ 不需要睡眠、很短? ──► tasklet ``` **追问**: - 追问1:内核为什么"不鼓励"普通驱动直接用软中断?(并发性高、易出错、需自己处理每 CPU 重入) - 追问2:卸载模块时忘记 `cancel_work_sync` 会导致什么问题?(work 在模块卸载后执行 → oops) - 追问3:`IRQF_ONESHOT` 的作用是什么?(原书描述为"单次中断,中断执行一次就结束";更准确地说,它让中断线在处理完成前保持屏蔽,线程化中断期间防止重复触发) --- ### Q23: 驱动如何获得并使用中断号?request_irq 的各个参数是什么含义? **答案要点**: 1. 中断号获取有三条路:`irq_of_parse_and_map`(设备树)、`gpio_to_irq`(GPIO 转中断)、`platform_get_irq`(platform 资源) 2. 设备树中用 `interrupt-parent` 和 `interrupts` 描述中断来源与触发方式 3. `request_irq(irq, handler, flags, name, dev_id)` 注册中断处理函数,返回 0 成功 4. `flags` 指定触发方式与共享属性:`IRQF_TRIGGER_FALLING/RISING/...`、`IRQF_SHARED`、`IRQF_ONESHOT` 5. 中断处理函数原型固定为 `irqreturn_t (*)(int irq, void *dev_id)`,返回 `IRQ_HANDLED`/`IRQ_NONE` **详细解答**: **设备树描述中断**: ```dts key { compatible = "atkalpha-key"; pinctrl-names = "default"; pinctrl-0 = <&pinctrl_key>; key-gpio = <&gpio1 18 GPIO_ACTIVE_LOW>; interrupt-parent = <&gpio1>; /* 中断控制器 */ interrupts = <18 IRQ_TYPE_EDGE_BOTH>; /* 引脚号 触发方式 */ status = "okay"; }; ``` `IRQ_TYPE_EDGE_BOTH` = 上升沿 + 下降沿,按下与松开都触发。 `include/linux/irq.h` 中的触发类型: ```c IRQ_TYPE_NONE = 0x00, IRQ_TYPE_EDGE_RISING = 0x01, IRQ_TYPE_EDGE_FALLING = 0x02, IRQ_TYPE_EDGE_BOTH = (IRQ_TYPE_EDGE_FALLING | IRQ_TYPE_EDGE_RISING), IRQ_TYPE_LEVEL_HIGH = 0x04, IRQ_TYPE_LEVEL_LOW = 0x08, ``` **获取中断号与应用**: ```c /* 方式一:从设备树解析 */ irqnum = irq_of_parse_and_map(nd, 0); /* 方式二:从 GPIO 编号转换 */ irqnum = gpio_to_irq(key_gpio); /* 方式三:platform 资源(DT 或 resource 中带 IORESOURCE_IRQ) */ irqnum = platform_get_irq(pdev, 0); /* 注册中断处理函数 */ ret = request_irq(irqnum, key_handler, IRQF_TRIGGER_FALLING | IRQF_TRIGGER_RISING, "KEY0", &imx6uirq); if (ret < 0) printk("irq %d request failed!\r\n", irqnum); /* 卸载时释放 */ free_irq(irqnum, &imx6uirq); ``` 参数详解: | 参数 | 含义 | | ------- | ------------------------------------------- | | irq | 中断号 | | handler | `irqreturn_t (*)(int, void *)` 处理函数 | | flags | 触发方式、共享标志 | | name | 显示在 `/proc/interrupts` 的名称 | | dev_id | 传给 handler 的参数,共享中断时用于区分设备 | flags 常用值: | flag | 含义 | | --------------------- | ---------------------- | | IRQF_TRIGGER_NONE | 无触发 | | IRQF_TRIGGER_RISING | 上升沿触发 | | IRQF_TRIGGER_FALLING | 下降沿触发 | | IRQF_TRIGGER_HIGH/LOW | 高/低电平触发 | | IRQF_SHARED | 共享中断线 | | IRQF_ONESHOT | 单次中断(中断线上保持屏蔽,线程化中断常用) | | IRQF_NO_SUSPEND | 休眠期间不关闭 | > **原书要点**:`request_irq` 可能会导致睡眠,因此不能在中断上下文或其他禁止睡眠的代码段中使用;`request_irq` 会激活(使能)中断,不需要我们手动去使能。返回 `-EBUSY` 表示中断已经被申请。 中断处理函数: ```c static irqreturn_t key_handler(int irq, void *dev_id) { struct imx6uirq_dev *dev = (struct imx6uirq_dev *)dev_id; /* 判断是否为本设备中断(共享中断必需) */ /* ... */ return IRQ_HANDLED; /* 已处理 */ /* return IRQ_NONE; */ /* 不是本设备的中断 */ } ``` **追问**: - 追问1:共享中断里为什么必须判断"是不是自己的设备产生的中断"?不判会怎样? - 追问2:`request_irq` 返回 `-EBUSY` 的常见原因?(中断号已被占用、未加 IRQF_SHARED) - 追问3:如何确认中断真的被触发和注册?(`cat /proc/interrupts`) --- ### Q24: 为什么中断上下文不能睡眠?为什么不能用 mutex?那需要互斥时怎么办? **答案要点**: 1. 中断上下文没有独立的 `task_struct`,无法被调度器调度,睡眠后无法被唤醒恢复 2. 硬中断上下文会屏蔽中断,睡眠会导致调度器无法运行 → 系统挂起/崩溃 3. `mutex_lock` 在竞争时会睡眠等待,因此中断里禁用 4. 中断里需要保护共享数据,只能用**自旋锁**(配 `irqsave`)或原子操作 5. 如果确实需要睡眠的复杂处理,应放到工作队列/线程化中断里,在进程上下文加 mutex **详细解答**: 中断上下文(interrupt context)与进程上下文(process context)的本质区别: | 对比项 | 进程上下文 | 中断上下文 | | ---------------------- | -------------- | ------------- | | task_struct | 有 | 无 | | 可被调度 | 是 | 否 | | 可睡眠 | 是 | 否 | | 能调用 mutex | 是 | 否 | | 能调用 msleep/schedule | 是 | 否 | | 可用锁 | mutex/spinlock | 只能 spinlock | 为什么不能睡眠: ``` 进程A 在某个中断处理函数里调用 mutex_lock() │ ├─ 若 mutex 已被其他上下文持有 → mutex_lock 调用 schedule() 主动让出 CPU │ ├─ 但当前没有可调度的 task(处于中断上下文)→ 内核无法切走 │ └─ 结果:要么触发内核警告 "BUG: scheduling while atomic", 要么直接死锁/panic ``` 内核会在 `might_sleep()` / `__schedule` 中检查"是否在原子上下文",一旦在中断里睡眠会打印警告甚至崩溃。 正确的做法: ```c /* 错误:中断里用 mutex */ static irqreturn_t bad_handler(int irq, void *dev_id) { mutex_lock(&dev->lock); /* 可能睡眠,禁止! */ mutex_unlock(&dev->lock); return IRQ_HANDLED; } /* 正确一:中断里用自旋锁 */ static irqreturn_t good_handler(int irq, void *dev_id) { unsigned long flags; spin_lock_irqsave(&dev->lock, flags); /* 极短临界区 */ spin_unlock_irqrestore(&dev->lock, flags); return IRQ_HANDLED; } /* 正确二:把需要 mutex 的工作丢到工作队列 */ static irqreturn_t handler_sched(int irq, void *dev_id) { schedule_work(&dev->work); /* 进程上下文里再用 mutex */ return IRQ_HANDLED; } ``` 如果需要"中断线程化": ```c /* 上半部只唤醒线程,下半部在内核线程里可以加 mutex */ request_threaded_irq(irq, hard_handler, thread_handler, IRQF_TRIGGER_FALLING | IRQF_ONESHOT, "dev", priv); static irqreturn_t thread_handler(int irq, void *dev_id) { mutex_lock(&priv->lock); /* 内核线程上下文,可以睡眠 */ /* 复杂处理 */ mutex_unlock(&priv->lock); return IRQ_HANDLED; } ``` **追问**: - 追问1:`spin_lock` 在中断上下文里能用吗?为什么自旋锁可以而 mutex 不行? - 追问2:`down_trylock`/`mutex_trylock` 在中断里能用吗?(trylock 不睡眠,理论可用,但仍不推荐在中断里做复杂同步) - 追问3:`GFP_ATOMIC` 和 `GFP_KERNEL` 的区别与此有关吗?(在中断里 kmalloc 必须用 GFP_ATOMIC) --- ### Q25: 实战题:设计一个按键中断驱动(含消抖),并说明如何避免抖动、丢失和误触发。 **答案要点**: 1. 上半部只做最小动作:记录键号、启动定时器(`mod_timer`),避免在中断里做耗时消抖 2. 定时器回调(下半部)延时 10ms 后再读 GPIO 电平,完成软件消抖 3. 用 `atomic_t` 保存键值与标志,避免与进程上下文的 read 竞态 4. 用等待队列/异步通知把按键事件传给用户空间,避免轮询 5. 卸载时严格 `del_timer_sync` + `free_irq`,顺序很重要 **详细解答**: 设备树: ```dts key { compatible = "atkalpha-key"; pinctrl-names = "default"; pinctrl-0 = <&pinctrl_key>; key-gpio = <&gpio1 18 GPIO_ACTIVE_LOW>; /* KEY0 */ interrupt-parent = <&gpio1>; interrupts = <18 IRQ_TYPE_EDGE_BOTH>; /* 按下+松开 */ status = "okay"; }; ``` 数据结构: ```c #define KEY_NUM 1 #define KEY0VALUE 0X01 #define INVAKEY 0XFF struct imx6uirq_dev { dev_t devid; struct cdev cdev; struct class *class; struct device *device; int major, minor; struct device_node *nd; atomic_t keyvalue; /* 有效键值 */ atomic_t releasekey; /* 松开标志 */ struct timer_list timer; /* 消抖定时器 */ unsigned char curkeynum; int gpio; int irqnum; wait_queue_head_t r_wait; /* 阻塞读等待队列 */ }; ``` 上半部 + 定时器下半部: ```c static irqreturn_t key0_handler(int irq, void *dev_id) { struct imx6uirq_dev *dev = (struct imx6uirq_dev *)dev_id; dev->curkeynum = 0; dev->timer.data = (volatile long)dev_id; mod_timer(&dev->timer, jiffies + msecs_to_jiffies(10)); /* 10ms后消抖 */ return IRQ_RETVAL(IRQ_HANDLED); } static void timer_function(unsigned long arg) { struct imx6uirq_dev *dev = (struct imx6uirq_dev *)arg; int value = gpio_get_value(dev->gpio); if (value == 0) { /* 仍为低:确实按下 */ atomic_set(&dev->keyvalue, KEY0VALUE); } else { /* 已恢复高:松开 */ atomic_set(&dev->keyvalue, 0x80 | KEY0VALUE); atomic_set(&dev->releasekey, 1); wake_up_interruptible(&dev->r_wait); /* 唤醒阻塞读 */ } } ``` 初始化: ```c static int keyio_init(void) { /* 获取 GPIO 与中断号 */ dev->gpio = of_get_named_gpio(dev->nd, "key-gpio", 0); gpio_request(dev->gpio, "KEY0"); gpio_direction_input(dev->gpio); dev->irqnum = irq_of_parse_and_map(dev->nd, 0); /* 定时器 */ init_timer(&dev->timer); dev->timer.function = timer_function; /* 等待队列 */ init_waitqueue_head(&dev->r_wait); /* 注册中断:双边沿 */ return request_irq(dev->irqnum, key0_handler, IRQF_TRIGGER_FALLING | IRQF_TRIGGER_RISING, "KEY0", dev); } ``` 避免各类问题的设计要点: | 问题 | 原因 | 对策 | | -------------- | ------------------ | ----------------------------- | | 按键抖动 | 机械触点抖动 | 定时器延时 10ms 后二次采样 | | 中断里太慢 | 消抖 sleep/延时 | 上半部只调度定时器 | | 事件丢失 | 中断期间数据被覆盖 | atomic + releasekey 标志 | | 用户轮询耗 CPU | 应用层忙等 | 等待队列 / poll / 异步通知 | | 卸载崩溃 | 定时器/中断未清 | `del_timer_sync` → `free_irq` | | 共享中断误判 | 不判断来源 | handler 里校验是否本设备触发 | 卸载顺序(关键): ```c static void __exit imx6uirq_exit(void) { del_timer_sync(&dev->timer); /* 先停定时器,避免回调访问已释放资源 */ free_irq(dev->irqnum, dev); /* 再释放中断 */ gpio_free(dev->gpio); cdev_del(&dev->cdev); unregister_chrdev_region(dev->devid, 1); device_destroy(dev->class, dev->devid); class_destroy(dev->class); } ``` **追问**: - 追问1:为什么用 `del_timer_sync` 而不是 `del_timer`?(等待正在执行的定时器回调结束,防竞态) - 追问2:硬件消抖(RC 电路)和软件消抖各有什么优缺点? - 追问3:如果要支持多个按键,用什么数据结构管理?(`struct irq_keydesc[]` 数组,每个元素含 gpio/irqnum/handler/value) --- ## 六、platform 总线模型 > 对应笔记:[[03-Linux驱动开发核心/07-platform总线模型]]、[[03-Linux驱动开发核心/07-platform驱动模型]] ### Q26: platform 总线模型的核心思想是什么?为什么要引入 platform_device 和 platform_driver? **答案要点**: 1. Linux 设备模型由 bus / device / driver 三者构成,总线的核心职责是**匹配** device 与 driver 2. platform 是"伪总线",用于描述 SoC 内部那些不挂在 I2C/SPI/USB 等真实总线上的片上外设 3. 核心思想是**设备与驱动分离**:device 描述"硬件资源"(地址、中断),driver 描述"怎么操作" 4. 分离后,同一驱动可适配多块板子(资源不同、驱动相同),硬件信息可放到设备树 5. 匹配成功后会调用 driver 的 `probe`,失败则调用 `remove` **详细解答**: 设备模型三要素: ``` ┌────────────┐ │ bus │ ← 总线:负责匹配、维护 device/driver 链表 └─────┬──────┘ ┌─────────┴─────────┐ ▼ ▼ ┌─────────┐ ┌──────────┐ │ device │◄─匹配─►│ driver │ └─────────┘ └──────────┘ 资源 行为 (reg/irq) (probe/remove) ``` 为什么需要 platform: - I2C 设备挂在 I2C 总线上,USB 设备挂在 USB 总线上,它们有实际的物理总线可以自动枚举。 - 但 SoC 内部的 GPIO 控制器、UART、SPI 控制器、LCD 控制器等**没有可枚举的物理总线**,需要一种机制把"硬件资源"和"驱动程序"关联起来。 - platform 总线就是为此设计的"虚拟总线"。 `platform_device` 描述硬件资源(原书示例代码 54.2.3.1): ```c struct platform_device { const char *name; /* 设备名,要和驱动的 name 相同(非 DT 方式) */ int id; /* 设备 ID,-1 表示自动分配 */ bool id_auto; /* 是否自动分配 ID */ struct device dev; /* 内含 of_node,指向设备树节点 */ u32 num_resources; struct resource *resource; /* 资源数组:内存、中断 */ const struct platform_device_id *id_entry; /* id_table 匹配项 */ char *driver_override; /* 强制绑定的驱动名 */ struct mfd_cell *mfd_cell; /* MFD cell pointer */ struct pdev_archdata archdata; /* arch specific additions */ }; struct resource { resource_size_t start; /* 起始 */ resource_size_t end; /* 结束 */ const char *name; unsigned long flags; /* IORESOURCE_MEM / IORESOURCE_IRQ */ struct resource *parent, *sibling, *child; /* 资源树 */ }; ``` 设备树方式下,platform_device 由内核从 DT 自动生成,`dev.of_node` 指向对应节点。 `platform_driver` 描述驱动行为(原书示例代码 54.2.2.1): ```c struct platform_driver { int (*probe)(struct platform_device *); int (*remove)(struct platform_device *); void (*shutdown)(struct platform_device *); int (*suspend)(struct platform_device *, pm_message_t state); int (*resume)(struct platform_device *); struct device_driver driver; /* 内含 name、of_match_table */ const struct platform_device_id *id_table; /* id_table 匹配表 */ bool prevent_deferred_probe; }; ``` 分离带来的好处: | 好处 | 说明 | | ------ | -------------------------------------- | | 复用 | 一个驱动适配多板,只要 compatible 相同 | | 解耦 | 硬件改动只改 DTS,不改驱动 | | 可维护 | 资源与逻辑分离,职责清晰 | | 统一 | 与设备模型、sysfs、电源管理统一集成 | **追问**: - 追问1:platform_device 和普通 device 的区别是什么? - 追问2:既然有设备树,还需要手动写 `platform_device` 的 C 代码吗?(通常不需要,内核从 DT 生成) - 追问3:platform 总线的匹配是在什么时候发生的?(设备注册或驱动注册时都会尝试匹配) --- ### Q27: platform 总线的匹配机制有哪些?compatible 与 of_match_table 是怎样配合的?module_platform_driver 展开成什么? **答案要点**: 1. 传统匹配:`platform_device->name` 与 `platform_driver->driver.name` 字符串相等 2. 设备树匹配:`of_device_id.compatible` 与 DTS 节点 `compatible` 相等(现代主流) 3. 还有 ACPI 匹配、id_table 匹配等,`platform_match()` 中的优先级为 `driver_override` > of > acpi > id_table > name 4. `of_match_table` 放在 `platform_driver.driver.of_match_table`,配合 `MODULE_DEVICE_TABLE(of, ...)` 5. `module_platform_driver(x)` 展开为 `module_init(x##_init)` + `module_exit(x##_exit)`,其中 init/exit 内部调用 `platform_driver_register/unregister` **详细解答**: **匹配方式一:name 匹配(传统)** ```c /* 设备 */ static struct platform_device led_device = { .name = "my-led", .id = -1, }; /* 驱动 */ static struct platform_driver led_driver = { .probe = led_probe, .remove = led_remove, .driver = { .name = "my-led", /* 与 device->name 相等即匹配 */ }, }; ``` **匹配方式二:compatible 匹配(设备树,推荐)** ```c static const struct of_device_id led_of_match[] = { { .compatible = "atkalpha-gpioled" }, { .compatible = "atkalpha-led" }, { /* sentinel */ } }; MODULE_DEVICE_TABLE(of, led_of_match); static struct platform_driver led_driver = { .probe = led_probe, .remove = led_remove, .driver = { .name = "led-driver", .of_match_table = led_of_match, /* 关键 */ }, }; ``` ```dts gpioled { compatible = "atkalpha-gpioled"; /* 与 of_match_table 匹配 */ ... }; ``` **匹配优先级**(`platform_match` 内部): ```c static int platform_match(struct device *dev, struct device_driver *drv) { struct platform_device *pdev = to_platform_device(dev); struct platform_driver *pdrv = to_platform_driver(drv); /* 0. driver_override 强制匹配(设置后只绑定指定驱动) */ if (pdev->driver_override) return !strcmp(pdev->driver_override, drv->name); /* 1. 先按 of_match_table / compatible 匹配 */ if (of_driver_match_device(dev, drv)) return 1; /* 2. 再按 acpi 匹配 */ if (acpi_driver_match_device(dev, drv)) return 1; /* 3. 再按 id_table 匹配 */ if (pdrv->id_table) return platform_match_id(pdrv->id_table, pdev) != NULL; /* 4. 最后按 name 匹配 */ return strcmp(pdev->name, drv->name) == 0; } ``` **module_platform_driver 宏展开**: ```c #define module_platform_driver(__platform_driver) \ module_driver(__platform_driver, platform_driver_register, \ platform_driver_unregister) /* 大致等价于 */ static int __init xxx_init(void) { return platform_driver_register(&xxx_driver); } module_init(xxx_init); static void __exit xxx_exit(void) { platform_driver_unregister(&xxx_driver); } module_exit(xxx_exit); ``` 因此下面两种写法等价: ```c /* 写法一:手动 */ static int __init led_init(void) { return platform_driver_register(&led_driver); } static void __exit led_exit(void) { platform_driver_unregister(&led_driver); } module_init(led_init); module_exit(led_exit); /* 写法二:宏(推荐) */ module_platform_driver(led_driver); ``` **追问**: - 追问1:为什么 `MODULE_DEVICE_TABLE(of, ...)` 是必须的?(导出设备别名,支持模块自动加载 `modprobe` / `depmod`) - 追问2:如果 name 和 compatible 都能匹配,哪个优先? - 追问3:`of_match_ptr` 宏的作用是什么?(在未启用 CONFIG_OF 时避免编译错误/返回 NULL) --- ### Q28: platform 驱动的 probe 函数里应该做哪些事?如何获取 reg 和中断资源? **答案要点**: 1. probe 是"驱动发现硬件后"的初始化入口,只针对当前这块 device 做初始化 2. 获取内存资源:`platform_get_resource(pdev, IORESOURCE_MEM, i)` → `devm_ioremap_resource` 3. 获取中断:`platform_get_irq(pdev, i)` 或 `platform_get_resource(IORESOURCE_IRQ)` 4. 解析设备树私有属性:`of_property_read_*`、`of_get_named_gpio` 5. 注册字符设备、创建 class/device(如果是字符设备驱动),并把实例挂到 `platform_set_drvdata` 6. probe 失败要返回错误码,且要能正确回滚(devm 系列可减负) **详细解答**: 标准 probe 骨架: ```c static int led_probe(struct platform_device *pdev) { struct resource *res; struct led_dev *dev; int ret; /* 1、分配私有数据结构(devm 自动释放) */ dev = devm_kzalloc(&pdev->dev, sizeof(*dev), GFP_KERNEL); if (!dev) return -ENOMEM; /* 2、获取内存资源并映射 */ res = platform_get_resource(pdev, IORESOURCE_MEM, 0); if (!res) return -ENODEV; dev->base = devm_ioremap_resource(&pdev->dev, res); if (IS_ERR(dev->base)) return PTR_ERR(dev->base); /* 3、获取中断号 */ dev->irq = platform_get_irq(pdev, 0); if (dev->irq < 0) return dev->irq; ret = devm_request_irq(&pdev->dev, dev->irq, led_irq_handler, 0, "led", dev); if (ret) return ret; /* 4、获取 GPIO */ dev->led = devm_gpiod_get(&pdev->dev, "led", GPIOD_OUT_HIGH); if (IS_ERR(dev->led)) return PTR_ERR(dev->led); /* 5、注册字符设备 */ ret = led_chrdev_register(dev); if (ret) return ret; /* 6、保存实例,供 remove 使用 */ platform_set_drvdata(pdev, dev); return 0; } static int led_remove(struct platform_device *pdev) { struct led_dev *dev = platform_get_drvdata(pdev); led_chrdev_unregister(dev); /* devm 资源自动回收 */ return 0; } ``` 资源获取 API: | API | 用途 | | ----------------------------------------------- | --------------------------- | | `platform_get_resource(pdev, type, num)` | 获取 resource(MEM/IRQ) | | `platform_get_irq(pdev, num)` | 直接获取中断号 | | `platform_get_resource_byname` | 按名字获取资源 | | `devm_ioremap_resource` | 映射 MEM 资源为 `__iomem *` | | `devm_kzalloc` | 分配内存并自动释放 | | `devm_request_irq` | 注册中断并自动释放 | | `platform_set_drvdata` / `platform_get_drvdata` | 保存/取回私有数据 | 非设备树方式的资源定义: ```c static struct resource led_res[] = { [0] = { .start = 0x0209C000, .end = 0x0209C3FF, .flags = IORESOURCE_MEM, }, [1] = { .start = 100, .end = 100, .flags = IORESOURCE_IRQ, }, }; ``` **追问**: - 追问1:`devm_` 系列资源是何时释放的?remove 里还需要手动释放吗? - 追问2:probe 返回 `-EPROBE_DEFER` 表示什么?(依赖的资源还没就绪,内核会稍后重试 probe) - 追问3:`platform_set_drvdata` 和 `filp->private_data` 有什么区别?(前者绑定 device 与驱动实例,后者绑定文件与驱动实例) --- ### Q29: 设备树方式下 platform_device 是从哪里来的?它和传统 platform_device_register 有什么区别? **答案要点**: 1. 设备树方式下,内核在启动时解析 DTB,为每个 `compatible` 节点自动生成 `platform_device` 2. 无需在驱动代码里手动 `platform_device_register`,驱动只需提供 `of_match_table` 3. 传统方式需要在板级 C 代码里静态定义 `platform_device` + resource,并与驱动分开注册 4. 设备树方式把资源描述从 C 代码迁移到 DTS,实现"一内核多板" 5. 两种方式的 `platform_driver` 结构基本一致,差别在匹配字段和资源来源 **详细解答**: 设备树方式的自动生成流程: ``` U-Boot 把 DTB 地址传给内核 │ ▼ 内核 early_init_dt_scan 解析 DTB │ ▼ unflatten_device_tree 建立内核内节点树 │ ▼ of_platform_default_populate → 为每个有 compatible 的节点创建 platform_device (dev.of_node 指向该节点) │ ▼ 驱动注册时 platform_driver_register │ ▼ platform_match: of_driver_match_device 匹配 compatible │ ▼ 调用 driver->probe(pdev) ``` 驱动侧只需: ```c static const struct of_device_id led_of_match[] = { { .compatible = "atkalpha-gpioled" }, { } }; MODULE_DEVICE_TABLE(of, led_of_match); static struct platform_driver led_driver = { .probe = led_probe, .remove = led_remove, .driver = { .name = "led-driver", .of_match_table = led_of_match, }, }; module_platform_driver(led_driver); ``` 对比: | 对比项 | 传统 platform_device_register | 设备树方式 | | -------------- | ----------------------------- | ------------------- | | 设备定义位置 | 板级 C 代码 | .dts 文件 | | 资源定义 | `struct resource[]` | reg/interrupts 属性 | | 匹配方式 | name 字符串 | compatible | | 是否需要改内核 | 换板需改 C 代码 | 只改 DTS | | 驱动代码 | 相同 | 相同 | | 现代推荐 | 否 | 是 | 传统方式写出设备再注册: ```c static struct platform_device *led_dev; static int __init led_dev_init(void) { led_dev = platform_device_register_simple("my-led", -1, led_res, ARRAY_SIZE(led_res)); return 0; } ``` **追问**: - 追问1:设备树方式下 `/sys/bus/platform/devices/` 里能看到什么?(自动生成的 device,名字通常是 `地址.节点名`) - 追问2:如果 dts 里有节点但驱动没加载,会发生什么?(device 存在但无 driver 匹配,`probe` 不执行) - 追问3:为什么说设备树让"驱动与板级信息彻底解耦"? --- ### Q30: 设计题:把一个字符设备驱动改造成 platform 驱动,需要改哪些地方?请给出完整框架。 **答案要点**: 1. 增加 `of_match_table` 和 `platform_driver` 结构,用 `module_platform_driver` 注册 2. 把原 `module_init` 里的硬件初始化搬到 `probe`,原 `module_exit` 的清理搬到 `remove` 3. 设备资源(GPIO、reg、irq)改为通过 `pdev` 从设备树获取 4. 私有数据结构通过 `platform_set_drvdata` / `priv` 传递,probe 里完成字符设备注册 5. 保留 `file_operations` 与字符设备注册逻辑不变,实现关注点分离 **详细解答**: 改造前后的对应关系: | 原字符设备驱动 | platform 驱动 | | --------------------------- | ---------------------- | | module_init 里做所有初始化 | probe 里做 | | module_exit 里做所有清理 | remove 里做 | | of_find_node_by_path 找节点 | `pdev->dev.of_node` | | 全局变量保存设备信息 | `platform_set_drvdata` | | 无需匹配 | of_match_table 匹配 | | module_init/module_exit | module_platform_driver | 完整框架: ```c #include #include #include #include #include #include #include #include #define LEDDEV_CNT 1 #define LEDDEV_NAME "platled" struct leddev_dev { dev_t devid; struct cdev cdev; struct class *class; struct device *device; struct gpio_desc *led; /* gpiod 方式 */ int major; }; /* file_operations 与之前完全一致 */ static int led_open(struct inode *inode, struct file *filp) { struct leddev_dev *dev = container_of(inode->i_cdev, struct leddev_dev, cdev); filp->private_data = dev; return 0; } static ssize_t led_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt) { struct leddev_dev *dev = filp->private_data; unsigned char databuf[1]; if (copy_from_user(databuf, buf, cnt)) return -EFAULT; gpiod_set_value(dev->led, databuf[0] ? 1 : 0); return cnt; } static struct file_operations led_fops = { .owner = THIS_MODULE, .open = led_open, .write = led_write, }; /* probe:原来 module_init 做的事 */ static int led_probe(struct platform_device *pdev) { struct leddev_dev *dev; int ret; dev = devm_kzalloc(&pdev->dev, sizeof(*dev), GFP_KERNEL); if (!dev) return -ENOMEM; /* 从设备树获取 GPIO(属性名 led-gpios) */ dev->led = devm_gpiod_get(&pdev->dev, "led", GPIOD_OUT_HIGH); if (IS_ERR(dev->led)) return PTR_ERR(dev->led); /* 申请设备号 */ ret = alloc_chrdev_region(&dev->devid, 0, LEDDEV_CNT, LEDDEV_NAME); if (ret < 0) return ret; dev->major = MAJOR(dev->devid); /* cdev */ cdev_init(&dev->cdev, &led_fops); dev->cdev.owner = THIS_MODULE; cdev_add(&dev->cdev, dev->devid, LEDDEV_CNT); /* class / device */ dev->class = class_create(THIS_MODULE, LEDDEV_NAME); if (IS_ERR(dev->class)) { cdev_del(&dev->cdev); unregister_chrdev_region(dev->devid, LEDDEV_CNT); return PTR_ERR(dev->class); } dev->device = device_create(dev->class, &pdev->dev, dev->devid, dev, "%s", LEDDEV_NAME); if (IS_ERR(dev->device)) { class_destroy(dev->class); cdev_del(&dev->cdev); unregister_chrdev_region(dev->devid, LEDDEV_CNT); return PTR_ERR(dev->device); } platform_set_drvdata(pdev, dev); dev_info(&pdev->dev, "platled probe ok, major=%d\n", dev->major); return 0; } /* remove:原来 module_exit 做的事 */ static int led_remove(struct platform_device *pdev) { struct leddev_dev *dev = platform_get_drvdata(pdev); device_destroy(dev->class, dev->devid); class_destroy(dev->class); cdev_del(&dev->cdev); unregister_chrdev_region(dev->devid, LEDDEV_CNT); return 0; } static const struct of_device_id led_of_match[] = { { .compatible = "atkalpha-gpioled" }, { } }; MODULE_DEVICE_TABLE(of, led_of_match); static struct platform_driver led_driver = { .probe = led_probe, .remove = led_remove, .driver = { .name = "platled", .of_match_table = led_of_match, }, }; module_platform_driver(led_driver); MODULE_LICENSE("GPL"); MODULE_AUTHOR("zuozhongkai"); ``` 设备树: ```dts gpioled { compatible = "atkalpha-gpioled"; pinctrl-names = "default"; pinctrl-0 = <&pinctrl_led>; led-gpios = <&gpio1 3 GPIO_ACTIVE_LOW>; status = "okay"; }; ``` 改造要点总结: 1. **入口出口改名**:`xxx_init/xxx_exit` → `probe/remove`。 2. **资源获取换源**:全局 `of_find_node_by_path` → `pdev->dev.of_node` / `platform_get_*`。 3. **实例传递**:全局变量 → `platform_set_drvdata` + `container_of` 从 `inode->i_cdev` 找回。 4. **注册方式**:`module_init/module_exit` → `module_platform_driver`。 5. **错误回滚**:probe 中任何一步失败都要逆序清理,或尽量用 devm。 **追问**: - 追问1:`container_of` 在 `open` 里是怎么从 `inode->i_cdev` 找回 `struct leddev_dev` 的? - 追问2:如果同一 compatible 有多个设备节点,probe 会被调用几次? - 追问3:为什么要用 `device_create(..., &pdev->dev, ...)` 而不是传 NULL 作为 parent?(建立 sysfs 设备层级关系) --- ## 七、IO 模型 > 对应笔记:[[03-Linux驱动开发核心/06-阻塞IO与poll机制]] > 对应原文:《I.MX6U嵌入式Linux驱动开发指南》第52章 Linux阻塞和非阻塞IO实验、第53章 异步通知实验 ### Q31: Linux 的五种 IO 模型分别是什么?各自的特点和适用场景? **答案要点**: 1. 阻塞 IO:进程休眠等待,不占 CPU,实现简单,适合低频设备 2. 非阻塞 IO:立即返回 `-EAGAIN`,应用轮询,占 CPU,适合实时性要求高 3. IO 多路复用:select/poll/epoll 同时监控多个 fd,适合多设备/高并发 4. 信号驱动 IO(异步通知):驱动发 `SIGIO` 信号,事件驱动,CPU 占用低 5. 异步 IO(AIO):发起后立即返回,完成后回调,Linux 上支持有限 **详细解答**: 五种模型对比: | 模型 | 机制 | CPU 占用 | 响应延迟 | 复杂度 | 适用 | | ----------- | ------------------ | ---------- | -------------- | ------ | ------------------ | | 阻塞 IO | 等待队列 + 休眠 | 低 | 高(醒即响应) | 低 | 低频设备 | | 非阻塞 IO | 立即返回 EAGAIN | 高(轮询) | 低 | 低 | 实时、单设备 | | IO 多路复用 | select/poll/epoll | 中 | 中 | 中 | 多设备并发 | | 异步通知 | SIGIO 信号 | 低 | 低 | 高 | 事件驱动 | | 异步 IO | aio_read/aio_write | 低 | 最低 | 高 | 高吞吐(有限支持) | 时序对比: ``` 阻塞 IO: read ────────────[休眠]───────────► 返回数据 非阻塞 IO: read → EAGAIN → read → EAGAIN → read → 返回 多路复用: select ─[等待N个fd]─► 就绪 → read 异步通知: read(注册) → 干别的 ──SIGIO──► 回调里 read 异步 IO: aio_read → 立即返回 → 完成后回调 ``` 正点原子做了一个非常直观的实验:按键驱动分别用阻塞、非阻塞、poll、异步通知实现,应用层对比 CPU 使用率: ```c /* 阻塞:read 直接睡,CPU 0%,按键才醒 */ ret = read(fd, &data, sizeof(data)); /* 非阻塞:应用 while 轮询,CPU 高 */ ret = read(fd, &data, sizeof(data)); /* O_NONBLOCK 打开时立即返回 */ if (ret < 0 && errno == EAGAIN) continue; /* 多路复用:应用 select/poll 等待 */ poll(fds, 1, -1); /* 异步通知:应用注册信号后干别的 */ signal(SIGIO, handler); fcntl(fd, F_SETOWN, getpid()); fcntl(fd, F_SETFL, flags | FASYNC); ``` **追问**: - 追问1:阻塞 IO 在等待期间 CPU 占用是多少?为什么? - 追问2:非阻塞 IO 的 `-EAGAIN` 和 `-EWOULDBLOCK` 是一样的吗? - 追问3:异步通知和异步 IO 有什么区别?(信号 vs 内核完成的真正异步读写) --- ### Q32: 阻塞 IO 在内核驱动里是怎么实现的?wait_event 系列的语义是什么? **答案要点**: 1. 内核用"等待队列(wait_queue_head_t)"管理所有等待该设备的进程 2. 定义并初始化:`init_waitqueue_head` 或 `DECLARE_WAIT_QUEUE_HEAD` 3. `wait_event_interruptible(wq, condition)`:条件不满足就休眠,条件满足返回 0,被信号打断返回非 0 4. 条件必须能被其他上下文改变:通常由中断/定时器里 `wake_up_interruptible` 唤醒 5. 唤醒后必须重新判断条件(可能虚假唤醒),因此 `wait_event` 内部是循环判断 **详细解答**: 等待队列核心结构(`include/linux/wait.h`): ```c struct __wait_queue_head { spinlock_t lock; /* 保护队列的自旋锁 */ struct list_head task_list; /* 等待进程链表 */ }; typedef struct __wait_queue_head wait_queue_head_t; ``` 初始化: ```c /* 方式一:静态 */ DECLARE_WAIT_QUEUE_HEAD(r_wait); /* 方式二:动态 */ wait_queue_head_t r_wait; init_waitqueue_head(&r_wait); ``` 等待事件 API: | 函数 | 进程状态 | 可被信号打断 | 超时 | | ----------------------------------------------- | -------------------- | ------------ | ---- | | `wait_event(wq, cond)` | TASK_UNINTERRUPTIBLE | 否 | 无 | | `wait_event_interruptible(wq, cond)` | TASK_INTERRUPTIBLE | 是 | 无 | | `wait_event_timeout(wq, cond, t)` | UNINTERRUPTIBLE | 否 | 有 | | `wait_event_interruptible_timeout(wq, cond, t)` | INTERRUPTIBLE | 是 | 有 | 驱动 read 实现: ```c static ssize_t imx6uirq_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt) { int ret; unsigned char keyvalue; struct imx6uirq_dev *dev = filp->private_data; /* 等待按键松开事件(条件:releasekey 非 0) */ ret = wait_event_interruptible(dev->r_wait, atomic_read(&dev->releasekey)); if (ret) return ret; /* 被信号打断 */ keyvalue = atomic_read(&dev->keyvalue); if (keyvalue & 0x80) { keyvalue &= ~0x80; if (copy_to_user(buf, &keyvalue, sizeof(keyvalue))) return -EFAULT; } return sizeof(keyvalue); } ``` 中断/定时器里唤醒: ```c static void timer_function(unsigned long arg) { struct imx6uirq_dev *dev = (struct imx6uirq_dev *)arg; /* 检测到松开 */ atomic_set(&dev->keyvalue, 0x80 | KEY0VALUE); atomic_set(&dev->releasekey, 1); wake_up_interruptible(&dev->r_wait); /* 唤醒阻塞的 read */ } ``` `wait_event` 内部展开逻辑(简化): ```c #define wait_event_interruptible(wq, condition) \ ({ \ int __ret = 0; \ if (!(condition)) \ __ret = __wait_event_interruptible(wq, condition); \ __ret; \ }) /* 内部循环 */ for (;;) { prepare_to_wait(&wq, &__wait, TASK_INTERRUPTIBLE); if (condition) break; if (signal_pending(current)) { ret = -ERESTARTSYS; break; } schedule(); /* 让出 CPU 开始休眠 */ } finish_wait(&wq, &__wait); ``` 关键点:**必须先判断条件再睡**,醒来后再判断,避免"丢失唤醒"。 唤醒函数: | 函数 | 说明 | | -------------------------------- | ---------------------------------------- | | `wake_up(wq)` | 唤醒所有状态(含 UNINTERRUPTIBLE,慎用) | | `wake_up_interruptible(wq)` | 只唤醒 INTERRUPTIBLE 的进程(推荐) | | `wake_up_interruptible_sync(wq)` | 唤醒但不立即抢占(当前 CPU 上) | 手动实现等待(老式写法,理解用): ```c DECLARE_WAITQUEUE(wait, current); add_wait_queue(&dev->r_wait, &wait); __set_current_state(TASK_INTERRUPTIBLE); if (!condition) schedule(); set_current_state(TASK_RUNNING); remove_wait_queue(&dev->r_wait, &wait); ``` **追问**: - 追问1:为什么 `wait_event` 要用循环判断条件?"虚假唤醒"指的是什么? - 追问2:如果驱动忘记在中断里 `wake_up`,应用会怎样?(永久阻塞,只能被信号/超时打断) - 追问3:`wait_event_interruptible` 返回非 0 时驱动应该返回什么?为什么不是直接返回 0? --- ### Q33: 驱动如何实现 poll?select、poll、epoll 有什么区别? **答案要点**: 1. 驱动实现 `file_operations.poll`,内部调用 `poll_wait(file, &wq, wait)` 把进程加入等待队列,再根据设备状态返回事件掩码 2. `poll_wait` 不阻塞,只做登记;真正等待由内核在 select/poll/epoll 里完成 3. 可读返回 `POLLIN | POLLRDNORM`,可写返回 `POLLOUT | POLLWRNORM` 4. select 的 fd 上限是 `FD_SETSIZE`(通常 1024),用位图;poll 用数组无上限;epoll 用红黑树+就绪链表 5. epoll 在大量 fd 时效率优势明显,支持水平触发(LT)与边沿触发(ET) **详细解答**: 驱动侧 poll 实现(正点原子 noblockio.c): ```c static unsigned int imx6uirq_poll(struct file *filp, struct poll_table_struct *wait) { unsigned int mask = 0; struct imx6uirq_dev *dev = filp->private_data; /* 把当前进程登记到等待队列(不阻塞) */ poll_wait(filp, &dev->r_wait, wait); /* 按键已松开 → 有数据可读 */ if (atomic_read(&dev->releasekey)) mask |= POLLIN | POLLRDNORM; return mask; } static struct file_operations imx6uirq_fops = { .owner = THIS_MODULE, .open = imx6uirq_open, .read = imx6uirq_read, .poll = imx6uirq_poll, }; ``` `poll_wait` 原型: ```c void poll_wait(struct file *filp, wait_queue_head_t *wait_address, poll_table *p); ``` 应用侧三种用法: ```c /* select */ fd_set readfds; FD_ZERO(&readfds); FD_SET(fd, &readfds); select(fd + 1, &readfds, NULL, NULL, NULL); if (FD_ISSET(fd, &readfds)) read(fd, &data, 1); /* poll */ struct pollfd fds[1]; fds[0].fd = fd; fds[0].events = POLLIN; poll(fds, 1, -1); if (fds[0].revents & POLLIN) read(fd, &data, 1); /* epoll */ int epfd = epoll_create(1); struct epoll_event ev, events[1]; ev.events = EPOLLIN; ev.data.fd = fd; epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ev); epoll_wait(epfd, events, 1, -1); ``` 三者对比: | 特性 | select | poll | epoll | | ------------ | ------------------ | ----------- | ----------------- | | fd 上限 | 1024(FD_SETSIZE) | 无 | 无 | | 数据结构 | 位图 fd_set | pollfd 数组 | 红黑树 + 就绪链表 | | 每次调用开销 | O(n) 全量拷贝/遍历 | O(n) 遍历 | O(1) 返回就绪 | | 触发方式 | 水平触发 LT | 水平触发 LT | LT / ET | | 跨平台 | POSIX 广泛 | POSIX | Linux 特有 | | 适用 | 少量 fd | 中等 | 高并发大量 fd | epoll 的 LT 与 ET: - LT(水平触发,默认):只要缓冲区有数据就一直通知。 - ET(边沿触发):只在状态变化时通知一次,必须一次读完,配合非阻塞 IO 使用。 **追问**: - 追问1:`poll_wait` 为什么叫"wait"却不阻塞?真正阻塞发生在哪一层? - 追问2:驱动 poll 返回 0(无任何事件)时,应用会一直等下去吗? - 追问3:如果驱动实现了非阻塞 read 但没实现 poll,应用还能用 select 吗?(select 会立即返回可读,read 却返回 EAGAIN,行为不一致) --- ### Q34: 异步通知(fasync/SIGIO)的原理是什么?应用层三步设置分别做什么? **答案要点**: 1. 异步通知是"驱动主动通知应用"的机制,类似软中断:设备就绪时驱动向应用发送 `SIGIO` 信号 2. 驱动侧实现 `file_operations.fasync`,调用 `fasync_helper` 维护 `fasync_struct` 链表 3. 事件发生时调用 `kill_fasync(&dev->async_queue, SIGIO, POLL_IN)` 发信号 4. 应用侧三步:`signal(SIGIO, handler)` 注册处理函数、`fcntl(F_SETOWN, getpid())` 告诉内核接收进程、`fcntl(F_SETFL, ...|FASYNC)` 开启异步通知 5. 关闭设备时要在 release 里 `fasync(-1, filp, 0)` 注销 **详细解答**: 原理流程: ``` 应用 open + signal(SIGIO) + F_SETOWN + FASYNC │ ▼ 内核调用驱动的 fasync() → fasync_helper() 建立 fasync_struct │ ▼(设备就绪,例如按键中断) 驱动 kill_fasync(&async_queue, SIGIO, POLL_IN) │ ▼ 内核向 FASYNC 绑定的进程发送 SIGIO │ ▼ 应用信号处理函数执行 → 在 handler 里 read() ``` 驱动侧实现: ```c struct imx6uirq_dev { /* ... */ struct fasync_struct *async_queue; /* 异步通知队列 */ }; /* fasync 函数 */ static int imx6uirq_fasync(int fd, struct file *filp, int on) { struct imx6uirq_dev *dev = filp->private_data; return fasync_helper(fd, filp, on, &dev->async_queue); } /* release 中注销 */ static int imx6uirq_release(struct inode *inode, struct file *filp) { return imx6uirq_fasync(-1, filp, 0); /* on=0 表示移除 */ } /* 事件发生时发送信号 */ static void timer_function(unsigned long arg) { struct imx6uirq_dev *dev = (struct imx6uirq_dev *)arg; /* ... 按键处理 ... */ if (atomic_read(&dev->releasekey)) { if (dev->async_queue) kill_fasync(&dev->async_queue, SIGIO, POLL_IN); } } static struct file_operations imx6uirq_fops = { .owner = THIS_MODULE, .open = imx6uirq_open, .read = imx6uirq_read, .fasync = imx6uirq_fasync, .release = imx6uirq_release, }; ``` `kill_fasync` 原型: ```c void kill_fasync(struct fasync_struct **fp, int sig, int band); /* band:可读用 POLL_IN,可写用 POLL_OUT */ ``` 应用侧三步: ```c static int fd; static void sigio_signal_func(int signum) { unsigned int keyvalue; if (read(fd, &keyvalue, sizeof(keyvalue)) > 0) printf("SIGIO signal! key value=%d\r\n", keyvalue); } int main(int argc, char *argv[]) { int flags; fd = open(argv[1], O_RDWR); if (fd < 0) return -1; /* 1、注册信号处理函数 */ signal(SIGIO, sigio_signal_func); /* 2、把进程号告诉内核,信号发给谁 */ fcntl(fd, F_SETOWN, getpid()); /* 3、开启异步通知 */ flags = fcntl(fd, F_GETFL); fcntl(fd, F_SETFL, flags | FASYNC); while (1) sleep(2); /* 主循环可以干别的事 */ close(fd); return 0; } ``` `fasync_struct` 结构(`include/linux/fs.h`): ```c struct fasync_struct { spinlock_t fa_lock; int magic; int fa_fd; struct fasync_struct *fa_next; struct file *fa_file; struct rcu_head fa_rcu; }; ``` 异步通知 vs poll: | 对比 | 异步通知 | poll/select | | -------- | ------------------------ | ------------------ | | 通知方向 | 驱动主动通知应用 | 应用主动查询 | | 应用开销 | 极低(等信号) | 中等(系统调用) | | 实时性 | 高 | 中 | | 复杂度 | 高(信号安全) | 低 | | 多设备 | 每设备一个信号源 | 一次监控多个 | | 限制 | 信号数量有限、不适合高频 | fd 多时 epoll 更优 | **追问**: - 追问1:为什么 `release` 里必须调用 `fasync(-1, filp, 0)`?不调会怎样?(野指针,异步队列残留) - 追问2:如果应用没设置 `F_SETOWN`,`kill_fasync` 的信号会发给谁? - 追问3:信号处理函数里能做哪些事?为什么只能用 `write` 等 async-signal-safe 函数?(标准 IO/printf 不可重入) --- ### Q35: 设计题:一个按键设备要同时支持阻塞、非阻塞、poll 和异步通知,驱动应如何组织? **答案要点**: 1. 统一用一份设备结构体 + 一份 `file_operations`,四个功能不冲突,依次实现 open/read/poll/fasync/release 2. `read` 内部根据 `filp->f_flags & O_NONBLOCK` 分两条路径:阻塞走 `wait_event_interruptible`,非阻塞判断无数据直接返回 `-EAGAIN` 3. `poll` 里 `poll_wait` 登记等待队列,有数据返回 `POLLIN`,无数据返回 0 4. `fasync` 用 `fasync_helper` 建立队列,事件发生时 `kill_fasync` 发 SIGIO 5. 所有通知路径最终都依赖同一个"事件发生点"(中断/定时器)去 `wake_up_interruptible` + `kill_fasync` 6. `release` 里注销 fasync,避免残留 **详细解答**: 关键认识:**四种 IO 模型不是互斥的,而是在同一驱动的不同位置各司其职**。设备结构体只加两个成员(等待队列头 + fasync 指针),`file_operations` 把这些操作串起来。 统一数据结构: ```c struct imx6uirq_dev { dev_t devid; struct cdev cdev; struct class *class; struct device *device; int major, minor; struct device_node *nd; int gpio; int irqnum; atomic_t keyvalue; atomic_t releasekey; struct timer_list timer; wait_queue_head_t r_wait; /* 阻塞 + poll 共用 */ struct fasync_struct *async_queue; /* 异步通知 */ }; ``` 统一事件发生点(中断消抖后的定时器回调): ```c static void timer_function(unsigned long arg) { struct imx6uirq_dev *dev = (struct imx6uirq_dev *)arg; int value = gpio_get_value(dev->gpio); if (value == 0) { atomic_set(&dev->keyvalue, KEY0VALUE); } else { atomic_set(&dev->keyvalue, 0x80 | KEY0VALUE); atomic_set(&dev->releasekey, 1); /* 通知三条路径 */ wake_up_interruptible(&dev->r_wait); /* 阻塞读者 */ if (dev->async_queue) kill_fasync(&dev->async_queue, SIGIO, POLL_IN); /* 异步通知 */ /* poll 无需显式通知:内核等待队列被唤醒后会重新调用 poll */ } } ``` read 同时支持阻塞与非阻塞: ```c static ssize_t imx6uirq_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt) { struct imx6uirq_dev *dev = filp->private_data; unsigned char keyvalue; if (filp->f_flags & O_NONBLOCK) { /* 非阻塞 */ if (atomic_read(&dev->releasekey) == 0) return -EAGAIN; /* 无数据立即返回 */ } else { /* 阻塞 */ int ret = wait_event_interruptible(dev->r_wait, atomic_read(&dev->releasekey)); if (ret) return ret; } keyvalue = atomic_read(&dev->keyvalue); if (keyvalue & 0x80) { keyvalue &= ~0x80; if (copy_to_user(buf, &keyvalue, sizeof(keyvalue))) return -EFAULT; return sizeof(keyvalue); } return -EINVAL; } ``` poll + fasync + release: ```c static unsigned int imx6uirq_poll(struct file *filp, struct poll_table_struct *wait) { struct imx6uirq_dev *dev = filp->private_data; unsigned int mask = 0; poll_wait(filp, &dev->r_wait, wait); if (atomic_read(&dev->releasekey)) mask |= POLLIN | POLLRDNORM; return mask; } static int imx6uirq_fasync(int fd, struct file *filp, int on) { struct imx6uirq_dev *dev = filp->private_data; return fasync_helper(fd, filp, on, &dev->async_queue); } static int imx6uirq_release(struct inode *inode, struct file *filp) { return imx6uirq_fasync(-1, filp, 0); /* 注销异步通知 */ } static struct file_operations imx6uirq_fops = { .owner = THIS_MODULE, .open = imx6uirq_open, .read = imx6uirq_read, .poll = imx6uirq_poll, .fasync = imx6uirq_fasync, .release = imx6uirq_release, }; ``` 四种模式对照: | 模型 | 驱动侧关键点 | 应用侧关键点 | | -------- | ------------------------------------- | -------------------------- | | 阻塞 | wait_event_interruptible + 中断唤醒 | 默认 open 不传 O_NONBLOCK | | 非阻塞 | 判 `O_NONBLOCK` 返回 -EAGAIN | open 时加 O_NONBLOCK | | poll | 实现 .poll + poll_wait + 返回掩码 | select/poll/epoll | | 异步通知 | .fasync + fasync_helper + kill_fasync | signal + F_SETOWN + FASYNC | 设计要点: 1. **单一事件源**:中断/定时器只在一处统一触发"数据就绪",集中唤醒,避免遗漏某条路径。 2. **等待队列复用**:阻塞 read 与 poll 共用同一个 `r_wait`,内核保证两者都被唤醒。 3. **状态用原子变量**:`keyvalue`/`releasekey` 用 `atomic_t`,因为可能被中断上下文与进程上下文同时访问。 4. **资源成对清理**:`del_timer_sync` → `free_irq` → `fasync(-1, flp, 0)` → cdev/class/device 逆序注销。 5. **非阻塞语义一致**:poll 返回可读与 read 不返回 EAGAIN 必须一致,否则应用会忙转。 **追问**: - 追问1:poll 路径为什么不需要驱动显式调用唤醒函数就能工作?(`poll_wait` 把进程挂在 `r_wait` 上,`wake_up_interruptible` 会唤醒它并重新 poll) - 追问2:如果 read 用 `copy_to_user` 后返回,但用户缓冲不足会怎样?如何设计更健壮?(先校验 `cnt`,或返回实际字节数) - 追问3:如何同时支持多个进程阻塞读同一设备?等待队列能容纳多个等待项吗? --- ## 附录:知识点速查表 ### A. 常用 API 速查 | 领域 | 关键 API | | ----------- | ----------------------------------------------------------------------------------------------- | | 设备号 | `MKDEV` `MAJOR` `MINOR` `alloc_chrdev_region` `register_chrdev_region` | | cdev | `cdev_init` `cdev_add` `cdev_del` | | 设备节点 | `class_create` `device_create` `device_destroy` `class_destroy` | | 数据拷贝 | `copy_to_user` `copy_from_user` | | 设备树 | `of_find_node_by_path` `of_property_read_u32` `of_get_named_gpio` `of_iomap` | | gpio | `gpio_request` `gpio_direction_output` `gpio_set_value` `gpiod_set_value` | | 原子 | `atomic_set` `atomic_inc` `atomic_dec_and_test` | | 自旋锁 | `spin_lock_init` `spin_lock_irqsave` `spin_unlock_irqrestore` | | 信号量/互斥 | `sema_init` `down` `up` `mutex_init` `mutex_lock` `mutex_unlock` | | 中断 | `request_irq` `free_irq` `irq_of_parse_and_map` `platform_get_irq` | | 下半部 | `tasklet_init` `tasklet_schedule` `INIT_WORK` `schedule_work` `request_threaded_irq` | | platform | `platform_get_resource` `devm_ioremap_resource` `platform_set_drvdata` `module_platform_driver` | | 阻塞 | `init_waitqueue_head` `wait_event_interruptible` `wake_up_interruptible` | | poll | `poll_wait` `POLLIN` `POLLRDNORM` | | 异步 | `fasync_helper` `kill_fasync` `fasync_struct` | ### B. 常见错误码 | 错误码 | 含义 | 常见场景 | | ------------- | ---------- | ----------------------------------- | | -EBUSY | 忙 | 设备已打开、GPIO 被占用、中断被占用 | | -EINVAL | 参数非法 | 设备树属性/节点错误、GPIO 编号非法 | | -EAGAIN | 再试一次 | 非阻塞读无数据 | | -ENOMEM | 内存不足 | kmalloc 失败 | | -EFAULT | 地址错误 | copy_*_user 失败 | | -ENODEV | 设备不存在 | 次设备号越界、资源缺失 | | -EPROBE_DEFER | 稍后重试 | 依赖资源未就绪 | ### C. 面试答题思路 1. **概念题**:先给定义,再讲原理与数据结构,最后说使用场景。 2. **对比题**:用表格列出维度(能否睡眠、上下文、开销、适用),再给选择原则。 3. **实战题**:给代码骨架 + 关键步骤 + 错误回滚,体现工程严谨性。 4. **排查题**:按"现象 → 可能原因 → 验证手段 → 解决"结构化输出。 5. **设计题**:先讲设计目标与约束,再讲数据结构与流程,最后讲扩展与权衡。 --- > 💡 **关联笔记复盘**: [[03-Linux驱动开发核心/01-字符设备驱动框架]] | [[03-Linux驱动开发核心/02-设备树语法与实战]] | [[03-Linux驱动开发核心/03-pinctrl与gpio子系统]] | [[03-Linux驱动开发核心/04-并发同步与原子操作]] | [[03-Linux驱动开发核心/05-中断下半部处理]] | [[03-Linux驱动开发核心/06-阻塞IO与poll机制]] | [[03-Linux驱动开发核心/07-platform总线模型]] | [[03-Linux驱动开发核心/08-misc与input子系统]] | [[02-嵌入式Linux内核基础/06-设备模型与Kobject]] > > **资料来源**: 【正点原子】I.MX6U嵌入式Linux驱动开发指南V2.0.1 第40/43/45/47/51/52/53/54章 > **最后更新**: 2026-09-17 --- **内容来源**: 《I.MX6U嵌入式Linux驱动开发指南》第40章 字符设备驱动开发、第43章 Linux设备树、第45章 pinctrl和gpio子系统实验、第47章 Linux并发与竞争、第51章 Linux中断实验、第52章 Linux阻塞和非阻塞IO实验、第53章 异步通知实验、第54章 platform设备驱动实验