您的位置:首页 > 其它

内核同步与互斥的总结

2013-07-23 09:32 302 查看
前面讲了那么多内核同步与互斥的技术,现在我们就来做一个总结。

我们可以随意使用前面所述的同步技术保护共享数据结构避免竞争条件。当然,系统性能可能随所选择同步原语种类的不同而有很大变化。通常情况下,内核开发者采用下述由经验得到的法则:把系统中的并发度保持在尽可能高的程度。

系统中的并发度又取决于两个主要因素:

(1)同时运转的I/O设备数

(2)进行有效工作的CPU数

为了使I/O吞吐量最大化,应该使中断禁止保持在很短的时间。因为,当中断被禁止时,由I/O设备产生的IRQ被PIC暂时忽略,因此,就没有新的动作在这种设备上开始。

另外,为了有效地利用CPU,应该尽可能避免使用基于自旋锁的同步原语。当一个CPU执行紧指令循环等待自旋锁打开时,是在浪费宝贵的机器周期。就像我们前面所描述的,更糟糕的是:由于自旋锁对硬件高速缓存的影响而使其对系统的整体性能产生不利影响。

让我们举例说明在下列两种情况下,既可以维持较高的并发度,也可以达到同步。

如果共享的数据结构是一个单独的整数值,可以把它声明为atomic_t类型并使用原子操作对其更新。原子操作比自旋锁和中断禁止都快,只有在几个内核控制路径同时访问这个数据结构时速度才会慢下来。

把一个元素插入到共享链表的操作决不是原子的,因为这至少涉及两个指针赋值。

不过,内核有时并不用锁或禁止中断就可以执行这种插入操作。我们把这种操作的工作机制作为例子来进行说明。考虑一种情况,系统调用服务例程把新元素插入到一个简单链表中,而中断处理程序或可延迟函数异步地查看该链表。

在C语言中,插入是通过下列指针赋值实现的:

new->next = list_element->next;

list_element->next = new;

在汇编语言中,插入简化为两个连续的原子指令。第一条指令建立new元素的next指针,但不修改链表。因此,如果中断处理程序在第一条指令和第二条指令执行的中间查看这个链表,看到的就是没有新元素的链表。如果该处理程序在第二条指令执行后查看链表,就会看到有新元素的链表。关键是,在任一种情况下,链表都是一致的且处于未损坏状态。然而,只有在中断处理程序不修改链表的情况下才能确保这种完整性(写还是要加锁的,只是读可以这么优化下)。如果修改了链表,那么在new元素内刚刚设置的next指针就可能变为无效的。

然而,开发者必须确保两个赋值操作的顺序不被编译器或CPU控制单元搅乱;否则,如果中断处理程序在两个赋值之间中断了系统调用服务例程,处理程序就会看到一个损坏的链表。因此,就需要一个写内存屏障原语:

new->next = list_element->next;

wmb( );

list_element->next = new;

遗憾的是,对大多数内核数据结构的访问模式非常复杂,远不像上例所示的那样简单,于是,迫使内核开发者使用信号量、自旋锁、中断禁止和软中断禁止。一般来说,同步原语的选取取决于访问数据结构的内核控制路径的种类,如下表所示。记住,只要内核控制路径获得自旋锁(还有读/写锁、顺序锁或RCU“读锁”),自动禁用内核抢占,如果中断需要访问这些数据,就禁用本地中断或本地软中断。

访问数据结构的内核控制路径

单处理器保护

多处理器进一步保护

异常

信号量



中断

本地中断禁止

自旋锁

可延迟函数



无或自旋锁

异常与中断

本地中断禁止

自旋锁

异常与可延迟函数

本地软中断禁止

自旋锁

中断与可延迟函数

本地中断禁止

自旋锁

异常、中断与可延迟函数

本地中断禁止

自旋锁

1 保护异常所访问的数据结构

当一个数据结构仅由异常处理程序访问时,竞争条件通常是易于理解也易于避免的。最常见的产生同步问题的异常就是系统调用服务例程,在这种情况下,CPU运行在内核态而为用户态程序提供服务。所以,仅由异常访问的数据结构通常表示一种资源,可以分配给一个或多个进程。

竞争条件可以通过信号量避免,因为信号量原语允许进程睡眠到资源变为可用。注意,信号量工作方式在单处理器系统和多处理器系统上完全相同。

