您的位置:首页 > 运维架构 > Linux

zz : Linux中对中断的支持

2011-12-30 14:33 267 查看
Linux内核中使用的门是在i386平台的门的基础上做了更进一步的细化后形成的,如下所示:

  1、中断门:处理器中DPL被设置为0的中断门,不能用户态下对其进行访问。Linux下所有的(硬)中断处理程序都是在核心态下通过中断门来激活的。 

  2、系统门:处理器中DPL被设置为3的陷阱门,可以在用户态下对其进行访问。Linux的三种异常处理函数(中断号为4、5、128)都是通过系统门来调用的,即指令into、bound和int $0x80可以在用户态下被执行。

  3、系统中断门:处理器中DPL被设置为3的中断门,可以在用户态下访问。Linux中中断向量号为3的中断(int3)是通过系统中断门来调用的,可在用户态下被执行。

  4、陷阱门:处理器中DPL被设置成0的陷阱门,不能在用户态下进行访问。Linux中的大多数异常处理函数都是通过陷阱门被激活的。

  5、任务门:处理器中DPL被设置成0的任务门,不能在用户态下进行访问。Linux中仅有“Double Fault”异常的处理函数是通过任务门进行调用的。

  在系统引导的过程中,内核调用/arch/i386/kernel/traps.c文件中的trap_init函数来对中断进行初始化。该函数通过调用同一文件下的set_trap_gate等多个函数来对中断描述符进行初始化。

  set_trap_gate等函数的本质是调用include/asm-i386/desc.h文件中的_set_gate函数来完成对中断描述符的初始化工作,该函数的代码如下:

QUOTE:
static inline void _set_gate(int gate, unsigned int type, void *addr, unsigned short seg)

{

__u32 a, b;

pack_gate(&a, &b, (unsigned long)addr, seg, type, 0);

write_idt_entry(idt_table, gate, a, b);

}
  其中,pack_gate函数的代码如下:

QUOTE:
static inline void pack_gate(__u32 *a, __u32 *b,

unsigned long base, unsigned short seg, unsigned char type, unsigned char flags)

{

*a = (seg << 16) | (base & 0xffff);

*b = (base & 0xffff0000) | ((type & 0xff) << 8) | (flags & 0xff);

}
  write_idt_entry宏展开后代码如下:

QUOTE:
#define write_idt_entry(dt, entry, a, b) write_dt_entry(dt, entry, a, b)

static inline void write_dt_entry(void *dt, int entry, __u32 entry_a, __u32 entry_b)

{

__u32 *lp = (__u32 *)((char *)dt + entry*8);

*lp = entry_a;

*(lp+1) = entry_b;

}
  其中,pack_gate函数的主要工作是设置相应的中断描述符,a为低32位,b为高32位。write_idt_entry的工作则是将在pack_gate函数中设置好的中断描述符填入到中断描述符表的相应位置。

最近在研究异步消息处理, 突然想起linux内核的中断处理, 里面由始至终都贯穿着"重要的事马上做, 不重要的事推后做"的异步处理思想. 于是整理一下~

第一阶段--获取中断号

每个CPU都有响应中断的能力, 每个CPU响应中断时都走相同的流程. 这个流程就是内核提供的中断服务程序.

在进入中断服务程序时, CPU已经自动禁止了本CPU上的中断响应, 因为CPU不能假定中断服务程序是可重入的.

[align=center][/align]
中断处理程序的第一步要做两件事情:

1. 将中断号压入栈中; (不同中断号的中断对应不同的中断服务程序入口)

2. 将当前寄存器信息压入栈中; (以便中断退出时恢复)

显然, 这两步都是不可重入的(如果在保存寄存器值时被中断了, 那么另外的操作很可能就把寄存器给改写了, 现场将无法恢复), 所以前面说到的CPU进入中断服务程序时要自动禁止中断.

栈上的信息被作为函数参数, 调用do_IRQ函数.



第二阶段--中断串行化

