--- title: 并发同步与原子操作 tags: [ Linux驱动, 并发, 锁, 原子操作, 嵌入式, 自旋锁, 信号量, 互斥体, 读写锁, 顺序锁, RCU, ] created: 2026-09-16 updated: 2026-09-17 pdf_ref: - "【正点原子】I.MX6U嵌入式Linux驱动开发指南V2.0.1 - 第四十七章 Linux并发与竞争" - "【正点原子】I.MX6U嵌入式Linux驱动开发指南V2.0.1 - 第四十八章 Linux并发与竞争实验" --- # 并发同步与原子操作 > 💡 **关联知识**: [[FreeRTOS学习笔记/07-中断管理与临界段]] | [[03-Linux驱动开发核心/05-中断下半部处理]] | [[03-Linux驱动开发核心/07-platform总线模型]] --- ## 一、并发问题概述 ### 1.1 什么是竞态条件 **竞态条件(Race Condition)** 是指多个执行单元(线程、中断、进程)同时访问共享资源,且最终结果依赖于执行时序的现象。 > 💡 **生活类比**:公司只有一台打印机,小明和小红同时发送打印任务。如果小明的文档先发送但打印较慢,小红的文档后发送但打印较快,最终小明可能拿到小红的文档——这就是竞态条件。 ```mermaid sequenceDiagram participant P1 as 进程A participant P2 as 进程B participant R as 共享资源 Note over P1,R: 理想执行顺序 P1->>R: 读取资源 P1->>R: 修改资源 P2->>R: 读取资源 P2->>R: 修改资源 Note over P1,R: 实际可能的执行顺序 P1->>R: 读取资源 P2->>R: 读取资源 P1->>R: 修改资源 P2->>R: 修改资源(覆盖P1的结果) ``` ### 1.2 竞态条件产生场景 | 场景 | 说明 | 危害程度 | | ----------------- | ---------------------------------------------------- | ---------- | | **中断与进程** | 进程访问共享数据时被中断,中断处理函数也访问同一数据 | ⭐⭐⭐⭐ | | **多进程/多线程** | 多个进程或线程同时读写共享内存 | ⭐⭐⭐ | | **多CPU(SMP)** | 多核处理器上多个CPU核心同时执行访问同一数据 | ⭐⭐⭐⭐⭐ | | **抢占调度** | 进程在内核态执行时被高优先级任务抢占 | ⭐⭐⭐ | ### 1.3 竞态条件的危害 1. **数据不一致**:共享变量值被意外覆盖 2. **逻辑错误**:程序执行流程偏离预期 3. **系统崩溃**:内核数据结构被破坏导致kernel panic 4. **难以复现**:时序相关的问题极难调试 ### 1.4 并发问题示意图 ```mermaid flowchart TD A[并发访问共享资源] --> B{资源类型} B --> C[简单变量] B --> D[复杂数据结构] B --> E[硬件寄存器] C --> F[原子操作] D --> G[自旋锁/互斥体] E --> H[原子操作/自旋锁] F --> I[保证操作不可中断] G --> J[保证临界区互斥] H --> K[保证硬件访问原子性] style A fill:#fee2e2,stroke:#dc2626 style F fill:#dcfce7,stroke:#16a34a style G fill:#dcfce7,stroke:#16a34a style H fill:#dcfce7,stroke:#16a34a ``` ### 1.5 为什么需要关注并发 Linux是多任务操作系统,以下因素导致并发不可避免: - **多线程/多进程**:用户空间程序并发执行 - **抢占式调度**:2.6版本后Linux内核支持抢占 - **中断程序并发**:硬件中断随时可能发生 - **SMP多核并行**:多CPU核心同时执行内核代码 > ⚠️ **注意**:编写驱动程序时必须考虑并发保护,否则会导致难以定位的bug。 ### 1.6 Linux并发控制机制总览 Linux内核提供的同步机制从"轻"到"重"排列如下,选择哪一种是驱动开发中最基础的判断: | 机制 | 典型类型 | 是否可睡眠 | 中断上下文 | 适用场景 | | ------------ | ------------------- | ---------- | ---------------------- | -------------------------- | | **原子操作** | `atomic_t` | 否 | 可用 | 单个整数计数、状态标志 | | **自旋锁** | `spinlock_t` | 否 | 可用 | 极短临界区、中断上下文 | | **读写锁** | `rwlock_t` | 否 | 可用 | 读多写少、临界区短 | | **顺序锁** | `seqlock_t` | 否 | 可用(写者不可被中断) | 读多写极少、读侧不能加锁 | | **RCU** | `rcu_read_lock` | 否 | 读侧可用 | 链表遍历为主、读多写极少 | | **信号量** | `struct semaphore` | 是 | 不可用 | 长临界区、允许并发数 > 1 | | **互斥体** | `struct mutex` | 是 | 不可用 | 长临界区、需要互斥且可睡眠 | | **完成量** | `struct completion` | 是 | 不可用 | 一个执行单元等待另一个完成 | > 💡 **选择原则**:能不用锁就不用锁(无锁设计优先,如 per-CPU 变量),必须加锁时按"临界区长度 + 是否可能睡眠 + 读多还是写多"三个维度决策。本文档按此顺序逐个讲解。 --- ## 二、原子操作 ### 2.1 什么是原子操作 **原子操作(Atomic Operation)** 是指在执行过程中不会被中断的操作,要么完全执行,要么完全不执行。适用于简单的变量操作。 ```c /* 原子变量定义 - include/linux/types.h */ typedef struct { int counter; } atomic_t; ``` ### 2.2 原子变量操作API | 函数 | 说明 | | --------------------------------------------- | ---------------------- | | `ATOMIC_INIT(int i)` | 定义原子变量时初始化值 | | `int atomic_read(atomic_t *v)` | 读取原子变量的值 | | `void atomic_set(atomic_t *v, int i)` | 设置原子变量的值 | | `void atomic_add(int i, atomic_t *v)` | 原子加法 | | `void atomic_sub(int i, atomic_t *v)` | 原子减法 | | `void atomic_inc(atomic_t *v)` | 原子自增 | | `void atomic_dec(atomic_t *v)` | 原子自减 | | `int atomic_inc_and_test(atomic_t *v)` | 自增并测试是否为0 | | `int atomic_dec_and_test(atomic_t *v)` | 自减并测试是否为0 | | `int atomic_sub_and_test(int i, atomic_t *v)` | 减法并测试是否为0 | ### 2.3 原子操作使用示例 ```c #include /* 定义并初始化原子变量 */ static atomic_t led_status = ATOMIC_INIT(0); /* 在驱动初始化时设置 */ atomic_set(&led_status, 1); /* 设置为1,表示LED可用 */ /* 检查并获取设备 */ static int led_open(struct inode *inode, struct file *filp) { /* 原子减1并测试是否为0 */ if (!atomic_dec_and_test(&led_status)) { atomic_inc(&led_status); /* 恢复原值 */ return -EBUSY; /* 设备被占用 */ } return 0; } /* 关闭设备时释放 */ static int led_release(struct inode *inode, struct file *filp) { atomic_inc(&led_status); /* 原子加1,释放设备 */ return 0; } ``` ### 2.4 原子位操作API | 函数 | 说明 | | ----------------------------------------- | -------------------- | | `void set_bit(int nr, void *p)` | 设置第nr位为1 | | `void clear_bit(int nr, void *p)` | 清除第nr位 | | `void change_bit(int nr, void *p)` | 反转第nr位 | | `int test_bit(int nr, void *p)` | 读取第nr位的值 | | `int test_and_set_bit(int nr, void *p)` | 设置第nr位并返回原值 | | `int test_and_clear_bit(int nr, void *p)` | 清除第nr位并返回原值 | --- ## 三、自旋锁 ### 3.1 什么是自旋锁 **自旋锁(Spinlock)** 是一种忙等待的锁机制。当一个线程尝试获取锁时,如果锁已被持有,该线程会在原地"自旋"等待,直到锁被释放。 ```mermaid stateDiagram-v2 [*] --> 空闲: 初始状态 空闲 --> 已锁定: spin_lock() 已锁定 --> 空闲: spin_unlock() 已锁定 --> 已锁定: 其他CPU尝试获取锁(自旋等待) note right of 已锁定 持有锁的CPU执行临界区代码 其他CPU在原地循环等待 end note ``` ### 3.2 自旋锁类型定义 ```c /* include/linux/spinlock_types.h */ typedef struct spinlock { union { struct raw_spinlock rlock; #ifdef CONFIG_DEBUG_LOCK_ALLOC struct { u8 __padding[LOCK_PADSIZE]; struct lockdep_map dep_map; }; #endif }; } spinlock_t; ``` ### 3.3 自旋锁API | 函数 | 说明 | 使用场景 | | --------------------------------------- | --------------------- | ------------ | | `DEFINE_SPINLOCK(spinlock_t lock)` | 静态定义并初始化 | 全局锁 | | `void spin_lock_init(spinlock_t *lock)` | 动态初始化 | 运行时初始化 | | `void spin_lock(spinlock_t *lock)` | 获取锁(忙等待) | 进程上下文 | | `void spin_unlock(spinlock_t *lock)` | 释放锁 | 进程上下文 | | `int spin_trylock(spinlock_t *lock)` | 尝试获取锁,失败返回0 | 非阻塞场景 | | `int spin_is_locked(spinlock_t *lock)` | 检查锁状态 | 调试 | ### 3.4 中断安全的自旋锁API | 函数 | 说明 | | -------------------------------------------------------------------- | ------------------------------ | | `void spin_lock_irq(spinlock_t *lock)` | 禁止本地中断并获取锁 | | `void spin_unlock_irq(spinlock_t *lock)` | 释放锁并开启本地中断 | | `void spin_lock_irqsave(spinlock_t *lock, unsigned long flags)` | 保存中断状态、禁止中断、获取锁 | | `void spin_unlock_irqrestore(spinlock_t *lock, unsigned long flags)` | 恢复中断状态、释放锁 | | `void spin_lock_bh(spinlock_t *lock)` | 禁止下半部并获取锁 | | `void spin_unlock_bh(spinlock_t *lock)` | 释放锁并开启下半部 | ### 3.5 自旋锁使用场景 ```mermaid flowchart TD A[需要保护共享资源] --> B{访问上下文} B -->|进程上下文| C{是否可能被中断访问?} B -->|中断上下文| D[使用spin_lock] C -->|是| E[使用spin_lock_irqsave] C -->|否| F[使用spin_lock] E --> G[临界区执行] D --> G F --> G G --> H{需要保护下半部?} H -->|是| I[使用spin_lock_bh] H -->|否| J[使用spin_unlock] style A fill:#fef3c7,stroke:#f59e0b style G fill:#dbeafe,stroke:#2563eb ``` ### 3.6 自旋锁使用注意事项 综合前面关于自旋锁的信息,我们需要在使用自旋锁的时候要注意以下几点(原书 47.3.4): 1. **锁的持有时间不能太长,一定要短**:因为在等待自旋锁的时候处于"自旋"状态,会降低系统性能。如果临界区比较大、运行时间比较长的话,要选择其他的并发处理方式,比如信号量和互斥体。 2. **自旋锁保护的临界区内不能调用任何可能导致线程休眠的 API 函数**:否则可能导致死锁。 3. **不能递归申请自旋锁**:因为一旦通过递归的方式申请一个你正在持有的锁,那么你就必须"自旋",等待锁被释放,然而你正处于"自旋"状态,根本没法释放锁。结果就是自己把自己锁死了! 4. **在编写驱动程序时必须考虑驱动的可移植性**:因此不管你用的是单核的还是多核的 SOC,都将其当做多核 SOC 来编写驱动程序。 > 💡 补充:若临界区可能被中断访问,需使用 `spin_lock_irqsave` / `spin_unlock_irqrestore` 保证中断安全(原书 47.3.2 自旋锁 API 中的 `_irqsave` 变体)。 --- ## 四、信号量 ### 4.1 什么是信号量 **信号量(Semaphore)** 是一种可睡眠的同步机制。当进程获取信号量失败时会进入睡眠状态,释放CPU给其他任务使用。 ```mermaid sequenceDiagram participant P1 as 进程A participant S as 信号量 participant P2 as 进程B P1->>S: down() - 获取信号量 S-->>P1: 成功,继续执行 P2->>S: down() - 尝试获取 S-->>P2: 失败,进入睡眠 P1->>S: up() - 释放信号量 S->>P2: 唤醒进程B P2->>S: 获取成功 ``` ### 4.2 信号量API ```c #include struct semaphore { raw_spinlock_t lock; unsigned int count; struct list_head wait_list; }; /* 初始化信号量 */ sem_init(&sem, 1); /* 互斥信号量(计数为1) */ sem_init(&sem, 10); /* 计数信号量(允许10个并发) */ /* 获取信号量 */ down(&sem); /* 不可中断,可能睡眠 */ down_trylock(&sem); /* 非阻塞,失败返回非0 */ down_interruptible(&sem); /* 可被信号中断 */ /* 释放信号量 */ up(&sem); ``` ### 4.3 信号量使用示例 ```c static struct semaphore led_sem; static int __init led_init(void) { sema_init(&led_sem, 1); /* 初始化为互斥信号量 */ return 0; } static ssize_t led_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt) { down(&led_sem); /* 获取信号量,可能睡眠 */ /* 临界区(可以睡眠的操作) */ gpiod_set_value(dev->led_gpio, ledstat); up(&led_sem); /* 释放信号量 */ return 0; } ``` ### 4.4 自旋锁 vs 信号量对比表 | 特性 | 自旋锁 | 信号量 | | -------------- | ------------------ | ------------------- | | **等待方式** | 忙等待(自旋) | 睡眠等待 | | **CPU占用** | 占用CPU | 不占用CPU | | **可睡眠** | ❌ 不可以 | ✅ 可以 | | **中断上下文** | ✅ 可以使用 | ❌ 不可以 | | **临界区长度** | 短(μs级) | 长(ms级) | | **性能开销** | 低(忙等) | 高(上下文切换) | | **适用场景** | 中断处理、短临界区 | 长时间操作、I/O操作 | ```mermaid quadrantChart title 自旋锁与信号量选择 x-axis 短临界区 --> 长临界区 y-axis 不可睡眠 --> 可睡眠 "中断处理": [0.2, 0.1] "短临界区": [0.3, 0.2] "驱动初始化": [0.7, 0.8] "文件操作": [0.6, 0.9] "网络传输": [0.8, 0.7] "磁盘I/O": [0.9, 0.9] ``` --- ## 五、互斥体 ### 5.1 什么是互斥体 **互斥体(Mutex)** 是Linux内核中最常用的互斥锁机制。它是一种特殊的信号量,只能由一个线程持有,提供了更严格的互斥保证。 ### 5.2 互斥体API ```c #include /* 定义互斥体 */ struct mutex my_mutex; /* 初始化 */ mutex_init(&my_mutex); /* 获取锁 */ mutex_lock(&my_mutex); /* 可能睡眠 */ mutex_trylock(&my_mutex); /* 非阻塞,失败返回0 */ /* 释放锁 */ mutex_unlock(&my_mutex); /* 销毁 */ mutex_destroy(&my_mutex); ``` ### 5.3 互斥体使用示例 ```c static struct mutex led_mutex; static int __init led_init(void) { mutex_init(&led_mutex); return 0; } static ssize_t led_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt) { mutex_lock(&led_mutex); /* 获取互斥锁 */ /* 临界区 */ gpiod_set_value(dev->led_gpio, ledstat); mutex_unlock(&led_mutex); /* 释放互斥锁 */ return 0; } ``` ### 5.4 互斥体 vs 自旋锁对比 | 特性 | 互斥体 | 自旋锁 | | -------------- | ----------- | ----------- | | **睡眠** | ✅ 可以睡眠 | ❌ 不能睡眠 | | **持有时长** | 可以较长 | 必须很短 | | **中断上下文** | ❌ 不能使用 | ✅ 可以使用 | | **优先级继承** | ✅ 支持 | ❌ 不支持 | | **递归锁** | ❌ 不支持 | ❌ 不支持 | | **调试支持** | ✅ 丰富 | ⚠️ 有限 | --- ## 六、读写自旋锁(rwlock) > 本节内容来自《I.MX6U嵌入式Linux驱动开发指南》**47.3.3 其他类型的锁**。原书指出:这些锁**在驱动中其实用的不多,更多的是在 Linux 内核中使用**。 ### 6.1 什么是读写自旋锁 **原书背景**:现在有个学生信息表,此表存放着学生的年龄、家庭住址、班级等信息,此表可以随时被修改和读取。此表肯定是数据,那么必须要对其进行保护。如果我们现在使用自旋锁对其进行保护,每次只能一个读操作或者写操作。但是,实际上此表是可以**并发读取**的,只需要保证在修改此表的时候没人读取,或者在其他人读取此表的时候没有人修改此表就行了。也就是此表的**读和写不能同时进行,但是可以多人并发的读取此表**。像这样,当某个数据结构符合**读/写**或**生产者/消费者**模型的时候就可以使用读写自旋锁。 **核心规则**: - **一次只能允许一个写操作**:也就是只能一个线程持有写锁,而且不能进行读操作 - **没有写操作时允许一个或多个线程持有读锁**:可以进行并发的读操作 ```mermaid flowchart TD A[读写锁状态] --> B{请求类型} B -->|读锁| C{当前有写者?} B -->|写锁| D{当前有任何持有者?} C -->|无写者| E[✅ 获取成功
可多个读者共存] C -->|有写者| F[⏳ 自旋等待] D -->|无| G[✅ 获取成功
排他] D -->|有读者或写者| H[⏳ 自旋等待] style E fill:#dcfce7,stroke:#16a34a style G fill:#dcfce7,stroke:#16a34a style F fill:#fef3c7,stroke:#f59e0b style H fill:#fef3c7,stroke:#f59e0b ``` > 💡 **一句话总结**:读写锁 = "允许 N 个读者 + 1 个写者,读者和写者互斥"。 ### 6.2 读写锁类型定义 Linux 内核使用 `rwlock_t` 结构体表示读写锁,结构体定义如下(原书示例**删除了条件编译**): ```c /* 示例代码 47.3.3.1 rwlock_t 结构体 */ typedef struct { arch_rwlock_t raw_lock; } rwlock_t; ``` 完整的内核定义(`include/linux/rwlock_types.h`)在开启调试选项时会附加调试字段: ```c typedef struct { arch_rwlock_t raw_lock; #ifdef CONFIG_DEBUG_SPINLOCK unsigned int magic, owner_cpu; void *owner; #endif #ifdef CONFIG_DEBUG_LOCK_ALLOC struct lockdep_map dep_map; #endif } rwlock_t; ``` `rwlock_t` 的本质与 `spinlock_t` 相同,都是基于架构相关的原子指令实现(ARM 上使用 `LDREX/STREX`),区别在于内部额外维护了读者计数和写者标志。 ### 6.3 读写锁 API ```c #include /* 初始化方式一:动态初始化 */ rwlock_t my_rwlock; rwlock_init(&my_rwlock); /* 初始化方式二:静态初始化 */ DEFINE_RWLOCK(my_rwlock); ``` **读锁 API**(共享)——原书表 47.3.3.1: | 函数 | 描述 | | ------------------------------------------------------------------ | ------------------------------------------------------ | | `void read_lock(rwlock_t *lock)` | 获取读锁 | | `void read_unlock(rwlock_t *lock)` | 释放读锁 | | `void read_lock_irq(rwlock_t *lock)` | 禁止本地中断,并且获取读锁 | | `void read_unlock_irq(rwlock_t *lock)` | 打开本地中断,并且释放读锁 | | `void read_lock_irqsave(rwlock_t *lock, unsigned long flags)` | 保存中断状态,禁止本地中断,并获取读锁 | | `void read_unlock_irqrestore(rwlock_t *lock, unsigned long flags)` | 将中断状态恢复到以前的状态,并且激活本地中断,释放读锁 | | `void read_lock_bh(rwlock_t *lock)` | 关闭下半部,并获取读锁 | | `void read_unlock_bh(rwlock_t *lock)` | 打开下半部,并释放读锁 | **写锁 API**(排他)——原书表 47.3.3.1: | 函数 | 描述 | | ------------------------------------------------------------------- | ------------------------------------------------------ | | `void write_lock(rwlock_t *lock)` | 获取写锁 | | `void write_unlock(rwlock_t *lock)` | 释放写锁 | | `void write_lock_irq(rwlock_t *lock)` | 禁止本地中断,并且获取写锁 | | `void write_unlock_irq(rwlock_t *lock)` | 打开本地中断,并且释放写锁 | | `void write_lock_irqsave(rwlock_t *lock, unsigned long flags)` | 保存中断状态,禁止本地中断,并获取写锁 | | `void write_unlock_irqrestore(rwlock_t *lock, unsigned long flags)` | 将中断状态恢复到以前的状态,并且激活本地中断,释放写锁 | | `void write_lock_bh(rwlock_t *lock)` | 关闭下半部,并获取写锁 | | `void write_unlock_bh(rwlock_t *lock)` | 打开下半部,并释放写锁 | > 💡 **补充**:内核另外还提供 `read_trylock()` 和 `write_trylock()`,尝试获取锁,成功返回非 0、失败返回 0 且不等待——原书表格未列出。 > > ⚠️ **命名规则与自旋锁一致**:`irq` 版本禁止本地中断,`irqsave` 版本保存并恢复中断状态,`bh` 版本关闭/打开下半部(软中断)。 ### 6.4 读写锁使用示例 以"一个全局配置表"为例,配置项读多写少: ```c #include #include /* 共享的配置数据 */ struct device_config { int sample_rate; int gain; int channel; }; static struct device_config g_config; static rwlock_t config_lock; /* 读写锁 */ /* 读操作:多个进程可并发进入 */ static int get_sample_rate(void) { unsigned long flags; int rate; read_lock_irqsave(&config_lock, flags); rate = g_config.sample_rate; /* 只读,不修改 */ read_unlock_irqrestore(&config_lock, flags); return rate; } /* 写操作:排他,与所有读写者互斥 */ static void set_sample_rate(int rate) { unsigned long flags; write_lock_irqsave(&config_lock, flags); g_config.sample_rate = rate; /* 修改共享数据 */ write_unlock_irqrestore(&config_lock, flags); } static int __init rwlock_demo_init(void) { rwlock_init(&config_lock); g_config.sample_rate = 8000; g_config.gain = 10; g_config.channel = 1; pr_info("read rate = %d\n", get_sample_rate()); set_sample_rate(16000); pr_info("read rate = %d\n", get_sample_rate()); return 0; } static void __exit rwlock_demo_exit(void) { pr_info("rwlock demo exit\n"); } module_init(rwlock_demo_init); module_exit(rwlock_demo_exit); MODULE_LICENSE("GPL"); ``` ### 6.5 读写锁 vs 自旋锁对比 | 特性 | 读写锁(rwlock_t) | 自旋锁(spinlock_t) | | -------------- | ----------------------------- | -------------------- | | **并发性** | 读者可并发 | 完全互斥 | | **实现开销** | 稍大(维护读者计数) | 更小 | | **写者开销** | 稍大 | 小 | | **中断上下文** | ✅ 可用 | ✅ 可用 | | **可睡眠** | ❌ 不可以 | ❌ 不可以 | | **适用场景** | 读多写少(读:写 > 10:1) | 读写均衡或写为主 | | **饥饿风险** | ⚠️ 读者持续涌入时写者可能饥饿 | 无 | | **优先级继承** | ❌ 不支持 | ❌ 不支持 | ### 6.6 读写锁注意事项 > ⚠️ **避坑指南**: > > 1. **不要试图升级锁**:持有读锁时调用 `write_lock()` 会**死锁**(自己等自己释放读锁)。 > 2. **不要试图降级锁**:持有写锁时调用 `read_lock()` 同样**死锁**。需要降级时必须先 `write_unlock()` 再 `read_lock()`,但中间存在"空窗期",另一写者可能插入。 > 3. **写者饥饿**:Linux 的 rwlock 实现中,新来的读者可以"插队"到等待的写者之前,读压力极大时写者可能长时间得不到锁。若写者不能忍受饥饿,应改用**顺序锁**或 **RCU**。 > 4. **临界区必须短**:读写锁本质仍是自旋锁,持锁期间**不能睡眠**,也不能调用可能睡眠的函数(如 `kmalloc(GFP_KERNEL)`、`copy_from_user()`)。 > 5. **中断场景用 irqsave**:读写锁保护的临界区若可能被中断访问,必须使用 `read_lock_irqsave` / `write_lock_irqsave`。 > 6. **优先考虑 RCU**:如果共享数据是"读多写极少"的链表或指针结构,RCU 的性能远优于读写锁。 --- ## 七、顺序锁(seqlock) ### 7.1 什么是顺序锁 **原书表述**:顺序锁在读写锁的基础上衍生而来的,使用读写锁的时候读操作和写操作不能同时进行。**使用顺序锁的话可以允许在写的时候进行读操作,也就是实现同时读写,但是不允许同时进行并发的写操作。** 虽然顺序锁的读和写操作可以同时进行,但是**如果在读的过程中发生了写操作,最好重新进行读取,保证数据完整性**。 > ⚠️ **原书重点警告**:**顺序锁保护的资源不能是指针**,因为如果在写操作的时候可能会导致指针无效,而这个时候恰巧有读操作访问指针的话就可能导致意外发生,比如**读取野指针导致系统崩溃**。 **实现机制**: 1. 有一个**序号(sequence number)**,初始为偶数 2. **写者进入**:序号 +1(变为奇数,表示"正在写")→ 修改数据 → 序号 +1(变回偶数) 3. **读者**: - 进入前读序号 `s1`,若为奇数说明正在写,等待/重试 - 读取数据 - 读完再读序号 `s2`,比较 `s1 == s2`,相等说明读取期间没有写者,数据有效;否则重读 ```mermaid sequenceDiagram participant R as 读者(无锁) participant W as 写者 participant S as 序号 R->>S: s1 = 读序号 (偶数=2) W->>S: 序号+1 (3, 奇数, 开始写) W->>W: 修改数据 R->>R: 读取数据 (可能读到半成品) W->>S: 序号+1 (4, 偶数, 写完) R->>S: s2 = 读序号 (4) Note over R: s1(2) != s2(4)
本次读取无效 → 重试 R->>S: s1 = 读序号 (4) R->>R: 读取数据 (完整) R->>S: s2 = 读序号 (4) Note over R: s1 == s2 → 数据有效 ✅ ``` > 💡 **本质**:顺序锁牺牲了读者的"确定性延迟"(可能重试),换来了读者**零加锁开销**。所以它适合"**读极多、写极少、且读操作非常快**"的场景。 ### 7.2 顺序锁类型与 API Linux 内核使用 `seqlock_t` 结构体表示顺序锁,结构体定义如下(原书示例代码 47.3.3.2): ```c /* 示例代码 47.3.3.2 seqlock_t 结构体 */ typedef struct { struct seqcount seqcount; spinlock_t lock; } seqlock_t; ``` > 💡 **结构体解读**:顺序锁内部由两部分组成——`seqcount` 是序号计数器(读者用来判断是否被写者打断),`spinlock_t lock` 是写者之间的互斥锁(保证不会并发写)。这也解释了为什么"允许写时读,但不允许并发写"。 初始化方式: ```c #include /* 初始化方式一:动态 */ seqlock_t my_seqlock; seqlock_init(&my_seqlock); /* 初始化方式二:静态 */ DEFINE_SEQLOCK(my_seqlock); ``` **顺序锁写操作 API**(原书表 47.3.3.2): | 函数 | 描述 | | --------------------------------------------------------------------- | ---------------------------------------------------------- | | `void write_seqlock(seqlock_t *sl)` | 获取写顺序锁 | | `void write_sequnlock(seqlock_t *sl)` | 释放写顺序锁 | | `void write_seqlock_irq(seqlock_t *sl)` | 禁止本地中断,并且获取写顺序锁 | | `void write_sequnlock_irq(seqlock_t *sl)` | 打开本地中断,并且释放写顺序锁 | | `void write_seqlock_irqsave(seqlock_t *sl, unsigned long flags)` | 保存中断状态,禁止本地中断,并获取写顺序锁 | | `void write_sequnlock_irqrestore(seqlock_t *sl, unsigned long flags)` | 将中断状态恢复到以前的状态,并且激活本地中断,释放写顺序锁 | | `void write_seqlock_bh(seqlock_t *sl)` | 关闭下半部,并获取写顺序锁 | | `void write_sequnlock_bh(seqlock_t *sl)` | 打开下半部,并释放写顺序锁 | **顺序锁读操作 API**(原书表 47.3.3.2): | 函数 | 描述 | | ------------------------------------------------------------- | ------------------------------------------------------------------------------ | | `unsigned read_seqbegin(const seqlock_t *sl)` | 读单元访问共享资源的时候调用此函数,此函数会返回顺序锁的顺序号 | | `unsigned read_seqretry(const seqlock_t *sl, unsigned start)` | 读结束以后调用此函数检查在读的过程中有没有对资源进行写操作,如果有的话就要重读 | ### 7.3 顺序锁使用示例 标准的"读侧重试"写法: ```c #include #include struct sensor_data { int temperature; int pressure; int humidity; }; static struct sensor_data g_sensor; static seqlock_t sensor_lock; /* 写侧:中断中更新传感器数据(写极少) */ static irqreturn_t sensor_irq_handler(int irq, void *dev_id) { unsigned long flags; write_seqlock_irqsave(&sensor_lock, flags); g_sensor.temperature = read_temp_reg(); g_sensor.pressure = read_press_reg(); g_sensor.humidity = read_humid_reg(); write_sequnlock_irqrestore(&sensor_lock, flags); return IRQ_HANDLED; } /* 读侧:用户进程高频读取(读极多),无锁 + 重试 */ static void get_sensor_data(struct sensor_data *out) { unsigned int seq; do { seq = read_seqbegin(&sensor_lock); out->temperature = g_sensor.temperature; out->pressure = g_sensor.pressure; out->humidity = g_sensor.humidity; } while (read_seqretry(&sensor_lock, seq)); /* 不一致则重读 */ } static ssize_t sensor_read(struct file *filp, char __user *buf, size_t count, loff_t *ppos) { struct sensor_data data; get_sensor_data(&data); /* 读到的一定是完整快照 */ if (copy_to_user(buf, &data, sizeof(data))) return -EFAULT; return sizeof(data); } ``` ### 7.4 顺序锁 vs 读写锁对比 | 特性 | 顺序锁(seqlock) | 读写锁(rwlock) | | ------------------ | -------------------------- | ----------------------- | | **读者加锁** | ❌ 完全不加锁 | ✅ 需要加读锁 | | **读者开销** | 极低(2 次读序号) | 中(原子操作) | | **读者可能重试** | ✅ 会被写者打断后重试 | ❌ 不会 | | **写者饥饿** | ❌ 不会(写者优先) | ⚠️ 可能 | | **可保护的数据** | 仅限**不含指针**的简单数据 | 任意数据(含链表/指针) | | **保护指针的危险** | ⚠️ 读侧可能读到野指针 | ✅ 安全 | | **典型场景** | jiffies、时间戳、统计计数 | 配置表、缓存 | | **是否可睡眠** | ❌ 写侧不可睡眠 | ❌ 不可睡眠 | > ⚠️ **关键限制**:顺序锁的读侧**不能保护指针**。因为读者在无锁读取指针时,写者可能已经释放(free)了指针指向的内存,读者解引用就是**野指针访问**。所以顺序锁只能用于"纯值类型"的数据(整数、结构体内全是整数)。 ### 7.5 顺序锁在内核中的典型应用 内核里最著名的顺序锁使用者是 `jiffies`: ```c /* 内核中的实际用法:读 jiffies_64 */ u64 get_jiffies_64(void) { unsigned int seq; u64 ret; do { seq = read_seqbegin(&jiffies_lock); ret = jiffies_64; } while (read_seqretry(&jiffies_lock, seq)); return ret; } ``` `jiffies` 被每个时钟中断更新(写少),而被大量内核代码读取(读极多),完美契合顺序锁的适用场景。 --- ## 八、RCU(扩展知识) > ⚠️ **来源说明**:本节**不属于**《I.MX6U嵌入式Linux驱动开发指南》的内容——第四十七章 47.3.3 只讲到**读写自旋锁**和**顺序锁**这两种"其他类型的锁",没有涉及 RCU 和完成量。此处作为**扩展知识**补充,便于阅读内核源码与面试准备。 ### 8.1 什么是 RCU **RCU** 是 Linux 内核中性能最高的读侧同步机制,全称 **Read-Copy Update(读-复制-更新)**。它把"读"的代价降到了接近零: > **读者:只是标记进入/退出临界区(在非抢占内核里甚至是一条编译器屏障,无原子操作)。** > **写者:不直接修改数据,而是"复制一份 → 修改副本 → 替换指针 → 等待宽限期 → 释放旧数据"。** ```mermaid flowchart TD A[写者要修改数据] --> B[1. 复制一份新数据] B --> C[2. 修改副本] C --> D[3. 原子替换指针
rcu_assign_pointer] D --> E[4. 等待宽限期
synchronize_rcu] E --> F[5. 释放旧数据] G[读者] --> H[rcu_read_lock
标记进入] H --> I[通过指针访问数据] I --> J[rcu_read_unlock
标记退出] style D fill:#dbeafe,stroke:#2563eb style E fill:#fef3c7,stroke:#f59e0b ``` **宽限期(Grace Period)** 是 RCU 的核心概念:写者替换指针后,必须等到**所有已经进入读侧临界区的读者都退出**,才能安全释放旧数据。此时保证没有任何读者还持有旧数据的引用。 ### 8.2 RCU 核心 API ```c #include /* ===== 读侧(极低开销)===== */ rcu_read_lock(); /* 标记进入读侧临界区 */ /* 访问数据(注意:rcu_read_lock 不会阻塞写者) */ rcu_read_unlock(); /* 标记退出读侧临界区 */ /* ===== 写侧 ===== */ rcu_assign_pointer(ptr, new); /* 释放语义地替换指针 */ struct foo *p = rcu_dereference(ptr); /* 读侧带屏障地解引用 */ synchronize_rcu(); /* 同步等待一个宽限期(会睡眠) */ void call_rcu(struct rcu_head *head, rcu_callback_t func); /* 异步回调 */ kfree_rcu(ptr, rcu_head_field); /* 延迟释放 */ ``` **RCU 链表 API**: | 函数 | 说明 | | -------------------------------------------- | ------------------------------------ | | `list_add_rcu(new, head)` | RCU 版链表插入 | | `list_del_rcu(entry)` | RCU 版链表删除(不释放内存,仅断链) | | `list_replace_rcu(old, new)` | RCU 版节点替换 | | `list_for_each_entry_rcu(pos, head, member)` | RCU 版遍历(需在`rcu_read_lock` 内) | ### 8.3 RCU 使用示例 一个典型的"读多写少链表"场景: ```c #include #include #include struct my_node { struct list_head list; int value; struct rcu_head rcu; /* RCU 延迟释放用 */ }; static LIST_HEAD(g_node_list); static DEFINE_MUTEX(g_write_lock); /* 写者之间还需互斥 */ /* 读侧:无锁遍历,多个读者可并发 */ static int lookup_max_value(void) { struct my_node *node; int max = INT_MIN; rcu_read_lock(); /* 进入 */ list_for_each_entry_rcu(node, &g_node_list, list) { /* 无锁遍历 */ if (node->value > max) max = node->value; } rcu_read_unlock(); /* 退出 */ return max; } /* 写侧:复制-修改-替换 */ static void add_node(int value) { struct my_node *new_node; new_node = kmalloc(sizeof(*new_node), GFP_KERNEL); if (!new_node) return; new_node->value = value; mutex_lock(&g_write_lock); /* 写者之间互斥 */ list_add_rcu(&new_node->list, &g_node_list); mutex_unlock(&g_write_lock); } /* 写侧:删除节点 */ static void del_first_node(void) { struct my_node *node; mutex_lock(&g_write_lock); node = list_first_entry_or_null(&g_node_list, struct my_node, list); if (node) { list_del_rcu(&node->list); /* 断链,但不释放 */ kfree_rcu(node, rcu); /* 等宽限期后自动释放 */ } mutex_unlock(&g_write_lock); } ``` ### 8.4 RCU vs 读写锁 vs 顺序锁 | 特性 | RCU | 顺序锁 | 读写锁 | | -------------------- | ---------------------------------- | --------------- | -------------- | | **读者开销** | 极低(近乎零) | 极低(读序号) | 中(原子操作) | | **读者会重试** | ❌ 不会 | ✅ 会 | ❌ 不会 | | **读者是否阻塞写者** | ❌ 完全不阻塞 | ❌ 不阻塞 | ✅ 阻塞 | | **可保护指针/链表** | ✅ 是主要用途 | ❌ 不可 | ✅ 可 | | **写者开销** | 高(需等宽限期) | 低 | 中 | | **写者语义** | 必须"复制-替换" | 原地修改 | 原地修改 | | **内存开销** | 高(新旧副本共存一段时间) | 无额外 | 无额外 | | **典型场景** | 网络路由表、文件描述符表、模块链表 | jiffies、时间戳 | 配置表、缓存 | > 💡 **一句话记忆**: > > - 读多写少 + 保护指针 → **RCU** > - 读多写少 + 纯值 + 写者不能被饿死 → **顺序锁** > - 读写均衡 → **读写锁** 或 **自旋锁** > - 临界区长且可睡眠 → **互斥体** ### 8.5 RCU 注意事项 > ⚠️ **避坑指南**: > > 1. **`rcu_read_lock()` 不会睡眠也不会阻塞**:它只是标记,所以读侧临界区**不能睡眠**(不能调用 `copy_to_user()`、`kmalloc(GFP_KERNEL)` 等可能睡眠的函数)。 > 2. **`synchronize_rcu()` 会睡眠**:不能在中断上下文、原子上下文或持有自旋锁时调用。 > 3. **写者必须"复制-替换"**:绝不能在读侧可能引用它时原地修改数据,否则读者会读到不一致的中间状态。 > 4. **写者之间仍需互斥**:RCU 只解决"读写"冲突,多个写者同时操作同一链表仍需额外的锁(如互斥体)保护。 > 5. **读侧指针必须用 `rcu_dereference()` 解引用**:否则编译器和 CPU 可能重排指令,导致读到未初始化的数据。 > 6. **RCU 不保证实时性**:宽限期可能持续较长时间,写者释放旧数据的时机不确定。 --- ## 九、完成量(扩展知识) > ⚠️ **来源说明**:同第八节,本节也**不属于**正点原子教材内容,作为扩展知识补充。 ### 9.1 什么是完成量 **完成量(completion)** 用于解决一种特定的同步需求:**一个执行单元等待另一个执行单元完成某件事**。 它和信号量的区别在于: - **信号量**可以用于"资源计数"(初值 > 1),也可以用于"互斥"(初值 = 1) - **完成量**专门用于"等待某事件发生",是最轻量的"等待-唤醒"原语,语义更清晰 典型场景: - 驱动初始化时等待硬件就绪(中断到来后 `complete()`) - 一个线程等待另一个线程完成 DMA 传输 - 等待内核线程退出 > 💡 **生活类比**:信号量像"停车场车位"(可以有很多个,谁先来谁停),完成量像"等人到齐"(一个人等另一个人做完某事,只关心"完成了没有")。 ### 9.2 完成量 API ```c #include /* 初始化方式一:动态 */ struct completion my_completion; init_completion(&my_completion); /* 初始化方式二:静态 */ DECLARE_COMPLETION(my_completion); DECLARE_COMPLETION_ONSTACK(my_completion); /* 栈上定义,用于局部等待 */ ``` | 函数 | 说明 | | ---------------------------------------------------------------------------------------- | -------------------------------------- | | `void init_completion(struct completion *x)` | 初始化完成量(计数=0) | | `void reinit_completion(struct completion *x)` | 重置完成量(允许重复使用) | | `void wait_for_completion(struct completion *x)` | 等待完成,**不可被信号打断** | | `int wait_for_completion_interruptible(struct completion *x)` | 可被信号打断,被打断返回`-ERESTARTSYS` | | `int wait_for_completion_killable(struct completion *x)` | 可被致命信号打断 | | `unsigned long wait_for_completion_timeout(struct completion *x, unsigned long timeout)` | 带超时等待,超时返回 0 | | `void complete(struct completion *x)` | 唤醒**一个**等待者(计数+1) | | `void complete_all(struct completion *x)` | 唤醒**所有**等待者 | ### 9.3 完成量使用示例 **场景**:等待一次中断到来(典型的"硬件就绪"同步): ```c #include #include #include static struct completion irq_done; static int irq_number; static irqreturn_t my_irq_handler(int irq, void *dev_id) { pr_info("IRQ %d triggered\n", irq); complete(&irq_done); /* 通知等待者:事件已发生 */ return IRQ_HANDLED; } static int __init completion_demo_init(void) { int ret; init_completion(&irq_done); irq_number = gpio_to_irq(/* ... */); ret = request_irq(irq_number, my_irq_handler, IRQF_TRIGGER_FALLING, "my_irq", NULL); if (ret) return ret; pr_info("waiting for interrupt...\n"); /* 等待中断到来,最长等 1 秒(1000 * 10ms) */ if (!wait_for_completion_timeout(&irq_done, msecs_to_jiffies(1000))) { pr_err("timeout: IRQ not received\n"); free_irq(irq_number, NULL); return -ETIMEDOUT; } pr_info("interrupt received, continue init\n"); free_irq(irq_number, NULL); return 0; } ``` ### 9.4 完成量 vs 信号量对比 | 特性 | 完成量(completion) | 信号量(semaphore) | | ------------------ | -------------------------- | ---------------------------- | | **语义** | "等待事件完成" | "资源计数 / 互斥" | | **初始值** | 固定为 0 | 可任意(`sema_init(&s, n)`) | | **主要用途** | 一对一/一对多事件通知 | 资源池、互斥 | | **能否重复使用** | 需`reinit_completion()` | 天然支持 | | **是否可睡眠** | ✅ 可以 | ✅ 可以 | | **中断上下文使用** | ✅ 只能`complete()` | ✅ 只能`up()` | | **代码可读性** | 语义更明确 | 需要看初值才知道意图 | | **典型场景** | 等待硬件就绪、等待线程退出 | 限制并发数、简单互斥 | > 💡 **选择建议**:如果只是想"等某件事做完",用**完成量**比信号量语义更清晰、更不容易出错;如果需要"限制最多 N 个并发"或"互斥",用信号量或互斥体。 --- ## 十、完整源码分析 ### 10.1 原子操作实验 - atomic.c ```c #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define GPIOLED_CNT 1 /* 设备号个数 */ #define GPIOLED_NAME "gpioled" /* 名字 */ #define LEDOFF 0 /* 关灯 */ #define LEDON 1 /* 开灯 */ /* gpioled设备结构体 */ struct gpioled_dev { dev_t devid; /* 设备号 */ struct cdev cdev; /* cdev */ struct class *class; /* 类 */ struct device *device; /* 设备 */ int major; /* 主设备号 */ int minor; /* 次设备号 */ struct device_node *nd; /* 设备节点 */ int led_gpio; /* led所使用的GPIO编号 */ atomic_t lock; /* 原子变量 */ }; struct gpioled_dev gpioled; /* led设备 */ /* * @description : 打开设备 * @param - inode : 传递给驱动的inode * @param - filp : 设备文件,file结构体有个叫做private_data的成员变量 * 一般在open的时候将private_data指向设备结构体 * @return : 0 成功; 其他 失败 */ static int led_open(struct inode *inode, struct file *filp) { /* 通过判断原子变量的值来检查LED有没有被别的应用使用 */ if (!atomic_dec_and_test(&gpioled.lock)) { atomic_inc(&gpioled.lock); /* 小于0的话就加1,使其原子变量等于0 */ return -EBUSY; /* LED被使用,返回忙 */ } filp->private_data = &gpioled; /* 设置私有数据 */ return 0; } /* * @description : 向设备写数据 * @param - filp : 设备文件,表示打开的文件描述符 * @param - buf : 要写给设备写入的数据 * @param - cnt : 要写入的数据长度 * @param - offt : 相对于文件首地址的偏移 * @return : 写入的字节数,如果为负值,表示写入失败 */ static ssize_t led_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt) { int retvalue; unsigned char databuf[1]; unsigned char ledstat; struct gpioled_dev *dev = filp->private_data; retvalue = copy_from_user(databuf, buf, cnt); if (retvalue < 0) { printk("kernel write failed!\r\n"); return -EFAULT; } ledstat = databuf[0]; /* 获取状态值 */ if (ledstat == LEDON) { gpio_set_value(dev->led_gpio, 0); /* 打开LED灯 */ } else if (ledstat == LEDOFF) { gpio_set_value(dev->led_gpio, 1); /* 关闭LED灯 */ } return 0; } /* * @description : 关闭/释放设备 * @param - filp : 要关闭的设备文件(文件描述符) * @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; } /* 设备操作函数 */ static struct file_operations gpioled_fops = { .owner = THIS_MODULE, .open = led_open, .read = led_read, .write = led_write, .release = led_release, }; /* * @description : 驱动入口函数 * @return : 无 */ static int __init led_init(void) { int ret = 0; /* 初始化原子变量 */ atomic_set(&gpioled.lock, 1); /* 原子变量初始值为1 */ /* 设置LED所使用的GPIO */ /* 1、获取设备节点:gpioled */ gpioled.nd = of_find_node_by_path("/gpioled"); if (gpioled.nd == NULL) { printk("gpioled node not find!\r\n"); return -EINVAL; } else { printk("gpioled node find!\r\n"); } /* 2、获取设备树中的gpio属性,得到LED所使用的LED编号 */ gpioled.led_gpio = of_get_named_gpio(gpioled.nd, "led-gpio", 0); if (gpioled.led_gpio < 0) { printk("can't get led-gpio"); return -EINVAL; } printk("led-gpio num = %d\r\n", gpioled.led_gpio); /* 3、设置GPIO1_IO03为输出,并且输出高电平,默认关闭LED灯 */ ret = gpio_direction_output(gpioled.led_gpio, 1); if (ret < 0) { printk("can't set gpio!\r\n"); } /* 注册字符设备驱动 */ /* 1、创建设备号 */ if (gpioled.major) { gpioled.devid = MKDEV(gpioled.major, 0); register_chrdev_region(gpioled.devid, GPIOLED_CNT, GPIOLED_NAME); } else { alloc_chrdev_region(&gpioled.devid, 0, GPIOLED_CNT, GPIOLED_NAME); gpioled.major = MAJOR(gpioled.devid); gpioled.minor = MINOR(gpioled.devid); } printk("gpioled major=%d,minor=%d\r\n", gpioled.major, gpioled.minor); /* 2、初始化cdev */ gpioled.cdev.owner = THIS_MODULE; cdev_init(&gpioled.cdev, &gpioled_fops); /* 3、添加一个cdev */ cdev_add(&gpioled.cdev, gpioled.devid, GPIOLED_CNT); /* 4、创建类 */ gpioled.class = class_create(THIS_MODULE, GPIOLED_NAME); if (IS_ERR(gpioled.class)) { return PTR_ERR(gpioled.class); } /* 5、创建设备 */ gpioled.device = device_create(gpioled.class, NULL, gpioled.devid, NULL, GPIOLED_NAME); if (IS_ERR(gpioled.device)) { return PTR_ERR(gpioled.device); } return 0; } /* * @description : 驱动出口函数 * @return : 无 */ static void __exit led_exit(void) { /* 注销字符设备驱动 */ cdev_del(&gpioled.cdev); unregister_chrdev_region(gpioled.devid, GPIOLED_CNT); device_destroy(gpioled.class, gpioled.devid); class_destroy(gpioled.class); } module_init(led_init); module_exit(led_exit); MODULE_LICENSE("GPL"); MODULE_AUTHOR("zuozhongkai"); ``` ### 10.2 代码逐行解析 | 行号 | 代码 | 说明 | | ------------------------------------ | ---------------------- | -------------------- | | `atomic_t lock` | 设备结构体中的原子变量 | 用于实现设备互斥访问 | | `atomic_set(&gpioled.lock, 1)` | 初始化原子变量为1 | 表示设备可用 | | `atomic_dec_and_test(&gpioled.lock)` | 原子减1并测试是否为0 | 如果为0表示获取成功 | | `atomic_inc(&gpioled.lock)` | 原子加1 | 释放设备 | ### 10.3 自旋锁实验 - spinlock.c ```c /* 使用自旋锁保护的设备结构体 */ struct gpioled_dev { dev_t devid; struct cdev cdev; struct class *class; struct device *device; int major; int minor; struct device_node *nd; int led_gpio; spinlock_t lock; /* 自旋锁 */ int dev_stats; /* 设备使用统计 */ }; static int led_open(struct inode *inode, struct file *filp) { unsigned long flags; struct gpioled_dev *dev = filp->private_data; spin_lock_irqsave(&dev->lock, flags); /* 获取自旋锁 */ if (dev->dev_stats != 0) { spin_unlock_irqrestore(&dev->lock, flags); return -EBUSY; } dev->dev_stats++; /* 标记设备被使用 */ spin_unlock_irqrestore(&dev->lock, flags); return 0; } static int led_release(struct inode *inode, struct file *filp) { unsigned long flags; struct gpioled_dev *dev = filp->private_data; spin_lock_irqsave(&dev->lock, flags); /* 获取自旋锁 */ dev->dev_stats--; /* 释放设备 */ spin_unlock_irqrestore(&dev->lock, flags); return 0; } ``` ### 10.4 信号量实验 - semaphore.c ```c struct gpioled_dev { dev_t devid; struct cdev cdev; struct class *class; struct device *device; int major; int minor; struct device_node *nd; int led_gpio; struct semaphore sem; /* 信号量 */ }; static int led_open(struct inode *inode, struct file *filp) { struct gpioled_dev *dev = filp->private_data; down(&dev->sem); /* 获取信号量,可能睡眠 */ return 0; } static int led_release(struct inode *inode, struct file *filp) { struct gpioled_dev *dev = filp->private_data; up(&dev->sem); /* 释放信号量 */ return 0; } ``` ### 10.5 互斥体实验 - mutex.c ```c struct gpioled_dev { dev_t devid; struct cdev cdev; struct class *class; struct device *device; int major; int minor; struct device_node *nd; int led_gpio; struct mutex lock; /* 互斥体 */ }; static int led_open(struct inode *inode, struct file *filp) { struct gpioled_dev *dev = filp->private_data; mutex_lock(&dev->lock); /* 获取互斥锁 */ return 0; } static int led_release(struct inode *inode, struct file *filp) { struct gpioled_dev *dev = filp->private_data; mutex_unlock(&dev->lock); /* 释放互斥锁 */ return 0; } ``` --- ## 十一、实验验证 ### 11.1 编译方法 ```bash # 1. 编译驱动模块 # 修改Makefile中obj-m变量 obj-m := atomic.o # 或 obj-m := spinlock.o # 编译 make -j32 # 2. 编译测试APP arm-linux-gnueabihf-gcc atomicApp.c -o atomicApp ``` ### 11.2 测试步骤 ```bash # 1. 加载驱动模块 depmod # 首次加载需要 modprobe atomic.ko # 加载驱动 # 2. 运行测试程序(后台运行) ./atomicApp /dev/gpioled 1 & # 打开LED # 3. 尝试第二个程序访问 ./atomicApp /dev/gpioled 1 # 应该失败,返回EBUSY # 4. 卸载驱动 rmmod atomic.ko ``` ### 11.3 结果分析 | 测试场景 | 预期结果 | 实际结果 | | ---------------- | ------------- | -------- | | 首次打开设备 | 成功 | ✅ 成功 | | 25秒内再次打开 | 失败(EBUSY) | ✅ 失败 | | 25秒后再次打开 | 成功 | ✅ 成功 | | 关闭设备后再打开 | 成功 | ✅ 成功 | ```mermaid gantt title 设备访问时序测试 dateFormat X axisFormat %s section 进程A 打开设备 :a1, 0, 5 占用设备 :a2, 5, 25 关闭设备 :a3, 25, 30 section 进程B 尝试打开 :b1, 10, 11 失败返回 :b2, 11, 11 等待 :b3, 11, 30 打开成功 :b4, 30, 35 ``` --- ## 十二、跨平台对比 ### 12.1 IMX6ULL vs STM32 vs RK3568并发机制差异 | 特性 | IMX6ULL | STM32 | RK3568 | | -------------------- | ------------------ | ------------ | ------------------------- | | **CPU架构** | Cortex-A7 单核 | Cortex-M4/M7 | Cortex-A55 四核 | | **并发模型** | 单核+中断 | 单核+中断 | 多核SMP+中断 | | **自旋锁** | ✅ 支持 | ❌ 不需要 | ✅ 核间同步 | | **原子操作** | ✅ 支持 | ⚠️ 部分支持 | ✅ 支持 | | **互斥体** | ✅ 支持 | ❌ 不支持 | ✅ 支持 | | **信号量** | ✅ 支持 | ✅ 支持 | ✅ 支持 | | **读写锁** | ✅ 支持 | ❌ 不需要 | ✅ 支持 | | **顺序锁** | ✅ 支持 | ❌ 不需要 | ✅ 支持 | | **RCU** | ✅ 支持 | ❌ 不需要 | ✅ 支持(重点) | | **中断优先级** | 32级 | 16级 | 32级 | | **典型锁选择** | spinlock_irqsave | 临界区 | mutex/spinlock | | **其他类型锁的使用** | 内核为主,驱动少用 | 无对应机制 | RCU 广泛用于网络/文件系统 | ### 12.2 多核 vs 单核的锁选择 ```mermaid flowchart TD A{是否为SMP多核系统?} -->|是| B{临界区是否可能被中断访问?} A -->|否| C{临界区是否可能被中断访问?} B -->|是| D[spin_lock_irqsave] B -->|否| E[spin_lock] C -->|是| F[local_irq_disable + spin_lock] C -->|否| G{临界区长度} G -->|短| H[自旋锁/原子操作] G -->|长| I[信号量/互斥体] style D fill:#dbeafe,stroke:#2563eb style E fill:#dbeafe,stroke:#2563eb style F fill:#fef3c7,stroke:#f59e0b style H fill:#dcfce7,stroke:#16a34a style I fill:#dcfce7,stroke:#16a34a ``` --- ## 十三、面试精选 ### 题目1:自旋锁和信号量有什么区别? **考察点**:锁机制理解 **参考答案**: | 特性 | 自旋锁 | 信号量 | | ---------- | ------------------ | ---------------- | | 等待方式 | 忙等待(占用CPU) | 睡眠(释放CPU) | | 可睡眠 | ❌ 不可以 | ✅ 可以 | | 中断上下文 | ✅ 可以使用 | ❌ 不可以 | | 临界区长度 | 短(μs级) | 长(ms级) | | CPU占用 | 高(忙等) | 低(上下文切换) | | 适用场景 | 中断处理、短临界区 | 长时间操作、I/O | ### 题目2:为什么要在中断处理函数中使用spin_lock_irqsave? **考察点**:中断安全 **参考答案**: 1. 中断可能打断持有锁的进程 2. 如果中断也尝试获取同一个锁,会导致**死锁** 3. `spin_lock_irqsave`会禁止本地中断,避免中断与进程竞争同一把锁 4. 使用`irqsave`版本是因为它可以保存并恢复中断状态,比`spin_lock_irq`更安全 ### 题目3:原子操作适用于什么场景? **考察点**:原子操作理解 **参考答案**: - **简单变量操作**:整数的加减、位操作 - **状态标志**:设备占用标志、错误标志 - **引用计数**:模块引用计数、文件引用计数 - **不适合复杂操作**:涉及多个变量的操作不能用原子操作 ### 题目4:如何避免死锁? **考察点**:死锁预防 **参考答案**: 1. **锁的顺序**:多把锁按固定顺序获取 2. **超时机制**:使用`trylock`避免永久等待 3. **锁粒度**:锁的范围尽可能小 4. **避免嵌套**:尽量减少锁的嵌套使用 5. **中断安全**:在可能被中断访问的临界区使用`spin_lock_irqsave` ### 题目5:写一个简单的设备互斥访问示例 **考察点**:综合应用 **参考答案**: ```c struct my_device { struct mutex lock; int in_use; }; static int device_open(struct inode *inode, struct file *filp) { struct my_device *dev = &my_dev; mutex_lock(&dev->lock); if (dev->in_use) { mutex_unlock(&dev->lock); return -EBUSY; } dev->in_use = 1; mutex_unlock(&dev->lock); return 0; } static int device_release(struct inode *inode, struct file *filp) { struct my_device *dev = &my_dev; mutex_lock(&dev->lock); dev->in_use = 0; mutex_unlock(&dev->lock); return 0; } ``` ### 题目6:读写锁和自旋锁有什么区别?什么场景下用读写锁? **考察点**:读写锁理解(教材 47.3.3) **参考答案**: | 特性 | 读写锁(rwlock_t) | 自旋锁(spinlock_t) | | ------------ | ------------------------ | -------------------- | | 读操作并发 | ✅ 允许多个读者同时持有 | ❌ 完全互斥 | | 写操作 | 排他,与所有读写者互斥 | 排他 | | 读写能否同时 | ❌ 不能 | ❌ 不能 | | 实现开销 | 稍大(维护读者计数) | 更小 | | 适用场景 | 读多写少,且读操作可并发 | 读写均衡、临界区极短 | **使用场景**:当数据结构符合**读/写**或**生产者/消费者**模型时使用,例如学生信息表、设备配置表这类"读远多于写"的数据。 **追问**: - 追问1:读写锁能否把写锁升级为读锁(降级)?→ **不能**,持有写锁时再获取读锁会死锁(自己等自己)。必须先 `write_unlock()` 再 `read_lock()`,但中间存在空窗期。 - 追问2:读写锁会导致写者饥饿吗?→ **会**。Linux 的实现中读者可以插队到等待的写者之前,读压力极大时写者可能长时间得不到锁。 ### 题目7:顺序锁的原理是什么?它为什么不能保护指针? **考察点**:顺序锁理解(教材 47.3.3) **参考答案**: 顺序锁**允许写的时候同时读**,但不允许并发写。读者不加锁,而是靠序号判断: 1. 读之前读序号 `s1` 2. 读取数据 3. 读之后再读序号 `s2` 4. 若 `s1 == s2` 说明读取期间没有写者,数据有效;否则**重读** 对应 API:`read_seqbegin()` / `read_seqretry()`,写侧用 `write_seqlock()` / `write_sequnlock()`。 **为什么不能保护指针**:原书明确指出——如果在写操作的时候可能会导致指针无效(例如写者把指针指向的旧内存 `kfree` 掉了),而此时恰巧有读操作访问该指针,就会**读取野指针导致系统崩溃**。所以顺序锁只能保护**纯值类型数据**(如 `jiffies`),不能保护含指针的结构。 **追问**: - 追问1:顺序锁和读写锁相比,谁的性能更好?→ 读者侧顺序锁更好(无加锁开销),但可能重试;写者侧顺序锁不会饥饿。 - 追问2:内核中顺序锁的典型使用者?→ `jiffies_64`(时钟中断频繁写、大量代码频繁读)。 ### 题目8:RCU 与读写锁的区别?(扩展) **考察点**:RCU 理解(超出教材范围) **参考答案**: | 特性 | RCU | 读写锁 | | --------------- | ----------------------- | -------------- | | 读者开销 | 近乎零(只标记) | 中(原子操作) | | 读者会重试 | ❌ 不会 | ❌ 不会 | | 读者阻塞写者 | ❌ 完全不阻塞 | ✅ 阻塞 | | 可保护指针/链表 | ✅ 主要用途 | ✅ 可 | | 写者语义 | 复制-替换-等宽限期-释放 | 原地加锁修改 | | 内存开销 | 高(新旧副本共存) | 无额外 | **核心区别**:RCU 的写者不是"原地加锁修改",而是"复制一份 → 改副本 → 原子替换指针 → 等宽限期 → 释放旧数据"。宽限期保证所有已进入读侧临界区的读者都退出后,旧数据才被释放。 **追问**: - 追问1:`rcu_read_lock()` 会阻塞写者吗?→ 不会,它只是标记进入临界区,所以读侧临界区**不能睡眠**。 - 追问2:`synchronize_rcu()` 能在中断里调用吗?→ 不能,它会睡眠。 --- **内容来源**: 《I.MX6U嵌入式Linux驱动开发指南》第四十七章 Linux并发与竞争(含 47.3.3 其他类型的锁)+ 第四十八章 并发与竞争实验;例程 07_atomic, 08_spinlock, 09_semaphore, 10_mutex **扩展内容**: 第八、九节(RCU、完成量)超出教材范围,为补充知识 **最后更新**: 2026-09-17