全部博文(573)
分类: LINUX
2015-12-09 15:49:55
并发与竞态
并发(concurrency)环境中的多个进程如果需要访问同一个临界资源,在不加以保护的情况下,很容易导致竞态(race condition)。
为了避免某临界资源的临界区被并发执行,编程者必须保证临界区原子地执行。
竞争状态出现的几率非常小,但是这种因竞争引起的错误非常不易重视,所以调试这种错误才会非常困难。
避免并发和防止竞态出现的机制被称为同步(synchronization)。
锁机制
任何需要进入临界区的进程首先需要占住对应的锁,这样就能阻止来自其他线程的并发访问。需要注意的是,锁的应用是自愿的,非强制的。
锁有多种多样的形式,而且加锁的粒度范围也各不相同。Linux自身实现了几种不同的锁机制,各种锁机制之间的区别主要在于锁被争用时的行为表现 --- 一些锁被争用时会简单地执行忙等待,而有些锁会使当前任务睡眠直到锁可用为止。
到底是什么造成了并发执行
内核中有以下可能造成并发执行的情况:
· 中断 —— 中断几乎可以在任何时刻异步发生,也就可能随时打断当前正在执行的代码。(中断服务程序访问被打断进程正在访问的资源)
· 软中断和tasklet —— 内核能在任何时刻唤醒或调度软中断和tasklet,打断当前正在执行的代码。
· 内核抢占 —— 因为内核具有抢占性,所以内核中的任务可能会被另一任务抢占。
· 睡眠以及用户空间的同步 —— 在内核执行的进程可能会睡眠,这就会唤醒调度程序,从而导致调度一个新的用户进程执行。(内核代码在临界区中睡眠)
· 对称对处理器 —— 两个或多个处理器可以同时执行代码。(多个处理器绝对不能同时访问同一共享资源)
当我们清楚什么样的数据需要保护时,提供锁来保护代码的安全也就不难做到了。然而,真正困难的却是发现上述这些潜在并发执行的可能,并有意识地采取某些措施来防止并发执行。
基本原则:在编写代码的开始阶段就要设计恰当的锁。
中断安全代码(interrupt-safe) —— 中断处理程序中能避免并发访问的代码。
SMP安全代码(SMP-safe) —— 对称多处理的机器中能避免并发访问的代码。
抢占安全代码(preempt-safe) —— 在内核抢占时能避免并发访问的安全代码。
要保护些什么
找出哪些数据需要保护是关键所在。
到底什么数据需要加锁呢?大多数内核数据结构都需要加锁!有一条很好的经验可以帮助我们判断:如果有其他执行线程可以访问这些数据,那么就给这些数据加上某种形式的锁;如果任何其他什么东西都能看见这个数据,那么就要锁住它。要给数据而不是代码加锁。
死锁
死锁的产生需要一定的条件:一个或多个执行线程和一个或多个资源,每个线程都在等待被其他线程持有的资源。
以下是一些简单的规则对避免死锁有很大帮助:
1. 加锁的顺序。使用嵌套的锁时必须保证以相同的顺序获取锁,这样可以阻止致命拥抱类型的死锁。尽管是否锁的顺序和死锁无关,但最好还是以获得锁相反的顺序来释放锁。
2. 防止发生饥饿。
3. 不要重复请求同一个锁。
4. 加锁力求简单。