面试-Linux驱动开发.md 133 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

追问

  • 追问1:alloc_chrdev_regionregister_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 找到 cdevcdev->ops 指向驱动的 file_operations
  5. 因此 open() 里常把设备结构体挂到 filp->private_data,后续 read/write 通过它取回设备对象

详细解答

理解这三个结构体是写字符设备驱动的基本功,很多"为什么 open 里能拿到设备、read 里怎么找到设备"的问题都源于此。

struct inodeinclude/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 fileinclude/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 驱动加载时静态定义 每个驱动一份 各种操作函数指针

追问

  • 追问1:inodefile 是一对一吗?多个进程同时打开同一个设备会创建几个 file?(多个,各自独立)
  • 追问2:private_data 为什么在 open 里赋值、在 release 里清理?如果忘记会怎样?
  • 追问3:f_pos 出现在 read/writeloff_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 实现里最容易出错、也是面试高频的点。

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

追问

  • 追问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 自动建节点机制。

驱动侧代码:

/* 创建类 */
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 产品、推荐

追问

  • 追问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],每个元素含 cdevdev_tdevice *,但 class 只需一个
  3. file_operations 只定义一份,open 里通过 inode->i_rdev 求次设备号 MINOR,定位到具体实例
  4. 把实例指针挂到 filp->private_dataread/write 通过它访问对应硬件
  5. 卸载时循环 cdev_deldevice_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 分级清理 避免卸载不干净导致设备号泄漏

追问

  • 追问1:如果不把实例指针放进 private_dataread/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 文件里的,例如:

/* 老式 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 匹配为主

追问

  • 追问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 内核源码根目录):

# 编译所有设备树
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 二进制 运行时动态打补丁

追问

  • 追问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——匹配的核心:

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

追问

  • 追问1:#address-cells 是写在父节点还是子节点?为什么容易写错?
  • 追问2:compatible 为什么要写多个字符串?匹配顺序是怎样的?
  • 追问3:regranges 一起用时,of_iomap 拿到的是映射前还是映射后的地址?

Q9: 驱动如何从设备树读取资源?of_ 函数族有哪些常用接口?of_iomap 和 ioremap 有什么区别?

答案要点

  1. 节点查找:of_find_node_by_pathof_find_node_by_nameof_get_next_childof_get_parent
  2. 属性读取:of_find_propertyof_property_read_u32of_property_read_stringof_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设备树》给出的完整读取流程,通常分四步:

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_iomapioremap 对比:

对比项 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));

追问

  • 追问1:of_property_read_u32_array 返回负值说明什么?如何先用 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 新不新"的顺序排查效率最高。

第一步:确认节点进了内核

# 方式一:运行时查看
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;
}

追问

  • 追问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,能配置功能却无法在运行时读写电平。所以二者是互补关系。

/* 正确顺序:内核会自动应用 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_probepinctrl_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 下定义配置组:

&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";
};

追问

  • 追问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 的典型流程(正点原子教程常用):

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
推荐程度 已不推荐 推荐

注意点:

  • 新 API 的设备树属性名有约定:单个用 led-gpiosled-gpiodevm_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_HIGHGPIOD_OUT_LOW 是设置物理电平还是逻辑电平?(逻辑电平,会按 active-low 反转)
  • 追问3:如果设备树既没写 led-gpios 也没写 led-gpiodevm_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,因此输出低电平点亮。设备树这样描述:

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)

排查点不亮问题:

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

# 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);

追问

  • 追问1:/sys/kernel/debug/gpio 需要内核打开哪些配置?(CONFIG_DEBUG_FSCONFIG_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 下半部与进程/中断并发

一个经典例子是"设备只能被一个进程打开":

/* 错误写法: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_INITatomic_read/set/inc/decatomic_dec_and_test 使用
  3. 底层依赖 CPU 的原子指令:ARM 的 LDREX/STREX 独占访问,x86 的 lock 前缀
  4. 对多位共享数据、复杂结构体的操作不能用原子操作,必须用锁
  5. 位操作 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) 需要睡眠的临界区
位标志 涉及外部设备的临界区

追问

  • 追问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:

/* 自旋锁 */
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)
   │
   └─ 进程上下文、可能睡眠 ──► 互斥体(首选)

追问

  • 追问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 的做法:

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);     /* 与上半部并发保护的场景 */

追问

  • 追问1:spin_lock_irqsave 里的 flags 变量为什么必须传同一个?(恢复的是加锁前的中断状态)
  • 追问2:在中断处理函数里加锁,还需要 irqsave 吗?(中断上下文本身不会被同 CPU 中断打断,但共享锁的进程上下文仍可能,所以要统一用 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 避免无限等待

加锁位置与粒度设计示例

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;
}

追问

  • 追问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 阻塞的进程

正点原子中断实验的经典划分:

/* 上半部:只做最小工作——启动定时器消抖 */
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

/* 定义 + 处理函数 */
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

追问

  • 追问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-parentinterrupts 描述中断来源与触发方式
  3. request_irq(irq, handler, flags, name, dev_id) 注册中断处理函数,返回 0 成功
  4. flags 指定触发方式与共享属性:IRQF_TRIGGER_FALLING/RISING/...IRQF_SHAREDIRQF_ONESHOT
  5. 中断处理函数原型固定为 irqreturn_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; */ /* 不是本设备的中断 */
}

