补充原语
Linux2.6还使用了另一种类似于信号量的原语:补充。引入这种原语是为了解决多处理器系统上发生的一种微妙的竞争条件,当进程A分配了一个临时信号量变量,把它初始化为关闭的MUTEX,并把其它地址传递给进程B,然后在A之上调用down(),进程A打算一但被唤醒就撤消该信号量。随后,运行在不同CPU上的进程B在同一信号量上调用up()。然而,up()和down()的目前实现还允许这两个函数在同一个信号量上并发执行。因此,进程A可以被唤醒并撤销临时信号量,而进程B还在运行up()函数。结果,up()可能试图访问一个不存在的数据结构。
当然,也可以改变up()和down()的实现以禁止在同一信号量上并发执行。然而,这种改变可能需要另外的指令,这对于频繁使用的函数来说不是什么好事。
补充是专门设计来解决以上问题的同步原语。Completion数据结构包含一个等待队列头和一个标志:
structcompletion{
unsignedint done;
wait_queue_head_twait;
};
与up()对应的函数叫做complete().complete()接收completion数据结构的地址作为参数,在补充等待队列的自旋锁上高用spin_lock_irqsave(),递增done字段,唤醒在wait等待队列上睡眠的互斥进程,最后调用spin_unlock_irqrestore()。
与down()对应的函数叫做wait_for_completion()。wait_for_completion()接收completion数据结构的地址作为参数,并检查done标志的值。如果该标志大于0,wait_for_completion()就终止,因为这说明complete()已经在另一个CPU上运行。否则,wait_for_completion()把current作为一个互斥进程加到等待队列的未尾,并把current置为TASK_UNINTERRUPTIBLE状态让其睡眠。一旦current被唤醒,该函数就把current从等待队列中删除。然后,函数检查done标志的值:如果等于0函数就结束,否则,再次挂起当前进程。与complete()函数中的情形一样,wait_for_completion()使用补充等待队列中的自旋锁。
补充原语和信号量之间的真正差别在于如何等待队列中包含的自旋锁。在补充原语中,自旋锁用来确保complete()和wait_for_completion()不会并发执行。在信号量中,自旋锁用于避免并发执行的down()函数弄乱信号量的数据结构。
禁止本地中断
确保一组内核语句被当作一个临界区处理的主要机制之一就是中断禁止。即使当硬件设备产生一个IRQ信号时,中断禁止也让内核控制路径继续执行,因此,这就提供了一种有效的方式,确保中断处理程序访问的数据结构也受到保护。然而,禁止本地中断并不保护运行在另一个CPU上的中断处理程序对数据结构的并发访问,因此,在多处理器系统上,禁止本地中断经常与自旋锁结合使用。
宏local_irq_disalbe()使用cli汇编语言指令关闭本地CPU上的中断,宏local_irq_enable()使用sti汇编语言指令打开被关闭的中断。汇编语言指令cli和sti分别清除和设置eflags控制寄存器的IF标志。如果eflags寄存器的IF标志被清0,宏irqs_disable()产生等于1的值;如果IF标志被设置,该宏也产生为1的值。
当内核进入临界区时,通过把eflags寄存器的IF标志清0关闭中断。但是,内核经常不能在临界区的末尾简单地再次设置这个标志。中断可以以嵌套的方式执行,所以内核不必知道当前控制路径被执行之前IF标志的值究竟是什么。在这种情况下,控制路径必須保存先前赋给该标志的值,并在执行结束时恢复它。
保存和恢复eflags的内容是分别通过宏local_irq_save和local_irq_restore来实现的。local_irq_save宏把eflags寄存器的内容拷贝到一个局部变量中,随后用cli汇编语言指令把IF标志清0。在临界区的末尾,宏local_irq_restore恢复eflags原来的内容,因此,只是在这个控制路径发出cli汇编语言指令之前,中断被激活的情况下,中断才处于打开状态。
禁止和激活可延迟函数
禁止可延迟函数在一个CPU上执行的一种简单方式就是禁止在哪个CPU上的中断。因为没有中断处理程序被激活,因此,软中断操作就不能异步地开始。
然而,我们在下一节会看到,内核有时需要只禁止可延迟函数而不禁止中断。通过操纵当前thread_info描述符preempt_count字段中存放的软中断计数器,可以在本地CPU上的所有可延迟函数。
回忆一下,如果软中断计数器是正数,do_softirq()函数就不会执行中断,而且,因为tasklet在软中断之前被执行,把这个计数器设置为大于0,由此禁止了给定CPU上的所有可延迟函数和软中断的执行。
宏local_bh_disable给本地CPU的软中断计数器加1,而函数local_bh_enable()从本地CPU的软中断计数器中减掉1。内核因此能使用几个嵌套的local_bh_disable()调用,只有宏local_bh_enable与第一个local_hb_disable调用相匹配,可延迟函数才再次被激活。
递减软中断计数器之后,local_bh_enable()执行两个重要的操作以有助于保证适时地执行长时间等待的线程:
检查本地CPU的preempt_count字段中硬中断计数器和软中断计数器,如果这两个计数器都等于0而且有挂起的软中断要执行,就调用do_softirq()来激活这些软中断。
检查本地CPU的TIF_NEED_RESCHED标志是否被设置,如果是,说明进程切换请求是挂起的,因此调用preempt_schedule()函数。