进入do_IRQ函数, 第一步进行中断的串行化处理, 将多个CPU同时产生的某一中断进行串行化. 其方法是如果当前中断处于"执行"状态(表明另一个CPU正在处理相同的中断), 则重新设置它的"触发"标记, http://www.linuxidc.com然后立即返回. 正在处理同一中断的那个CPU完成一次处理后, 会再次检查"触发"标记, 如果设置, 则再次触发处理过程.

于是, 中断的处理是一个循环过程, 每次循环调用handle_IRQ_event来处理中断.

第三阶段--关中断条件下的中断处理

进入handle_IRQ_event函数, 调用对应的内核或内核模块通过request_irq函数注册的中断处理函数.

注册的中断处理函数有个中断开关属性, 一般情况下, 中断处理函数总是在关中断的情况下进行的. 而调用request_irq注册中断处理函数时也可以设置该中断处理函数在开中断的情况下进行, 这种情况比较少见, 因为这要求中断处理代码必须是可重入的. (另外, 这里如果开中断, 正在处理的这个中断一般也是会被阻塞的. 因为正在处理某个中断的时候, 硬件中断控制器上的这个中断并未被ack, 硬件不会发起下一次相同的中断.)

中断处理函数的过程可能会很长, 如果整个过程都在关中断的情况下进行, 那么后续的中断将被阻塞很长的时间.

于是, 有了soft_irq. 把不可重入的一部分在中断处理程序中(关中断)去完成, 然后调用raise_softirq设置一个软中断, 中断处理程序结束. 后面的工作将放在soft_irq里面去做.

第四阶段--开中断条件下的软中断

上一阶段循环调用完当前所有被触发的中断处理函数后, do_softirq函数被调用, 开始处理软件中断.

在软中断机制中, 为每个CPU维护了一个若干位的掩码集, 每位掩码代表一个中断号. 在上一阶段的中断处理函数中, 调用raise_softirq设置了对应的软中断, 到了这里, 软中断对应的处理函数就会被调用(处理函数由open_softirq函数来注册).

[align=center][/align]
可以看出, 软中断与中断的模型很类似, 每个CPU有一组中断号, 中断有其对应的优先级, 每个CPU处理属于自己的中断. 最大的不同是开中断与关中断.

于是, 一个中断处理过程被分成了两部分, 第一部分在中断处理函数里面关中断的进行, 第二部分在软中断处理函数里面开中断的进行.

由于这一步是在开中断条件下进行的,这里还可能发生新的中断(中断嵌套),然后新中断对应的中断处理又将开始一个新的第一阶段~第三阶段。在新的这个第三阶段中,可能又会触发新的软中断。但是这个新的中断处理过程并不会进入第四阶段,而是当它发现自己是嵌套的中断时,完成第三阶段之后就会退出了。也就是说,只有第一层中断处理过程会进入第四阶段,嵌套发生的中断处理过程只执行到第三阶段。

然而嵌套发生的中断处理过程也可能会触发软中断,所以第一层中断处理过程在第四阶段需要是一个循环的过程,需要循环处理嵌套发生的所有软中断。为什么要这样做呢?因为这样可以按软中断触发的顺序来执行这些软中断,否则后来的软中断可能就会先执行完成了。

极端情况下,嵌套发生的软中断可能非常多,全部处理完可能需要很长的时间,于是内核会在处理完一定数量的软中断后,将剩下未处理的软中断推给一个叫ksoftirqd的内核线程来处理,然后结束本次中断处理过程。

第五阶段--开中断条件下的tasklet

实际上, 软中断很少直接被使用. 而第二部分开中断情况下的进行的处理过程一般是由tasklet机制来完成的.

tasklet是由软中断引出的, 内核定义了两个软中断掩码HI_SOFTIRQ和TASKLET_SOFTIRQ(两者优先级不同), 这两个掩码对应的软中断处理函数作为入口, 进入tasklet处理过程.

于是, 在第三阶段的中断处理函数中, 完成关中断的部分后, 然后调用tasklet_schedule/tasklet_hi_schedule标记一个tasklet, 然后中断处理程序结束. 后面的工作由HI_SOFTIRQ/TASKLET_SOFTIRQ对应的软中断处理程序去处理被标记的tasklet(每个tasklet在其初始化时都设置了处理函数).

