面试-Linux驱动开发.md 140 KB


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 示例:

#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

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_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

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/releasekeyatomic_t,因为可能被中断上下文与进程上下文同时访问。
  4. 资源成对清理del_timer_syncfree_irqfasync(-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设备驱动实验