内核抢占不会引起太大的问题。如果一个拥有信号量的进程是可以被抢占的,运行在同一个CPU上的新进程就可能试图获得这个信号量。在这种情况下,让新进程处于睡眠状态,而且原来拥有信号量的进程最终会释放信号量。只有在访问每CPU变量的情况下,必须显式地禁用内核抢占,就像在本单元前面“每CPU变量
”一节中所描述的那样。

2 保护中断所访问的数据结构

假定一个数据结构仅被中断处理程序的“上半部分”访问。我们在中断专题中了解到每个中断处理程序都相对自己串行地执行——也就是说,中断处理程序本身不能同时多次运行。因此,访问数据结构就无需任何同步技术。

但是,如果多个中断处理程序访问一个数据结构,情况就有所不同了。一个处理程序可以中断另一个处理程序,不同的中断处理程序可以在多处理器系统上同时运行。没有同步,共享的数据结构就很容易被破坏。

在单处理器系统上,必须通过在中断处理程序的所有临界区上禁止中断来避免竞争条件。只能用这种方式进行同步,因为其他的同步原语都不能完成这件事。信号量能够阻塞进程,因此,不能用在中断处理程序上。另一个方面,自旋锁可能使系统冻结:如果访问数据结构的处理程序被中断,它就不能释放锁;因此,新的中断处理程序在自旋锁的紧循环上保持等待,这时就发生了死锁。

===》这里是否有问题,UP系统自旋锁只是禁止内核抢占,并没有真的实现自旋??

同样,多处理器系统的要求甚至更加苛刻。不能简单地通过禁止本地中断来避免竞争条件。事实上,即使在一个CPU上禁止了中断,中断处理程序还可以在其他CPU上执行。避免竞争条件最简单的方法是禁止本地中断(以便运行在同一个CPU上的其他中断处理程序不会造成干扰),并获取保护数据结构的自旋锁或读/写自旋锁。注意,这些附加的自旋锁是不会冻结系统的,因为即使中断处理程序发现锁被关闭,在另一个CPU上拥有锁的中断处理程序最终也会释放这个锁。

Linux内核使用了几个宏,把本地中断激活/禁止与自旋锁结合起来。下表描述了其中的所有宏。在单处理器系统上,这些宏仅激活或禁止本地中断和内核抢占。



说明(相当于)
spin_lock_irq(l)
local_irq_disable( ); spin_lock(l)
spin_unlock_irq(l)
spin_unlock(l); local_irq_enable()
spin_lock_bh(l)
local_bh_disable( ); spin_lock(l)
spin_unlock_bh(l)
spin_unlock(l); local_bh_enable()
spin_lock_irqsave(l,f)
local_irq_save(f); spin_lock(l)
spin_unlock_irqrestore(l,f)
spin_unlock(l); local_irq_restore(f)
read_lock_irq(l)
local_irq_disable( ); read_lock(l)
read_unlock_irq(l)
read_unlock(l); local_irq_enable( )
read_lock_bh(l)
local_bh_disable( ); read_lock(l)
read_unlock_bh(l)
read_unlock(l); local_bh_enable( )
write_lock_irq(l)
local_irq_disable( ); write_lock(l)
write_unlock_irq(l)
write_unlock(l); local_irq_enable( )
write_lock_bh(l)
local_bh_disable( ); write_lock(l)
write_unlock_bh(l)
write_unlock(l); local_bh_enable( )
read_lock_irqsave(l,f)
local_irq_save(f); read_lock(l)
read_unlock_irqrestore(l,f)
read_unlock(l); local_irq_restore(f)
write_lock_irqsave(l,f)
local_irq_save(f); write_lock(l)
write_unlock_irqrestore(l,f)
write_unlock(l); local_irq_restore(f)
read_seqbegin_irqsave(l,f)
local_irq_save(f); read_seqbegin(l)
read_seqretry_irqrestore(l,v,f)
read_seqretry(l,v); local_irq_restore(f)
write_seqlock_irqsave(l,f)
local_irq_save(f); write_seqlock(l)
write_sequnlock_irqrestore(l,f)
write_sequnlock(l); local_irq_restore(f)
write_seqlock_irq(l)
local_irq_disable( ); write_seqlock(l)
write_sequnlock_irq(l)
write_sequnlock(l); local_irq_enable( )
write_seqlock_bh(l)
local_bh_disable( ); write_seqlock(l);
write_sequnlock_bh(l)
write_sequnlock(l); local_bh_enable( )
我们只举一个例子:

