1. 概述
Linux内核提供了多种同步机制,选择合适的同步机制对驱动性能和稳定性至关重要。本文系统介绍各种同步机制的特点、适用场景和选型决策。
1.1 同步机制分类
同步机制
├── 原子操作
│ ├── atomic_t
│ └── 位操作
├── 锁机制
│ ├── 自旋锁 (spinlock)
│ ├── 读写自旋锁 (rwlock)
│ ├── 顺序锁 (seqlock)
│ ├── 互斥锁 (mutex)
│ └── 信号量 (semaphore)
├── RCU机制
└── 其他
├── 完成量 (completion)
└── 等待队列 (wait_queue)
2. 同步机制对比
2.1 性能对比表
| 机制 | 开销 | 可睡眠 | 中断上下文 | 适用场景 |
|---|---|---|---|---|
| 原子操作 | 极低 | 否 | 是 | 简单计数器 |
| 自旋锁 | 低 | 否 | 是 | 临界区短 |
| 读写锁 | 中 | 否 | 是 | 读多写少 |
| 顺序锁 | 低 | 否 | 是 | 读极多 |
| RCU | 极低(读) | 否 | 是 | 读多写少 |
| 互斥锁 | 高 | 是 | 否 | 临界区长 |
| 信号量 | 高 | 是 | 否 | 资源计数 |
2.2 决策树
开始
│
├─ 只需要简单计数?
│ └─ 是 → 原子操作
│
├─ 在中断上下文?
│ ├─ 是 → 自旋锁/RCU
│ └─ 否 → 继续
│
├─ 临界区会睡眠?
│ ├─ 是 → 互斥锁/信号量
│ └─ 否 → 继续
│
├─ 读写比例?
│ ├─ 读多写少(>10:1) → RCU
│ ├─ 读多写少(<10:1) → 读写锁
│ └─ 读写均衡 → 自旋锁/互斥锁
│
└─ 临界区长度?
├─ 很短(<100us) → 自旋锁
└─ 较长(>100us) → 互斥锁
3. 原子操作
3.1 适用场景
✅ 适合:
- 简单的计数器(引用计数、统计)
- 标志位设置
- 无需保护复杂数据结构
❌ 不适合:
- 需要保护多个变量
- 需要执行复杂操作
3.2 示例代码
// 引用计数
struct my_device {
atomic_t refcount;
// ...
};
static void device_get(struct my_device *dev)
{
atomic_inc(&dev->refcount);
}
static void device_put(struct my_device *dev)
{
if (atomic_dec_and_test(&dev->refcount)) {
// 释放设备
kfree(dev);
}
}
// 状态标志
static atomic_t device_ready = ATOMIC_INIT(0);
if (atomic_read(&device_ready)) {
// 设备已就绪
}
3.3 性能特点
- 开销:1-2个CPU周期
- 适用:高频操作(每秒百万次)
4. 自旋锁
4.1 适用场景
✅ 适合:
- 临界区很短(<100微秒)
- 中断上下文
- 不能睡眠的场景
- 多核竞争不激烈
❌ 不适合:
- 临界区较长
- 需要睡眠
- 单核系统(效率低)
4.2 示例代码
static DEFINE_SPINLOCK(my_lock);
static int shared_data;
// 进程上下文
void update_data(int value)
{
spin_lock(&my_lock);
shared_data = value;
spin_unlock(&my_lock);
}
// 中断上下文
irqreturn_t my_interrupt(int irq, void *dev_id)
{
unsigned long flags;
spin_lock_irqsave(&my_lock, flags);
shared_data++;
spin_unlock_irqrestore(&my_lock, flags);
return IRQ_HANDLED;
}
4.3 变种选择
// 1. 基本自旋锁(进程上下文,无中断)
spin_lock(&lock);
spin_unlock(&lock);
// 2. 禁用本地中断(有中断访问)
spin_lock_irqsave(&lock, flags);
spin_unlock_irqrestore(&lock, flags);
// 3. 禁用软中断(软中断上下文)
spin_lock_bh(&lock);
spin_unlock_bh(&lock);
// 4. 禁用抢占(单核优化)
spin_lock(&lock); // 自动禁用抢占
spin_unlock(&lock);
5. 读写锁
5.1 适用场景
✅ 适合:
- 读操作多于写操作(2:1 ~ 10:1)
- 读操作之间不互斥
- 临界区较短
❌ 不适合:
- 读写比例接近1:1
- 写操作频繁
- 读操作极多(用RCU更好)
5.2 示例代码
static DEFINE_RWLOCK(data_lock);
static struct data_struct shared_data;
// 读操作(多个读者可并发)
int read_data(void)
{
int value;
read_lock(&data_lock);
value = shared_data.value;
read_unlock(&data_lock);
return value;
}
// 写操作(独占访问)
void write_data(int value)
{
write_lock(&data_lock);
shared_data.value = value;
shared_data.timestamp = jiffies;
write_unlock(&data_lock);
}
5.3 性能分析
读写比例 自旋锁 读写锁 RCU
1:1 100% 90% N/A
5:1 100% 150% 300%
10:1 100% 200% 500%
100:1 100% 300% 1000%
6. RCU机制
6.1 适用场景
✅ 适合:
- 读操作远多于写操作(>10:1)
- 数据结构较小
- 可以接受写延迟
- 指针或链表保护
❌ 不适合:
- 读写比例接近
- 数据结构很大
- 需要立即释放
- 读者需要长时间持有
6.2 示例代码
struct config __rcu *global_config;
// 读操作(无锁,极快)
int get_config_value(void)
{
struct config *cfg;
int value;
rcu_read_lock();
cfg = rcu_dereference(global_config);
value = cfg ? cfg->value : 0;
rcu_read_unlock();
return value;
}
// 写操作(较慢)
void update_config(int new_value)
{
struct config *old_cfg, *new_cfg;
new_cfg = kmalloc(sizeof(*new_cfg), GFP_KERNEL);
new_cfg->value = new_value;
old_cfg = global_config;
rcu_assign_pointer(global_config, new_cfg);
synchronize_rcu();
kfree(old_cfg);
}
6.3 性能特点
- 读开销:几乎为零
- 写开销:较高(需要复制和等待)
- 最佳场景:读写比 > 100:1
7. 互斥锁
7.1 适用场景
✅ 适合:
- 临界区较长(>100微秒)
- 可以睡眠的场景
- 进程上下文
- 需要优先级继承
❌ 不适合:
- 中断上下文
- 临界区很短
- 高频操作
7.2 示例代码
static DEFINE_MUTEX(dev_mutex);
// 文件操作
static ssize_t device_read(struct file *file, char __user *buf,
size_t count, loff_t *ppos)
{
int ret;
if (mutex_lock_interruptible(&dev_mutex))
return -ERESTARTSYS;
// 可能睡眠的操作
ret = copy_to_user(buf, data, count);
mutex_unlock(&dev_mutex);
return ret;
}
// 可中断版本
if (mutex_lock_interruptible(&dev_mutex)) {
return -ERESTARTSYS; // 被信号中断
}
// 超时版本
if (mutex_lock_timeout(&dev_mutex, msecs_to_jiffies(1000))) {
return -ETIMEDOUT; // 超时
}
7.3 与自旋锁对比
| 特性 | 自旋锁 | 互斥锁 |
|---|---|---|
| 等待方式 | 忙等待 | 睡眠等待 |
| 开销 | 低 | 高 |
| 临界区 | 短 | 长 |
| 中断上下文 | 可用 | 不可用 |
| 优先级继承 | 无 | 有 |
8. 信号量
8.1 适用场景
✅ 适合:
- 资源计数(如缓冲区)
- 生产者-消费者模型
- 需要计数功能
❌ 不适合:
- 简单互斥(用mutex更好)
- 中断上下文
8.2 示例代码
// 缓冲区管理
#define BUFFER_SIZE 10
static DEFINE_SEMAPHORE(empty_slots, BUFFER_SIZE);
static DEFINE_SEMAPHORE(filled_slots, 0);
// 生产者
void produce_item(void)
{
down(&empty_slots); // 等待空槽位
// 生产数据
add_to_buffer(item);
up(&filled_slots); // 增加已填充槽位
}
// 消费者
void consume_item(void)
{
down(&filled_slots); // 等待已填充槽位
// 消费数据
item = get_from_buffer();
up(&empty_slots); // 增加空槽位
}
9. 实际案例分析
9.1 案例1:设备状态管理
需求:管理设备开关状态
// 方案1:原子操作(推荐)
static atomic_t device_state = ATOMIC_INIT(0);
void set_device_state(int state)
{
atomic_set(&device_state, state);
}
int get_device_state(void)
{
return atomic_read(&device_state);
}
选择理由:
- 只有一个变量
- 操作简单
- 高频访问
- 原子操作最合适
9.2 案例2:设备配置管理
需求:管理多个配置参数,读多写少
// 方案1:RCU(推荐)
struct device_config {
int param1;
int param2;
char name[32];
};
static struct device_config __rcu *dev_config;
// 读取(无锁,高性能)
int get_param1(void)
{
struct device_config *cfg;
int value;
rcu_read_lock();
cfg = rcu_dereference(dev_config);
value = cfg->param1;
rcu_read_unlock();
return value;
}
// 更新(较少调用)
void update_config(int p1, int p2)
{
struct device_config *old, *new;
new = kmalloc(sizeof(*new), GFP_KERNEL);
new->param1 = p1;
new->param2 = p2;
old = dev_config;
rcu_assign_pointer(dev_config, new);
synchronize_rcu();
kfree(old);
}
选择理由:
- 读操作频繁(每秒数千次)
- 写操作很少(每分钟几次)
- 数据结构小
- RCU性能最优
9.3 案例3:中断与进程共享数据
需求:中断处理程序和进程上下文共享计数器
// 方案1:自旋锁(推荐)
static DEFINE_SPINLOCK(counter_lock);
static int counter;
// 进程上下文
void process_update(void)
{
unsigned long flags;
spin_lock_irqsave(&counter_lock, flags);
counter++;
spin_unlock_irqrestore(&counter_lock, flags);
}
// 中断上下文
irqreturn_t irq_handler(int irq, void *dev_id)
{
spin_lock(&counter_lock);
counter++;
spin_unlock(&counter_lock);
return IRQ_HANDLED;
}
选择理由:
- 中断上下文不能睡眠
- 临界区很短
- 必须使用自旋锁
9.4 案例4:文件操作保护
需求:保护设备文件的读写操作
// 方案1:互斥锁(推荐)
static DEFINE_MUTEX(file_mutex);
static ssize_t device_read(struct file *file, char __user *buf,
size_t count, loff_t *ppos)
{
ssize_t ret;
if (mutex_lock_interruptible(&file_mutex))
return -ERESTARTSYS;
// 可能的长时间操作
ret = do_read_operation(buf, count);
mutex_unlock(&file_mutex);
return ret;
}
选择理由:
- 进程上下文
- 可能睡眠(copy_to_user)
- 临界区较长
- 互斥锁最合适
10. 选型决策表
10.1 快速选型表
| 场景 | 推荐机制 | 备选方案 |
|---|---|---|
| 简单计数器 | 原子操作 | - |
| 中断+进程共享 | 自旋锁 | - |
| 短临界区(<100us) | 自旋锁 | - |
| 长临界区(>100us) | 互斥锁 | 信号量 |
| 读多写少(2:1~10:1) | 读写锁 | 顺序锁 |
| 读多写少(>10:1) | RCU | 读写锁 |
| 资源计数 | 信号量 | - |
| 等待事件 | 完成量/等待队列 | - |
10.2 性能优先级
性能从高到低:
1. 原子操作
2. RCU(读操作)
3. 自旋锁
4. 读写锁
5. 顺序锁
6. 互斥锁
7. 信号量
11. 常见错误
11.1 错误1:在中断中使用互斥锁
// 错误示例
irqreturn_t my_irq(int irq, void *dev_id)
{
mutex_lock(&my_mutex); // 错误!中断中不能睡眠
// ...
mutex_unlock(&my_mutex);
return IRQ_HANDLED;
}
// 正确示例
irqreturn_t my_irq(int irq, void *dev_id)
{
spin_lock(&my_lock);
// ...
spin_unlock(&my_lock);
return IRQ_HANDLED;
}
11.2 错误2:自旋锁保护长临界区
// 错误示例
spin_lock(&my_lock);
msleep(100); // 错误!自旋锁不能睡眠
spin_unlock(&my_lock);
// 正确示例
mutex_lock(&my_mutex);
msleep(100); // 正确
mutex_unlock(&my_mutex);
11.3 错误3:过度使用锁
// 错误示例
static DEFINE_SPINLOCK(lock);
static int counter;
void increment(void)
{
spin_lock(&lock);
counter++; // 简单操作不需要锁
spin_unlock(&lock);
}
// 正确示例
static atomic_t counter = ATOMIC_INIT(0);
void increment(void)
{
atomic_inc(&counter);
}
12. 调试技巧
12.1 死锁检测
# 启用lockdep
CONFIG_PROVE_LOCKING=y
CONFIG_DEBUG_LOCK_ALLOC=y
# 查看死锁信息
dmesg | grep -i deadlock
12.2 锁统计
# 启用锁统计
echo 1 > /proc/sys/kernel/lock_stat
# 查看统计
cat /proc/lock_stat
13. 总结
同步机制选型要点:
- 优先考虑:能否睡眠、是否在中断上下文
- 性能考虑:临界区长度、读写比例
- 简单优先:能用原子操作就不用锁
- 避免过度:不要过度使用锁
- 测试验证:实际测试性能和正确性
选型口诀:
- 简单计数用原子
- 中断场景用自旋
- 可睡眠用互斥
- 读多写少用RCU
- 资源计数用信号量