《Linux 设备驱动开发详解:基于最新的 Linux 4.0 内核》
第 7 章 Linux 设备驱动中的并发控制
参考:宋宝华 著,机械工业出版社,2015年版
7.1 并发与竞态
7.1.1 并发的概念
**并发(Concurrency)**是指多个执行单元同时、并行地被执行。在 Linux 内核中,并发来源于以下几个方面:
Linux 内核中并发的来源:
1. 多处理器(SMP,Symmetric Multi-Processing)
多个 CPU 核心真正同时执行不同的代码路径
CPU0 正在执行驱动的 read 函数
CPU1 同时执行驱动的 write 函数
→ 真正的并行执行
2. 内核抢占(Preemption)
高优先级任务可以抢占低优先级任务
进程A 正在执行驱动代码
→ 被高优先级进程B 抢占
→ 进程B 也访问同一驱动
→ 进程A 恢复执行时数据已被修改
3. 中断
硬件中断可以在任何时刻打断当前执行的代码
驱动的 read 函数正在执行
→ 硬件中断触发,执行中断处理函数
→ 中断处理函数也访问同一数据
→ read 函数恢复时数据已被修改
4. 软中断和 tasklet
软中断和 tasklet 可以在进程上下文之外执行
与进程上下文代码并发执行
5. 内核线程
多个内核线程并发访问共享资源
7.1.2 竞态的概念
**竞态(Race Condition)**是指多个执行单元并发访问共享资源,且最终结果依赖于执行顺序的情况。竞态会导致数据损坏、系统崩溃等严重问题。
经典竞态案例:银行账户转账
c
/* 错误的驱动代码:没有并发保护 */
static int counter = 0; /* 共享变量 */
/* 进程A 和进程B 同时执行以下代码 */
static ssize_t my_write(struct file *filp, const char __user *buf,
size_t count, loff_t *ppos)
{
int tmp;
tmp = counter; /* 步骤1:读取 counter */
tmp = tmp + 1; /* 步骤2:加1 */
counter = tmp; /* 步骤3:写回 counter */
return count;
}
/*
* 竞态场景(初始 counter = 0):
*
* 进程A 进程B
* tmp = counter; (tmp=0)
* tmp = counter; (tmp=0) ← 进程A还没写回!
* tmp = tmp + 1; (tmp=1)
* tmp = tmp + 1; (tmp=1)
* counter = tmp; (counter=1)
* counter = tmp; (counter=1)
*
* 预期结果:counter = 2
* 实际结果:counter = 1 ← 数据丢失!
*/
7.1.3 临界区
**临界区(Critical Section)**是访问共享资源的代码段,同一时刻只能有一个执行单元进入临界区:
临界区的保护原则:
进入临界区前:获取锁(加锁)
执行临界区代码:访问共享资源
离开临界区后:释放锁(解锁)
┌─────────────────────────────────────────────────────┐
│ 进程A 进程B │
│ lock() │
│ ┌──────────────────┐ lock()(阻塞等待) │
│ │ 临界区 │ │
│ │ 访问共享资源 │ │
│ └──────────────────┘ │
│ unlock() → lock()(获得锁,进入临界区)│
└─────────────────────────────────────────────────────┘
7.1.4 并发控制机制总览
Linux 内核提供了多种并发控制机制,适用于不同场景:
Linux 并发控制机制对比:
机制 适用场景 是否可睡眠 开销
─────────────────────────────────────────────────────────────
中断屏蔽 单CPU,防止中断竞争 否 极低
原子操作 简单整数/位操作 否 极低
自旋锁 短临界区,中断上下文 否 低
读写自旋锁 读多写少,短临界区 否 低
顺序锁 读多写少,写优先 否 低
RCU 读多写少,读性能极致 读:否 极低
信号量 长临界区,进程上下文 是 中
互斥体(mutex) 长临界区,进程上下文 是 中
完成量 同步事件通知 是 中
7.2 编译乱序和执行乱序
7.2.1 编译乱序
编译器为了优化性能,可能会重新排列指令的执行顺序,这在单线程程序中没有问题,但在多线程/多处理器环境中可能导致竞态:
c
/* 原始代码 */
int flag = 0;
int data = 0;
/* 线程A(生产者) */
data = 100; /* 步骤1:写数据 */
flag = 1; /* 步骤2:设置标志 */
/* 线程B(消费者) */
while (!flag); /* 等待标志 */
use(data); /* 使用数据 */
/*
* 问题:编译器可能将步骤1和步骤2重排为:
* flag = 1; ← 先设置标志
* data = 100; ← 再写数据
*
* 线程B 看到 flag=1 后立即读取 data,但 data 可能还是0!
*/
编译屏障(Compiler Barrier):
c
#include <linux/compiler.h>
/*
* barrier():编译屏障
* 告诉编译器:不要将 barrier() 前后的指令重排
* 只影响编译器,不影响 CPU 执行顺序
*/
data = 100;
barrier(); /* 编译屏障:确保 data 的写入在 flag 之前 */
flag = 1;
/* 在驱动中的典型用法 */
static void producer(void)
{
dev->data = prepare_data();
barrier(); /* 确保数据写入完成后再设置 ready 标志 */
dev->ready = 1;
}
7.2.2 执行乱序(CPU 乱序执行)
现代 CPU 为了提高性能,会乱序执行指令(Out-of-Order Execution)。即使编译器生成了正确顺序的指令,CPU 也可能以不同顺序执行它们:
CPU 乱序执行示例(多处理器):
CPU0 执行: CPU1 执行:
store A = 1 store B = 1
load B load A
预期:CPU0 看到 B=1 或 CPU1 看到 A=1(至少一个)
实际:由于 CPU 乱序,可能 CPU0 看到 B=0 且 CPU1 看到 A=0!
内存屏障(Memory Barrier):
c
#include <asm/barrier.h>
/*
* Linux 内核提供的内存屏障:
*
* mb():完全内存屏障(读+写)
* mb() 之前的所有内存访问必须在 mb() 之后的访问之前完成
*
* rmb():读内存屏障
* 确保 rmb() 之前的读操作在 rmb() 之后的读操作之前完成
*
* wmb():写内存屏障
* 确保 wmb() 之前的写操作在 wmb() 之后的写操作之前完成
*
* smp_mb():SMP 内存屏障(单处理器上为空操作)
* smp_rmb():SMP 读内存屏障
* smp_wmb():SMP 写内存屏障
*/
/* 生产者(CPU0) */
void producer(void)
{
ring_buf[head] = data; /* 写入数据 */
wmb(); /* 写内存屏障:确保数据写入后再更新 head */
head = (head + 1) % SIZE;
}
/* 消费者(CPU1) */
void consumer(void)
{
while (head == tail); /* 等待数据 */
rmb(); /* 读内存屏障:确保先读 head 再读数据 */
data = ring_buf[tail];
tail = (tail + 1) % SIZE;
}
/*
* I/O 内存屏障(访问设备寄存器时使用):
* readl()/writel() 等函数内部已包含必要的内存屏障
* 但在某些特殊情况下需要手动添加
*/
writel(val1, reg1);
wmb(); /* 确保 reg1 写入完成后再写 reg2 */
writel(val2, reg2);
7.3 中断屏蔽
7.3.1 中断屏蔽的原理
中断屏蔽是最简单的并发控制手段,通过禁止本地 CPU 的中断来防止中断处理函数与当前代码并发执行:
c
#include <linux/irqflags.h>
/*
* local_irq_disable():禁止本地 CPU 中断
* local_irq_enable():使能本地 CPU 中断
*
* 注意:只禁止本地 CPU 的中断,不影响其他 CPU
* 在 SMP 系统中,其他 CPU 仍然可以访问共享资源
* 因此中断屏蔽通常与自旋锁配合使用
*/
/* 基本用法(不推荐,可能丢失中断状态) */
local_irq_disable();
/* 临界区 */
local_irq_enable();
/*
* 推荐用法:保存和恢复中断状态
* 避免在中断已经被禁止的情况下调用 local_irq_enable() 导致问题
*/
unsigned long flags;
local_irq_save(flags); /* 保存当前中断状态并禁止中断 */
/* 临界区 */
local_irq_restore(flags); /* 恢复之前的中断状态 */
7.3.2 中断屏蔽的适用场景
c
/* 场景:驱动的进程上下文代码与中断处理函数共享数据 */
struct my_dev {
int data;
int irq_count;
};
/* 中断处理函数(中断上下文) */
static irqreturn_t my_irq_handler(int irq, void *dev_id)
{
struct my_dev *dev = dev_id;
dev->irq_count++; /* 修改共享数据 */
dev->data = read_hw_data();
return IRQ_HANDLED;
}
/* 进程上下文代码(如 read 函数) */
static ssize_t my_read(struct file *filp, char __user *buf,
size_t count, loff_t *ppos)
{
struct my_dev *dev = filp->private_data;
unsigned long flags;
int data;
/*
* 必须禁止中断,防止 my_irq_handler 在读取 dev->data 时
* 修改 dev->data,导致读到不一致的数据
*/
local_irq_save(flags);
data = dev->data; /* 临界区:读取共享数据 */
local_irq_restore(flags);
return copy_to_user(buf, &data, sizeof(data)) ? -EFAULT : sizeof(data);
}
7.3.3 中断屏蔽的局限性
中断屏蔽的局限性:
1. 只对本地 CPU 有效
SMP 系统中,其他 CPU 的中断不受影响
其他 CPU 上的代码仍然可以并发访问共享资源
2. 禁止中断时间不能太长
中断被禁止期间,所有硬件中断都无法响应
可能导致中断丢失、系统响应延迟
3. 不能在中断屏蔽期间睡眠
睡眠会导致调度,而调度需要时钟中断
结论:中断屏蔽通常与自旋锁配合使用(spin_lock_irqsave)
单独使用中断屏蔽只适用于单处理器系统
7.4 原子操作
7.4.1 原子整数操作
原子操作是不可分割的操作,在执行过程中不会被中断或其他 CPU 干扰,适用于简单的整数计数场景:
c
#include <linux/atomic.h>
/* ── 原子整数类型 ──────────────────────────────────────── */
atomic_t v; /* 32位原子整数 */
atomic64_t v64; /* 64位原子整数 */
/* ── 初始化 ────────────────────────────────────────────── */
atomic_t v = ATOMIC_INIT(0); /* 静态初始化为0 */
atomic_set(&v, 5); /* 动态设置值为5 */
/* ── 读取 ──────────────────────────────────────────────── */
int val = atomic_read(&v); /* 读取原子变量的值 */
/* ── 加减操作 ──────────────────────────────────────────── */
atomic_inc(&v); /* v++ */
atomic_dec(&v); /* v-- */
atomic_add(5, &v); /* v += 5 */
atomic_sub(3, &v); /* v -= 3 */
/* ── 操作并返回结果 ────────────────────────────────────── */
int old = atomic_inc_return(&v); /* v++,返回新值 */
int old = atomic_dec_return(&v); /* v--,返回新值 */
int old = atomic_add_return(5, &v);/* v+=5,返回新值 */
/* ── 操作并测试结果 ────────────────────────────────────── */
/* 返回操作后是否为零 */
if (atomic_dec_and_test(&v)) {
/* v 减1后变为0,执行清理操作 */
pr_info("引用计数归零,释放资源\n");
}
if (atomic_inc_and_test(&v)) {
/* v 加1后变为0(溢出情况,极少见) */
}
/* ── 条件操作 ──────────────────────────────────────────── */
/* 如果 v == old,则将 v 设置为 new,返回 v 的原始值 */
int result = atomic_cmpxchg(&v, old_val, new_val);
if (result == old_val) {
/* 交换成功 */
}
7.4.2 原子操作的典型应用
应用一:设备引用计数
c
struct my_dev {
atomic_t ref_count; /* 引用计数 */
/* ... */
};
static int my_open(struct inode *inode, struct file *filp)
{
struct my_dev *dev = container_of(inode->i_cdev, struct my_dev, cdev);
atomic_inc(&dev->ref_count); /* 引用计数加1 */
filp->private_data = dev;
return 0;
}
static int my_release(struct inode *inode, struct file *filp)
{
struct my_dev *dev = filp->private_data;
if (atomic_dec_and_test(&dev->ref_count)) {
/* 引用计数归零,最后一个用户关闭了设备 */
pr_info("最后一个用户关闭设备,执行清理\n");
/* 执行设备关闭操作 */
}
return 0;
}
应用二:限制设备只能被一个进程打开
c
static atomic_t device_available = ATOMIC_INIT(1); /* 1表示可用 */
static int my_open(struct inode *inode, struct file *filp)
{
/*
* atomic_dec_and_test:将 device_available 减1
* 如果减1后为0,说明之前是1(设备可用),返回 true
* 如果减1后不为0(变为负数),说明设备已被占用
*/
if (!atomic_dec_and_test(&device_available)) {
atomic_inc(&device_available); /* 恢复计数 */
return -EBUSY; /* 设备忙 */
}
/* 成功获取设备 */
return 0;
}
static int my_release(struct inode *inode, struct file *filp)
{
atomic_inc(&device_available); /* 释放设备 */
return 0;
}
7.4.3 原子位操作
c
#include <linux/bitops.h>
unsigned long flags = 0;
/* 设置位 */
set_bit(3, &flags); /* 设置第3位为1 */
__set_bit(3, &flags); /* 非原子版本(更快,但不安全) */
/* 清除位 */
clear_bit(3, &flags); /* 清除第3位 */
__clear_bit(3, &flags); /* 非原子版本 */
/* 翻转位 */
change_bit(3, &flags); /* 翻转第3位 */
/* 测试位 */
int val = test_bit(3, &flags); /* 返回第3位的值(0或1) */
/* 测试并设置/清除(原子操作,返回旧值) */
int old = test_and_set_bit(3, &flags); /* 设置第3位,返回旧值 */
int old = test_and_clear_bit(3, &flags); /* 清除第3位,返回旧值 */
/* 典型应用:标志位管理 */
#define FLAG_INITIALIZED 0
#define FLAG_RUNNING 1
#define FLAG_ERROR 2
unsigned long dev_flags = 0;
/* 设置初始化标志 */
set_bit(FLAG_INITIALIZED, &dev_flags);
/* 检查是否正在运行 */
if (test_bit(FLAG_RUNNING, &dev_flags)) {
pr_info("设备正在运行\n");
}
/* 原子地检查并设置运行标志(防止重复启动) */
if (test_and_set_bit(FLAG_RUNNING, &dev_flags)) {
pr_err("设备已在运行\n");
return -EBUSY;
}
7.5 自旋锁
7.5.1 自旋锁的原理
自旋锁(Spinlock)是一种忙等待 锁:当一个执行单元试图获取已被占用的自旋锁时,它会循环等待(自旋),直到锁被释放,而不是进入睡眠状态。
自旋锁工作原理:
CPU0(持锁者) CPU1(等待者)
lock() lock()
获取锁成功 锁被占用
进入临界区 while(lock_is_held) { /* 自旋 */ }
... ...
... ...
unlock() → 获取锁成功
进入临界区
特点:
✓ 不会引起进程睡眠,适用于中断上下文
✓ 临界区很短时,比睡眠锁效率高(避免上下文切换开销)
✗ 持锁期间 CPU 一直在忙等,浪费 CPU 资源
✗ 不能在持锁期间睡眠(会导致死锁)
✗ 临界区很长时,效率低于睡眠锁
7.5.2 自旋锁的 API
c
#include <linux/spinlock.h>
/* ── 定义和初始化 ──────────────────────────────────────── */
spinlock_t my_lock;
spin_lock_init(&my_lock); /* 动态初始化 */
DEFINE_SPINLOCK(my_lock); /* 静态定义并初始化 */
/* ── 基本操作 ──────────────────────────────────────────── */
spin_lock(&my_lock); /* 获取锁(自旋等待) */
/* 临界区(不能睡眠!) */
spin_unlock(&my_lock); /* 释放锁 */
/* ── 禁止中断版本(最常用)────────────────────────────── */
unsigned long flags;
spin_lock_irqsave(&my_lock, flags); /* 禁止本地中断并获取锁 */
/* 临界区 */
spin_unlock_irqrestore(&my_lock, flags); /* 释放锁并恢复中断状态 */
/* ── 禁止下半部版本 ────────────────────────────────────── */
spin_lock_bh(&my_lock); /* 禁止软中断(下半部)并获取锁 */
/* 临界区 */
spin_unlock_bh(&my_lock); /* 释放锁并使能软中断 */
/* ── 尝试获取锁(非阻塞)──────────────────────────────── */
if (spin_trylock(&my_lock)) {
/* 成功获取锁 */
/* 临界区 */
spin_unlock(&my_lock);
} else {
/* 锁已被占用,立即返回 */
pr_info("锁忙,稍后重试\n");
}
7.5.3 自旋锁的使用规则
自旋锁使用的黄金规则:
规则1:持锁期间不能睡眠
✗ 错误:spin_lock(&lock); msleep(100); spin_unlock(&lock);
✗ 错误:spin_lock(&lock); kmalloc(size, GFP_KERNEL); spin_unlock(&lock);
✓ 正确:spin_lock(&lock); kmalloc(size, GFP_ATOMIC); spin_unlock(&lock);
规则2:持锁期间不能调用可能睡眠的函数
✗ 错误:spin_lock(&lock); mutex_lock(&mutex); spin_unlock(&lock);
✗ 错误:spin_lock(&lock); copy_to_user(...); spin_unlock(&lock);
规则3:中断处理函数中使用自旋锁时,进程上下文必须用 irqsave 版本
进程上下文:spin_lock_irqsave(&lock, flags);
中断上下文:spin_lock(&lock); /* 中断上下文中中断已被禁止 */
规则4:不能递归获取同一个自旋锁(会死锁)
✗ 错误:spin_lock(&lock); spin_lock(&lock); /* 死锁! */
规则5:临界区应尽量短
自旋锁持有时间越长,其他 CPU 等待时间越长,系统性能越差
7.5.4 自旋锁的完整使用案例
c
struct my_device {
spinlock_t lock;
int data;
int irq_count;
struct list_head pending_list;
};
/* 中断处理函数 */
static irqreturn_t my_irq_handler(int irq, void *dev_id)
{
struct my_device *dev = dev_id;
struct pending_item *item;
/*
* 中断上下文中使用 spin_lock(不用 irqsave,因为中断已被禁止)
* 但如果中断处理函数可能被更高优先级中断打断,需要用 irqsave
*/
spin_lock(&dev->lock);
dev->irq_count++;
dev->data = read_hw_register();
/* 添加待处理项到链表 */
item = kzalloc(sizeof(*item), GFP_ATOMIC); /* 中断上下文用 GFP_ATOMIC */
if (item) {
item->data = dev->data;
list_add_tail(&item->list, &dev->pending_list);
}
spin_unlock(&dev->lock);
return IRQ_HANDLED;
}
/* 进程上下文(read 函数) */
static ssize_t my_read(struct file *filp, char __user *buf,
size_t count, loff_t *ppos)
{
struct my_device *dev = filp->private_data;
unsigned long flags;
int data;
/*
* 进程上下文中,如果中断处理函数也访问同一数据,
* 必须使用 spin_lock_irqsave 禁止本地中断
* 防止在持锁期间被中断打断,中断处理函数再次尝试获取锁导致死锁
*/
spin_lock_irqsave(&dev->lock, flags);
data = dev->data;
spin_unlock_irqrestore(&dev->lock, flags);
return copy_to_user(buf, &data, sizeof(data)) ? -EFAULT : sizeof(data);
}
7.6 读写自旋锁
7.6.1 读写自旋锁的原理
读写自旋锁(rwlock)允许多个读者同时持锁,但写者必须独占:
读写自旋锁的并发规则:
状态 读者请求 写者请求
─────────────────────────────────
无锁 允许 允许
有读锁(N个) 允许 阻塞
有写锁 阻塞 阻塞
适用场景:读操作远多于写操作的共享数据
例如:路由表、配置参数、设备状态信息
7.6.2 读写自旋锁的 API
c
#include <linux/rwlock.h>
/* ── 定义和初始化 ──────────────────────────────────────── */
rwlock_t my_rwlock;
rwlock_init(&my_rwlock);
DEFINE_RWLOCK(my_rwlock); /* 静态定义并初始化 */
/* ── 读操作(允许多个读者同时持锁)────────────────────── */
read_lock(&my_rwlock);
/* 读临界区(只读,不修改共享数据) */
read_unlock(&my_rwlock);
/* 禁止中断版本 */
unsigned long flags;
read_lock_irqsave(&my_rwlock, flags);
/* 读临界区 */
read_unlock_irqrestore(&my_rwlock, flags);
/* ── 写操作(独占,排斥所有读者和写者)────────────────── */
write_lock(&my_rwlock);
/* 写临界区(修改共享数据) */
write_unlock(&my_rwlock);
/* 禁止中断版本 */
write_lock_irqsave(&my_rwlock, flags);
/* 写临界区 */
write_unlock_irqrestore(&my_rwlock, flags);
7.6.3 读写自旋锁的使用案例
c
/* 场景:设备配置参数,读多写少 */
struct device_config {
rwlock_t lock;
int baudrate;
int data_bits;
int parity;
int stop_bits;
};
static struct device_config config = {
.lock = __RW_LOCK_UNLOCKED(config.lock),
.baudrate = 115200,
.data_bits = 8,
.parity = 0,
.stop_bits = 1,
};
/* 读取配置(多个线程可以同时读) */
int get_baudrate(void)
{
int rate;
read_lock(&config.lock);
rate = config.baudrate;
read_unlock(&config.lock);
return rate;
}
/* 更新配置(独占写) */
void set_baudrate(int rate)
{
write_lock(&config.lock);
config.baudrate = rate;
/* 同时更新相关硬件寄存器 */
write_hw_baudrate(rate);
write_unlock(&config.lock);
}
7.7 顺序锁
7.7.1 顺序锁的原理
顺序锁(seqlock)是一种写者优先的读写锁,读者不阻塞写者,但读者需要检测是否发生了写操作:
顺序锁工作原理:
写者:
获取写锁 → 序列号加1(变为奇数)→ 修改数据 → 序列号加1(变为偶数)→ 释放写锁
读者:
读取序列号(seq1)→ 读取数据 → 读取序列号(seq2)
如果 seq1 == seq2 且为偶数:读取成功
如果 seq1 != seq2 或为奇数:写者正在写,重试
特点:
✓ 读者不阻塞写者(写者优先)
✓ 适合写操作很少但对写实时性要求高的场景
✗ 读者可能需要多次重试(写操作频繁时性能差)
✗ 读者不能修改数据(否则破坏一致性)
✗ 读者不能持有指针(写者可能释放指针指向的内存)
7.7.2 顺序锁的 API
c
#include <linux/seqlock.h>
/* ── 定义和初始化 ──────────────────────────────────────── */
seqlock_t my_seqlock;
seqlock_init(&my_seqlock);
DEFINE_SEQLOCK(my_seqlock); /* 静态定义并初始化 */
/* ── 写操作 ────────────────────────────────────────────── */
write_seqlock(&my_seqlock);
/* 写临界区:修改共享数据 */
write_sequnlock(&my_seqlock);
/* 禁止中断版本 */
unsigned long flags;
write_seqlock_irqsave(&my_seqlock, flags);
/* 写临界区 */
write_sequnlock_irqrestore(&my_seqlock, flags);
/* ── 读操作(可能需要重试)────────────────────────────── */
unsigned int seq;
do {
seq = read_seqbegin(&my_seqlock); /* 读取序列号 */
/* 读取共享数据(不能修改!) */
x = shared_data.x;
y = shared_data.y;
} while (read_seqretry(&my_seqlock, seq)); /* 检测是否有写操作 */
/* 读取成功,x 和 y 是一致的 */
7.7.3 顺序锁的典型应用
c
/* Linux 内核中 jiffies 的读取就使用了顺序锁 */
/* 内核时钟更新(写操作,在时钟中断中执行) */
write_seqlock(&xtime_lock);
xtime.tv_sec++;
xtime.tv_nsec = 0;
write_sequnlock(&xtime_lock);
/* 用户空间读取时间(读操作) */
struct timespec ts;
unsigned int seq;
do {
seq = read_seqbegin(&xtime_lock);
ts = xtime;
} while (read_seqretry(&xtime_lock, seq));
/* ts 是一致的时间值 */
7.8 读-复制-更新(RCU)
7.8.1 RCU 的原理
RCU(Read-Copy-Update)是 Linux 内核中最高效的并发控制机制之一,专为读多写少场景设计:
RCU 工作原理:
读者:
rcu_read_lock() ← 进入 RCU 读临界区(几乎零开销)
读取数据指针
访问数据
rcu_read_unlock() ← 离开 RCU 读临界区
写者(更新数据):
1. 复制(Copy):复制要修改的数据结构
2. 修改(Update):修改副本
3. 替换(Replace):原子地替换指针,使读者看到新数据
4. 等待(Wait):等待所有正在读旧数据的读者完成
5. 释放(Free):释放旧数据
关键特性:
✓ 读者几乎零开销(不需要获取任何锁)
✓ 读者不会阻塞写者
✓ 写者不会阻塞读者
✗ 写者需要等待所有读者完成(宽限期)
✗ 只适用于指针保护的数据结构
✗ 读者不能修改数据
7.8.2 RCU 的 API
c
#include <linux/rcupdate.h>
/* ── 读者 API ──────────────────────────────────────────── */
rcu_read_lock(); /* 进入 RCU 读临界区 */
/* 读取 RCU 保护的指针 */
struct my_data *p = rcu_dereference(rcu_ptr); /* 安全地读取指针 */
/* 使用 p 指向的数据 */
rcu_read_unlock(); /* 离开 RCU 读临界区 */
/* ── 写者 API ──────────────────────────────────────────── */
/* 1. 分配新数据并初始化 */
struct my_data *new_data = kmalloc(sizeof(*new_data), GFP_KERNEL);
*new_data = *old_data; /* 复制旧数据 */
new_data->value = new_value; /* 修改副本 */
/* 2. 原子地替换指针(读者立即看到新数据) */
rcu_assign_pointer(rcu_ptr, new_data);
/* 3. 等待所有读者完成(宽限期) */
synchronize_rcu(); /* 阻塞等待,直到所有读者离开临界区 */
/* 或异步版本 */
call_rcu(&old_data->rcu_head, my_rcu_callback); /* 异步,回调中释放 */
/* 4. 释放旧数据 */
kfree(old_data);
7.8.3 RCU 的完整案例
c
/* 场景:全局配置结构体,读操作极频繁,写操作极少 */
#include <linux/rcupdate.h>
#include <linux/slab.h>
struct global_config {
int speed;
int mode;
char name[32];
struct rcu_head rcu; /* RCU 回调头 */
};
/* RCU 保护的全局指针 */
static struct global_config __rcu *g_config;
static DEFINE_SPINLOCK(config_update_lock); /* 保护写操作 */
/* 读取配置(高频操作,几乎零开销) */
int get_speed(void)
{
struct global_config *cfg;
int speed;
rcu_read_lock();
cfg = rcu_dereference(g_config);
speed = cfg->speed;
rcu_read_unlock();
return speed;
}
/* RCU 回调:宽限期结束后释放旧数据 */
static void config_rcu_free(struct rcu_head *head)
{
struct global_config *cfg = container_of(head,
struct global_config, rcu);
kfree(cfg);
}
/* 更新配置(低频操作) */
int update_config(int new_speed, int new_mode)
{
struct global_config *new_cfg, *old_cfg;
/* 分配新配置 */
new_cfg = kmalloc(sizeof(*new_cfg), GFP_KERNEL);
if (!new_cfg)
return -ENOMEM;
/* 使用写锁保护写操作(防止多个写者并发) */
spin_lock(&config_update_lock);
/* 复制旧配置 */
old_cfg = rcu_dereference_protected(g_config,
lockdep_is_held(&config_update_lock));
*new_cfg = *old_cfg;
/* 修改新配置 */
new_cfg->speed = new_speed;
new_cfg->mode = new_mode;
/* 原子替换指针 */
rcu_assign_pointer(g_config, new_cfg);
spin_unlock(&config_update_lock);
/* 异步等待宽限期,然后释放旧配置 */
call_rcu(&old_cfg->rcu, config_rcu_free);
return 0;
}
7.9 信号量
7.9.1 信号量的原理
信号量(Semaphore)是一种可以睡眠等待的锁,当无法获取信号量时,进程会进入睡眠状态,让出 CPU:
信号量工作原理:
信号量内部维护一个计数值(count):
count > 0:可以获取,count--
count = 0:不可获取,进程进入睡眠队列等待
释放时:count++,唤醒等待队列中的进程
二值信号量(count初始为1):等价于互斥锁
计数信号量(count初始为N):允许N个进程同时访问
与自旋锁的区别:
自旋锁:等待时 CPU 忙等(自旋),不睡眠
信号量:等待时进程睡眠,让出 CPU
适用场景:
自旋锁 → 临界区很短(微秒级),中断上下文
信号量 → 临界区较长(毫秒级),进程上下文
7.9.2 信号量的 API
c
#include <linux/semaphore.h>
/* ── 定义和初始化 ──────────────────────────────────────── */
struct semaphore sem;
sema_init(&sem, 1); /* 初始化为1(二值信号量,相当于互斥锁) */
sema_init(&sem, 5); /* 初始化为5(计数信号量,允许5个并发) */
/* 静态初始化 */
static DEFINE_SEMAPHORE(sem); /* 初始化为1的二值信号量 */
/* ── P 操作(获取信号量)──────────────────────────────── */
down(&sem); /* 获取信号量(不可被信号中断) */
/* 可被信号中断的版本(推荐在驱动中使用) */
if (down_interruptible(&sem)) {
return -ERESTARTSYS; /* 被信号中断,返回错误 */
}
/* 非阻塞版本 */
if (down_trylock(&sem)) {
/* 获取失败,信号量已被占用 */
return -EBUSY;
}
/* ── V 操作(释放信号量)──────────────────────────────── */
up(&sem); /* 释放信号量,唤醒等待的进程 */
7.9.3 信号量的使用案例
c
/* 场景:限制同时访问设备的进程数量(最多3个) */
static struct semaphore access_sem;
static int __init my_init(void)
{
sema_init(&access_sem, 3); /* 最多允许3个进程同时访问 */
/* ... */
return 0;
}
static int my_open(struct inode *inode, struct file *filp)
{
/* 获取信号量(如果已有3个进程在访问,则等待) */
if (down_interruptible(&access_sem))
return -ERESTARTSYS;
/* 成功获取,进行初始化 */
return 0;
}
static int my_release(struct inode *inode, struct file *filp)
{
up(&access_sem); /* 释放信号量 */
return 0;
}
7.10 互斥体
7.10.1 互斥体的原理
互斥体(Mutex)是 Linux 2.6.16 引入的专用互斥锁,比信号量更高效,语义更清晰:
互斥体 vs 信号量:
互斥体(mutex):
✓ 专为互斥设计,语义清晰
✓ 比信号量更快(优化的实现)
✓ 支持优先级继承(防止优先级反转)
✓ 调试支持更好(lockdep 检测死锁)
✗ 只能用于进程上下文(不能在中断上下文使用)
✗ 只能由持锁者释放(不能在一个函数加锁,另一个函数解锁)
信号量(semaphore):
✓ 可以在一个上下文加锁,另一个上下文解锁
✓ 支持计数(允许多个并发访问)
✗ 比互斥体慢
✗ 没有优先级继承
结论:在进程上下文的互斥场景,优先使用 mutex
7.10.2 互斥体的 API
c
#include <linux/mutex.h>
/* ── 定义和初始化 ──────────────────────────────────────── */
struct mutex my_mutex;
mutex_init(&my_mutex); /* 动态初始化 */
DEFINE_MUTEX(my_mutex); /* 静态定义并初始化 */
/* ── 加锁 ──────────────────────────────────────────────── */
mutex_lock(&my_mutex); /* 获取锁(不可被信号中断) */
/* 可被信号中断的版本(推荐在驱动中使用) */
if (mutex_lock_interruptible(&my_mutex)) {
return -ERESTARTSYS;
}
/* 可被任何信号中断的版本 */
if (mutex_lock_killable(&my_mutex)) {
return -EINTR;
}
/* 非阻塞版本 */
if (!mutex_trylock(&my_mutex)) {
return -EBUSY;
}
/* ── 解锁 ──────────────────────────────────────────────── */
mutex_unlock(&my_mutex); /* 释放锁 */
/* ── 检查锁状态 ────────────────────────────────────────── */
bool locked = mutex_is_locked(&my_mutex); /* 是否已被锁定 */
7.10.3 互斥体的使用规则
互斥体使用规则:
1. 只能在进程上下文使用
✗ 不能在中断处理函数中使用 mutex_lock
✗ 不能在 tasklet 或软中断中使用
2. 只能由持锁者解锁
✗ 错误:线程A 加锁,线程B 解锁
3. 不能递归加锁
✗ 错误:mutex_lock(&m); mutex_lock(&m); /* 死锁! */
4. 持锁期间不能退出进程
持锁的进程退出会导致锁永远无法释放
5. 多个锁的加锁顺序必须一致(防止死锁)
所有代码路径都必须按相同顺序获取多个锁
7.10.4 互斥体的完整使用案例
c
struct globalmem_dev {
struct cdev cdev;
unsigned char mem[GLOBALMEM_SIZE];
struct mutex mutex; /* 互斥体保护 mem 数组 */
};
static int globalmem_open(struct inode *inode, struct file *filp)
{
struct globalmem_dev *dev = container_of(inode->i_cdev,
struct globalmem_dev, cdev);
filp->private_data = dev;
return 0;
}
static ssize_t globalmem_read(struct file *filp, char __user *buf,
size_t size, loff_t *ppos)
{
struct globalmem_dev *dev = filp->private_data;
unsigned long p = *ppos;
unsigned int count = size;
int ret = 0;
if (p >= GLOBALMEM_SIZE) return 0;
if (count > GLOBALMEM_SIZE - p) count = GLOBALMEM_SIZE - p;
/*
* 使用 mutex_lock_interruptible 而不是 mutex_lock
* 允许用户通过 Ctrl+C 中断等待,提高用户体验
*/
if (mutex_lock_interruptible(&dev->mutex))
return -ERESTARTSYS;
if (copy_to_user(buf, dev->mem + p, count))
ret = -EFAULT;
else {
*ppos += count;
ret = count;
}
mutex_unlock(&dev->mutex);
return ret;
}
static ssize_t globalmem_write(struct file *filp, const char __user *buf,
size_t size, loff_t *ppos)
{
struct globalmem_dev *dev = filp->private_data;
unsigned long p = *ppos;
unsigned int count = size;
int ret = 0;
if (p >= GLOBALMEM_SIZE) return 0;
if (count > GLOBALMEM_SIZE - p) count = GLOBALMEM_SIZE - p;
if (mutex_lock_interruptible(&dev->mutex))
return -ERESTARTSYS;
if (copy_from_user(dev->mem + p, buf, count))
ret = -EFAULT;
else {
*ppos += count;
ret = count;
}
mutex_unlock(&dev->mutex);
return ret;
}
7.11 完成量
7.11.1 完成量的原理
完成量(Completion)用于线程间的同步,一个线程等待另一个线程完成某项工作:
完成量工作原理:
线程A(等待者):
init_completion(&comp)
wait_for_completion(&comp) ← 阻塞等待
/* 线程B 完成工作后继续执行 */
线程B(完成者):
/* 执行某项工作 */
complete(&comp) ← 通知线程A
典型应用场景:
1. 驱动初始化:等待硬件初始化完成
2. DMA 传输:等待 DMA 传输完成
3. 中断同步:等待中断处理完成
4. 内核线程:等待内核线程启动完成
7.11.2 完成量的 API
c
#include <linux/completion.h>
/* ── 定义和初始化 ──────────────────────────────────────── */
struct completion my_comp;
init_completion(&my_comp); /* 动态初始化 */
DECLARE_COMPLETION(my_comp); /* 静态定义并初始化 */
/* ── 等待完成 ──────────────────────────────────────────── */
wait_for_completion(&my_comp); /* 无限等待 */
/* 带超时的等待(返回剩余 jiffies,0 表示超时) */
unsigned long ret = wait_for_completion_timeout(&my_comp,
msecs_to_jiffies(5000));
if (ret == 0) {
pr_err("等待超时!\n");
return -ETIMEDOUT;
}
/* 可被信号中断的等待 */
if (wait_for_completion_interruptible(&my_comp)) {
return -ERESTARTSYS;
}
/* ── 发出完成信号 ──────────────────────────────────────── */
complete(&my_comp); /* 唤醒一个等待者 */
complete_all(&my_comp); /* 唤醒所有等待者 */
/* ── 重新初始化(复用完成量)──────────────────────────── */
reinit_completion(&my_comp);
7.11.3 完成量的使用案例
c
/* 场景:等待 DMA 传输完成 */
struct dma_dev {
struct completion dma_done;
/* ... */
};
/* DMA 完成中断处理函数 */
static irqreturn_t dma_irq_handler(int irq, void *dev_id)
{
struct dma_dev *dev = dev_id;
/* DMA 传输完成,发出完成信号 */
complete(&dev->dma_done);
return IRQ_HANDLED;
}
/* 发起 DMA 传输并等待完成 */
static int do_dma_transfer(struct dma_dev *dev, void *buf, size_t len)
{
unsigned long timeout;
/* 初始化完成量 */
init_completion(&dev->dma_done);
/* 启动 DMA 传输 */
start_dma(dev, buf, len);
/* 等待 DMA 完成(最多等待 5 秒) */
timeout = wait_for_completion_timeout(&dev->dma_done,
msecs_to_jiffies(5000));
if (timeout == 0) {
pr_err("DMA 传输超时!\n");
stop_dma(dev);
return -ETIMEDOUT;
}
pr_info("DMA 传输完成\n");
return 0;
}
/* 场景:等待内核线程启动 */
static DECLARE_COMPLETION(thread_started);
static int my_thread_func(void *data)
{
/* 线程初始化工作 */
pr_info("内核线程启动\n");
/* 通知主线程:线程已启动 */
complete(&thread_started);
/* 线程主循环 */
while (!kthread_should_stop()) {
/* 执行工作 */
msleep(100);
}
return 0;
}
static int __init my_init(void)
{
struct task_struct *thread;
thread = kthread_run(my_thread_func, NULL, "my_thread");
if (IS_ERR(thread))
return PTR_ERR(thread);
/* 等待线程启动完成 */
wait_for_completion(&thread_started);
pr_info("内核线程已就绪\n");
return 0;
}
7.12 增加并发控制后的 globalmem
7.12.1 并发问题分析
在第 6 章的 globalmem 驱动中,已经使用了 mutex 保护设备内存。本节对并发控制进行完整分析和优化:
globalmem 驱动的并发场景分析:
场景1:多进程同时 read/write
进程A 执行 read,进程B 执行 write
→ 可能读到写了一半的数据
→ 解决:mutex 保护 mem 数组
场景2:多进程同时 open
进程A 和进程B 同时 open
→ open 函数本身无共享数据,无需保护
场景3:ioctl MEM_CLEAR 与 read/write 并发
进程A 执行 read,进程B 执行 ioctl MEM_CLEAR
→ 可能读到清空了一半的数据
→ 解决:同一个 mutex 保护所有对 mem 的访问
场景4:多个设备实例的并发
每个设备实例有独立的 mutex
不同设备实例之间不需要同步
7.12.2 完整的并发安全 globalmem 驱动
c
/*
* globalmem_safe.c ------ 增加完整并发控制的 globalmem 驱动
*
* 并发控制策略:
* 1. 使用 mutex 保护设备内存(mem 数组)的所有访问
* 2. 使用 atomic_t 实现设备引用计数
* 3. 使用 completion 实现初始化同步
*/
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/slab.h>
#include <linux/uaccess.h>
#include <linux/device.h>
#include <linux/mutex.h>
#include <linux/atomic.h>
#include <linux/completion.h>
#define GLOBALMEM_SIZE 0x1000
#define GLOBALMEM_MAJOR 230
#define DEVICE_NUM 2
/* ioctl 命令定义 */
#define GLOBALMEM_MAGIC 'g'
#define MEM_CLEAR _IO(GLOBALMEM_MAGIC, 0)
#define MEM_GETSIZE _IOR(GLOBALMEM_MAGIC, 1, int)
struct globalmem_dev {
struct cdev cdev;
unsigned char mem[GLOBALMEM_SIZE];
/* 并发控制 */
struct mutex mutex; /* 保护 mem 数组 */
atomic_t open_count; /* 当前打开的文件描述符数量 */
struct completion init_done; /* 初始化完成信号 */
};
static int globalmem_major = GLOBALMEM_MAJOR;
static struct globalmem_dev globalmem_devs[DEVICE_NUM];
static struct class *globalmem_class;
/* ── open ────────────────────────────────────────────────── */
static int globalmem_open(struct inode *inode, struct file *filp)
{
struct globalmem_dev *dev = container_of(inode->i_cdev,
struct globalmem_dev, cdev);
/* 等待设备初始化完成(如果初始化需要时间) */
wait_for_completion(&dev->init_done);
/* 增加引用计数 */
atomic_inc(&dev->open_count);
filp->private_data = dev;
pr_info("globalmem: 设备打开,当前打开数 = %d\n",
atomic_read(&dev->open_count));
return 0;
}
/* ── release ─────────────────────────────────────────────── */
static int globalmem_release(struct inode *inode, struct file *filp)
{
struct globalmem_dev *dev = filp->private_data;
/* 减少引用计数 */
atomic_dec(&dev->open_count);
pr_info("globalmem: 设备关闭,当前打开数 = %d\n",
atomic_read(&dev->open_count));
return 0;
}
/* ── read ────────────────────────────────────────────────── */
static ssize_t globalmem_read(struct file *filp, char __user *buf,
size_t size, loff_t *ppos)
{
unsigned long p = *ppos;
unsigned int count = size;
int ret = 0;
struct globalmem_dev *dev = filp->private_data;
if (p >= GLOBALMEM_SIZE) return 0;
if (count > GLOBALMEM_SIZE - p) count = GLOBALMEM_SIZE - p;
/*
* 使用 mutex_lock_interruptible:
* 允许用户通过信号(如 Ctrl+C)中断等待
* 比 mutex_lock 更友好
*/
if (mutex_lock_interruptible(&dev->mutex))
return -ERESTARTSYS;
if (copy_to_user(buf, dev->mem + p, count))
ret = -EFAULT;
else {
*ppos += count;
ret = count;
pr_info("globalmem: 读取 %u 字节,位置 %lu\n", count, p);
}
mutex_unlock(&dev->mutex);
return ret;
}
/* ── write ───────────────────────────────────────────────── */
static ssize_t globalmem_write(struct file *filp, const char __user *buf,
size_t size, loff_t *ppos)
{
unsigned long p = *ppos;
unsigned int count = size;
int ret = 0;
struct globalmem_dev *dev = filp->private_data;
if (p >= GLOBALMEM_SIZE) return 0;
if (count > GLOBALMEM_SIZE - p) count = GLOBALMEM_SIZE - p;
if (mutex_lock_interruptible(&dev->mutex))
return -ERESTARTSYS;
if (copy_from_user(dev->mem + p, buf, count))
ret = -EFAULT;
else {
*ppos += count;
ret = count;
pr_info("globalmem: 写入 %u 字节,位置 %lu\n", count, p);
}
mutex_unlock(&dev->mutex);
return ret;
}
/* ── llseek ──────────────────────────────────────────────── */
static loff_t globalmem_llseek(struct file *filp, loff_t offset, int orig)
{
loff_t ret = 0;
switch (orig) {
case SEEK_SET:
if (offset < 0 || (unsigned int)offset > GLOBALMEM_SIZE) {
ret = -EINVAL;
break;
}
filp->f_pos = (unsigned int)offset;
ret = filp->f_pos;
break;
case SEEK_CUR:
if ((filp->f_pos + offset) > GLOBALMEM_SIZE ||
(filp->f_pos + offset) < 0) {
ret = -EINVAL;
break;
}
filp->f_pos += offset;
ret = filp->f_pos;
break;
default:
ret = -EINVAL;
}
return ret;
}
/* ── ioctl ───────────────────────────────────────────────── */
static long globalmem_ioctl(struct file *filp, unsigned int cmd,
unsigned long arg)
{
struct globalmem_dev *dev = filp->private_data;
int size;
if (_IOC_TYPE(cmd) != GLOBALMEM_MAGIC)
return -ENOTTY;
switch (cmd) {
case MEM_CLEAR:
if (mutex_lock_interruptible(&dev->mutex))
return -ERESTARTSYS;
memset(dev->mem, 0, GLOBALMEM_SIZE);
mutex_unlock(&dev->mutex);
pr_info("globalmem: 内存已清空\n");
break;
case MEM_GETSIZE:
size = GLOBALMEM_SIZE;
if (copy_to_user((int __user *)arg, &size, sizeof(int)))
return -EFAULT;
break;
default:
return -EINVAL;
}
return 0;
}
static const struct file_operations globalmem_fops = {
.owner = THIS_MODULE,
.llseek = globalmem_llseek,
.read = globalmem_read,
.write = globalmem_write,
.unlocked_ioctl = globalmem_ioctl,
.open = globalmem_open,
.release = globalmem_release,
};
/* ── 模块加载 ─────────────────────────────────────────────── */
static int __init globalmem_init(void)
{
int ret, i;
dev_t devno = MKDEV(globalmem_major, 0);
ret = register_chrdev_region(devno, DEVICE_NUM, "globalmem");
if (ret < 0) return ret;
globalmem_class = class_create(THIS_MODULE, "globalmem");
if (IS_ERR(globalmem_class)) {
ret = PTR_ERR(globalmem_class);
goto fail_class;
}
for (i = 0; i < DEVICE_NUM; i++) {
/* 初始化并发控制机制 */
mutex_init(&globalmem_devs[i].mutex);
atomic_set(&globalmem_devs[i].open_count, 0);
init_completion(&globalmem_devs[i].init_done);
/* 初始化 cdev */
cdev_init(&globalmem_devs[i].cdev, &globalmem_fops);
globalmem_devs[i].cdev.owner = THIS_MODULE;
ret = cdev_add(&globalmem_devs[i].cdev,
MKDEV(globalmem_major, i), 1);
if (ret) goto fail_cdev;
/* 创建设备文件 */
device_create(globalmem_class, NULL,
MKDEV(globalmem_major, i),
NULL, "globalmem%d", i);
/* 模拟初始化完成(实际驱动中可能需要等待硬件就绪) */
complete(&globalmem_devs[i].init_done);
}
pr_info("globalmem: %d 个设备初始化完成\n", DEVICE_NUM);
return 0;
fail_cdev:
while (--i >= 0) {
device_destroy(globalmem_class, MKDEV(globalmem_major, i));
cdev_del(&globalmem_devs[i].cdev);
}
class_destroy(globalmem_class);
fail_class:
unregister_chrdev_region(devno, DEVICE_NUM);
return ret;
}
/* ── 模块卸载 ─────────────────────────────────────────────── */
static void __exit globalmem_exit(void)
{
int i;
for (i = 0; i < DEVICE_NUM; i++) {
device_destroy(globalmem_class, MKDEV(globalmem_major, i));
cdev_del(&globalmem_devs[i].cdev);
}
class_destroy(globalmem_class);
unregister_chrdev_region(MKDEV(globalmem_major, 0), DEVICE_NUM);
pr_info("globalmem: 驱动已卸载\n");
}
module_init(globalmem_init);
module_exit(globalmem_exit);
MODULE_LICENSE("GPL v2");
MODULE_DESCRIPTION("并发安全的 globalmem 字符设备驱动");
本章小结
| 章节 | 核心知识点 | 关键 API |
|---|---|---|
| 7.1 并发与竞态 | 并发的4个来源(SMP/抢占/中断/软中断);竞态的概念;临界区 | 概念理解 |
| 7.2 编译/执行乱序 | 编译器重排;CPU 乱序执行;编译屏障;内存屏障 | barrier()、mb()、rmb()、wmb() |
| 7.3 中断屏蔽 | 禁止本地中断;保存/恢复中断状态;局限性 | local_irq_save()、local_irq_restore() |
| 7.4 原子操作 | 原子整数操作;原子位操作;引用计数;设备独占 | atomic_inc()、atomic_dec_and_test()、set_bit() |
| 7.5 自旋锁 | 忙等待原理;irqsave 版本;使用规则 | spin_lock_irqsave()、spin_unlock_irqrestore() |
| 7.6 读写自旋锁 | 读者共享/写者独占;读多写少场景 | read_lock()、write_lock() |
| 7.7 顺序锁 | 写者优先;序列号机制;读者重试 | write_seqlock()、read_seqbegin()、read_seqretry() |
| 7.8 RCU | 读-复制-更新;宽限期;零开销读 | rcu_read_lock()、rcu_dereference()、synchronize_rcu() |
| 7.9 信号量 | 可睡眠等待;计数信号量;P/V 操作 | down_interruptible()、up() |
| 7.10 互斥体 | 专用互斥锁;优先级继承;使用规则 | mutex_lock_interruptible()、mutex_unlock() |
| 7.11 完成量 | 线程同步;等待/通知机制;超时等待 | wait_for_completion_timeout()、complete() |
| 7.12 并发安全globalmem | mutex保护内存;atomic引用计数;completion初始化同步 | 综合应用 |
并发控制机制选择指南
选择并发控制机制的决策树:
是否在中断上下文?
是 → 使用原子操作 或 自旋锁(spin_lock)
否 → 继续判断
临界区是否很短(< 几十微秒)?
是 → 使用自旋锁(spin_lock_irqsave)
否 → 继续判断
是否读多写少?
是 → 使用读写自旋锁(rwlock)或 RCU
否 → 继续判断
是否需要等待某个事件完成?
是 → 使用完成量(completion)
否 → 使用互斥体(mutex)
特殊场景:
简单整数计数 → 原子操作(atomic_t)
写者优先的读写 → 顺序锁(seqlock)
极致读性能 → RCU
参考文献:宋宝华《Linux设备驱动开发详解:基于最新的Linux 4.0内核》,机械工业出版社,2015年