Linux下386中断处理
一、中断和异常
这两个概念虽然处理的方式大致相同,但是本质上是有很大差别的,而且在386下它们的处理和语义对系统中最为重要的内容的理解是很重要的。
首先一个最为重要的差别就是:
当中断发生的时候,处理器在执行了Cs IP EFLAGS(可能还由用户态的SS和ESP)这个一气呵成的寄存器保存之后,处理器将会清空新设置的EFLGAS中的IF标志位,这个标志位的最大作用就是禁止该CPU响应自己INTR(8259A)或者LINT(APIC)引脚的中断,而对于异常来说则不会对新的IF标志位做任何处理(由于系统调用也是通过一个特殊的异常0x80进入内核的,所以系统调用是不管中断的)。这样一个最为直观的推论就是当异常发生之后,中断同样可以发生。
另一个差别:
中断可以被禁止、而异常不能被禁止。为什么呢?因为中断是来自CPU外部的,通过CPU的一个引脚告诉CPU,这个信号对CPU来说可以认为是透明不可控的。进一步说,这些信号代表什么意义本身对CPU是没有任何意义的,CPU只是给外部设备一个向CPU发送请求并执行相应代码的机制(执行中断向量对应的服务程序)。但是异常时CPU内部本身的问题,CPU明确知道这些异常代表的是什么情况。这些异常不依赖于外部设备,它们一般是CPU在执行代码的时候发生的(因为CPU正常工作的时候一直在孜孜不倦的执行代码,所以总有可能会发生异常)。当异常发生之后,CPU可能无法继续执行,所以异常时必须要及时响应的。
这里的禁止还要和中断控制器8259A的中断像区别。根据8259A的说明,当8259A向CPU的引脚发出了中断请求之后,会等待CPU发送一个INTA回应,当这个回应被8259A接收到之后,它会把最高优先级的中断的中断向量保存到它内部的中断请求寄存器ISR中,然后清空中断请求寄存器中该中断对应的标志位。然后当CPU的第二个INTA发送到8259的时候,它把ISR中的中断向量放到总线上。如果是采用自动中断响应模式AEOI,那么在第二个中断发送到控制器之后,控制器就认为CPU已经完成了中断服务程序,也就是控制器可以继续选择下一个中断来项CPU发送中断请求了。但是,如果不是AEOI模式,那么这个中断完成就要求CPU手动的向中断控制器中写入EOI中断控制字,只有写入该控制字之后控制器才会认为中断已经结束,从而可以选择下一个中断来通知CPU。所以我们可以推测,当CPU内部的IF标志被置位之后,当CPU中断引脚接收到中断之后,CPU不会向中断控制器发送INTA信号,从而导致控制器一直保持CPU的INT引脚一直有效。注意:这一点要和程序向控制器主动写入EOI控制字区别开。前者是CPU硬件实现的,对程序透明,而后则则需要CPU指令来主动完成。
Linux设置8259A的工作方式为全嵌套方式,这也就意味着,当中断控制器向CPU发送了一个低优先级的中断请求之后,但是CPU没有发送EOI之前,如果此时控制器再次有一个更高优先级的中断到来,此时控制器还是会向CPU发送中断请求。此时就要考虑CPU的IF标志位了,该标志如果没有被置位,那么它就可以中断某个中断开始新的中断,否则同样需要等待。这里就说明了IF和中断控制器屏蔽的一个非常重要的区别,就是IF只是禁止本CPU处理中断事件、而对于中断控制器本身的禁止将会导致整个系统中该中断不会传递到所有的CPU,这一点在多CPU系统中比较有助于对系统代码的理解。而这个本CPU的禁止一般称为local_irq。
中断和异常在处理上的区别:
中断不可以sleep或者阻塞,但是异常可以。因为异常时同步的,也就是当异常发生时,引起异常的线程一定是当前线程(不考虑中断),但是中断的线程是无辜的不相关线程,我们不能以为在一个线程的执行期间发生了外部中断而对该线程做特殊处理;但是异常就可以。举个例子:假设说一个线程命中了一个数据断点,我们就可以在异常中将当前线程设置为TASK_STOPPED,然后执行schedule,从而让调度器把该线程设置为不可运行状态,但是中断就不行。
顺便提及一个细节:
这里还有一个细节,就是无论是中断还是异常发生的时候,CPU都会禁止掉EFLAGS中的TF标志,这个TF也就是单步Trap跟踪的标志,如果该标志置位,那么用户态的调试器就可以通过这个标志来调试内核代码了。由于我们知道用户态调试器是无法调试内核态代码的,所以说明了的确会清空这个标志。另一方面说,为什么用户态不能调试内核态线程呢?因为用户态调试器是工作在内核的基础之上的,如果说它工作的基础内核被中断的话,所有用户态程序都无法执行,同样包括这个用户态调试器。
一个小问题:
既然异常没有关中断,那么如果在如果在异常处理开始的时候发生异常,或者说异常现场的所有寄存器只保存了一部分,那么此时发生了中断,这个异常会不会丢失,现场寄存器会不会被破坏。或者说为什么中断要自动关中断(这个是绝大部分处理器(如果不是全部的话)的默认行为,都会在中断发生之后自动关中断)?
二、Linux内核中断和异常处理流程分析
1、中断何时打开
前面说过,如果说当一个中断发生的时候,CPU会自动禁止该CPU的中断,也就是在CPU的EFLAGS中的IF寄存器被清零,也就是禁止该CPU上接受中断。我们也都知道,为了减少中断处理的延迟,内核中引入了softirq中断机制。那么这个是应该会使能中断,但是我们在handle_level_irq和handle_edge_irq中都没有看到在中断处理之后打开中断,那么这个被CPU自动禁止的中断是什么时候被再次使能的?
我们看一下在
linux-2.6.21\kernel\softirq.c中的irq_exit函数,该函数将会完成对于系统中中断退出的逻辑操作。
sub_preempt_count(IRQ_EXIT_OFFSET);
if (!in_interrupt() && local_softirq_pending())
invoke_softirq();
在__do_softirq函数中
local_irq_enable();注意这里的操作,该操作将会打该CPU的中断允许标志,之后该CPU就可以继续接受中断了。
h = softirq_vec;
do {
if (pending & 1) {
h->action(h);这里执行某个软中断安装的中断处理函数。
rcu_bh_qsctr_inc(cpu);
}
h++;
pending >>= 1;
} while (pending);
local_irq_disable();
我们看一下386对于这个local_irq_enable的处理方式linux-2.6.21\include\linux\irqflags.h
#define local_irq_enable() \
do { trace_hardirqs_on(); raw_local_irq_enable(); } while (0)
linux-2.6.21\include\asm-i386\irqflags.h
static inline void raw_local_irq_enable(void)
{
__asm__ __volatile__("sti" : : : "memory");
}
所以也就是使能了中断,这也就说明了程序在处理软中断的时候,硬件的中断是依然可以发生的,另一方面,当执行通过request_irq注册的中断处理程序的时候,这个中断处理函数是在关中断的情况下执行的。还有一点,就是在软中断处理的时候同样不能进行阻塞,因为它依然是在CPU的中断上下文中执行的,而不是在制定的确定线程上下文中执行。下面是Linux Device Driver Develop中对于这个的一个简单而明确的说明http://www.xml.com/ldd/chapter/book/ch09.html
One thing to keep in mind with bottom-half processing is that all of the restrictions that apply to interrupt handlers also apply to bottom halves. Thus, bottom halves cannot sleep, cannot access user space, and cannot invoke the scheduler.简言之,所有适用于中断处理函数的限制同样适用于bottom halves。这是因为中断是异步的,我们不能因为中断的到来而对中断发生时正在运行的线程产生任何调度性影响(因为schedule和block将会导致中断发生时正在运行的线程被停止调度。而由于异常时同步的,所以异常的处理函数中可以进行这些block和sleep等操作)。
2、软中断的安装
再顺便看一下软中断的安装和一般的执行步骤,以高精度时钟为例:
static void run_timer_softirq(struct softirq_action *h)
{
tvec_base_t *base = __get_cpu_var(tvec_bases);
hrtimer_run_queues();
if (time_after_eq(jiffies, base->timer_jiffies))
__run_timers(base);
}
void __init init_timers(void)
open_softirq(TIMER_SOFTIRQ, run_timer_softirq, NULL);这里的run_timer_softirq就是在上面__do_softirq中执行的action函数,可以看到这个定时器的执行都是在中断中执行的,虽然是开中断,但是依然是在随机线程的上下文中执行。
在__do_softirq中,我们可以看到下面的代码
pending = local_softirq_pending();
if (pending && --max_restart)
goto restart;
if (pending)
wakeup_softirqd();
这里也就是在软中断上下文中执行的软中断次数也有一个限制,当执行的次数大于这个限制的时候,就唤醒softirqd线程,从而可以减少对中断发生时正在执行的线程的影响。这个次数设置为10.,由MAX_SOFTIRQ_RESTART确定。在这个守护线程中,它的优先级非常低,因为它通过nice将自己的优先级降到非常低的值。
当然还有一些就是在irq_enter和irq_exit中对系统中中断次数计数的操作。
3、工作队列
前面说过,在软中断中是无法进行随眠等待的,但是如果是一个情况下必须进行等待,那么久需要使用workqueue了,内核中有专门的一个线程用来处理工作队列,这样当需要等待的时候就可以让这个线程来等待。由于它本身就是处理所有等待的,所以不会影响其他功能。或者说,它的正常功能就是等待可能的等待,所以让它等待不会有问题。
内核中的workqueue的创建为
static int worker_thread(void *__cwq)
这个线程一般通过create_workqueue来创建一个内核态专门的工作队列线程,这个线程就是处理某些工作。这个函数返回的就是一个workqueu_struct结构指针,当需要让内核在可能睡眠的情况下执行某个工作的时候,可以通过queue_work来唤醒该线程。
当执行queue_work的执行流程
queue_work--->>>__queue_work---->>>wake_up(&cwq->more_work);.
这样就可以通过一个约定的位置将等待线程唤醒。
三、386下中断的初始化
\linux-2.6.21\arch\i386\kernel\entry.S
/*
* Build the entry stubs and pointer table with
* some assembler magic.
*/
.data
ENTRY(interrupt) 这里声明了安装时即将使用的中断向量表,这个向量表的生成使用了汇编语言的循环语法,所以中断的向量号都是通过他们所在的位置确定的。
.text
ENTRY(irq_entries_start)
RING0_INT_FRAME
vector=0 中断向量从0开始,供224项,不包括intel保留的前32个中断向量。
.rept NR_IRQS
ALIGN
.if vector
CFI_ADJUST_CFA_OFFSET -4
.endif
1: pushl $~(vector)这样生成中断向量表的时候此时已经确定了它的中中断向量号,从而可以在中断处理程序中使用具体是哪个中断向量,例如do_IRQ中。
CFI_ADJUST_CFA_OFFSET 4
jmp common_interrupt
.previous
.long 1b
.text
vector=vector+1
.endr
END(irq_entries_start)
/*
* the CPU automatically disables interrupts when executing an IRQ vector,
* so IRQ-flags tracing has to follow that:
*/
ALIGN
common_interrupt:
SAVE_ALL
TRACE_IRQS_OFF
movl %esp,%eax
call do_IRQ
jmp ret_from_intr
ENDPROC(common_interrupt)
CFI_ENDPROC
而386的处理器需要的是通过处理器内部的idt表来注册中断,也就是这个寄存器中要填入中断向量表的地址,下面我们看一下这个结构是在什么时候初始化的
linux-2.6.21\arch\i386\kernel\cpu\common.c
load_idt(&idt_descr);
这个函数的内核调用连为
(gdb) bt
#0 _cpu_init (cpu=65533, curr=0xfffd) at arch/i386/kernel/cpu/common.c:726
#1 0xc0a093c1 in cpu_init () at arch/i386/kernel/cpu/common.c:815
#2 0xc0a03162 in trap_init () at arch/i386/kernel/traps.c:1186
#3 0xc09fa155 in start_kernel () at init/main.c:560
#4 0x00000000 in ?? ()
然后我们看这个idt_descr的定义和初始化。这个初始化分为两个部分,一个是内部异常初始化,也就是trap_init,另一个是外部中断初始化,也就是init_IRQ(这个函数和native_init_IRQ函数同名)函数。虽然有如此差别,但是它们都是在同一个IDT(供256项)中。由于intel保留了前32个中断,所以外部中断是从32开始的(这个值在init_8259A函数中设置 outb_p(0x20 + 0, PIC_MASTER_IMR); /* ICW2: 8259A-1 IR0-7 mapped to 0x20-0x27 */
)。
对于外部中断的初始化
/*
* Cover the whole vector space, no vector can escape
* us. (some of these will be overridden and become
* 'special' SMP interrupts)
*/
for (i = 0; i < (NR_VECTORS - FIRST_EXTERNAL_VECTOR); i++) {
int vector = FIRST_EXTERNAL_VECTOR + i;
if (i >= NR_IRQS)
break;
if (vector != SYSCALL_VECTOR)
set_intr_gate(vector, interrupt[i]);这里的变量就是在entry.S中定义的变量,这里初始化了所有的外部中断(不包括intel保留的32个中断,所以这个NR_IRQS定义为256-32=224),注意,这里set_intr_gate的第一个参数是中断号,而这个中断号是从FIRST_EXTERNAL_VECTOR开始的。
}
而内部保留中断的初始化则位于trap_init中,例如
set_trap_gate(7,&device_not_available);
这些都是比较简单的操作,所以这里不再解释。