禁止本地CPU中断是确保一组内核语句被当作一个临界区处理的主要机制。这个机制的意义是:即使当硬件设备产生了一个IRQ信号时,中断禁止也让内核控制路径继续执行,因此,这就提供了一种有效的方式,确保内核控制路径中的一些中断处理程序能访问的数据结构也受到保护。
1 禁止本地中断
然而,禁止本地中断并不保护运行在另一个CPU上的中断处理程序对该数据结构的并发访问,因此,在多处理器系统上,禁止本地中断经常与自旋锁结合使用。
宏local_irq_disable()使用cli汇编语言指令关闭本地CPU上的中断(x86体系的禁止本地中断位于include/linux/Irqflags.h):
#define local_irq_disable() /
do { raw_local_irq_disable(); trace_hardirqs_off(); } while (0)
//include/asm-i386/Irqflags.h
static inline void raw_local_irq_disable(void)
{
__asm__ __volatile__("cli" : : : "memory");
}
宏local_irq_enable()使用sti汇编语言指令打开被关闭的中断(同样在include/linux/Irqflags.h):
#define local_irq_enable() /
do { trace_hardirqs_on(); raw_local_irq_enable(); } while (0)
//include/asm-i386/Irqflags.h
static inline void raw_local_irq_enable(void)
{
__asm__ __volatile__("sti" : : : "memory");
}
汇编语言指令cli和sti分别清除和设置eflags控制寄存器的IF标志。如果eflags寄存器的IF标志被清0,宏irqs_disabled()产生等于1的值;如果IF标志被设置,该宏也产生为1的值。
有时候,需要防止中断处理程序对eflags的寄存器内容进行破坏,需要另一种机制来保护临界资源。保存和恢复eflags的内容是分别通过宏local_irq_save和local_irq_restore来实现的。local_irq_save宏把eflags寄存器的内容拷贝到一个局部变量中,随后用cli汇编语言指令把IF标志清0(同样在include/linux/Irqflags.h):
#define local_irq_save(flags) /
do { raw_local_irq_save(flags); trace_hardirqs_off(); } while (0)
//include/asm-i386/Irqflags.h
#define raw_local_irq_save(flags) /
do { (flags) = __raw_local_irq_save(); } while (0)
static inline unsigned long __raw_local_irq_save(void)
{
unsigned long flags = __raw_local_save_flags();
raw_local_irq_disable();
return flags;
}
static inline unsigned long __raw_local_save_flags(void)
{
unsigned long flags;
__asm__ __volatile__(
"pushfl ; popl %0"
: "=g" (flags)
: /* no input */
);
return flags;
}
在临界区的末尾,宏local_irq_restore恢复eflags原来的内容:
#define local_irq_restore(flags) /
do { /
if (raw_irqs_disabled_flags(flags)) { /
raw_local_irq_restore(flags); /
trace_hardirqs_off(); /
} else { /
trace_hardirqs_on(); /
raw_local_irq_restore(flags); /
} /
} while (0)
static inline int raw_irqs_disabled_flags(unsigned long flags)
{
return !(flags & (1 << 9));
}
static inline void raw_local_irq_restore(unsigned long flags)
{
__asm__ __volatile__(
"pushl %0 ; popfl"
: /* no output */
:"g" (flags)
:"memory", "cc"
);
}
因此,只是在这个控制路径发出cli汇编语言指令之前,即raw_local_irq_disable()之前中断被激活的情况下,中断才处于打开状态。
2 禁止下半部(可延迟函数)
在中断处理专题的“下半部分”一节,我们说明了可延迟函数可能在不可预知的时间执行(实际上是在硬件中断处理程序结束时)。因此,必须保护可延迟函数访问的数据结构使其避免竞争条件。
禁止可延迟函数在单个CPU上执行的一种简单方式就是禁止在那个CPU上的中断。因为没有中断处理程序被激活,因此,软中断操作就不能异步地开始。
然而,内核有时需要只禁止可延迟函数而不禁止中断。这种情况是需要通过操纵当前thread_info描述符preempt_count字段中存放的软中断计数器,可以在本地CPU上激活或禁止可延迟函数。
回忆一下,如果软中断计数器是正数,do_softirq()函数就不会执行软中断,而且,因为tasklet在软中断之前被执行,把这个计数器设置为大于0的值,由此禁止了在给定CPU上的所有可延迟函数和软中断的执行。
宏local_bh_disable给本地CPU的软中断计数器加1,而函数local_bh_enable()从本地CPU的软中断计数器中减掉1。
void local_bh_disable(void) //kernel/Softirq.c
{
__local_bh_disable((unsigned long)__builtin_return_address(0));
}
static inline void __local_bh_disable(unsigned long ip)
{
add_preempt_count(SOFTIRQ_OFFSET);
barrier();
}
#define SOFTIRQ_OFFSET (1UL << SOFTIRQ_SHIFT) //0x00000100 -> 512
#define SOFTIRQ_SHIFT (PREEMPT_SHIFT + PREEMPT_BITS)
#define PREEMPT_SHIFT 0
#define PREEMPT_BITS 8
# define add_preempt_count(val) do { preempt_count() += (val); } while (0)
内核因此能使用几个嵌套的local_bh_disable调用,只有宏local_bh_enable与第一个local_bh_disable调用相匹配,可延迟函数才再次被激活(do_softirq):
void local_bh_enable(void)
{
#ifdef CONFIG_TRACE_IRQFLAGS
unsigned long flags;
WARN_ON_ONCE(in_irq());
#endif
WARN_ON_ONCE(irqs_disabled());
#ifdef CONFIG_TRACE_IRQFLAGS
local_irq_save(flags);
#endif
/*
* Are softirqs going to be turned on now:
*/
if (softirq_count() == SOFTIRQ_OFFSET)
trace_softirqs_on((unsigned long)__builtin_return_address(0));
/*
* Keep preemption disabled until we are done with
* softirq processing:
*/
sub_preempt_count(SOFTIRQ_OFFSET - 1);
if (unlikely(!in_interrupt() && local_softirq_pending()))
do_softirq();
dec_preempt_count();
#ifdef CONFIG_TRACE_IRQFLAGS
local_irq_restore(flags);
#endif
preempt_check_resched();
}
递减软中断计数器之后,local_bh_enable()执行两个重要的操作以有助于保证适时地执行长时间等待的线程:
1. 检查本地CPU的preempt_count字段中硬中断计数器和软中断计数器,如果这两个计数器的值都等于0而且有挂起的软中断要执行,就调用do_softirq()来激活这些软中断(见中断专题“下半部”博文)。
2. 调用preempt_check_resched函数检查本地CPU的TIF_NEED_RESCHED标志是否被设置,如果是,说明进程切换请求是挂起的,因此调用preempt_schedule()函数(参见专题前面的“内核抢占”博文)。
#define preempt_check_resched() /
do { /
if (unlikely(test_thread_flag(TIF_NEED_RESCHED))) /
preempt_schedule(); /
} while (0)