3、内核的中断处理
3.1、中断处理入口
由上节可知,中断向量的对应的处理程序位于interrupt数组中,下面来看看interrupt:
341
.data #数据段
342
ENTRY(interrupt)
343
.text
344
345
vector=
0
346
ENTRY(irq_entries_start)
347
.rept NR_IRQS #
348
-354行重复NR_IRQS次
348
ALIGN
349
1
: pushl $vector-
256
#vector在354行递增
350
jmp
common_interrupt #所有的外部中断处理函数的统一部分,以后再讲述
351
.data
352
.long 1b #存储着指向349行的地址,但是随着348行-354被gcc展开,每次的值都不同
353
.text
354
vector=vector+
1
355
.endr #与347行呼应
356
357
ALIGN
#公共处理函数
common_interrupt:
SAVE_ALL /*寄存器值入栈*/
movl %esp,%eax /*栈顶指针保存到eax*/
call
do_IRQ /*处理中断*/
jmp
ret_from_intr /*从中断返回*/
分析如下:
首先342行和352行都处于.data段,虽然看起来它们是隔开的,但实际上被gcc安排在了连续的数据段内存 中,同理在代码段内存中,354行与350行的指令序列也是连续存储的。另外,348-354行会被gcc展开NR_IRQS次,因此每次352行都会存 储一个新的指针,该指针指向每个349行展开的新对象。最后在代码段内存中连续存储了NR_IRQS个代码片断,首地址由 irq_entries_start指向。而在数据段内存中连续存储了NR_IRQS个指针,首址存储在interrupt这个全局变量中。这样,例如 IRQ号是0 (从init_IRQ()调用,它对应的中断向量是FIRST_EXTERNAL_VECTOR)的中断通过中断门后会触发 interrput[0],从而执行:
pushl 0-256
jmp common_interrupt
的代码片断,进入到Linux内核安排好的中断入口路径。
3.2、数据结构
3.2.1、IRQ描述符
Linux支持多个外设共享一个IRQ,同时,为了维护中断向量和中断服务例程(ISR)之间的映射关系,Linux用一个irq_desc_t数据结构来描述,叫做IRQ描述符。除了分配给异常的
32个向量外,其余224(NR_IRQS)个中断向量对应的IRQ构成一个数组irq_desc[],定义如下:
//
位于linux/irq.h
typedef
struct
irq_desc {
unsigned
int
status;
/*
IRQ status
*/
hw_irq_controller
*
handler;
struct
irqaction
*
action;
/*
IRQ action list
*/
unsigned
int
depth;
/*
nested irq disables
*/
unsigned
int
irq_count;
/*
For detecting broken interrupts
*/
unsigned
int
irqs_unhandled;
spinlock_t
lock
;
} ____cacheline_aligned irq_desc_t;
//
IRQ描述符表
extern
irq_desc_t irq_desc [NR_IRQS];
“____cacheline_aligned”表示这个数据结构的存放按32字节(高速缓存行的大小)进行对齐,以便于将来存放在高速缓存并容易存取。
status:描述IRQ中断线状态,在irq.h中定义。如下:
#define
IRQ_INPROGRESS 1 /* 正在执行这个IRQ的一个处理程序*/
#define
IRQ_DISABLED 2 /* 由设备驱动程序已经禁用了这条IRQ中断线 */
#define
IRQ_PENDING 4 /* 一个IRQ已经出现在中断线上,且被应答,但还没有为它提供服务 */
#define
IRQ_REPLAY 8 /* 当Linux重新发送一个已被删除的IRQ时 */
#define
IRQ_AUTODETECT 16 /* 当进行硬件设备探测时,内核使用这条IRQ中断线 */
#define
IRQ_WAITING 32 /*当对硬件设备进行探测时,设置这个状态以标记正在被测试的irq */
#define
IRQ_LEVEL 64 /* IRQ level triggered */
#define
IRQ_MASKED 128 /* IRQ masked - shouldn't be seen again */
#define
IRQ_PER_CPU 256 /* IRQ is per CPU */
handler:指向hw_interrupt_type描述符,这个描述符是对中断控制器的描述。下面有描述。
action:指向一个单向链表的指针,这个链表就是对中断服务例程进行描述的irqaction结构。下面有描述。
depth:如果启用这条IRQ中断线,depth则为0,如果禁用这条IRQ中断线不止一次,则为一个正数。每当调用一次disable_irq(),该函数就对这个域的值加1;如果depth等于0,该函数就禁用这条IRQ中断线。相反,每当调用enable_irq()函数时,该函数就对这个域的值减1;如果depth变为0,该函数就启用这条IRQ中断线。
lock:保护该数据结构的自旋锁。
IRQ描述符的初始化:
//
位于arch/i386/kernel/i8259.c
void
__init init_ISA_irqs (
void
)
{
int
i;
#ifdef CONFIG_X86_LOCAL_APIC
init_bsp_APIC();
#endif
//
初始化8259A
init_8259A(
0
);
//
IRQ描述符的初始化
for
(i
=
0
; i
<
NR_IRQS; i
++
) {
irq_desc[i].status
=
IRQ_DISABLED;
irq_desc[i].action
=
NULL;
irq_desc[i].depth
=
1
;
if
(i
<
16
) {
/*
* 16 old-style INTA-cycle interrupts:
*/
irq_desc[i].handler
=
&
i8259A_irq_type;
}
else
{
/*
* 'high' PCI IRQs filled in on demand
*/
irq_desc[i].handler
=
&
no_irq_type;
}
}
}
从这段程序可以看出,初始化时,让所有的中断线都处于禁用状态;每条中断线上还没有任何中断服务例程(action为0);因为中断线被禁用,因此depth为1;对中断控制器的描述分为两种情况,一种就是通常所说的8259A,另一种是其它控制器。
3.2.2、中断控制器描述符hw_interrupt_type
这个描述符包含一组指针,指向与特定的可编程中断控制器电路(PIC)打交道的低级I/O例程,定义如下:
//
位于linux/irq.h
struct
hw_interrupt_type {
const
char
*
typename;
unsigned
int
(
*
startup)(unsigned
int
irq);
void
(
*
shutdown)(unsigned
int
irq);
void
(
*
enable)(unsigned
int
irq);
void
(
*
disable)(unsigned
int
irq);
void
(
*
ack)(unsigned
int
irq);
void
(
*
end)(unsigned
int
irq);
void
(
*
set_affinity)(unsigned
int
irq, cpumask_t dest);
};
typedef
struct
hw_interrupt_type hw_irq_controller;
Linux除了支持常见的8259A芯片外,也支持其他的PIC电路,如SMP IO-APIC、PIIX4的内部 8259 PIC及 SGI的Visual Workstation Cobalt (IO-)APIC。8259A的描述符如下:
//
位于arch/i386/kernel/i8259.c
static
struct
hw_interrupt_type i8259A_irq_type
=
{
"
XT-PIC
"
,
startup_8259A_irq,
shutdown_8259A_irq,
enable_8259A_irq,
disable_8259A_irq,
mask_and_ack_8259A,
end_8259A_irq,
NULL
};
在这个结构中的第一个域“XT-PIC”是一个名字。接下来,8259A_irq_type包含的指针指向五个不同的函数,这些函数就是对PIC编程的函数。前两个函数分别启动和关闭这个芯片的中断线。但是,在使用8259A芯片的情况下,这两个函数的作用与后两个函数是一样的,后两个函数是启用和禁用中断线。mask_and_ack_8259A函数通过把适当的字节发往8259A I/O端口来应答所接收的IRQ。end_8259A_irq在IRQ的中断处理程序结束时被调用。
3.2.3、中断服务例程描述符irqaction
为了处理多个设备共享一个IRQ,Linux中引入了irqaction数据结构。定义如下:
//
位于linux/interrupt.h
struct
irqaction {
irqreturn_t (
*
handler)(
int
,
void
*
,
struct
pt_regs
*
);
unsigned
long
flags;
cpumask_t mask;
const
char
*
name;
void
*
dev_id;
struct
irqaction
*
next;
int
irq;
struct
proc_dir_entry
*
dir;
};
handler:指向一个具体I/O设备的中断服务例程。这是允许多个设备共享同一中断线的关键域。
flags:用一组标志描述中断线与I/O设备之间的关系。
SA_INTERRUPT
中断处理程序必须以禁用中断来执行
SA_SHIRQ
该设备允许其中断线与其他设备共享。
SA_SAMPLE_RANDOM
可以把这个设备看作是随机事件发生源;因此,内核可以用它做随机数产生器。(用户可以从/dev/random 和/dev/urandom设备文件中取得随机数而访问这种特征)
SA_PROBE
内核在执行硬件设备探测时正在使用这条中断线。
name:I/O设备名(通过读取/proc/interrupts文件,可以看到,在列出中断号时也显示设备名。)
dev_id:指定I/O设备的主设备号和次设备号。
next:指向irqaction描述符链表的下一个元素。共享同一中断线的每个硬件设备都有其对应的中断服务例程,链表中的每个元素就是对相应设备及中断服务例程的描述。
irq:IRQ线。
3.2.4、中断服务例程(Interrupt Service Routine)
在Linux中,中断服务例程和中断处理程序(Interrupt Handler)是两个不同的概念。可以这样认为,中断处理程序相当于某个中断向量的总处理程序,它与中断描述表(IDT)相关;中断服务例程(ISR)在中断处理过程被调用,它与IRQ描述符相关,一般来说,它是设备驱动的一部分。
(1) 注册中断服务例程
中断服务例程是硬件驱动的组成部分,如果设备要使用中断,相应的驱动程序在初始化的过程中可以通过调用request_irq函数注册中断服务例程。
//
位于kernel/irq/manage.c
/*
irq:IRQ号
**handler:中断服务例程
**irqflags:SA_SHIRQ,SA_INTERRUPT或SA_SAMPLE_RANDOM
**devname:设备名称,这些名称会被/proc/irq和/proc/interrupt使用
**dev_id:主要用于设备共享
*/
int
request_irq(unsigned
int
irq,
irqreturn_t (
*
handler)(
int
,
void
*
,
struct
pt_regs
*
),
unsigned
long
irqflags,
const
char
*
devname,
void
*
dev_id)
{
struct
irqaction
*
action;
int
retval;
/*
* Sanity-check: shared interrupts must pass in a real dev-ID,
* otherwise we'll have trouble later trying to figure out
* which interrupt is which (messes up the interrupt freeing
* logic etc).
*/
if
((irqflags
&
SA_SHIRQ)
&&
!
dev_id)
return
-
EINVAL;
if
(irq
>=
NR_IRQS)
return
-
EINVAL;
if
(
!
handler)
return
-
EINVAL;
//
分配数据结构空间
action
=
kmalloc(
sizeof
(
struct
irqaction), GFP_ATOMIC);
if
(
!
action)
return
-
ENOMEM;
action
->
handler
=
handler;
action
->
flags
=
irqflags;
cpus_clear(action
->
mask);
action
->
name
=
devname;
action
->
next
=
NULL;
action
->
dev_id
=
dev_id;
//
调用setup_irq完成真正的注册,驱动程序也可以调用它来完成注册
retval
=
setup_irq(irq, action);
if
(retval)
kfree(action);
return
retval;
}
来看实时时钟初始化函数如何使用request_irq():
//
位于driver/char/rtc.c
static
int
__init rtc_init(
void
)
{
request_irq(RTC_IRQ, rtc_int_handler_ptr, SA_INTERRUPT,
"
rtc
"
, NULL);
}
再看看时钟中断初始化函数:
//
位于arch/i386/mach_default/setup.c
static
struct
irqaction irq0
=
{ timer_interrupt, SA_INTERRUPT, CPU_MASK_NONE,
"
timer
"
, NULL, NULL};
//
由time_init()调用
void
__init time_init_hook(
void
)
{
setup_irq(
0
,
&
irq0);
}
3.3、中断处理流程
整个流程如下:
所有I/O中断处理函数的过程如下:
(1)把IRQ值和所有寄存器值压入内核栈;
(2) 给与IRQ中断线相连的中断控制器发送一个应答,这将允许在这条中断线上进一步发出中断请求;
(3)执行共享这个IRQ的所有设备的中断服务例程(ISR);
(4)跳到ret_from_intr()处结束。
3.3.1、保存现场与恢复现场
中断处理程序做的第一件事就是保存现场,由宏SAVE_ALL(位于entry.S中)完成:
#define SAVE_ALL \
cld
;
\
pushl %es
;
\
pushl %ds
;
\
pushl %eax
;
\
pushl %ebp
;
\
pushl %edi
;
\
pushl %esi
;
\
pushl %edx
;
\
pushl %ecx
;
\
pushl %ebx
;
\
movl $(__USER_DS), %edx
;
\
movl %edx, %ds
;
\
movl %edx, %es
;
当内核执行SAVE_ALL后,内核栈中的内容如下:
恢复现场由宏RESTORE_ALL完成
#define RESTORE_ALL \
RESTORE_REGS \
addl $
4
, %esp
;
\
1
:
iret
;
\
.section .fixup,
"
ax
"
;
\
2
:
sti
;
\
movl $(__USER_DS), %edx
;
\
movl %edx, %ds
;
\
movl %edx, %es
;
\
movl $
11
,%eax
;
\
call
do_exit
;
\
.previous
;
\
.section __ex_table,
"
a
"
;
\
.align
4
;
\
.long 1b,2b
;
\
.previous
3.3.2、do_IRQ()函数
该函数的大致内容如下:
//
arch/i386/kernel/irq.c
fastcall unsigned
int
do_IRQ(
struct
pt_regs
*
regs)
{
/*
high bits used in ret_from_ code
*/
//
取得中断号
int
irq
=
regs
->
orig_eax
&
0xff
;
//
增加代表嵌套中断数量的计数器的值,该值保存在current->thread_info->preempt_count
irq_enter();
__do_IRQ(irq, regs);
//
减中断计数器preempt_count的值,检查是否有软中断要处理
irq_exit();
}
结构体pt_regs如下,位于inclue/asm-i386/ptrace.h:
struct pt_regs {
long ebx
;
long ecx
;
long edx
;
long esi
;
long edi
;
long ebp
;
long eax
;
int
xds
;
int
xes
;
long orig_eax
;
long eip
;
int
xcs
;
long eflags
;
long esp
;
int
xss
;
}
;
与内核栈相比,是内核栈中内容的一致。
3.3.3、__do_IRQ()函数
该函数的内容如下:
//
位于kernel/irq/handle.c
fastcall unsigned
int
__do_IRQ(unsigned
int
irq,
struct
pt_regs
*
regs)
{
irq_desc_t
*
desc
=
irq_desc
+
irq;
struct
irqaction
*
action;
unsigned
int
status;
kstat_this_cpu.irqs[irq]
++
;
if
(desc
->
status
&
IRQ_PER_CPU) {
irqreturn_t action_ret;
/*
* No locking required for CPU-local interrupts:
*/
//
确认中断
desc
->
handler
->
ack(irq);
action_ret
=
handle_IRQ_event(irq, regs, desc
->
action);
if
(
!
noirqdebug)
note_interrupt(irq, desc, action_ret);
desc
->
handler
->
end(irq);
return
1
;
}
/*
加自旋锁.对于多CPU系统,这是必须的,因为同类型的其它中断可能产生,并被其它的CPU处理,
**没有自旋锁,IRQ描述符将被多个CPU同时访问.
*/
spin_lock(
&
desc
->
lock
);
/*
确认中断.对于8259A PIC,由mask_and_ack_8259A()完成确认,并禁用当前IRQ线.
**屏蔽中断是为了确保该中断处理程序结束前,CPU不会又接受这种中断.虽然,CPU在处理中断会自动
**清除eflags中的IF标志,但是在执行中断服务例程前,可能重新激活本地中断.见handle_IRQ_event.
*/
/*
在多处理器上,应答中断依赖于具体的中断类型.可能由ack方法做,也可能由end方法做.不管怎样,在中断处理结束
*前,本地APIC不再接收同样的中断,尽管这种中断可以被其它CPU接收.
*/
desc
->
handler
->
ack(irq);
/*
* REPLAY is when Linux resends an IRQ that was dropped earlier
* WAITING is used by probe to mark irqs that are being tested
*/
//
清除IRQ_REPLAY和IRQ_WAITING标志
status
=
desc
->
status
&
~
(IRQ_REPLAY
|
IRQ_WAITING);
/*
IRQ_PENDING表示一个IRQ已经出现在中断线上,且被应答,但还没有为它提供服务
*/
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;
/*
现在开始检查是否真的需要处理中断.在三种情况下什么也不做.
*(1)IRQ_DISABLED被设置.即使在相应的IRQ线被禁止的情况下,do_IRQ()也可能执行.
*(2)IRQ_INPROGRESS被设置时,在多CPU系统中,表示其它CPU正在处理同样中断的前一次发生.Linux中,同类型
*中断的中断服务例程由同一个CPU处理.这样使得中断服务例程不必是可重入的(在同一CPU上串行执行).
*
*(3)action==NULL.此时,直接跳到out处执行.
*/
if
(likely(
!
(status
&
(IRQ_DISABLED
|
IRQ_INPROGRESS)))) {
action
=
desc
->
action;
//
清除IRQ_PENDING标志
status
&=
~
IRQ_PENDING;
/*
we commit to handling
*/
/*
表示当前CPU正在处理该中断,其它CPU不应该处理同样的中断,而应该让给本CPU处理.一旦设置
*IRQ_INPROGRESS,其它CPU即使进行do_IRQ,也不会执行该程序段,则action==NULL,则其它CPU什么也不做.
*当调用handle_IRQ_event执行中断服务例程时,由于释放了自旋锁,其它CPU可能接受到同类型的中断(本CPU
*不会接受同类型中断),而进入do_IRQ(),并设置IRQ_PENDING.
*/
status
|=
IRQ_INPROGRESS;
/*
we are handling it
*/
}
desc
->
status
=
status;
/*
* If there is no IRQ handler or it was disabled, exit early.
* Since we set PENDING, if another processor is handling
* a different instance of this same irq, the other processor
* will take care of it.
*/
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);
/*
如果此时IRQ_PENDING处于清除状态,说明中断服务例程已经执行完毕,退出循环.反之,说明在执行中断服务例程时,
*其它CPU进入过do_IRQ,并设置了IRQ_PENDING.也就是说其它CPU收到了同类型的中断.此时,应该清除
*IRQ_INPROGRESS,并重新循环,执行中断服务例程,处理其它CPU收到的中断.
*/
if
(likely(
!
(desc
->
status
&
IRQ_PENDING)))
break
;
desc
->
status
&=
~
IRQ_PENDING;
}
/*
所有中断处理完毕,则清除IRQ_INPROGRESS
*/
desc
->
status
&=
~
IRQ_INPROGRESS;
out
:
/*
* The ->end() handler has to deal with interrupts which got
* disabled while the handler was running.
*/
//
结束中断处理.对end_8259A_irq()仅仅是重新激活中断线.
/*
对于多处器,end应答中断(如果ack方法还没有做的话)
*/
desc
->
handler
->
end(irq);
//
最后,释放自旋锁,
spin_unlock(
&
desc
->
lock
);
return
1
;
}
3.3.4、handle_IRQ_event
//kernel/irq/handle.c
fastcall int handle_IRQ_event(unsigned int irq, struct pt_regs *regs,
struct irqaction *action)
{
int ret, retval = 0, status = 0;
//开启本地中断,对于单CPU,仅仅是sti指令
if (!(action->flags & SA_INTERRUPT))
local_irq_enable();
//依次调用共享该中断向量的服务例程
do {
//调用中断服务例程
ret = action->handler(irq, action->dev_id, regs);
if (ret == IRQ_HANDLED)
status |= action->flags;
retval |= ret;
action = action->next;
} while (action);
if (status & SA_SAMPLE_RANDOM)
add_interrupt_randomness(irq);
//关本地中断,对于单CPU,为cli指令
local_irq_disable();
return retval;
}