void __lockfunc _spin_lock_irq(spinlock_t *lock)

{

local_irq_disable();

preempt_disable();

spin_acquire(&lock->dep_map, 0, 0, _RET_IP_);

LOCK_CONTENDED(lock, _raw_spin_trylock, _raw_spin_lock);

}

3 保护可延迟函数所访问的数据结构

只被可延迟函数访问的数据结构需要哪种保护呢?这主要取决与可延迟函数的种类。在中断专题“下半部分 ”一博,我们说明了软中断和tasklet本质上有不同的并发度。

首先,在单处理器系统上不存在竞争条件。这是因为可延迟函数的执行总是在一个CPU上串行进行——也就是说,一个可延迟函数不会被另一个可延迟函数中断。因此,根本不需要同步原语。

相反,在多处理器系统上,竞争条件的确存在,因为几个可延迟函数可以并发运行。下表列出了所有可能的情况:

访问数据结构的可延迟函数 保护

软中断 自旋锁

一个 tasklet 无

多个 tasklet 自旋锁

由软中断访问的数据结构必须受到保护,通常使用自旋锁进行保护,因为同一个软中断可以在两个或多个CPU上并发运行。相反,仅由一种tasklet访问的数据结构不需要保护,因为同种tasklet不能并发运行。但是,如果数据结构被几种tasklet访问,那么,就必须对数据结构进行保护。

4 保护由异常和中断访问的数据结构

让我们现在考虑一下同时由异常处理程序(例如系统调用服务例程)和中断处理程序访问的数据结构。

在单处理器系统上,竞争条件的防止是相当简单的,因为中断处理程序不是可重入的且不能被异常中断。只要内核以本地中断禁止访问数据结构,内核在访问数据结构的过程中就不会被中断。不过,如果数据结构正好是被一种中断处理程序访问,那么,中断处理程序不用禁止本地中断就可以自由地访问数据结构。

在多处理器系统上,我们必须关注异常和中断在其他CPU上的并发执行。本地中断禁止还必须外加自旋锁,强制并发的内核控制路径进行等待,直到访问数据结构的处理程序完成自己的工作。

有时,用信号量代替自旋锁可能更好。因为中断处理程序不能被挂起,它们必须用紧循环和down_trylock()函数获得信号量;对这些中断处理程序来说,信号量起的作用本质上与自旋锁一样。另一方面,系统调用服务例程可以在信号量忙时挂起调用进程。对大部分系统调用而言,这是所期望的行为。在这种情况下,信号量比自旋锁更好,因为信号量使系统具有更高的并发度。

5 保护由异常和可延迟函数访问的数据结构

异常和可延迟函数都访问的数据结构与异常和中断处理程序访问的数据结构处理方式类似。事实上,可延迟函数本质上是由中断的出现而激活的,而可延迟函数执行时不可能产生异常。因此;把本地中断禁止与自旋锁结合起来就足够了。

实际上,这更加充分:异常处理程序可以通过使用local_bh_disable()宏简单地禁止可延迟函数,而不禁止本地中断。仅禁止可延迟函数比禁止中断更可取,因为中断还可以继续在CPU上得到服务。在每个CPU上可延迟函数的执行都被串行化,因此,不存在竞争条件。

同样,在多处理器系统上,要用自旋锁确保任何时候只有一个内核控制路径访问数据结构。

6 保护由中断和可延迟函数访问的数据结构

这种情况类似于中断和异常处理程序访问的数据结构。当可延迟函数运行时可能产生中断,但是,可延迟函数不能阻止中断处理程序。因此,必须通过在可延迟函数执行期间禁用本地中断来避免竞争条件。不过,中断处理程序可以随意访问被可延迟函数访问的数据结构而不用关中断,前提是没有其他的中断处理程序访问这个数据结构。

在多处理器系统上,还是需要自旋锁禁止对多个CPU上数据结构的并发访问。

7 保护由异常、中断和可延迟函数访问的数据结构

类似于前面的情况,禁止本地中断和获取自旋锁几乎总是避免竞争条件所必需的。注意,没有必要显式地禁止可延迟函数,因为当中断处理程序终止执行时,可延迟函数才能被实质激活;因此,禁止本地中断就足够了。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: