7.6 中断上下文
当执行一个中断处理程序时,内核处于中断上下文中。进程上下文是一种内核所处的操作模式,此时内核代表进程执行——例如,执行系统调用或允许内核线程。在进程上下文中,可以通过current宏关联当前进程。此外,因为进程是以进程上下文的形式连接到内核中的,因此,进程上下文可以睡眠,也可以调用调度程序。
中断上下文和进程没有瓜葛。与current宏不相干。因为没有后备进程,所以中断上下文不可以睡眠,否则又怎能再对它重新调度呢?因此,不能从中断上下文中调用某些函数。如果一个函数睡眠,就不能在中断处理程序中使用它——这是对什么样的函数可以在中断处理程序中使用的限制。
中断上下文具有较为严格的时间限制,因为它打断了其他代码。中断上下文中的代码应当迅速、简洁,尽量不要使用循环去处理繁重的工作。中断处理程序打断了其他的代码。正是因为这种异步执行的特性,所以所有的中断处理程序必须尽可能的迅速、简洁。尽量把工作从中断处理程序中分离出来,放在下半部执行,因为下半部可以在更合适的时间运行。
中断处理程序栈的设置是一个配置选项。曾经,中断处理程序并不具有自己的栈。相反,它们共享所中断进程的内核栈。内核栈的大小是两页,在32位体系结构上是8KB,在64位体系结构上是16KB。因为在这种设置中,中断处理程序共享别人的堆栈,所以它们在栈中获取空间时必须非常节约。当然,内核栈本来就很有限,因此,所有的内核代码都应该谨慎利用它。
在2.6版本早期的内核中,增加了一个选项,把栈的大小从两页减到一页,也就是在32位的系统上只提供4KB的栈。这就减轻了内存的压力,因为系统中每个进程原先都需要两页连续,且不可换出的内核内存。为了应对栈大小的减少,中断处理程序拥有了自己的栈,每个处理器一个,大小为一页。这个栈称为中断栈,尽管中断栈的大小是原先共享栈的一半,但平均可用栈空间大得多,因为中断处理程序把这一整页占为己有。
中断处理程序不必关心栈如何设置,或者内核栈的大小是多少。总之,尽量节约内核栈空间。
7.7 中断处理机制的实现
中断处理系统在Linux中的实现依赖于体系结构的。实现依赖于处理器、所使用的中断控制器的类型、体系结构的设计及机器本身。
图7-1是中断从硬件到内核的路由。
设备产生中断,通过总线把电信号发送给中断控制器。如果中断线是激活的,那么中断控制器就会把中断发往处理器。在大多数体系结构中,这个工作就是通过电信号给处理器的特定管脚发送一个信号。除非在处理器上禁止该中断,否则,处理器会立即停止它正在做的事,关闭中断系统,然后跳到内存中预定义的位置开始执行那里的代码。这个预定义的位置是由内核设置的,是中断处理程序的入口点。
在内核中,中断的旅程开始于预定义入口点,这类似于系统调用通过预定义的异常句柄进入内核。对于每条中断线,处理器都会跳到对应的一个唯一的位置。这样,内核就可知道所接收中断的IRQ号了。初始入口点只是在栈中保存这个号,并存放当前寄存器的值;然后,内核调用函数do_IRQ()。从这里开始,大多数中断处理代码是用C编写的——但它们依然与体系结构相关。
do_IRQ()声明如下:
unsigned int do_IRQ(struct pt_regs *regs);
因为C的调用惯例是要把函数参数放在栈的顶部,因此pt_regs结构包含原始寄存器的值,这些值是以前在汇编入口例程中保存在栈中的。中断的值也会得以保存,所以,do_IRQ()可以将它提取出来。
计算出中断号后,do_IRQ()对所接收的中断进行应答,禁止这条线上的中断传递。在普通的PC机上,这些操作由mask_and_ack_8259A()来完成的。
接下来,do_IRQ()需要确保在这条中断线上有一个有效的处理程序,而且这个程序已经启动,但是当前并没有执行。如果是这样的话,do_IRQ()就调用handle_IRQ_event()来运行为这条中断线所安装的中断处理程序。handle_IRQ_event()方法被定义在kernel/irq/handle.c中。
/**
* handle_IRQ_event - irq action chain handler
* @irq: the interrupt number
* @action: the interrupt action chain for this irq
*
* Handles the action chain of an irq event
*/
irqreturn_t handle_IRQ_event(unsigned int irq, struct irqaction *action)
{
irqreturn_t ret, retval = IRQ_NONE;
unsigned int status = 0;
if (!(action->flags & IRQF_DISABLED))
local_irq_enable_in_hardirq();
do {
trace_irq_handler_entry(irq, action);
ret = action->handler(irq, action->dev_id);
trace_irq_handler_exit(irq, action, ret);
switch (ret) {
case IRQ_WAKE_THREAD:
/*
* Set result to handled so the spurious check
* does not trigger.
*/
ret = IRQ_HANDLED;
/*
* Catch drivers which return WAKE_THREAD but
* did not set up a thread function
*/
if (unlikely(!action->thread_fn)) {
warn_no_thread(irq, action);
break;
}
/*
* Wake up the handler thread for this
* action. In case the thread crashed and was
* killed we just pretend that we handled the
* interrupt. The hardirq handler above has
* disabled the device interrupt, so no irq
* storm is lurking.
*/
if (likely(!test_bit(IRQTF_DIED,
&action->thread_flags))) {
set_bit(IRQTF_RUNTHREAD, &action->thread_flags);
wake_up_process(action->thread);
}
/* Fall through to add to randomness */
case IRQ_HANDLED:
status |= action->flags;
break;
default:
break;
}
retval |= ret;
action = action->next;
} while (action);
if (status & IRQF_SAMPLE_RANDOM)
add_interrupt_randomness(irq);
local_irq_disable();
return retval;
}
首先,因为处理器禁止中断,这里要把它们打开,就必须在处理程序注册期间指定IRQF_DISABLED标志。IRQF_DISABLED表示处理程序必须在中断禁止的情况下运行。接下来,每个潜在的处理程序在循环中依次执行。如果这条线不是共享的,第一次执行后就退出循环。否则,所有的处理程序都要被执行。之后,如果在注册期间指定了IRQF_SAMPLE_RANDOM标志,则还要调用函数add_interrupt_randomness()。这个函数使用中断间隔时间为随机数产生器产生熵。最后,再将中断禁止,函数返回。回到do_IRQ(),该函数做清理工作并返回到初始入口点,然后再从这个入口点跳到函数ret_from_intr()。
ret_from_intr()例程类似于初始入口代码,以汇编语言编写。这个例程检查重新调度是否正在挂起。如果重新调度正在挂起,而且内核正在返回用户空间,那么,schedule()被调用。如果内核正在返回内核空间,只有在preempt_count为0时,schedule()才会被调用,否则,抢占内核是不安全的。在schedule()返回之后,或者如果没有挂起的工作,那么,原来的寄存器被恢复,内核恢复到曾经中断的点。
在x86上,初始的汇编例程位于arch/x86/kernel/entry_64.S(文件entry_32.S 对应32位的x86体系架构),C方法位于arch/x86/kernel/irq.c。
7.8 /proc/interrupts
procfs是一个虚拟文件系统,存在于内核内存,一般安装在/proc目录。在procfs中读写文件都要调用内核函数,这些函数模拟从真实文件中读写。与此相关的例子是/proc/interrupts文件,该文件存放的是系统中与中断相关的统计信息。如下是从单处理器PC上输出的信息:
第1列是中断号。在这个系统中,现有的中断号为0~2、4、5、12及15。第2列是一个接收中断数目的计数器。系统中的每个处理器都存在这样的列,但是,这个机器只有一个处理器。看到,时钟中断已接收3602371次中断,这里,声卡(EMU10K1)没有接收一次中断。第3列是处理这个中断的中断控制器。XT-PIC对应于标准的PC可编程中断控制器。最后一列是与这个中断相关的设备名字。这个名字是通过参数devname提供给函数request_irq()的。如果中断是共享的,则这条中断线上注册的所有设备都会列出来。
procfs代码位于fs/proc中。提供/proc/interrupts的函数是与体系结构相关的,叫做show_interrupts()。
在x86体系结构下:
int show_interrupts(struct seq_file *p, void *v)
{
unsigned long flags, any_count = 0;
int i = *(loff_t *) v, j, prec;
struct irqaction *action;
struct irq_desc *desc;
if (i > nr_irqs)
return 0;
for (prec = 3, j = 1000; prec < 10 && j <= nr_irqs; ++prec)
j *= 10;
if (i == nr_irqs)
return show_other_interrupts(p, prec);
/* print header */
if (i == 0) {
seq_printf(p, "%*s", prec + 8, "");
for_each_online_cpu(j)
seq_printf(p, "CPU%-8d", j);
seq_putc(p, '\n');
}
desc = irq_to_desc(i);
if (!desc)
return 0;
raw_spin_lock_irqsave(&desc->lock, flags);
for_each_online_cpu(j)
any_count |= kstat_irqs_cpu(i, j);
action = desc->action;
if (!action && !any_count)
goto out;
seq_printf(p, "%*d: ", prec, i);
for_each_online_cpu(j)
seq_printf(p, "%10u ", kstat_irqs_cpu(i, j));
seq_printf(p, " %8s", desc->chip->name);
seq_printf(p, "-%-8s", desc->name);
if (action) {
seq_printf(p, " %s", action->name);
while ((action = action->next) != NULL)
seq_printf(p, ", %s", action->name);
}
seq_putc(p, '\n');
out:
raw_spin_unlock_irqrestore(&desc->lock, flags);
return 0;
}