【RK3588 Android12】同步机制选型指南

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. 总结

同步机制选型要点:

  1. 优先考虑:能否睡眠、是否在中断上下文
  2. 性能考虑:临界区长度、读写比例
  3. 简单优先:能用原子操作就不用锁
  4. 避免过度:不要过度使用锁
  5. 测试验证:实际测试性能和正确性

选型口诀

  • 简单计数用原子
  • 中断场景用自旋
  • 可睡眠用互斥
  • 读多写少用RCU
  • 资源计数用信号量

参考资料

  1. Linux内核同步机制
  2. Unreliable Guide To Locking
  3. Linux设备驱动程序 - 并发与竞态
相关推荐
sweetone7 小时前
飞利浦HX333S冲牙器原理及维修
单片机·嵌入式硬件
云山工作室7 小时前
基于单片机的飞机客舱窗帘控制系统(论文+源码)
stm32·单片机·嵌入式硬件·毕业设计
llilian_168 小时前
延迟信号发生器 延迟脉冲信号发生器在激光触发领域的应用 高速脉冲信号发生器
功能测试·单片机·嵌入式硬件·测试工具·自动化
代码游侠8 小时前
ARM嵌入式开发代码实践——LED灯闪烁(汇编版)
arm开发·笔记·嵌入式硬件·学习·架构
不怕犯错,就怕不做8 小时前
RK3562+RK817在关机状态下提升充电电流至2A解决方案
linux·驱动开发·嵌入式硬件
2023自学中9 小时前
Cortex-M系列,Cortex-A系列,汇编启动文件的区别
linux·嵌入式硬件
三伏5229 小时前
stm32f103系列手册IIC笔记2
笔记·stm32·嵌入式硬件
国科安芯9 小时前
RISC-V架构抗辐照MCU在航天器载荷中的SEU/SEL阈值测试与防护策略
单片机·嵌入式硬件·安全·架构·安全威胁分析·risc-v