分类: LINUX
2010-08-19 19:06:54
8.1 临界区和竞争条件
所谓临界区就是访问和操作共享数据的代码段。多个执行线程并发访问同一个资源通常是不安全的,为了避免在临界区中并发访问,编程者必须保证这些代码原子的 执行——也就是说,代码在执行结束前不可被打断,就如同整个临界区是一个不可分割的指令一样。如果两个执行线程有可能出于同一个临界区,那么这就是程序包 含的一个bug。如果这种情况确实发生了,我们就称它是竞争条件(race condition),这样命名是因为这里会存在线程竞争。避免并发和防止竞争条件(个人感觉这里的condition翻译成情形更为恰当)被称作同步。
8.2 加锁
现在我们来讨论一个更为复杂的竞争条件,相应的解决方法也更为复杂。假设需要处理一个队列上的所有服务请求,我们可以任意选一种方法实现这种队列,这里我 们假定该队列是一个链表,链表中的每个节点就代表一个请求。有两个函数可以用来操作此队列:一个函数将新请求添加到队列尾部,另一个函数从队列头删除请 求,然后处理它。内核各个部分都会调用这两个函数,所以内核会频繁的在队列中加入请求,从队列中删除和处理请求。对请求队列的操作无疑要用到多条指令。如 果一个线程试图读取队列,而这时正好另一个线程正在处理该队列,那么读取线程就会发现队列此刻正处于不一致状态。很明显,如果允许并发访问队列,就会产生 危害。我们需要一种方法确保一次有且只有一个线程对数据结构操作,或者当另一个线程在对临界区标记时,就禁止其他访问。
锁提供的就是这种机制: 它就如同一把锁,门内的房间就是一个临界区。在一个指定的时间里,房间只能有一个执行线程存在,当一个线程进入房间后,它会锁住房门,当他结束对共享数据 的操作后,就会走出房间,打开门锁。如果另一个线程在门上锁的时候来了,就必须等到房间内的线程出来并打开锁后,才能进入房间。
锁的使用是自愿的、非强制性的,它完全属于一种编程者自选的编程手段。没有什么可以强制编程者在操作我们虚构的队列时必须使用锁。
在中断处理程序中能避免并发访问的安全代码称作中断安全代码,在对称多处理的机器中能避免并发访问的安全代码称为SMP安全代码,在内核抢占时能避免并发访问的安全代码称为抢占安全代码。
要保护些什么
找出哪些数据需要保护是关键所在。由于任何可能被并发访问的代码都可能需要保护,所以寻找那些代码不需要保护反而更容易些,我们也就从这里入手。执行线程 的局部数据仅仅被他本身访问,显然不需要保护,比如,局部自动变量(还有动态分配的数据结构,其地址仅存放在堆栈中)不需要任何形式的锁,因为他们独立存 在于执行线程的栈中。同样,如果数据只是被特定的进程访问,那么也不需要加锁。
到底什么数据需要加锁呢?如果其他执行线程可以访问这些数据,那么就给这些数据加上某种形式的锁,如果任何其他什么东西能看到它,就要锁住它。记住:要给数据加锁而不是代码。
8.3 死锁
预防死锁的发生非常重要,虽然很难证明代码不会发生死锁,但是可以写出避免死锁的代码,以下是一些简单的规则:
(1)加锁的顺序是关键。使用嵌套的锁时必须保证以相同的顺序获取锁,这样可以阻止致命拥抱类型的死锁。最好能记录下锁的顺序,以便其他人也能照此顺序使用。
(2)防止发生饥饿。试问,这个代码的执行是否一定会结束?
(3)不可重复请求同一个锁。
(4)越复杂的加锁方案越有可能造成死锁——设计应力求简单。