看上去, tasklet只不过是在softirq的基础上多了一层调用, 其作用是什么呢? 前面说过, softirq是与CPU相对应的, 每个CPU处理自己的softirq. 这些softirq的处理函数需要设计为可重入的, 因为它们可能在多个CPU上同时运行. 而tasklet则是在多个CPU间被串行化执行的, 其处理函数不必考虑可重入的事情.

然而, softirq毕竟还是要比tasklet少绕点弯路, 所以少数实时性要求相对较高的处理过程还是在精心设计之后, 直接使用softirq了. 比如: 时钟中断处理过程, 网络发送/接收处理过程.

结尾阶段

CPU接收到中断以后, 以历以上五个阶段, 中断处理完成. 最后需要恢复第一阶段中被保存在栈上的寄存器信息. 中断处理结束.

关于调度

上面的流程中, 还隐含了一个问题, 整个处理过程是持续占有CPU的(除了开中断情况下可能被新的中断打断以外). 并且, 中断处理的这几个阶段中, 程序不能够让出CPU!

这是由内核的设计决定的, 中断服务程序没有自己的task结构(即操作系统教科书上说的进程控制块), 所以它不能被内核调度. 通常说一个进程让出CPU, 在之后如果满足某种条件, 内核会通过它的task结构找到它, 并调度其运行.

这里可能存在两方面的问题:

1. 连续的低优先的中断可能持续占有CPU, 而高优先的某些进程则无法获得CPU;

2. 中断处理的这几个阶段中不能调用可能导致睡眠的函数(包括分配内存);

对于第一个问题, 较新的linux内核增加了ksoftirqd内核线程, 如果持续处理的softirq超过一定数量, 则结束中断处理过程, 然后唤醒ksoftirqd, 让它来继续处理. 虽然softirq可能被推后到ksoftirqd内核线程去处理, 但是还是不能在softirq处理过程中睡眠, 因为不能保证softirq一定在ksoftirqd内核线程中被处理.

据说在montavista(一种嵌入式实时linux)中, 将内核的中断机制做了修改. (某些中断的)中断处理过程被赋予了task结构, 能够被内核调度. 解决了上述两个问题. (montavista的目标是实时性, 这样的做法牺牲了一定的整体性能.)

工作队列

linux基线版本的内核在解决上述问题上, 提供了workqueue机制.

定义一个work结构(包含了处理函数), 然后在上述的中断处理的几个阶段的某一步中调用schedule_work函数, work便被添加到workqueue中, 等待处理.

工作队列有着自己的处理线程, 这些work被推迟到这些线程中去处理. 处理过程只可能发生在这些工作线程中, 所以这里可以睡眠.

内核默认启动了一个工作队列, 对应一组工作线程events/n(n代表处理器编号, 这样的线程有n个). 驱动程序可以直接向这个工作队列添加任务. 某些驱动程序还可能会创建并使用属于自己的工作队列.

中断是计算机与外界联系的唯一途径。本文将分析在IA-32体系结构上的Linux内核对待中断系统的处理,针对的是2.6内核,引用的代码则具体则是2.6.14的。

一。几个相关概念的澄清

1, 中断信号:

在电路级别来说,中断就是输送到CPU的INTR引脚上的电平信号。

2, 可编程中断控制器(PIC,Programmable Interrupt Controller):

PIC是在计算机外部设备与CPU之间的芯片,它负责把自己接收到的外部中断信号,提交给CPU。在80386中,PIC是两片i8259A芯片级联;在Pentium以及后来的CPU中,集成了一个叫做高级可编程中断控制器(Advanced Programmable Interrupt Controller)的PIC。如果你想用IA32处理器搭建SMP系统,则APIC是必不可少的。

3, 中断向量与中断号:

中断向量是Intel从IA-32 CPU角度看到的中断信号划分;中断号则是Linux系统对外部中断的号码分配。当外设把中断信号递送给PIC时,与之关联的是一个“中断号”(每个中断号对应一条中断线,从软件的角度来看,这两个术语可以混用);当PIC把这个中断信号发送给CPU时,与之关联的是一个“中断向量”。

