title: 并发同步与原子操作 tags: [Linux驱动, 并发, 锁, 原子操作, 嵌入式, 自旋锁, 信号量, 互斥体, 读写锁, 顺序锁, RCU] created: 2026-09-16 updated: 2026-09-17 pdf_ref:
💡 关联知识: [[FreeRTOS学习笔记/07-中断管理与临界段]] | [[03-Linux驱动开发核心/05-中断下半部处理]] | [[03-Linux驱动开发核心/07-platform总线模型]]
竞态条件(Race Condition) 是指多个执行单元(线程、中断、进程)同时访问共享资源,且最终结果依赖于执行时序的现象。
💡 生活类比:公司只有一台打印机,小明和小红同时发送打印任务。如果小明的文档先发送但打印较慢,小红的文档后发送但打印较快,最终小明可能拿到小红的文档——这就是竞态条件。
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的结果)
| 场景 | 说明 | 危害程度 |
|---|---|---|
| 中断与进程 | 进程访问共享数据时被中断,中断处理函数也访问同一数据 | ⭐⭐⭐⭐ |
| 多进程/多线程 | 多个进程或线程同时读写共享内存 | ⭐⭐⭐ |
| 多CPU(SMP) | 多核处理器上多个CPU核心同时执行访问同一数据 | ⭐⭐⭐⭐⭐ |
| 抢占调度 | 进程在内核态执行时被高优先级任务抢占 | ⭐⭐⭐ |
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
Linux是多任务操作系统,以下因素导致并发不可避免:
⚠️ 注意:编写驱动程序时必须考虑并发保护,否则会导致难以定位的bug。
Linux内核提供的同步机制从"轻"到"重"排列如下,选择哪一种是驱动开发中最基础的判断:
| 机制 | 典型类型 | 是否可睡眠 | 中断上下文 | 适用场景 |
|---|---|---|---|---|
| 原子操作 | atomic_t |
否 | 可用 | 单个整数计数、状态标志 |
| 自旋锁 | spinlock_t |
否 | 可用 | 极短临界区、中断上下文 |
| 读写锁 | rwlock_t |
否 | 可用 | 读多写少、临界区短 |
| 顺序锁 | seqlock_t |
否 | 可用(写者不可被中断) | 读多写极少、读侧不能加锁 |
| RCU | rcu_read_lock |
否 | 读侧可用 | 链表遍历为主、读多写极少 |
| 信号量 | struct semaphore |
是 | 不可用 | 长临界区、允许并发数 > 1 |
| 互斥体 | struct mutex |
是 | 不可用 | 长临界区、需要互斥且可睡眠 |
| 完成量 | struct completion |
是 | 不可用 | 一个执行单元等待另一个完成 |
💡 选择原则:能不用锁就不用锁(无锁设计优先,如 per-CPU 变量),必须加锁时按"临界区长度 + 是否可能睡眠 + 读多还是写多"三个维度决策。本文档按此顺序逐个讲解。
原子操作(Atomic Operation) 是指在执行过程中不会被中断的操作,要么完全执行,要么完全不执行。适用于简单的变量操作。
/* 原子变量定义 - include/linux/types.h */
typedef struct {
int counter;
} atomic_t;
| 函数 | 说明 |
|---|---|
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 |
#include <linux/atomic.h>
/* 定义并初始化原子变量 */
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;
}
| 函数 | 说明 |
|---|---|
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位并返回原值 |
自旋锁(Spinlock) 是一种忙等待的锁机制。当一个线程尝试获取锁时,如果锁已被持有,该线程会在原地"自旋"等待,直到锁被释放。
stateDiagram-v2
[*] --> 空闲: 初始状态
空闲 --> 已锁定: spin_lock()
已锁定 --> 空闲: spin_unlock()
已锁定 --> 已锁定: 其他CPU尝试获取锁(自旋等待)
note right of 已锁定
持有锁的CPU执行临界区代码
其他CPU在原地循环等待
end note
/* 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;
| 函数 | 说明 | 使用场景 |
|---|---|---|
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) |
检查锁状态 | 调试 |
| 函数 | 说明 |
|---|---|
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) |
释放锁并开启下半部 |
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
综合前面关于自旋锁的信息,我们需要在使用自旋锁的时候要注意以下几点(原书 47.3.4):
💡 补充:若临界区可能被中断访问,需使用
spin_lock_irqsave/spin_unlock_irqrestore保证中断安全(原书 47.3.2 自旋锁 API 中的_irqsave变体)。
信号量(Semaphore) 是一种可睡眠的同步机制。当进程获取信号量失败时会进入睡眠状态,释放CPU给其他任务使用。
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: 获取成功
#include <linux/semaphore.h>
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);
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;
}
| 特性 | 自旋锁 | 信号量 |
|---|---|---|
| 等待方式 | 忙等待(自旋) | 睡眠等待 |
| CPU占用 | 占用CPU | 不占用CPU |
| 可睡眠 | ❌ 不可以 | ✅ 可以 |
| 中断上下文 | ✅ 可以使用 | ❌ 不可以 |
| 临界区长度 | 短(μs级) | 长(ms级) |
| 性能开销 | 低(忙等) | 高(上下文切换) |
| 适用场景 | 中断处理、短临界区 | 长时间操作、I/O操作 |
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]
互斥体(Mutex) 是Linux内核中最常用的互斥锁机制。它是一种特殊的信号量,只能由一个线程持有,提供了更严格的互斥保证。
#include <linux/mutex.h>
/* 定义互斥体 */
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);
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;
}
| 特性 | 互斥体 | 自旋锁 |
|---|---|---|
| 睡眠 | ✅ 可以睡眠 | ❌ 不能睡眠 |
| 持有时长 | 可以较长 | 必须很短 |
| 中断上下文 | ❌ 不能使用 | ✅ 可以使用 |
| 优先级继承 | ✅ 支持 | ❌ 不支持 |
| 递归锁 | ❌ 不支持 | ❌ 不支持 |
| 调试支持 | ✅ 丰富 | ⚠️ 有限 |
本节内容来自《I.MX6U嵌入式Linux驱动开发指南》47.3.3 其他类型的锁。原书指出:这些锁在驱动中其实用的不多,更多的是在 Linux 内核中使用。
原书背景:现在有个学生信息表,此表存放着学生的年龄、家庭住址、班级等信息,此表可以随时被修改和读取。此表肯定是数据,那么必须要对其进行保护。如果我们现在使用自旋锁对其进行保护,每次只能一个读操作或者写操作。但是,实际上此表是可以并发读取的,只需要保证在修改此表的时候没人读取,或者在其他人读取此表的时候没有人修改此表就行了。也就是此表的读和写不能同时进行,但是可以多人并发的读取此表。像这样,当某个数据结构符合读/写或生产者/消费者模型的时候就可以使用读写自旋锁。
核心规则:
没有写操作时允许一个或多个线程持有读锁:可以进行并发的读操作
flowchart TD
A[读写锁状态] --> B{请求类型}
B -->|读锁| C{当前有写者?}
B -->|写锁| D{当前有任何持有者?}
C -->|无写者| E[✅ 获取成功<br/>可多个读者共存]
C -->|有写者| F[⏳ 自旋等待]
D -->|无| G[✅ 获取成功<br/>排他]
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 个写者,读者和写者互斥"。
Linux 内核使用 rwlock_t 结构体表示读写锁,结构体定义如下(原书示例删除了条件编译):
/* 示例代码 47.3.3.1 rwlock_t 结构体 */
typedef struct {
arch_rwlock_t raw_lock;
} rwlock_t;
完整的内核定义(include/linux/rwlock_types.h)在开启调试选项时会附加调试字段:
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),区别在于内部额外维护了读者计数和写者标志。
#include <linux/rwlock.h>
/* 初始化方式一:动态初始化 */
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版本关闭/打开下半部(软中断)。
以"一个全局配置表"为例,配置项读多写少:
#include <linux/rwlock.h>
#include <linux/module.h>
/* 共享的配置数据 */
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");
| 特性 | 读写锁(rwlock_t) | 自旋锁(spinlock_t) |
|---|---|---|
| 并发性 | 读者可并发 | 完全互斥 |
| 实现开销 | 稍大(维护读者计数) | 更小 |
| 写者开销 | 稍大 | 小 |
| 中断上下文 | ✅ 可用 | ✅ 可用 |
| 可睡眠 | ❌ 不可以 | ❌ 不可以 |
| 适用场景 | 读多写少(读:写 > 10:1) | 读写均衡或写为主 |
| 饥饿风险 | ⚠️ 读者持续涌入时写者可能饥饿 | 无 |
| 优先级继承 | ❌ 不支持 | ❌ 不支持 |
⚠️ 避坑指南:
- 不要试图升级锁:持有读锁时调用
write_lock()会死锁(自己等自己释放读锁)。- 不要试图降级锁:持有写锁时调用
read_lock()同样死锁。需要降级时必须先write_unlock()再read_lock(),但中间存在"空窗期",另一写者可能插入。- 写者饥饿:Linux 的 rwlock 实现中,新来的读者可以"插队"到等待的写者之前,读压力极大时写者可能长时间得不到锁。若写者不能忍受饥饿,应改用顺序锁或 RCU。
- 临界区必须短:读写锁本质仍是自旋锁,持锁期间不能睡眠,也不能调用可能睡眠的函数(如
kmalloc(GFP_KERNEL)、copy_from_user())。- 中断场景用 irqsave:读写锁保护的临界区若可能被中断访问,必须使用
read_lock_irqsave/write_lock_irqsave。- 优先考虑 RCU:如果共享数据是"读多写极少"的链表或指针结构,RCU 的性能远优于读写锁。
原书表述:顺序锁在读写锁的基础上衍生而来的,使用读写锁的时候读操作和写操作不能同时进行。使用顺序锁的话可以允许在写的时候进行读操作,也就是实现同时读写,但是不允许同时进行并发的写操作。
虽然顺序锁的读和写操作可以同时进行,但是如果在读的过程中发生了写操作,最好重新进行读取,保证数据完整性。
⚠️ 原书重点警告:顺序锁保护的资源不能是指针,因为如果在写操作的时候可能会导致指针无效,而这个时候恰巧有读操作访问指针的话就可能导致意外发生,比如读取野指针导致系统崩溃。
实现机制:
读者:
s1,若为奇数说明正在写,等待/重试读完再读序号 s2,比较 s1 == s2,相等说明读取期间没有写者,数据有效;否则重读
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)<br/>本次读取无效 → 重试
R->>S: s1 = 读序号 (4)
R->>R: 读取数据 (完整)
R->>S: s2 = 读序号 (4)
Note over R: s1 == s2 → 数据有效 ✅
💡 本质:顺序锁牺牲了读者的"确定性延迟"(可能重试),换来了读者零加锁开销。所以它适合"读极多、写极少、且读操作非常快"的场景。
Linux 内核使用 seqlock_t 结构体表示顺序锁,结构体定义如下(原书示例代码 47.3.3.2):
/* 示例代码 47.3.3.2 seqlock_t 结构体 */
typedef struct {
struct seqcount seqcount;
spinlock_t lock;
} seqlock_t;
💡 结构体解读:顺序锁内部由两部分组成——
seqcount是序号计数器(读者用来判断是否被写者打断),spinlock_t lock是写者之间的互斥锁(保证不会并发写)。这也解释了为什么"允许写时读,但不允许并发写"。
初始化方式:
#include <linux/seqlock.h>
/* 初始化方式一:动态 */
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) |
读结束以后调用此函数检查在读的过程中有没有对资源进行写操作,如果有的话就要重读 |
标准的"读侧重试"写法:
#include <linux/seqlock.h>
#include <linux/module.h>
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);
}
| 特性 | 顺序锁(seqlock) | 读写锁(rwlock) |
|---|---|---|
| 读者加锁 | ❌ 完全不加锁 | ✅ 需要加读锁 |
| 读者开销 | 极低(2 次读序号) | 中(原子操作) |
| 读者可能重试 | ✅ 会被写者打断后重试 | ❌ 不会 |
| 写者饥饿 | ❌ 不会(写者优先) | ⚠️ 可能 |
| 可保护的数据 | 仅限不含指针的简单数据 | 任意数据(含链表/指针) |
| 保护指针的危险 | ⚠️ 读侧可能读到野指针 | ✅ 安全 |
| 典型场景 | jiffies、时间戳、统计计数 | 配置表、缓存 |
| 是否可睡眠 | ❌ 写侧不可睡眠 | ❌ 不可睡眠 |
⚠️ 关键限制:顺序锁的读侧不能保护指针。因为读者在无锁读取指针时,写者可能已经释放(free)了指针指向的内存,读者解引用就是野指针访问。所以顺序锁只能用于"纯值类型"的数据(整数、结构体内全是整数)。
内核里最著名的顺序锁使用者是 jiffies:
/* 内核中的实际用法:读 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 被每个时钟中断更新(写少),而被大量内核代码读取(读极多),完美契合顺序锁的适用场景。
⚠️ 来源说明:本节不属于《I.MX6U嵌入式Linux驱动开发指南》的内容——第四十七章 47.3.3 只讲到读写自旋锁和顺序锁这两种"其他类型的锁",没有涉及 RCU 和完成量。此处作为扩展知识补充,便于阅读内核源码与面试准备。
RCU 是 Linux 内核中性能最高的读侧同步机制,全称 Read-Copy Update(读-复制-更新)。它把"读"的代价降到了接近零:
读者:只是标记进入/退出临界区(在非抢占内核里甚至是一条编译器屏障,无原子操作)。 写者:不直接修改数据,而是"复制一份 → 修改副本 → 替换指针 → 等待宽限期 → 释放旧数据"。
flowchart TD
A[写者要修改数据] --> B[1. 复制一份新数据]
B --> C[2. 修改副本]
C --> D[3. 原子替换指针<br/>rcu_assign_pointer]
D --> E[4. 等待宽限期<br/>synchronize_rcu]
E --> F[5. 释放旧数据]
G[读者] --> H[rcu_read_lock<br/>标记进入]
H --> I[通过指针访问数据]
I --> J[rcu_read_unlock<br/>标记退出]
style D fill:#dbeafe,stroke:#2563eb
style E fill:#fef3c7,stroke:#f59e0b
宽限期(Grace Period) 是 RCU 的核心概念:写者替换指针后,必须等到所有已经进入读侧临界区的读者都退出,才能安全释放旧数据。此时保证没有任何读者还持有旧数据的引用。
#include <linux/rcupdate.h>
/* ===== 读侧(极低开销)===== */
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 内) |
一个典型的"读多写少链表"场景:
#include <linux/rcupdate.h>
#include <linux/list.h>
#include <linux/slab.h>
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);
}
| 特性 | RCU | 顺序锁 | 读写锁 |
|---|---|---|---|
| 读者开销 | 极低(近乎零) | 极低(读序号) | 中(原子操作) |
| 读者会重试 | ❌ 不会 | ✅ 会 | ❌ 不会 |
| 读者是否阻塞写者 | ❌ 完全不阻塞 | ❌ 不阻塞 | ✅ 阻塞 |
| 可保护指针/链表 | ✅ 是主要用途 | ❌ 不可 | ✅ 可 |
| 写者开销 | 高(需等宽限期) | 低 | 中 |
| 写者语义 | 必须"复制-替换" | 原地修改 | 原地修改 |
| 内存开销 | 高(新旧副本共存一段时间) | 无额外 | 无额外 |
| 典型场景 | 网络路由表、文件描述符表、模块链表 | jiffies、时间戳 | 配置表、缓存 |
💡 一句话记忆:
- 读多写少 + 保护指针 → RCU
- 读多写少 + 纯值 + 写者不能被饿死 → 顺序锁
- 读写均衡 → 读写锁 或 自旋锁
- 临界区长且可睡眠 → 互斥体
⚠️ 避坑指南:
rcu_read_lock()不会睡眠也不会阻塞:它只是标记,所以读侧临界区不能睡眠(不能调用copy_to_user()、kmalloc(GFP_KERNEL)等可能睡眠的函数)。synchronize_rcu()会睡眠:不能在中断上下文、原子上下文或持有自旋锁时调用。- 写者必须"复制-替换":绝不能在读侧可能引用它时原地修改数据,否则读者会读到不一致的中间状态。
- 写者之间仍需互斥:RCU 只解决"读写"冲突,多个写者同时操作同一链表仍需额外的锁(如互斥体)保护。
- 读侧指针必须用
rcu_dereference()解引用:否则编译器和 CPU 可能重排指令,导致读到未初始化的数据。- RCU 不保证实时性:宽限期可能持续较长时间,写者释放旧数据的时机不确定。
⚠️ 来源说明:同第八节,本节也不属于正点原子教材内容,作为扩展知识补充。
完成量(completion) 用于解决一种特定的同步需求:一个执行单元等待另一个执行单元完成某件事。
它和信号量的区别在于:
典型场景:
complete())💡 生活类比:信号量像"停车场车位"(可以有很多个,谁先来谁停),完成量像"等人到齐"(一个人等另一个人做完某事,只关心"完成了没有")。
#include <linux/completion.h>
/* 初始化方式一:动态 */
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) |
唤醒所有等待者 |
场景:等待一次中断到来(典型的"硬件就绪"同步):
#include <linux/completion.h>
#include <linux/interrupt.h>
#include <linux/module.h>
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;
}
| 特性 | 完成量(completion) | 信号量(semaphore) |
|---|---|---|
| 语义 | "等待事件完成" | "资源计数 / 互斥" |
| 初始值 | 固定为 0 | 可任意(sema_init(&s, n)) |
| 主要用途 | 一对一/一对多事件通知 | 资源池、互斥 |
| 能否重复使用 | 需reinit_completion() |
天然支持 |
| 是否可睡眠 | ✅ 可以 | ✅ 可以 |
| 中断上下文使用 | ✅ 只能complete() |
✅ 只能up() |
| 代码可读性 | 语义更明确 | 需要看初值才知道意图 |
| 典型场景 | 等待硬件就绪、等待线程退出 | 限制并发数、简单互斥 |
💡 选择建议:如果只是想"等某件事做完",用完成量比信号量语义更清晰、更不容易出错;如果需要"限制最多 N 个并发"或"互斥",用信号量或互斥体。
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/gpio.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/of.h>
#include <linux/of_address.h>
#include <linux/of_gpio.h>
#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>
#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");
| 行号 | 代码 | 说明 |
|---|---|---|
atomic_t lock |
设备结构体中的原子变量 | 用于实现设备互斥访问 |
atomic_set(&gpioled.lock, 1) |
初始化原子变量为1 | 表示设备可用 |
atomic_dec_and_test(&gpioled.lock) |
原子减1并测试是否为0 | 如果为0表示获取成功 |
atomic_inc(&gpioled.lock) |
原子加1 | 释放设备 |
/* 使用自旋锁保护的设备结构体 */
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;
}
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;
}
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;
}
# 1. 编译驱动模块
# 修改Makefile中obj-m变量
obj-m := atomic.o
# 或
obj-m := spinlock.o
# 编译
make -j32
# 2. 编译测试APP
arm-linux-gnueabihf-gcc atomicApp.c -o atomicApp
# 1. 加载驱动模块
depmod # 首次加载需要
modprobe atomic.ko # 加载驱动
# 2. 运行测试程序(后台运行)
./atomicApp /dev/gpioled 1 & # 打开LED
# 3. 尝试第二个程序访问
./atomicApp /dev/gpioled 1 # 应该失败,返回EBUSY
# 4. 卸载驱动
rmmod atomic.ko
| 测试场景 | 预期结果 | 实际结果 |
|---|---|---|
| 首次打开设备 | 成功 | ✅ 成功 |
| 25秒内再次打开 | 失败(EBUSY) | ✅ 失败 |
| 25秒后再次打开 | 成功 | ✅ 成功 |
| 关闭设备后再打开 | 成功 | ✅ 成功 |
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
| 特性 | IMX6ULL | STM32 | RK3568 |
|---|---|---|---|
| CPU架构 | Cortex-A7 单核 | Cortex-M4/M7 | Cortex-A55 四核 |
| 并发模型 | 单核+中断 | 单核+中断 | 多核SMP+中断 |
| 自旋锁 | ✅ 支持 | ❌ 不需要 | ✅ 核间同步 |
| 原子操作 | ✅ 支持 | ⚠️ 部分支持 | ✅ 支持 |
| 互斥体 | ✅ 支持 | ❌ 不支持 | ✅ 支持 |
| 信号量 | ✅ 支持 | ✅ 支持 | ✅ 支持 |
| 读写锁 | ✅ 支持 | ❌ 不需要 | ✅ 支持 |
| 顺序锁 | ✅ 支持 | ❌ 不需要 | ✅ 支持 |
| RCU | ✅ 支持 | ❌ 不需要 | ✅ 支持(重点) |
| 中断优先级 | 32级 | 16级 | 32级 |
| 典型锁选择 | spinlock_irqsave | 临界区 | mutex/spinlock |
| 其他类型锁的使用 | 内核为主,驱动少用 | 无对应机制 | RCU 广泛用于网络/文件系统 |
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
考察点:锁机制理解
参考答案:
| 特性 | 自旋锁 | 信号量 |
|---|---|---|
| 等待方式 | 忙等待(占用CPU) | 睡眠(释放CPU) |
| 可睡眠 | ❌ 不可以 | ✅ 可以 |
| 中断上下文 | ✅ 可以使用 | ❌ 不可以 |
| 临界区长度 | 短(μs级) | 长(ms级) |
| CPU占用 | 高(忙等) | 低(上下文切换) |
| 适用场景 | 中断处理、短临界区 | 长时间操作、I/O |
考察点:中断安全
参考答案:
spin_lock_irqsave会禁止本地中断,避免中断与进程竞争同一把锁irqsave版本是因为它可以保存并恢复中断状态,比spin_lock_irq更安全考察点:原子操作理解
参考答案:
考察点:死锁预防
参考答案:
trylock避免永久等待spin_lock_irqsave考察点:综合应用
参考答案:
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;
}
考察点:读写锁理解(教材 47.3.3)
参考答案:
| 特性 | 读写锁(rwlock_t) | 自旋锁(spinlock_t) |
|---|---|---|
| 读操作并发 | ✅ 允许多个读者同时持有 | ❌ 完全互斥 |
| 写操作 | 排他,与所有读写者互斥 | 排他 |
| 读写能否同时 | ❌ 不能 | ❌ 不能 |
| 实现开销 | 稍大(维护读者计数) | 更小 |
| 适用场景 | 读多写少,且读操作可并发 | 读写均衡、临界区极短 |
使用场景:当数据结构符合读/写或生产者/消费者模型时使用,例如学生信息表、设备配置表这类"读远多于写"的数据。
追问:
write_unlock() 再 read_lock(),但中间存在空窗期。考察点:顺序锁理解(教材 47.3.3)
参考答案:
顺序锁允许写的时候同时读,但不允许并发写。读者不加锁,而是靠序号判断:
s1s2s1 == s2 说明读取期间没有写者,数据有效;否则重读对应 API:read_seqbegin() / read_seqretry(),写侧用 write_seqlock() / write_sequnlock()。
为什么不能保护指针:原书明确指出——如果在写操作的时候可能会导致指针无效(例如写者把指针指向的旧内存 kfree 掉了),而此时恰巧有读操作访问该指针,就会读取野指针导致系统崩溃。所以顺序锁只能保护纯值类型数据(如 jiffies),不能保护含指针的结构。
追问:
jiffies_64(时钟中断频繁写、大量代码频繁读)。考察点:RCU 理解(超出教材范围)
参考答案:
| 特性 | RCU | 读写锁 |
|---|---|---|
| 读者开销 | 近乎零(只标记) | 中(原子操作) |
| 读者会重试 | ❌ 不会 | ❌ 不会 |
| 读者阻塞写者 | ❌ 完全不阻塞 | ✅ 阻塞 |
| 可保护指针/链表 | ✅ 主要用途 | ✅ 可 |
| 写者语义 | 复制-替换-等宽限期-释放 | 原地加锁修改 |
| 内存开销 | 高(新旧副本共存) | 无额外 |
核心区别:RCU 的写者不是"原地加锁修改",而是"复制一份 → 改副本 → 原子替换指针 → 等宽限期 → 释放旧数据"。宽限期保证所有已进入读侧临界区的读者都退出后,旧数据才被释放。
追问:
rcu_read_lock() 会阻塞写者吗?→ 不会,它只是标记进入临界区,所以读侧临界区不能睡眠。synchronize_rcu() 能在中断里调用吗?→ 不能,它会睡眠。内容来源: 《I.MX6U嵌入式Linux驱动开发指南》第四十七章 Linux并发与竞争(含 47.3.3 其他类型的锁)+ 第四十八章 并发与竞争实验;例程 07_atomic, 08_spinlock, 09_semaphore, 10_mutex 扩展内容: 第八、九节(RCU、完成量)超出教材范围,为补充知识 最后更新: 2026-09-17