对内核数据结构的同步访问

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

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

  1. 同进运转的I/O设备数

  2. 进行有效工作的CPU

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

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

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

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

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

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

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

new->next=list_element->next;

list_element->next= new

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


在自旋锁、信号量及中断禁止之间选择


对大多数内核数据结构的访问模式非常复杂,远不像上便所示的哪样简单,于是,迫使内核开发者使用信号量、自旋锁、中断禁止和软中断禁止。一般来说,同步原语的选取取决于访问数据结构的内核控制路径的种类,如表5-8所示。记住,只要内核控制路径获得自旋锁,就禁用本地中断或本地软中断,自动禁用内核抢占。

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

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

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

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

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

假定一个数据结构仅被中断处理程序的”上半部分”访问。也就是说,中断处理程序本身不能同时多次运行。因此,访问数据结构就无需任何同步原语。

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

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

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

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

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

只被可延迟函数访问的数据结构需要哪种保护呢?这主要取决于可延迟函数的种类。

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

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

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

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

让我们考虑一下由异常处理程序和中断处理程序访问的数据结构。

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

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

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


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

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

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

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

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

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

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



  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值