在IA-32体系结构中,所有的异常和不可屏蔽中断(Non-Maskable Interrupt)的中断向量都是Intel预先定义的,软件无法更改;可屏蔽中断的中断向量可以通过编程来更改。在Linux上,0号中断(也就是时钟中断)对应的中断向量是0x20,也就是十进制的32。

4, 异常(Exception)

顾名思义,异常是指CPU检测到了某种不正常的情形出现。CPU产生的异常是不可屏蔽的(eflags寄存器的IF位对异常不起作用),根据异常处理程序返回时,是否需要重新执行引发异常的那条指令,又可以把异常分为3种:

1)故障(Fault)。故障是比较轻微的异常,返回时重新执行引发故障的那条指令。

2)陷阱(Trap)。陷阱处理返回时,不重新执行引发陷阱的那条指令。

3)中止(Abort)。中止是严重的异常,将导致任务的中止而不会返回。

还有一种是程序产生的异常,如INT3指令、BOUND指令等。CPU把这种异常当作是陷阱来处理。

5, 中断描述表IDT

异常与中断发生时,都需要到IDT中查找相关信息,以找到对应的处理程序以及其他动作。需要注意的是,保护模式下发生权限提升时,中断穿越的是中断门,而异常穿越的是陷阱门。二者的区别是:当CPU穿越中断门时,是自动关中断的;而穿越异常门则不会。

二。重要数据结构与函数

在系统引导期间,需要初试化中断处理(asm/i386/kernel/entry.S):

422 #define BUILD_INTERRUPT(name, nr) \

423 ENTRY(name) \

424 pushl $nr-256; \ #这里得到一个负数,因为正数留给系统调用

425 SAVE_ALL \ #保存寄存器

426 movl %esp,%eax; \ 

427 call smp_/**/name; \

428 jmp ret_from_intr;

其中,SAVE_ALL宏就是用来保存寄存器的。

内核书籍中经常提到的中断上下文,指的是内核正在运行中断服务程序或softirq,无法代表当前进程的情形。中断上下文没有自己专有的堆栈,相反,它借用被中断进程的内核堆栈──IA-32上的Linux默认这个堆栈只有8k大小,而且很可能在处理中断的过程中又被另一个中断源中断。因此如果你自己编写中断处理程序,递归层次太深或者函数局部变量太大,都有可能导致栈溢出。(i386有一个4KStacks补丁,如果编译时打开该选项,则中断上下文使用独立的栈,而不占用被中断进程的。)

在include/linux/irq.h文件中,定义了一个中断描述数组iqr_desc[NR_IRQS],每一个中断向量都与它的一个元素相关联:

70 typedef struct irq_desc {

71 hw_irq_controller *handler;

72 void *handler_data;

73 struct irqaction *action; /* IRQ action list */

74 unsigned int status; /* IRQ status */

75 unsigned int depth; /* nested irq disables */

76 unsigned int irq_count; /* For detecting broken interrupts */

77 unsigned int irqs_unhandled;

78 spinlock_t lock;

79 #if defined (CONFIG_GENERIC_PENDING_IRQ) || defined (CONFIG_IRQBALANCE)

80 unsigned int move_irq; /* Flag need to re-target intr dest*/

81 #endif

82 } ____cacheline_aligned irq_desc_t; /*告诉GCC与CPU的L1告诉缓存对齐*/

83

84 extern irq_desc_t irq_desc [NR_IRQS];

当一个中断发生时,内核的处理是这样的(arch/i386/kernel/entry.S):

416 common_interrupt:

417 SAVE_ALL

418 movl %esp,%eax

419 call do_IRQ

420 jmp ret_from_intr

SAVE_ALL宏定义在entry.S中,负责保存寄存器,再将%esp寄存器移送到%eax中,调用do_IRQ()函数(arch/i386/kernel/irq.c):

