首先先统计下Linux应用程序下进程间、线程间的同步与互斥方式:
进程间:
无名管道(pipe)及有名管道(fifo);
信号(signal);
消息队列(message queue);
共享内存(shared memory);
信号量(semaphore);
套接字(socket)。
线程间:
互斥锁(mutex);
信号量(sem);
接着说下Linux内核中的并发与竞态:
在Linux内核中, 主要的竞态发生于以下3种情况:
1.对称多处理器( SMP) 的多个CPU;
2.单CPU内进程与抢占它的进程;
3.中断( 硬中断、 软中断、 Tasklet、 底半部) 与进程之间。
如图所示:
问共享资源的代码区域称为临界区( Critical Sections) , 临界区需要被以某种互斥机制加以保护。中断屏蔽、 原子操作、 自旋锁、 信号量、 互斥体等是Linux设备驱动中可采用的互斥途径。(另外,还有完成量,用于同步)各种情况下可以使用的互斥途径。
上图显示的不推荐及基础自旋锁代表了上述的互斥手段需要搭配使用。如
整套自旋锁机制:(针对SMP或者单CPU且内核可抢占的系统,还可以避免中断带来的影响)
自旋锁+中断屏蔽
在每个核的进程的上下文中应调用一组函数: spin_lock_irqsave()/spin_unlock_irqstore() ,同时在中断上下文中应调用另一组函数: spin_lock()/spin_unlock() ,这是为了确保在SMP平台上的可移植性。
而自旋锁也可以进行优化和衍生,如:
自旋锁 (衍生之后) 读写锁 (优化之后) 顺序锁
(高性能后) 读-复制-更新(RCU)
衍生与优化之后出现的各种锁可以适用于各种不同的条件。
接下来说信号量和自旋锁区别:
- 如果作为互斥手段的话,新版Linux内核倾向于使用mutex互斥体,信号量不再被推荐使用。
- 但是对于关心具体数值的生产者/消费者问题,使用信号量较为合适。
- 信号量也可以用于同步。
- 互斥体的实现依赖于自旋锁,自旋锁属于更底层的手段。
- 当获取不到信号量时,进程进入休眠(睡眠)等待状态,而当获取不到自旋锁时,进程原地打转。
互斥体和自旋锁选用的3点原则:
- 考虑开销:当资源不能被获取到时, 使用互斥体的开销是进程上下文切换时间, 使用自旋锁的开销是等待获取自旋锁( 由临界区执行时间决定) 。 若临界区比较小, 宜使用自旋锁, 若临界区很大, 应使用互斥体 。
- 考虑阻塞和死锁:互斥体所保护的临界区可包含可能引起阻塞的代码, 而自旋锁则绝对要避免用来保护包含这样代码的临界区。 因为阻塞意味着要进行进程的切换, 如果进程被切换出去后, 另一个进程企图获取本自旋锁, 死锁就会发生。 所以对于临界区含有可能引起阻塞的代码(如copy_from_user()、copy_to_user()、kmalloc()、msleep()等)这种情况,只能使用互斥体。另外,如果是递归使用一个自旋锁,即如果一个已经拥有某个自旋锁的CPU想第二次获得这个自旋锁,则该CPU将死锁。
- 考虑中断:互斥体存在于进程上下文, 是进程级的。因此, 如果被保护的共享资源需要在中断或软中断情况下使用, 则在互斥体和自旋锁之间只能选择自旋锁。 当然, 如果一定要使用互斥体, 则只能通过mutex_trylock()方式进行, 不能获取就立即返回以避免阻塞。