Linux并发与竞争:从生活例子到内核实战
Linux并发与竞争:从生活例子到内核实战
一、并发与竞争:多车道公路的交通问题
想象一条四车道的高速公路(多核CPU),所有车辆(线程/进程)都想通过同一个收费站(共享资源)。如果没有交通规则(同步机制),就会发生:
- 数据竞争:两辆车同时抢一个收费口,导致碰撞(数据损坏)
- 死锁:四辆车互相阻挡,形成十字死锁(deadlock)
- 饥饿:大货车长期占用车道,小车永远过不去(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 死锁预防(交通规则)
- 避免嵌套锁:不要在一个锁内获取另一个锁
- 统一顺序:多个锁按固定顺序获取
- 超时机制:
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 TDA[需要保护共享资源?] -->|是| B{临界区执行时间}B -->|短(<10us)| C[自旋锁]B -->|长(>10us)| D{是否在中断上下文}D -->|是| CD -->|否| 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);
}
八、性能优化技巧
-
减小临界区:只把必须保护的代码放在锁内
// 不好 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);
-
读写分离:读多写少时用读写锁
DEFINE_RWLOCK(rwlock);// 读线程 read_lock(&rwlock); /* 只读操作 */ read_unlock(&rwlock);// 写线程 write_lock(&rwlock); /* 写操作 */ write_unlock(&rwlock);
记住关键原则:锁的粒度要尽可能小,持有时间要尽可能短。