/*

* do_IRQ()函数负责处理所有的外部设备中断(处理器间中断由它们各自

* 的处理函数来处理

*/

fastcall unsigned int do_IRQ(struct pt_regs *regs)



/* high bits used in ret_from_ code */

int irq = regs->orig_eax & 0xff;

/* i386上如果定义了CONFIG_4KSTAKS,就申请独立的栈,而不占用被中断进程的*/

#ifdef CONFIG_4KSTACKS

union irq_ctx *curctx, *irqctx;

u32 *isp;

#endif

irq_enter();

#ifdef CONFIG_DEBUG_STACKOVERFLOW

/* 检查堆栈溢出的代码,此处省去。 */

#endif

#ifdef CONFIG_4KSTACKS

curctx = (union irq_ctx *) current_thread_info();

irqctx = hardirq_ctx[smp_processor_id()];

/*

* 这是我们切换到中断栈的地方。然而,如果我们已经在使用中断栈(也

* 就是说,我们这次是中断了一个中断处理程序),我们就不切换栈,而

* 是继续使用当前的栈(此时,“当前的栈”是一个中断栈)

*/

if (curctx != irqctx) {

int arg1, arg2, ebx;

/* build the stack frame on the IRQ stack */

isp = (u32*) ((char*)irqctx + sizeof(*irqctx));

irqctx->tinfo.task = curctx->tinfo.task;

irqctx->tinfo.previous_esp = current_stack_pointer;

asm volatile(

" xchgl %%ebx,%%esp \n"

" call __do_IRQ \n"

" movl %%ebx,%%esp \n"

: "=a" (arg1), "=d" (arg2), "=b" (ebx)

: "0" (irq), "1" (regs), "2" (isp)

: "memory", "cc", "ecx"

);

} else

#endif

__do_IRQ(irq, regs); //真正的中断处理

irq_exit(); /* 如果需要,处理softirq。注意,这里有两种可能不需要处理softirq:1, local_softirq_pending为假;2,我们刚刚是中断了一个中断,嵌套中断没有最终返回之前,softirq是不能处理的。 */

return 1;

}

注意,fastcall是在include/asm-i386/linkage.h中定义的宏,它指导GCC连接时把fastcall修饰的函数的前三个参数用寄存器传递。另外一个类似的宏asmlinkage则告诉GCC不要用寄存器传递参数,asmlinkage和fastcall不能共存。

上面的do_IRQ()函数调用的__do_IRQ()代码如下(arch/i386/kernel/irq.c)

fastcall unsigned int __do_IRQ(unsigned int irq, struct pt_regs *regs)

{

irq_desc_t *desc = irq_desc + irq; /* 找到在irq_desc数组中的位置 */

struct irqaction * action; /* 取得相应的irqaction结构 */

unsigned int status;

kstat_this_cpu.irqs[irq]++; 

if (CHECK_IRQ_PER_CPU(desc->status)) {

irqreturn_t action_ret;

/*

* 因为irq_desc[]数组中,每个CPU占一个元素,这里的desc就是本CPU

* 数据,所以此处不需要加锁。

*/

desc->handler->ack(irq);

action_ret = handle_IRQ_event(irq, regs, desc->action);

desc->handler->end(irq);

return 1;

}

spin_lock(&desc->lock);

desc->handler->ack(irq); /* 给i8259A或APIC应答信号 */

/*

* REPLAY is when Linux resends an IRQ that was dropped earlier

* WAITING is used by probe to mark irqs that are being tested

*/

status = desc->status & ~(IRQ_REPLAY | IRQ_WAITING);

status |= IRQ_PENDING; /* we _want_ to handle it */

/*

* If the IRQ is disabled for whatever reason, we cannot

* use the action we have.

*/

action = NULL;

if (likely(!(status & (IRQ_DISABLED | IRQ_INPROGRESS)))) { /* 判断该IRQ是否是被禁止的,或者是已经在其他CPU上被处理 */

action = desc->action;

status &= ~IRQ_PENDING; /* 我们将处理它 */

status |= IRQ_INPROGRESS; /* 置位IRQ_INPROGRESS,以便其他CPU注意 */

}

desc->status = status;

/* 

* 如果该IRQ没有处理函数,或者被禁止了,及早离开。

* 因为我们置位了PENDING,如果别的CPU正在处理该IRQ的

* 另一个实例,它就会小心些。

*/

if (unlikely(!action))

goto out;

/*

* Edge triggered interrupts need to remember

* pending events.

* This applies to any hw interrupts that allow a second

* instance of the same irq to arrive while we are in do_IRQ

* or in the handler. But the code here only handles the _second_

* instance of the irq, not the third or fourth. So it is mostly

* useful for irq hardware that does not mask cleanly in an

* SMP environment.

*/

for (;;) {

irqreturn_t action_ret;

spin_unlock(&desc->lock);

action_ret = handle_IRQ_event(irq, regs, action);

spin_lock(&desc->lock);

if (!noirqdebug)

note_interrupt(irq, desc, action_ret, regs);

if (likely(!(desc->status & IRQ_PENDING)))

break;

desc->status &= ~IRQ_PENDING;

}

desc->status &= ~IRQ_INPROGRESS;

out:

/*

* ->end()用来处理那些由于别的CPU正在运行其处理程序而被禁止的中断

*/ 

desc->handler->end(irq);

spin_unlock(&desc->lock);

return 1;

}

