title: 面试-Linux驱动开发核心 tags: [
面试,
Linux驱动,
字符设备,
设备树,
pinctrl,
gpio,
并发,
自旋锁,
互斥体,
中断,
下半部,
platform,
IO模型,
poll,
嵌入式,
] created: 2026-09-17
本文档涵盖 Linux 驱动开发核心的综合面试题,覆盖字符设备驱动、设备树、pinctrl 与 gpio 子系统、并发与同步、中断处理、platform 总线模型、IO 模型七大核心领域,共 35 道深度面试题。题目类型包括概念理解、对比分析、实战应用、问题排查与方案设计,适合 I.MX6ULL / RK3568 等嵌入式 Linux 平台求职复习。
💡 关联知识: [[03-Linux驱动开发核心/01-字符设备驱动基础]] | [[03-Linux驱动开发核心/01-字符设备驱动框架]] | [[03-Linux驱动开发核心/02-设备树语法与实战]] | [[03-Linux驱动开发核心/03-pinctrl与gpio子系统]] | [[03-Linux驱动开发核心/04-并发同步与原子操作]] | [[03-Linux驱动开发核心/05-中断下半部处理]] | [[03-Linux驱动开发核心/06-阻塞IO与poll机制]] | [[03-Linux驱动开发核心/07-platform总线模型]] | [[03-Linux驱动开发核心/08-misc与input子系统]]
| 板块 | 题号 | 核心考点 |
|---|---|---|
| 字符设备驱动 | Q1-Q5 | 注册流程、file/inode、数据拷贝、设备节点、多实例设计 |
| 设备树 | Q6-Q10 | DT 由来、DTS/DTB、属性语法、OF API、调试排查 |
| pinctrl 与 gpio | Q11-Q15 | 子系统分工、DTS 写法、新旧 API、电平极性、冲突排查 |
| 并发与同步 | Q16-Q20 | 竞态来源、原子操作、锁对比、irqsave、死锁 |
| 中断处理 | Q21-Q25 | 上下半部、tasklet/workqueue、中断号、上下文限制、消抖 |
| platform 总线 | Q26-Q30 | 模型思想、匹配机制、probe 流程、DT 化、框架改造 |
| IO 模型 | Q31-Q35 | 五种模型、等待队列、poll/epoll、异步通知、综合设计 |
对应笔记:[[03-Linux驱动开发核心/01-字符设备驱动基础]]、[[03-Linux驱动开发核心/01-字符设备驱动框架]]
答案要点:
file_operations 绑定,并告诉内核register_chrdev 一次性完成"设备号 + fops"注册,但会占用一个主设备号下的所有次设备号register_chrdev_region/alloc_chrdev_region 申请设备号、cdev_init 初始化 cdev、cdev_add 添加到内核cdev_del + unregister_chrdev_region详细解答:
字符设备是 Linux 中最基础的一类设备,用户空间通过 /dev/xxx 对设备进行 open/read/write/ioctl 操作,最终被内核转发到驱动注册的 file_operations。
老式方式(register_chrdev)——正点原子《第四十章 字符设备驱动开发》中的 chrdevbase 示例:
#define CHRDEVBASE_MAJOR 200
#define CHRDEVBASE_NAME "chrdevbase"
static int __init chrdevbase_init(void)
{
int retvalue = 0;
retvalue = register_chrdev(CHRDEVBASE_MAJOR, CHRDEVBASE_NAME,
&chrdevbase_fops);
if (retvalue < 0) {
printk("chrdevbase driver register failed\r\n");
}
return 0;
}
static void __exit chrdevbase_exit(void)
{
unregister_chrdev(CHRDEVBASE_MAJOR, CHRDEVBASE_NAME);
}
module_init(chrdevbase_init);
module_exit(chrdevbase_exit);
MODULE_LICENSE("GPL");
它的缺点很明确:只要注册了主设备号 200,就意味着 200 这个主设备号下的 0~255 全部次设备号都被这一个驱动占用了,其他驱动无法再使用主设备号 200 的任何一个次设备号。
新式方式(推荐)——申请设备号、注册 cdev、创建设备节点三段式:
struct newchrled_dev {
dev_t devid; /* 设备号 */
struct cdev cdev; /* cdev */
struct class *class; /* 类 */
struct device *device; /* 设备 */
int major; /* 主设备号 */
int minor; /* 次设备号 */
};
static int __init newchrled_init(void)
{
int ret = 0;
/* 1、申请设备号 */
if (newchrled.major) { /* 定义了主设备号 */
newchrled.devid = MKDEV(newchrled.major, 0);
ret = register_chrdev_region(newchrled.devid, NEWCHRLED_CNT,
NEWCHRLED_NAME);
} else { /* 未定义主设备号,动态申请 */
ret = alloc_chrdev_region(&newchrled.devid, 0, NEWCHRLED_CNT,
NEWCHRLED_NAME);
newchrled.major = MAJOR(newchrled.devid);
newchrled.minor = MINOR(newchrled.devid);
}
if (ret < 0) {
printk("newchrled chrdev_region err!\r\n");
return ret;
}
/* 2、初始化 cdev 并添加到内核 */
newchrled.cdev.owner = THIS_MODULE;
cdev_init(&newchrled.cdev, &newchrled_fops);
cdev_add(&newchrled.cdev, newchrled.devid, NEWCHRLED_CNT);
/* 3、创建类与设备(见 Q4) */
return 0;
}
两者对比:
| 对比项 | register_chrdev | register_chrdev_region + cdev |
|---|---|---|
| 占用设备号 | 占用整个主设备号(256 个次设备号) | 只占用申请的 cnt 个次设备号 |
| 是否绑定 fops | 一次绑定 | cdev_init 时绑定 |
| 多设备支持 | 差 | 好,可精确分配次设备号 |
| 内核推荐度 | 已过时 | 推荐 |
| 底层实现 | 内部同样是封装 cdev | 直接操作 cdev |
追问:
alloc_chrdev_region 和 register_chrdev_region 该如何选择?动态申请后如何让用户知道主设备号?(/proc/devices)cdev 在内核里是如何与 inode->i_cdev 关联的?为什么 open 时能找回我们的设备结构体?cdev_add 的 count 应传多少?答案要点:
struct inode 代表文件系统层面的一个文件节点(含设备号、cdev 指针),一个文件在磁盘/内存中只有一份struct file 代表进程打开文件后的一条"打开实例",每 open 一次就有一个,含私有数据 private_data 和打开标志 f_flagsstruct file_operations 是驱动的"方法表",把系统调用映射到具体函数指针open → VFS 创建 struct file → 通过 inode->i_rdev 找到设备号 → 通过 inode->i_cdev 找到 cdev → cdev->ops 指向驱动的 file_operationsopen() 里常把设备结构体挂到 filp->private_data,后续 read/write 通过它取回设备对象详细解答:
理解这三个结构体是写字符设备驱动的基本功,很多"为什么 open 里能拿到设备、read 里怎么找到设备"的问题都源于此。
struct inode(include/linux/fs.h):
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):
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(节选):
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 *);
__poll_t (*poll)(struct file *, struct poll_table_struct *);
int (*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);
/* ... */
};
三者的关系可用下图理解:
用户 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 | 驱动加载时静态定义 | 每个驱动一份 | 各种操作函数指针 |
追问:
inode 和 file 是一对一吗?多个进程同时打开同一个设备会创建几个 file?(多个,各自独立)private_data 为什么在 open 里赋值、在 release 里清理?如果忘记会怎样?f_pos 出现在 read/write 的 loff_t *offt 参数里,它和 filp->f_pos 是什么关系?答案要点:
memcpy 访问用户指针,在用户地址未映射、非法或换页时会触发 oops/kernel paniccopy_to_user/copy_from_user 会做地址合法性检查、缺页处理,并能正确检查访问权限__user 标注,便于 sparse 静态检查详细解答:
这是字符设备驱动 read/write 实现里最容易出错、也是面试高频的点。
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;
}
为什么不能用 memcpy:
| 场景 | memcpy 直接访问用户指针 | copy_*_user |
|---|---|---|
| 用户地址未映射 | 触发页错误,内核 oops | 内部异常表处理,返回未拷贝字节数 |
| 用户传入非法指针 | kernel panic | 安全失败 |
| 用户空间发生换页 | 无法处理 | 自动处理缺页 |
| 权限校验 | 无 | 检查范围合法性 |
| 返回值语义 | 无 | 返回失败字节数 |
正确的返回值处理范式应该是:
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。
追问:
copy_to_user 返回值到底是"成功拷贝的字节数"还是"未拷贝的字节数"?如何据此返回正确的系统调用结果?read 里拷贝 100 字节,用户只给了 10 字节缓冲,会发生什么?cnt 应该信任用户传进来的值吗?__user、__iomem 这些标注对编译产物有影响吗?(无,仅用于 sparse 检查)答案要点:
mknod,或由内核通过 class/device 机制自动创建class_create 在 /sys/class/ 下创建类目录,device_create 在类目录下创建具体设备并发出 uevent/dev 下创建带正确主次设备号的节点mknod /dev/xxx c major minor 需要知道主次设备号,适合调试/proc、/sys 已挂载详细解答:
在用动态设备号(alloc_chrdev_region)时,主设备号不固定,如果每次都要查 /proc/devices 再手动 mknod 就非常低效,因此内核提供了 class/device 自动建节点机制。
驱动侧代码:
/* 创建类 */
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);
}
卸载时对称销毁:
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
手动创建(调试常用):
# 查看内核分配的主设备号
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 | 产品、推荐 |
追问:
device_create 之后 /dev 下没有节点,可能是什么原因?(mdev/udev 未启动、没有热插拔、/sys 未挂载、uevent 被禁用)class_create 在新内核里参数变成了什么?(旧版需传 THIS_MODULE,新内核 class_create 只接受 name,owner 自动设置)答案要点:
struct mydev g_mydev[4],每个元素含 cdev、dev_t、device *,但 class 只需一个file_operations 只定义一份,open 里通过 inode->i_rdev 求次设备号 MINOR,定位到具体实例filp->private_data,read/write 通过它访问对应硬件cdev_del、device_destroy,最后统一 unregister_chrdev_region(count=4)详细解答:
这是一个典型的"驱动框架设计"题,考察你对设备号、cdev 与实例关系的理解。多实例的关键在于一个 fops、一个 class、N 个 cdev 与 N 个 device。
数据结构设计:
#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 里通过次设备号定位实例:
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:
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 分级清理 | 避免卸载不干净导致设备号泄漏 |
追问:
private_data,read/write 还能找到当前操作的是哪一路设备吗?cdev_add 的 count 参数传 1 还是 4?传 4 会怎样?devm_* 系列(如 device_create 没有 devm 版本,但 class_create 也没有)能否简化这种回滚逻辑?为什么内核对字符设备这部分 devm 支持有限?对应笔记:[[03-Linux驱动开发核心/02-设备树语法与实战]]
答案要点:
arch/arm/mach-xxx/board-xxx.c 用 C 代码硬编码板级信息,导致内核里充斥大量板级垃圾代码.dts 数据文件,内核只负责解析详细解答:
在设备树出现之前,ARM Linux 的板级信息是写死在 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 结构体难以表达复杂的总线拓扑与属性 |
设备树方案把这些内容抽成数据文件:
/* 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 匹配为主 |
追问:
答案要点:
#include 被 DTS 引用make dtbs;可用 DTC 反编译;运行时通过 /proc/device-tree 查看详细解答:
四者的关系:
SoC 级公共描述 板级差异描述
imx6ull.dtsi ──include──► imx6ull-alientek-emmc.dts
│ │
└────────── DTC 编译 ──────────┘
│
▼
imx6ull-alientek-emmc.dtb
│
内核启动时解析
│
▼
/proc/device-tree/ 下的节点树
编译命令(在 Linux 内核源码根目录):
# 编译所有设备树
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)手动编译与反编译:
# 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:
# 编译 overlay 需要基础 DTB 带 -@(symbols)编译
dtc -@ -I dts -O dtb -o myoverlay.dtbo myoverlay.dts
# 在 U-Boot 中应用 overlay
fdt apply 0x83000000 myoverlay.dtbo
运行时查看设备树:
# 查看根节点 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 | 二进制 | 运行时动态打补丁 |
追问:
#include 和 /include/ 两种语法有何不同用途?/proc/device-tree 里读到的 int 属性为什么是"反的"?(大端存储,需转换成 CPU 字节序)答案要点:
compatible 是"兼容性字符串列表",驱动靠它匹配设备(最重要)reg 描述设备地址范围,格式由父节点的 #address-cells、#size-cells 决定#address-cells / #size-cells 声明子节点 reg 中"地址"和"长度"各占几个 32 位单元ranges 描述子总线地址到父总线地址的映射;空 ranges 表示 1:1 映射status 为 "okay" 才使能设备,"disabled" 表示禁用phandle 是节点唯一编号,用于其他节点引用(&label 最终编译成 phandle)详细解答:
compatible——匹配的核心:
gpioled {
compatible = "atkalpha-gpioled"; /* 驱动 of_match_table 匹配这一项 */
status = "okay";
};
驱动侧匹配:
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——地址描述:
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:
reg = <0x00000000 0x020e0000 0x4000>; /* 高位 低位 长度 */
ranges——地址映射:
/* 空 ranges 表示子节点地址与父节点 1:1 映射 */
ranges;
/* 非空表示映射:子地址 0x0 映射到父地址 0x02000000,长度 0x100000 */
ranges = <0x0 0x02000000 0x100000>;
status——设备开关:
status = "okay"; /* 使能 */
status = "disabled"; /* 禁用,驱动 probe 不会被调用 */
phandle 与 &label 引用:
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 |
追问:
#address-cells 是写在父节点还是子节点?为什么容易写错?compatible 为什么要写多个字符串?匹配顺序是怎样的?reg 和 ranges 一起用时,of_iomap 拿到的是映射前还是映射后的地址?答案要点:
of_find_node_by_path、of_find_node_by_name、of_get_next_child、of_get_parentof_find_property、of_property_read_u32、of_property_read_string、of_property_read_u32_arrayof_get_named_gpio;中断:irq_of_parse_and_mapof_iomap 直接由节点+索引映射寄存器,ioremap 需要自己先拿到物理地址reg 转成 struct resource 交给 platform_get_resource详细解答:
正点原子《第四十三章 Linux设备树》给出的完整读取流程,通常分四步:
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 一览:
/* 节点查找 */
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);
int of_property_count_u32_elems(const struct device_node *np,
const char *propname);
/* 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 |
| 推荐度 | 设备树平台推荐 | 传统 / 裸地址场景 |
/* of_iomap 方式:一步到位 */
base = of_iomap(nd, 0);
/* ioremap 方式:先拿物理地址,再映射 */
of_address_to_resource(nd, 0, &res);
base = ioremap(res.start, resource_size(&res));
追问:
of_property_read_u32_array 返回负值说明什么?如何先用 of_property_count_u32_elems 探测长度?of_find_node_by_path 里的路径是 /atkalientek,为什么不用 /soc/atkalientek?(取决于节点实际层级,DT 是树状路径)ioremap 得到的指针必须用 readl/writel 访问,为什么不能直接 *base = x?(内存屏障与端序,__iomem 标注)答案要点:
ls /proc/device-tree/... 或反编译 DTBstatus 是否为 "okay"(默认缺省是 okay,但被 dtsi 设成 disabled 时不会 probe)compatible 与驱动 of_match_table 完全一致(大小写、连字符、厂商前缀)make dtbs 并替换到开发板(最常见坑)#address-cells/#size-cells 不匹配、属性名拼写错、字节序未转换详细解答:
这是实战中最耗时的一类问题,按"节点在不在 → 使能没使能 → 匹配没匹配 → DTB 新不新"的顺序排查效率最高。
第一步:确认节点进了内核
# 方式一:运行时查看
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
/* 父节点或自身被 disabled,都不会 probe */
status = "disabled"; /* 改成 "okay" */
注意:某些 SoC 的 .dtsi 里默认把外设设成 disabled,如果只在板级 dts 里追加节点却没写 status = "okay",节点会继承/被显式禁用。
第三步:确认 compatible 匹配
static const struct of_device_id gpioled_of_match[] = {
{ .compatible = "atkalpha-gpioled" }, /* 必须与 dts 一字不差 */
{ }
};
MODULE_DEVICE_TABLE(of, gpioled_of_match);
compatible = "atkalpha-gpioled"; /* 大小写、连字符都要一致 */
常见错误:驱动写 atkalpha,gpioled,dts 写 atkalpha-gpioled;或厂商前缀写了但 dts 没写。
第四步:确认 DTB 是最新的
# 内核源码目录
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 里打印节点与属性:
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;
}
追问:
compatible 正确但 probe 还是不执行,还可能是什么原因?(驱动没加载 depmod/modprobe、probe 返回 -EPROBE_DEFER 依赖未就绪)status = "okay" 写在了错误层级会怎样?fdt addr、/sys/firmware/devicetree/base)对应笔记:[[03-Linux驱动开发核心/03-pinctrl与gpio子系统]]
答案要点:
reg 里硬编码 IOMUXC 寄存器地址,可移植性差、无法检测冲突详细解答:
职责分工:
| 方面 | 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,能配置功能却无法在运行时读写电平。所以二者是互补关系。
/* 正确顺序:内核会自动应用 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;
}
追问:
pinctrl-0 的?(驱动 probe 前,really_probe → pinctrl_bind_pins)pinctrl_select_state)fsl,pins 五元组为例说明。答案要点:
&iomuxc 节点下定义 pinctrl 子节点(配置组),用 fsl,pins 描述引脚pinctrl-names = "default" 和 pinctrl-0 = <&配置组> 引用MX6UL_PAD_xxx__yyy 宏展开为 5 个值:mux_reg、conf_reg、input_reg、mux_mode、input_valfsl,pins 里每一行是"宏 + 电气属性值",电气属性值(如 0x17059)编码上下拉、驱动强度等详细解答:
&iomuxc 下定义配置组:
&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:
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 个值:
#define MX6UL_PAD_UART1_CTS_B__GPIO1_IO18 0x008C 0x0318 0x0000 0x5 0x0
/* │ │ │ │ │
* mux_reg ─────────┘ │ │ │ └ input_val
* conf_reg ────────────────┘ │ └ mux_mode(5=GPIO)
* input_reg ───────────────────────┘
*/
含义:
| 顺序 | 名称 | 示例值 | 含义 |
|---|---|---|---|
| 1 | mux_reg | 0x008C | MUX 控制寄存器相对 IOMUXC 基址偏移 |
| 2 | conf_reg | 0x0318 | PAD 电气属性寄存器偏移 |
| 3 | input_reg | 0x0000 | 输入选择寄存器偏移 |
| 4 | mux_mode | 0x5 | 复用模式,5 = ALT5 = GPIO |
| 5 | input_val | 0x0 | 输入选择值 |
地址计算:
IOMUXC 基址 = 0x020E0000
mux_reg = 0x020E0000 + 0x008C = 0x020E008C (IOMUXC_SW_MUX_CTL_PAD_...)
conf_reg = 0x020E0000 + 0x0318 = 0x020E0318 (IOMUXC_SW_PAD_CTL_PAD_...)
电气属性值(fsl,pins 每行最后那个值)的位定义:
bit 0: SRE 压摆率 (0:低速 1:高速)
bit 1-2: DSE 驱动强度
bit 3-4: SPEED 速度等级 (11=200MHz)
bit 5: ODE 开漏使能
bit 6: PKE 上下拉使能
bit 7: PUE 上下拉选择 (0:下拉 1:上拉)
bit 11: HYS 施密特滞后使能
多状态 pinctrl(低功耗场景):
usdhc1 {
pinctrl-names = "default", "state_100mhz", "state_200mhz";
pinctrl-0 = <&pinctrl_usdhc1>;
pinctrl-1 = <&pinctrl_usdhc1_100mhz>;
pinctrl-2 = <&pinctrl_usdhc1_200mhz>;
status = "okay";
};
追问:
fsl,pins 里一行的"宏 + 一个值"和宏展开的 5 个值是什么关系?(宏展开 5 个,加电气属性那个值,实际是 6 个 32 位数)pinctrl-names 的第一个必须是 "default" 吗?为什么?(内核默认选 default)答案要点:
gpio_request/free,从设备树解析要自己来struct gpio_desc *,通常用 devm_gpiod_get 直接从设备树获取,自动管理生命周期GPIO_ACTIVE_LOW 极性处理、GPIO 数组、name 映射devm_ 前缀意味着随设备卸载自动释放,避免资源泄漏详细解答:
旧 API 的典型流程(正点原子教程常用):
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 的典型流程:
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 |
| 推荐程度 | 已不推荐 | 推荐 |
注意点:
led-gpios 或 led-gpio,devm_gpiod_get(dev, "led", ...) 会去匹配 <con_id>-gpios/<con_id>-gpio。追问:
devm_gpiod_get 里的 con_id="led" 对应设备树哪个属性?GPIOD_OUT_HIGH 和 GPIOD_OUT_LOW 是设置物理电平还是逻辑电平?(逻辑电平,会按 active-low 反转)led-gpios 也没写 led-gpio,devm_gpiod_get 会怎样?(返回 ERR_PTR(-ENOENT))GPIO_ACTIVE_LOW 后,驱动写 gpio_set_value(gpio, 1) 到底输出高还是低?答案要点:
GPIO_ACTIVE_LOW 表示"逻辑有效电平为低",即逻辑 1 对应物理低电平GPIO_ACTIVE_LOW 不会被自动处理,通常仍需按实际物理电平调用详细解答:
以正点原子 ALPHA 开发板的 LED 为例,硬件上 LED 阳极接 3.3V,阴极经限流电阻接 GPIO1_IO03,因此输出低电平点亮。设备树这样描述:
gpioled {
compatible = "atkalpha-gpioled";
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_led>;
led-gpio = <&gpio1 3 GPIO_ACTIVE_LOW>; /* 低电平有效 */
status = "okay";
};
使用旧 API 时(正点原子教程写法),需要按物理电平来,所以代码里出现"反直觉"的取值:
/* 旧 API:直接写物理电平,低电平点亮 */
gpio_set_value(dev->led_gpio, 0); /* 点亮 LED */
gpio_set_value(dev->led_gpio, 1); /* 熄灭 LED */
使用新 gpiod API 时,内核读到了 GPIO_ACTIVE_LOW,会自动反转:
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 = 无效 = 物理高 = 熄灭 */
内核内部的处理逻辑等价于:
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) | 高 | 亮 |
排查点不亮问题:
gpio_direction_output 成功(返回 0)devmem2 读 GPIO1_DR追问:
GPIO_ACTIVE_LOW,用 gpiod 会出现什么现象?(逻辑反了,写 1 灭、写 0 亮)gpiod_set_raw_value 和 gpiod_set_value 区别是什么?(前者绕过极性反转,操作物理值)gpio_request: already requested 或 GPIO 输出异常,怎么定位和解决?答案要点:
cat /sys/kernel/debug/gpio 看占用者;/proc/device-tree 搜索引脚;devmem2 读寄存器详细解答:
排查already requested:
# 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,就会失败。
# 2. 在设备树里搜索使用同一引脚的节点
grep -rn "GPIO1_IO03\|<&gpio1 3" /proc/device-tree/
很多时候是历史遗留:某个默认设备(如某个未用的外设)在 .dtsi 里引用了这个引脚。
排查输出异常:
# 直接读 GPIO 数据寄存器,确认物理电平
devmem2 0x0209C000 w # GPIO1_DR
# 0x0209C000 是 GPIO1 数据寄存器,bit3 对应 GPIO1_IO03
# 读方向寄存器
devmem2 0x0209C004 w # GPIO1_GDIR
# 读 MUX 寄存器,确认引脚功能是否被配成 GPIO
devmem2 0x020E008C w # 对应 GPIO1_IO03 的 MUX 寄存器
排查清单:
| 现象 | 可能原因 | 处理 |
|---|---|---|
| gpio_request 返回 -EBUSY | 被占用 | 注释冲突节点 / 查 debugfs |
| gpio_request 返回 -EINVAL | GPIO 编号非法 | 检查 of_get_named_gpio 返回值 |
| 输出电平不跟随 | 方向没设输出 | gpio_direction_output |
| 输出电平不跟随 | pinctrl 没配(仍复用其他功能) | 检查 pinctrl-0 |
| 输出反过来 | active-low 理解错 | 修正极性或改用 gpiod |
| 读输入恒为 0/1 | 上下拉/外部电路 | 检查 PAD 配置与硬件 |
调试示例:
/* 打印 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);
追问:
/sys/kernel/debug/gpio 需要内核打开哪些配置?(CONFIG_DEBUG_FS、CONFIG_GPIOLIB)gpio_request 有无共享机制?gpio_request 会失败吗?(不一定,MUX 冲突更隐蔽,需查 pinctrl)对应笔记:[[03-Linux驱动开发核心/04-并发同步与原子操作]]
答案要点:
详细解答:
竞态条件(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 | 下半部与进程/中断并发 | 中 |
一个经典例子是"设备只能被一个进程打开":
/* 错误写法: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)。
追问:
volatile 能解决竞态吗?(不能,它只阻止编译器优化,不保证原子性)答案要点:
atomic_t 是封装了 int counter 的结构体,配合 ATOMIC_INIT、atomic_read/set/inc/dec、atomic_dec_and_test 使用LDREX/STREX 独占访问,x86 的 lock 前缀set_bit/clear_bit/test_bit 也是原子的详细解答:
atomic_t 定义(include/linux/types.h):
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_and_test(v) |
自减并测试是否为 0 |
atomic_inc_and_test(v) |
自增并测试是否为 0 |
atomic_sub_and_test(i, v) |
减并测试是否为 0 |
设备互斥访问的经典用法:
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);
位操作:
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);
底层原理:
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) | 需要睡眠的临界区 |
| 位标志 | 涉及外部设备的临界区 |
追问:
atomic_t 为什么不能用于 64 位计数?(32 位原子在多数架构上才有保证,见 atomic64_t)atomic_dec_and_test 相比"先 read 再判断再 dec"好在哪?答案要点:
详细解答:
三者核心 API:
/* 自旋锁 */
DEFINE_SPINLOCK(lock); /* 静态定义 */
spin_lock_init(&lock); /* 动态初始化 */
spin_lock(&lock);
spin_unlock(&lock);
spin_lock_irqsave(&lock, flags); /* 中断安全 */
spin_unlock_irqrestore(&lock, flags);
/* 信号量 */
struct semaphore sem;
sema_init(&sem, 1); /* 计数为 1 即互斥 */
down(&sem); /* 不可中断 */
down_interruptible(&sem); /* 可被信号打断 */
down_trylock(&sem); /* 非阻塞 */
up(&sem);
/* 互斥体 */
struct mutex lock;
mutex_init(&lock);
mutex_lock(&lock);
mutex_unlock(&lock);
mutex_trylock(&lock);
使用示例:
/* 自旋锁保护短临界区 */
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) | 可长 |
| 典型场景 | 中断、短临界区 | 计数并发控制 | 通用互斥 |
选择决策:
需要保护临界区
│
├─ 会在中断上下文访问? ──是──► 自旋锁(irqsave)
│
├─ 需要允许多个并发? ──是──► 信号量(计数 >1)
│
└─ 进程上下文、可能睡眠 ──► 互斥体(首选)
追问:
copy_to_user(可能缺页睡眠),会有什么后果?答案要点:
spin_lock_irqsave 在加锁前禁止本地中断并保存中断状态,解锁时恢复spin_lock_irq 也禁止中断,但它假设调用前中断是开启的,解锁时无条件开启中断irqsave/irqrestore 保存并恢复原状态,在中断上下文/已关中断的场景下更安全spin_lock详细解答:
死锁场景:
CPU0:
进程A: spin_lock(&lock) ← 获得锁
|(此时发生中断)
中断处理: spin_lock(&lock) ← 锁已被自己持有,本 CPU 自旋 → 死锁!
(中断不返回,进程A永远无法 spin_unlock)
spin_lock_irqsave 的做法:
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 变体。其他变体:
spin_lock_bh(&lock); /* 禁止下半部(软中断/tasklet),不禁止硬中断 */
spin_unlock_bh(&lock); /* 与上半部并发保护的场景 */
追问:
spin_lock_irqsave 里的 flags 变量为什么必须传同一个?(恢复的是加锁前的中断状态)irqsave 吗?(中断上下文本身不会被同 CPU 中断打断,但共享锁的进程上下文仍可能,所以要统一用 irqsave)spin_lock_bh?(与软中断/tasklet 共享数据时)答案要点:
trylock 做容错路径,用 mutex_lock_interruptible 响应信号详细解答:
死锁四种典型场景:
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 避免无限等待 |
加锁位置与粒度设计示例:
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++ 在锁内,这就是"细粒度"。
统一出口写法:
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;
}
追问:
mutex_lock_interruptible 返回非 0 时为什么可以直接 return 而不解锁?(它没拿到锁)CONFIG_PROVE_LOCKING)对应笔记:[[03-Linux驱动开发核心/05-中断下半部处理]]、[[03-Linux驱动开发核心/05-中断下半部机制]]
答案要点:
详细解答:
中断处理的总原则是快进快出。一个中断处理函数如果执行 10ms,就意味着这个 CPU 在这 10ms 内无法响应其他中断。
硬件中断触发
│
▼
┌─────────────────────────┐
│ 上半部(硬中断上下文) │ → 读取硬件状态、清中断标志、
│ 极短、不可睡眠 │ 保存数据、调度下半部
└─────────────────────────┘
│
▼(延迟)
┌─────────────────────────┐
│ 下半部 │ → 数据加工、唤醒等待进程、
│ 软中断/进程/内核线程 │ 耗时操作
└─────────────────────────┘
划分原则:
| 归属 | 特征 | 例子 |
|---|---|---|
| 上半部 | 硬件相关、必须立即、极短 | 清中断标志、ACK 中断控制器 |
| 下半部 | 数据处理、可延迟、可能耗时 | 解析报文、唤醒 read 阻塞的进程 |
正点原子中断实验的经典划分:
/* 上半部:只做最小工作——启动定时器消抖 */
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) |
| 可否睡眠 | 否 | 工作队列/线程化:可 |
| 是否屏蔽中断 | 会 | 一般不 |
| 优先级 | 最高 | 较低 |
追问:
答案要点:
详细解答:
tasklet:
/* 定义 + 处理函数 */
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)。
工作队列:
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;
}
自定义工作队列:
struct workqueue_struct *wq = alloc_workqueue("mywq",
WQ_UNBOUND, 1);
queue_work(wq, &my_work);
destroy_workqueue(wq);
延迟工作:
struct delayed_work dwork;
INIT_DELAYED_WORK(&dwork, my_work_func);
schedule_delayed_work(&dwork, msecs_to_jiffies(100));
cancel_delayed_work_sync(&dwork); /* 卸载时必须取消 */
线程化中断:
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
追问:
cancel_work_sync 会导致什么问题?(work 在模块卸载后执行 → oops)IRQF_ONESHOT 的作用是什么?(线程化中断期间保持中断线屏蔽,防止重复触发)答案要点:
irq_of_parse_and_map(设备树)、gpio_to_irq(GPIO 转中断)、platform_get_irq(platform 资源)interrupt-parent 和 interrupts 描述中断来源与触发方式request_irq(irq, handler, flags, name, dev_id) 注册中断处理函数,返回 0 成功flags 指定触发方式与共享属性:IRQF_TRIGGER_FALLING/RISING/...、IRQF_SHARED、IRQF_ONESHOTirqreturn_t (*)(int irq, void *dev_id),返回 IRQ_HANDLED/IRQ_NONE详细解答:
设备树描述中断:
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 中的触发类型:
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,
获取中断号与应用:
/* 方式一:从设备树解析 */
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_RISING | 上升沿触发 |
| IRQF_TRIGGER_FALLING | 下降沿触发 |
| IRQF_TRIGGER_HIGH/LOW | 高/低电平触发 |
| IRQF_SHARED | 共享中断线 |
| IRQF_ONESHOT | 线程化中断期间保持屏蔽 |
| IRQF_NO_SUSPEND | 休眠期间不关闭 |
中断处理函数:
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; */ /* 不是本设备的中断 */
}
追问:
request_irq 返回 -EBUSY 的常见原因?(中断号已被占用、未加 IRQF_SHARED)cat /proc/interrupts)答案要点:
task_struct,无法被调度器调度,睡眠后无法被唤醒恢复mutex_lock 在竞争时会睡眠等待,因此中断里禁用irqsave)或原子操作详细解答:
中断上下文(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 中检查"是否在原子上下文",一旦在中断里睡眠会打印警告甚至崩溃。
正确的做法:
/* 错误:中断里用 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;
}
如果需要"中断线程化":
/* 上半部只唤醒线程,下半部在内核线程里可以加 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;
}
追问:
spin_lock 在中断上下文里能用吗?为什么自旋锁可以而 mutex 不行?down_trylock/mutex_trylock 在中断里能用吗?(trylock 不睡眠,理论可用,但仍不推荐在中断里做复杂同步)GFP_ATOMIC 和 GFP_KERNEL 的区别与此有关吗?(在中断里 kmalloc 必须用 GFP_ATOMIC)答案要点:
mod_timer),避免在中断里做耗时消抖atomic_t 保存键值与标志,避免与进程上下文的 read 竞态del_timer_sync + free_irq,顺序很重要详细解答:
设备树:
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";
};
数据结构:
#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; /* 阻塞读等待队列 */
};
上半部 + 定时器下半部:
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); /* 唤醒阻塞读 */
}
}
初始化:
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 里校验是否本设备触发 |
卸载顺序(关键):
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);
}
追问:
del_timer_sync 而不是 del_timer?(等待正在执行的定时器回调结束,防竞态)struct irq_keydesc[] 数组,每个元素含 gpio/irqnum/handler/value)对应笔记:[[03-Linux驱动开发核心/07-platform总线模型]]、[[03-Linux驱动开发核心/07-platform驱动模型]]
答案要点:
probe,失败则调用 remove详细解答:
设备模型三要素:
┌────────────┐
│ bus │ ← 总线:负责匹配、维护 device/driver 链表
└─────┬──────┘
┌─────────┴─────────┐
▼ ▼
┌─────────┐ ┌──────────┐
│ device │◄─匹配─►│ driver │
└─────────┘ └──────────┘
资源 行为
(reg/irq) (probe/remove)
为什么需要 platform:
platform_device 描述硬件资源:
struct platform_device {
const char *name; /* 匹配用(非 DT 方式) */
int id;
struct device dev; /* 内含 of_node,指向设备树节点 */
u32 num_resources;
struct resource *resource; /* 资源数组:内存、中断 */
};
struct resource {
resource_size_t start; /* 起始 */
resource_size_t end; /* 结束 */
const char *name;
unsigned long flags; /* IORESOURCE_MEM / IORESOURCE_IRQ */
};
设备树方式下,platform_device 由内核从 DT 自动生成,dev.of_node 指向对应节点。
platform_driver 描述驱动行为:
struct platform_driver {
int (*probe)(struct platform_device *);
int (*remove)(struct platform_device *);
void (*shutdown)(struct platform_device *);
struct device_driver driver; /* 内含 name、of_match_table */
};
分离带来的好处:
| 好处 | 说明 |
|---|---|
| 复用 | 一个驱动适配多板,只要 compatible 相同 |
| 解耦 | 硬件改动只改 DTS,不改驱动 |
| 可维护 | 资源与逻辑分离,职责清晰 |
| 统一 | 与设备模型、sysfs、电源管理统一集成 |
追问:
platform_device 的 C 代码吗?(通常不需要,内核从 DT 生成)答案要点:
platform_device->name 与 platform_driver->driver.name 字符串相等of_device_id.compatible 与 DTS 节点 compatible 相等(现代主流)of_match_table 放在 platform_driver.driver.of_match_table,配合 MODULE_DEVICE_TABLE(of, ...)module_platform_driver(x) 展开为 module_init(x##_init) + module_exit(x##_exit),其中 init/exit 内部调用 platform_driver_register/unregister详细解答:
匹配方式一:name 匹配(传统)
/* 设备 */
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 匹配(设备树,推荐)
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, /* 关键 */
},
};
gpioled {
compatible = "atkalpha-gpioled"; /* 与 of_match_table 匹配 */
...
};
匹配优先级(platform_match 内部):
static int platform_match(struct device *dev, struct device_driver *drv)
{
/* 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 匹配 */
...
/* 4. 最后按 name 匹配 */
return strcmp(pdev->name, drv->name) == 0;
}
module_platform_driver 宏展开:
#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);
因此下面两种写法等价:
/* 写法一:手动 */
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);
追问:
MODULE_DEVICE_TABLE(of, ...) 是必须的?(导出设备别名,支持模块自动加载 modprobe / depmod)of_match_ptr 宏的作用是什么?(在未启用 CONFIG_OF 时避免编译错误/返回 NULL)答案要点:
platform_get_resource(pdev, IORESOURCE_MEM, i) → devm_ioremap_resourceplatform_get_irq(pdev, i) 或 platform_get_resource(IORESOURCE_IRQ)of_property_read_*、of_get_named_gpioplatform_set_drvdata详细解答:
标准 probe 骨架:
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 |
保存/取回私有数据 |
非设备树方式的资源定义:
static struct resource led_res[] = {
[0] = {
.start = 0x0209C000,
.end = 0x0209C3FF,
.flags = IORESOURCE_MEM,
},
[1] = {
.start = 100,
.end = 100,
.flags = IORESOURCE_IRQ,
},
};
追问:
devm_ 系列资源是何时释放的?remove 里还需要手动释放吗?-EPROBE_DEFER 表示什么?(依赖的资源还没就绪,内核会稍后重试 probe)platform_set_drvdata 和 filp->private_data 有什么区别?(前者绑定 device 与驱动实例,后者绑定文件与驱动实例)答案要点:
compatible 节点自动生成 platform_deviceplatform_device_register,驱动只需提供 of_match_tableplatform_device + resource,并与驱动分开注册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)
驱动侧只需:
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 |
| 驱动代码 | 相同 | 相同 |
| 现代推荐 | 否 | 是 |
传统方式写出设备再注册:
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;
}
追问:
/sys/bus/platform/devices/ 里能看到什么?(自动生成的 device,名字通常是 地址.节点名)probe 不执行)答案要点:
of_match_table 和 platform_driver 结构,用 module_platform_driver 注册module_init 里的硬件初始化搬到 probe,原 module_exit 的清理搬到 removepdev 从设备树获取platform_set_drvdata / priv 传递,probe 里完成字符设备注册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 |
完整框架:
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/of.h>
#include <linux/of_gpio.h>
#include <linux/cdev.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <linux/gpio/consumer.h>
#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");
设备树:
gpioled {
compatible = "atkalpha-gpioled";
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_led>;
led-gpios = <&gpio1 3 GPIO_ACTIVE_LOW>;
status = "okay";
};
改造要点总结:
xxx_init/xxx_exit → probe/remove。of_find_node_by_path → pdev->dev.of_node / platform_get_*。platform_set_drvdata + container_of 从 inode->i_cdev 找回。module_init/module_exit → module_platform_driver。追问:
container_of 在 open 里是怎么从 inode->i_cdev 找回 struct leddev_dev 的?device_create(..., &pdev->dev, ...) 而不是传 NULL 作为 parent?(建立 sysfs 设备层级关系)对应笔记:[[03-Linux驱动开发核心/06-阻塞IO与poll机制]]
答案要点:
-EAGAIN,应用轮询,占 CPU,适合实时性要求高SIGIO 信号,事件驱动,CPU 占用低详细解答:
五种模型对比:
| 模型 | 机制 | 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 使用率:
/* 阻塞: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);
追问:
-EAGAIN 和 -EWOULDBLOCK 是一样的吗?答案要点:
init_waitqueue_head 或 DECLARE_WAIT_QUEUE_HEADwait_event_interruptible(wq, condition):条件不满足就休眠,条件满足返回 0,被信号打断返回非 0wake_up_interruptible 唤醒wait_event 内部是循环判断详细解答:
等待队列核心结构(include/linux/wait.h):
struct __wait_queue_head {
spinlock_t lock; /* 保护队列的自旋锁 */
struct list_head task_list; /* 等待进程链表 */
};
typedef struct __wait_queue_head wait_queue_head_t;
初始化:
/* 方式一:静态 */
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 实现:
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);
}
中断/定时器里唤醒:
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 内部展开逻辑(简化):
#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 上) |
手动实现等待(老式写法,理解用):
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);
追问:
wait_event 要用循环判断条件?"虚假唤醒"指的是什么?wake_up,应用会怎样?(永久阻塞,只能被信号/超时打断)wait_event_interruptible 返回非 0 时驱动应该返回什么?为什么不是直接返回 0?答案要点:
file_operations.poll,内部调用 poll_wait(file, &wq, wait) 把进程加入等待队列,再根据设备状态返回事件掩码poll_wait 不阻塞,只做登记;真正等待由内核在 select/poll/epoll 里完成POLLIN | POLLRDNORM,可写返回 POLLOUT | POLLWRNORMFD_SETSIZE(通常 1024),用位图;poll 用数组无上限;epoll 用红黑树+就绪链表详细解答:
驱动侧 poll 实现(正点原子 noblockio.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 原型:
void poll_wait(struct file *filp, wait_queue_head_t *wait_address,
poll_table *p);
应用侧三种用法:
/* 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:
追问:
poll_wait 为什么叫"wait"却不阻塞?真正阻塞发生在哪一层?答案要点:
SIGIO 信号file_operations.fasync,调用 fasync_helper 维护 fasync_struct 链表kill_fasync(&dev->async_queue, SIGIO, POLL_IN) 发信号signal(SIGIO, handler) 注册处理函数、fcntl(F_SETOWN, getpid()) 告诉内核接收进程、fcntl(F_SETFL, ...|FASYNC) 开启异步通知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()
驱动侧实现:
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 原型:
void kill_fasync(struct fasync_struct **fp, int sig, int band);
/* band:可读用 POLL_IN,可写用 POLL_OUT */
应用侧三步:
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):
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 更优 |
追问:
release 里必须调用 fasync(-1, filp, 0)?不调会怎样?(野指针,异步队列残留)F_SETOWN,kill_fasync 的信号会发给谁?write 等 async-signal-safe 函数?(标准 IO/printf 不可重入)答案要点:
file_operations,四个功能不冲突,依次实现 open/read/poll/fasync/releaseread 内部根据 filp->f_flags & O_NONBLOCK 分两条路径:阻塞走 wait_event_interruptible,非阻塞判断无数据直接返回 -EAGAINpoll 里 poll_wait 登记等待队列,有数据返回 POLLIN,无数据返回 0fasync 用 fasync_helper 建立队列,事件发生时 kill_fasync 发 SIGIOwake_up_interruptible + kill_fasyncrelease 里注销 fasync,避免残留详细解答:
关键认识:四种 IO 模型不是互斥的,而是在同一驱动的不同位置各司其职。设备结构体只加两个成员(等待队列头 + fasync 指针),file_operations 把这些操作串起来。
统一数据结构:
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; /* 异步通知 */
};
统一事件发生点(中断消抖后的定时器回调):
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 同时支持阻塞与非阻塞:
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:
static unsigned int imx6uirq_poll(struct file *filp,
struct poll_table_struct *wait)
{
struct imx6uirq_dev *dev = filp->private_data;
unsigned int mask = 0;
poll_wait(filp, &dev->r_wait, wait);
if (atomic_read(&dev->releasekey))
mask |= POLLIN | POLLRDNORM;
return mask;
}
static int imx6uirq_fasync(int fd, struct file *filp, int on)
{
struct imx6uirq_dev *dev = filp->private_data;
return fasync_helper(fd, filp, on, &dev->async_queue);
}
static int imx6uirq_release(struct inode *inode, struct file *filp)
{
return imx6uirq_fasync(-1, filp, 0); /* 注销异步通知 */
}
static struct file_operations imx6uirq_fops = {
.owner = THIS_MODULE,
.open = imx6uirq_open,
.read = imx6uirq_read,
.poll = imx6uirq_poll,
.fasync = imx6uirq_fasync,
.release = imx6uirq_release,
};
四种模式对照:
| 模型 | 驱动侧关键点 | 应用侧关键点 |
|---|---|---|
| 阻塞 | wait_event_interruptible + 中断唤醒 | 默认 open 不传 O_NONBLOCK |
| 非阻塞 | 判 O_NONBLOCK 返回 -EAGAIN |
open 时加 O_NONBLOCK |
| poll | 实现 .poll + poll_wait + 返回掩码 | select/poll/epoll |
| 异步通知 | .fasync + fasync_helper + kill_fasync | signal + F_SETOWN + FASYNC |
设计要点:
r_wait,内核保证两者都被唤醒。keyvalue/releasekey 用 atomic_t,因为可能被中断上下文与进程上下文同时访问。del_timer_sync → free_irq → fasync(-1, flp, 0) → cdev/class/device 逆序注销。追问:
poll_wait 把进程挂在 r_wait 上,wake_up_interruptible 会唤醒它并重新 poll)copy_to_user 后返回,但用户缓冲不足会怎样?如何设计更健壮?(先校验 cnt,或返回实际字节数)| 领域 | 关键 API |
|---|---|
| 设备号 | MKDEV MAJOR MINOR alloc_chrdev_region register_chrdev_region |
| cdev | cdev_init cdev_add cdev_del |
| 设备节点 | class_create device_create device_destroy class_destroy |
| 数据拷贝 | copy_to_user copy_from_user |
| 设备树 | of_find_node_by_path of_property_read_u32 of_get_named_gpio of_iomap |
| gpio | gpio_request gpio_direction_output gpio_set_value gpiod_set_value |
| 原子 | atomic_set atomic_inc atomic_dec_and_test |
| 自旋锁 | spin_lock_init spin_lock_irqsave spin_unlock_irqrestore |
| 信号量/互斥 | sema_init down up mutex_init mutex_lock mutex_unlock |
| 中断 | request_irq free_irq irq_of_parse_and_map platform_get_irq |
| 下半部 | tasklet_init tasklet_schedule INIT_WORK schedule_work request_threaded_irq |
| platform | platform_get_resource devm_ioremap_resource platform_set_drvdata module_platform_driver |
| 阻塞 | init_waitqueue_head wait_event_interruptible wake_up_interruptible |
| poll | poll_wait POLLIN POLLRDNORM |
| 异步 | fasync_helper kill_fasync fasync_struct |
| 错误码 | 含义 | 常见场景 |
|---|---|---|
| -EBUSY | 忙 | 设备已打开、GPIO 被占用、中断被占用 |
| -EINVAL | 参数非法 | 设备树属性/节点错误、GPIO 编号非法 |
| -EAGAIN | 再试一次 | 非阻塞读无数据 |
| -ENOMEM | 内存不足 | kmalloc 失败 |
| -EFAULT | 地址错误 | copy_*_user 失败 |
| -ENODEV | 设备不存在 | 次设备号越界、资源缺失 |
| -EPROBE_DEFER | 稍后重试 | 依赖资源未就绪 |
💡 关联笔记复盘: [[03-Linux驱动开发核心/01-字符设备驱动框架]] | [[03-Linux驱动开发核心/02-设备树语法与实战]] | [[03-Linux驱动开发核心/03-pinctrl与gpio子系统]] | [[03-Linux驱动开发核心/04-并发同步与原子操作]] | [[03-Linux驱动开发核心/05-中断下半部处理]] | [[03-Linux驱动开发核心/06-阻塞IO与poll机制]] | [[03-Linux驱动开发核心/07-platform总线模型]] | [[03-Linux驱动开发核心/08-misc与input子系统]] | [[02-嵌入式Linux内核基础/06-设备模型与Kobject]]
资料来源: 【正点原子】I.MX6U嵌入式Linux驱动开发指南V2.0.1 第40/43/45/47/51/54章 最后更新: 2026-09-17