Linux并发与竞争:从生活例子到内核实战

发布于:2025-05-01 ⋅ 阅读:(25) ⋅ 点赞:(0)

Linux并发与竞争:从生活例子到内核实战

一、并发与竞争:多车道公路的交通问题

想象一条四车道的高速公路(多核CPU),所有车辆(线程/进程)都想通过同一个收费站(共享资源)。如果没有交通规则(同步机制),就会发生:

  1. 数据竞争:两辆车同时抢一个收费口,导致碰撞(数据损坏)
  2. 死锁:四辆车互相阻挡,形成十字死锁(deadlock)
  3. 饥饿:大货车长期占用车道,小车永远过不去(starvation)

二、原子操作:不可分割的收费卡

2.1 原子操作原理

就像高速公路的ETC系统,收费过程(i++这样的操作)必须一次性完成,不能被其他车辆打断。内核提供两类原子操作:

2.2 原子整形操作

atomic_t v = ATOMIC_INIT(0);  // 初始化原子变量
atomic_inc(&v);               // v++(不可打断)
int val = atomic_read(&v);    // 安全读取

生活例子:银行金库的黄金数量统计,必须整块计算,不能出现"数到一半被其他人拿走"的情况。

2.3 原子位操作

unsigned long word = 0;
set_bit(0, &word);  // 第0位置1(类似|=操作)
clear_bit(1, &word); // 第1位清0

适用场景:设备寄存器位操作、标志位管理。

三、自旋锁:不停旋转的收费站岗亭

3.1 自旋锁特性

  • 忙等待:获取不到锁时,CPU会"空转"(就像司机在收费口不停探头张望)
  • 短期持有:适合锁持有时间短于线程切换开销的情况
DEFINE_SPINLOCK(my_lock);  // 定义锁

spin_lock(&my_lock);       // 获取锁
/* 临界区操作 */           // 只有一辆车能通过
spin_unlock(&my_lock);     // 释放锁

3.2 锁的类型选择指南

锁类型 生活类比 适用场景
普通自旋锁 普通收费岗亭 单核CPU/非中断上下文
IRQ自旋锁 应急车道专用岗亭 中断上下文共享数据
读写自旋锁 ETC与人工通道分离 读多写少场景(如配置表)

3.3 死锁预防(交通规则)

  1. 避免嵌套锁:不要在一个锁内获取另一个锁
  2. 统一顺序:多个锁按固定顺序获取
  3. 超时机制spin_trylock_irqsave尝试获取锁

四、信号量:停车场剩余车位显示

4.1 信号量特点

  • 允许睡眠:获取不到资源时,线程可以休眠(不像自旋锁那样空转)
  • 资源计数:可以管理多个同类资源(如停车场剩余车位)
struct semaphore sem;
sema_init(&sem, 5);  // 初始化5个车位

down(&sem);          // 获取车位(没有就睡觉)
/* 停车操作 */
up(&sem);            // 释放车位

生活场景

  • 消费者生产者问题(停车场进出车辆)
  • 打印机池管理(多个打印任务排队)

五、互斥体:单间厕所的门锁

5.1 互斥体特性

  • 唯一访问:同一时刻只有一个线程能进入临界区
  • 可睡眠:比自旋锁更适合长时间持有的场景
struct mutex my_mutex;
mutex_init(&my_mutex);

mutex_lock(&my_mutex);
/* 临界区操作 */      // 如厕所单间使用
mutex_unlock(&my_mutex);

5.2 与自旋锁的对比

特性 自旋锁 互斥体
等待方式 CPU空转 线程休眠
开销 低(无上下文切换) 高(需调度)
持有时间 短(纳秒级) 长(毫秒级)
中断上下文 可用 不可用

六、实战选择流程图

graph TD
    A[需要保护共享资源?] -->|是| B{临界区执行时间}
    B -->|短(<10us)| C[自旋锁]
    B -->|长(>10us)| D{是否在中断上下文}
    D -->|是| C
    D -->|否| E[互斥体]
    A -->|否| F[无需同步]
    C --> G{是否需要读写分离}
    G -->|是| H[读写自旋锁]
    G -->|否| I[普通自旋锁]
    E --> J{资源是否可计数}
    J -->|是| K[信号量]
    J -->|否| E

七、常见错误案例

7.1 错误示范:中断中误用互斥体

// 错误代码(中断上下文不能睡眠)
irq_handler() {
    mutex_lock(&lock);  // 可能引发系统崩溃
    /* 操作硬件寄存器 */
    mutex_unlock(&lock);
}

// 正确做法(使用自旋锁)
irq_handler() {
    spin_lock_irqsave(&lock, flags);
    /* 操作硬件 */
    spin_unlock_irqrestore(&lock, flags);
}

7.2 错误示范:忘记释放锁

void func() {
    mutex_lock(&lock);
    if (error) {
        return; // 直接返回导致锁未释放!
    }
    mutex_unlock(&lock);
}

// 正确做法(使用goto统一释放)
void func() {
    mutex_lock(&lock);
    if (error) {
        goto unlock;
    }
unlock:
    mutex_unlock(&lock);
}

八、性能优化技巧

  1. 减小临界区:只把必须保护的代码放在锁内

    // 不好
    mutex_lock(&lock);
    process_data(data);
    save_to_disk(data);
    mutex_unlock(&lock);
    
    // 优化后
    mutex_lock(&lock);
    tmp = data; // 只保护数据拷贝
    mutex_unlock(&lock);
    process_data(tmp);
    save_to_disk(tmp);
    
  2. 读写分离:读多写少时用读写锁

    DEFINE_RWLOCK(rwlock);
    
    // 读线程
    read_lock(&rwlock);
    /* 只读操作 */
    read_unlock(&rwlock);
    
    // 写线程
    write_lock(&rwlock);
    /* 写操作 */
    write_unlock(&rwlock);
    

记住关键原则:锁的粒度要尽可能小,持有时间要尽可能短


网站公告

今日签到

点亮在社区的每一天
去签到