三。中断机制在SMP系统上的变化

当intel考虑如何在IA-32上架构SMP时,原来的中断控制器i8259A就显得力不从心了。在SMP上,必须考虑外部设备来的中断信号如何传递给某个合适的CPU问题,必须考虑IPI(Inter-Percossor Interrupt,处理器间中断)问题。Intel自Pentium之后,在CPU中集成了APIC,在SMP上,主板上有一个(至少一个,有的主板有多个IO-APIC,用来更好的分发中断信号)全局的APIC,它负责从外设接收中断信号,再分发到CPU上,这个全局的APIC被称作IO-APIC。

SMP的中断机制如下图所示:



图1: SMP系统中的中断分发示意图

在系统引导的时候,通过setup_IO_APIC()函数(arch/i386/kernel/io_apic.c)对IO-APIC进行初试化;每个CPU被激活成为online状态的时候,通过setup_local_APIC()函数(arch/kernel/i386/apic.c)对本地APIC进行初试化。

在SMP系统上,Linux除了处理CPU异常、外部设备中断之外,还要处理处理器间中断。当一个CPU想对另一个CPU发送中断信号时,就在自己的本地APIC的ICR寄存器(Interrupt Command Register,中断命令寄存器)中存放其中断向量,和目标CPU拥有的本地APIC的标识,触发中断。IPI中断信号经由APIC总线传递到目标APIC,那个收到中断的APIC就向自己所属的CPU发送一个中断。

Linux针对IA32的SMP系统定义了五种IPI:

1, CALL_FUNCTION_VECTOR。发往自己除外的所有CPU,强制它们执行指定的函数;

2, RESCHEDULE_VECTOR。使被中断的CPU重新调度;

3, INVLIDATE_TLB_VECTOR。使被中断的CPU废弃自己的TLB缓存内容。

4, ERROR_APIC_VECTOR。

5, SPUROUS_APIC_VECTOR。

在IA-32体系结构中,SMP的高速缓存一致性(Cache Coherence)问题是通过一种叫做总线监视(Bus watching,也叫Snoopying)的硬件技术来解决的。每当某个CPU或DMA控制器改写了某块内存区域的内容(这总是要通过总线来进行的,所以逃不过总线监视),别的CPU就会自动废弃缓存了该内存区域的Cache。然而对TLB的情况则有所不同(为什么不同?Intel的手册说TLB也可以对软件透明,这里有点疑惑),Linux内核中,每个CPU在改变了页表的时候,都需要给其它所有运行着与该页表有关的任务的CPU发送IPI,使它们废弃自己的TLB内容。

参考:

Understanding the Linux Kernel,2nd

IA-32 Intel Architecture Software Developer’s Manual, Volume 3: System Programming Guide
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息