追问

  • 追问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 中检查"是否在原子上下文",一旦在中断里睡眠会打印警告甚至崩溃。

正确的做法:

/* 错误:中断里用 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;
}

追问

  • 追问1:spin_lock 在中断上下文里能用吗?为什么自旋锁可以而 mutex 不行?
  • 追问2:down_trylock/mutex_trylock 在中断里能用吗?(trylock 不睡眠,理论可用,但仍不推荐在中断里做复杂同步)
  • 追问3:GFP_ATOMICGFP_KERNEL 的区别与此有关吗?(在中断里 kmalloc 必须用 GFP_ATOMIC)

Q25: 实战题:设计一个按键中断驱动(含消抖),并说明如何避免抖动、丢失和误触发。

答案要点

  1. 上半部只做最小动作:记录键号、启动定时器(mod_timer),避免在中断里做耗时消抖
  2. 定时器回调(下半部)延时 10ms 后再读 GPIO 电平,完成软件消抖
  3. atomic_t 保存键值与标志,避免与进程上下文的 read 竞态
  4. 用等待队列/异步通知把按键事件传给用户空间,避免轮询
  5. 卸载时严格 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_syncfree_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);
}

追问

  • 追问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 描述硬件资源:

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、电源管理统一集成

追问

  • 追问1:platform_device 和普通 device 的区别是什么?
  • 追问2:既然有设备树,还需要手动写 platform_device 的 C 代码吗?(通常不需要,内核从 DT 生成)
  • 追问3:platform 总线的匹配是在什么时候发生的?(设备注册或驱动注册时都会尝试匹配)

Q27: platform 总线的匹配机制有哪些?compatible 与 of_match_table 是怎样配合的?module_platform_driver 展开成什么?

答案要点

  1. 传统匹配:platform_device->nameplatform_driver->driver.name 字符串相等
  2. 设备树匹配:of_device_id.compatible 与 DTS 节点 compatible 相等(现代主流)
  3. 还有 ACPI 匹配、id_table 匹配等,优先级一般为 of > acpi > id > 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 匹配(传统)

/* 设备 */
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);

追问

  • 追问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 骨架:

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,
    },
};

追问

  • 追问1:devm_ 系列资源是何时释放的?remove 里还需要手动释放吗?
  • 追问2:probe 返回 -EPROBE_DEFER 表示什么?(依赖的资源还没就绪,内核会稍后重试 probe)
  • 追问3:platform_set_drvdatafilp->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)

驱动侧只需:

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;
}

追问

  • 追问1:设备树方式下 /sys/bus/platform/devices/ 里能看到什么?(自动生成的 device,名字通常是 地址.节点名
  • 追问2:如果 dts 里有节点但驱动没加载,会发生什么?(device 存在但无 driver 匹配,probe 不执行)
  • 追问3:为什么说设备树让"驱动与板级信息彻底解耦"?

Q30: 设计题:把一个字符设备驱动改造成 platform 驱动,需要改哪些地方?请给出完整框架。

答案要点

  1. 增加 of_match_tableplatform_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

完整框架:

#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";
};

改造要点总结:

  1. 入口出口改名xxx_init/xxx_exitprobe/remove
  2. 资源获取换源:全局 of_find_node_by_pathpdev->dev.of_node / platform_get_*
  3. 实例传递:全局变量 → platform_set_drvdata + container_ofinode->i_cdev 找回。
  4. 注册方式module_init/module_exitmodule_platform_driver
  5. 错误回滚:probe 中任何一步失败都要逆序清理,或尽量用 devm。

追问

  • 追问1:container_ofopen 里是怎么从 inode->i_cdev 找回 struct leddev_dev 的?
  • 追问2:如果同一 compatible 有多个设备节点,probe 会被调用几次?
  • 追问3:为什么要用 device_create(..., &pdev->dev, ...) 而不是传 NULL 作为 parent?(建立 sysfs 设备层级关系)

七、IO 模型

对应笔记:[[03-Linux驱动开发核心/06-阻塞IO与poll机制]]

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 使用率:

/* 阻塞: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_headDECLARE_WAIT_QUEUE_HEAD
  3. wait_event_interruptible(wq, condition):条件不满足就休眠,条件满足返回 0,被信号打断返回非 0
  4. 条件必须能被其他上下文改变:通常由中断/定时器里 wake_up_interruptible 唤醒
  5. 唤醒后必须重新判断条件(可能虚假唤醒),因此 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);

追问

  • 追问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):

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:

  • 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()

驱动侧实现:

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 更优

追问

  • 追问1:为什么 release 里必须调用 fasync(-1, filp, 0)?不调会怎样?(野指针,异步队列残留)
  • 追问2:如果应用没设置 F_SETOWNkill_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. pollpoll_wait 登记等待队列,有数据返回 POLLIN,无数据返回 0
  4. fasyncfasync_helper 建立队列,事件发生时 kill_fasync 发 SIGIO
  5. 所有通知路径最终都依赖同一个"事件发生点"(中断/定时器)去 wake_up_interruptible + kill_fasync
  6. release 里注销 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

设计要点:

  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/54章 最后更新: 2026-09-17