title: 面试-Linux驱动开发核心 tags: [
面试,
Linux驱动,
字符设备,
设备树,
pinctrl,
gpio,
并发,
自旋锁,
互斥体,
中断,
下半部,
platform,
IO模型,
poll,
嵌入式,
] created: 2026-09-17
本文档涵盖 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-字符设备驱动框架]]
答案要点:
file_operations 绑定,并告诉内核register_chrdev 一次性完成"设备号 + fops"注册,但会占用一个主设备号下的所有次设备号register_chrdev_region/alloc_chrdev_region 申请设备号、cdev_init 初始化 cdev、cdev_add 添加到内核cdev_del + unregister_chrdev_region详细解答:
字符设备是 Linux 中最基础的一类设备,用户空间通过 /dev/xxx 对设备进行 open/read/write/ioctl 操作,最终被内核转发到驱动注册的 file_operations。
老式方式(register_chrdev)——正点原子《第四十章 字符设备驱动开发》中的 chrdevbase 示例:
#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、创建设备节点三段式:
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:> #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
mknod /dev/newchrled c 200 0
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
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- imx6ull-alientek-emmc.dtb
DTC(Device Tree Compiler)手动编译与反编译:
bash
dtc -I dts -O dtb -o myboard.dtb myboard.dts
dtc -I dtb -O dts -o myboard.dts myboard.dtb
Overlay 与 DTBO:
bash
dtc -@ -I dts -O dtb -o myoverlay.dtbo myoverlay.dts
fdt apply 0x83000000 myoverlay.dtbo
运行时查看设备树:
bash
cat /proc/device-tree/model
cat /proc/device-tree/compatible
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
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
cp arch/arm/boot/dts/imx6ull-alientek-emmc.dtb /tftpboot/
**第五步:属性读取出错排查**
| 现象 | 原因 | 解决 |
| --------------------------------- | --------------------------- | ----------------------------------- |
| 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 /* │ │ │ │ │
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_CTLPAD...) conf_reg = 0x020E0000 + 0x0318 = 0x020E0318 (IOMUXC_SW_PAD_CTLPAD...)
电气属性值(`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", ...)` 会去匹配 `<con_id>-gpios`/`<con_id>-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
cat /sys/kernel/debug/gpio
20a0000.gpio, gpio0:
如果 `gpio-3` 已经被 `led` 占用,而你的驱动还想 request,就会失败。
bash
grep -rn "GPIO1_IO03|<&gpio1 3" /proc/device-tree/
很多时候是历史遗留:某个默认设备(如某个未用的外设)在 `.dtsi` 里引用了这个引脚。
**排查输出异常**:
bash
devmem2 0x0209C000 w # GPIO1_DR
devmem2 0x0209C004 w # GPIO1_GDIR
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 统一出口)
**详细解答**:
**死锁四种典型场景**:
同锁递归: spin_lock(&lock); ... spin_lock(&lock); // 自己等自己
锁顺序不一致(AB-BA): 进程A: lock(a) → lock(b) 进程B: lock(b) → lock(a) // 互相等待
持锁睡眠: spin_lock(&lock); copy_to_user(...); // 可能缺页睡眠 → 与中断死锁
中断/进程交叉: 进程持锁 → 中断打断 → 中断抢同一把锁(见 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 |
设计要点:
r_wait,内核保证两者都被唤醒。keyvalue/releasekey 用 atomic_t,因为可能被中断上下文与进程上下文同时访问。del_timer_sync → free_irq → fasync(-1, flp, 0) → cdev/class/device 逆序注销。追问:
poll_wait 把进程挂在 r_wait 上,wake_up_interruptible 会唤醒它并重新 poll)copy_to_user 后返回,但用户缓冲不足会怎样?如何设计更健壮?(先校验 cnt,或返回实际字节数)| 领域 | 关键 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 |
| 错误码 | 含义 | 常见场景 |
|---|---|---|
| -EBUSY | 忙 | 设备已打开、GPIO 被占用、中断被占用 |
| -EINVAL | 参数非法 | 设备树属性/节点错误、GPIO 编号非法 |
| -EAGAIN | 再试一次 | 非阻塞读无数据 |
| -ENOMEM | 内存不足 | kmalloc 失败 |
| -EFAULT | 地址错误 | copy_*_user 失败 |
| -ENODEV | 设备不存在 | 次设备号越界、资源缺失 |
| -EPROBE_DEFER | 稍后重试 | 依赖资源未就绪 |
💡 关联笔记复盘: [[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设备驱动实验