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

Linux内核的中断机制

2013-03-15 11:02 260 查看
§5.1 I386的中断与异常

中断通常被分为“同步中断”和异步中断两大类。同步中断是指当指令执行时由CPU控制单元产生的中断,之所以称为“同步中断”是因为只有在一条指令中止执行后CPU才会发出这类中断信号。而异步中断则是指由其他硬件设备依照CPU时钟随机产生的中断信号。

在Intel 80x86 CPU手册中,同步中断和异步中断也被分别称为“异常(Exception)”和“中断(Interrupt)”。Intel又详细地把中断和异常细分为以下几类:

(1)中断

1. 可屏蔽中断(Maskable Interrupt):这类中断被送到CPU的INTR引脚,通过清除eflag寄存器的IF标志可以关闭中断。

2. 不可屏蔽中断(Nonmaskable Interrupt):被送到CPU的NMI引脚,通常只有几个危急的事件,如:硬件故障等,才产生不可屏蔽中断信号。寄存器eflag中的IF标志对这类中断不起作用。

(2)异常

1. 处理器探测异常(Processor-detected exception):当CPU执行一条指令时所探测到的一个反常条件所产生的异常。依据CPU控制单元产生异常时保存在内核态堆栈eip寄存器的值,这类异常又可以细分为三种:

n 故障(Fault):保存在eip中的值是引起故障的指令地址,因此但异常处理程序结束后,会重新执行那条指令。“缺页故障”是这类异常的一个常见例子。

n 陷阱(Trap):保存在eip中的值是一个指令地址,但该指令在引起陷阱的指令地址之后。只有当没有必要重新执行已执行过的指令时,才会触发trap,其主要用途是调试程序。

n 异常中止(Abort):当发生了一个严重的错误,致使CPU控制单元除了麻烦而不能在eip寄存器中保存有意义的值。异常中止通常是由硬件故障或系统表中无效的值引起的。由CPU控制单元发生的这个中断是一种紧急信号,用来把CPU的执行路径切换到异常中止的处理程序,而相应的ISR通常除了迫使受到影响的进程中止外,别无选择。

2. 编程异常(Programmed Exception):通常也称为“软中断(software interrupt)”,是由编程者发出中断请求指令时发生的中断,如:int指令和int3指令。当into(检查溢出)和bound(检查地址越界)指令检查的条件不为真时,也引起编程异常。CPU控制单元把编程异常当作Trap来处理,这类异常有两个典型的用途:一、执行系统调用;二、给调试程序通报一个特定条件。

5.1.1 中断向量

每个中断和异常都可以用一个0-255之间的无符号整数来标识,Intel称之为“中断向量(Interrupt Vector)”。通常,不可屏蔽中断和异常的中断向量是固定的,而可屏蔽中断的中断向量则可以对中断控制器进行编程来改变。I386 CPU的256个中断向量是这样分配的:

1. 从0-31这一共32个向量用于异常和不可屏蔽中断。

2. 从32-47这一共16个向量用于可屏蔽中断,分别对应于主、从8259A中断控制器的IRQ输入线。

3. 剩余的48-255用于标识软中断。

Linux全部使用了0-47之间的向量。但对于48-255之间的软中断向量,Linux只使用了其中的一个,即用于实现系统调用的中断向量128(0x80)。当用户态下的进程执行一条int 0x80汇编指令时,CPU切换到内核态,以服务于系统调用。

Linux在头文件include/asm-i386/hw_irq.h中定义了宏FIRST_EXTERNAL_VECTOR来表示第一个外设中断(即8259A的IRQ0)所对应的中断向量,此外还定义了SYSCALL_VECTOR来表示用于系统调用的中断向量。如下所示:

/*

* IDT vectors usable for external interrupt sources start

* at 0x20:

*/

#define FIRST_EXTERNAL_VECTOR 0x20

#define SYSCALL_VECTOR 0x80

5.1.2 I386的IDT

i386 CPU的IDT表一共有256项,分别对应每一个中断向量。每一个表项就是一个中断描述符,用以描述相对应的中断向量,中断向量就是该描述符在IDT中的索引,每一个中断描述符的大小都是8个字节。根据INTEL的术语,中断描述符也称为“门(Gate)”。

中断描述符有下列4种类型:

(1)任务们(Task Gate):包含了一个进程的TSS段选择符。每当中断信号发生时,它被用来取代当前进程的那个TSS段选择符。Linux并没有使用任务们。任务们的格式如下:

(2)中断门(Interrupt Gate):中断门中包含了一个段选择符和一个中断处理程序的段内偏移。注意,当I386 CPU穿越一个中断门进入相应的中断处理程序时,它会清除eflag寄存器中的IF标志,从而屏蔽接下来可能发生的可屏蔽中断。

(3)陷阱门(Trap Gate):与中断门类似,不同之处在于CPU通过陷阱门转入中断处理程序时不会清除IF标志。

(4)调用门(Call Gate):Linux并没有使用调用门。

这三种门的格式如图5-2所示。

5.1.3 中断控制器8259A

我们都知道,PC机中都使用两个级联的8359A PIC(Programmable Interrupt Controller,可编程中断控制器,简称PIC)来管理来自系统外设的中断信号。每个8259A PIC提供8根IRQ(Interrupt ReQuest,中断请求,简称IRQ)输入线。在级联方式中,Master 8259A PIC(第一个PIC)的中断信号输入线IR2用于级联Slave 8259A PIC(第二个PIC)的INT引脚,因此两个8259A一共可以提供15根可用的IRQ输入线。如下图所示:

图5-3 主、从8259A中断控制器的级联

5.1.3.1 8259A PIC的基本原理

8259A PIC芯片的基本逻辑块图如下所示:

“中断屏蔽寄存器”(Interrupt Mask Register,简称IMR)用于屏蔽8259A的中断信号输入,每一位对应一个输入。当IMR中的bit(0≤i≤7)位被置1时,相对应的中断信号输入线IRi上的中断信号将被8259A所屏蔽,也即IRi被禁止。

当外设产生中断信号时(由低到高的跳变信号,80x86系统中的8259A是边缘触发的,Edge Triggered),中断信号被输入到“中断请求寄存器”(Interrupt Request Register,简称IRR),并同时看看IMR中的相应位是否已被设置。如果没有被设置,则IRR中的相应位被设置为1,表示外设产生一个中断请求,等待CPU服务。

然后,8259A的优先级仲裁部分从IRR中选出一个优先级最高中断请求。优先级仲裁之后,8259A就通过其INT引脚向CPU发出中断信号,以通知CPU有外设请求中断服务。CPU在其当前指令执行完后就通过他的INTA引脚给8259A发出中断应答信号,以告诉8259A,CPU已经检测到有中断信号产生。

8259A在收到CPU的INTA信号后,将优先级最高的那个中断请求在ISR寄存器(In-Service Register,简称ISR)中对应的bit置1,表示该中断请求已得到CPU的服务,同时IRR寄存器中的相应位被清零重置。

然后,CPU再向8259A发出一个INTA脉冲信号,8259A在收到CPU的第二个INTA信号后,将中断请求对应的中断向量放到数据总线上,以供CPU读取。CPU读到中断向量后,就可以装入执行相应的中断处理程序。

如果8259A工作在AEOI(Auto End Of Interrupt,简称AEOI)模式下,则当他收到CPU的第二个INTA信号时,它就自动重置ISR寄存器中的相应位。否则,ISR寄存器中的相应位就一直保持为1,直到8259A显示地收到来自于CPU的EOI命令。

5.1.3.2 8259A的I/O端口

Master 8259A的IO端口地址位0x20和0x21,Slave 8259A的IO端口地址为0xA0和0xA1。对这些IO端口进行读写操作时的功能如下表所示:

I/O Port Addrss Read/Write Function

Port A(0x20/0xA0) W Initialization Command Word1(ICW1)

W Operation Command Word2(OCW2)

W Operation Command Word3(OCW3)

R Interrupt Request Register(IRR)

R In-Service Register(ISR)

Port B(0x21/0xA1) W Initialization Command Word2(ICW2)

W Initialization Command Word3(ICW3)

W Initialization Command Word4(ICW4)

W Operation Command Word1(OCW1)

R Interrupt Mask Register(IMR)

表5-1 8259A的I/O端口地址列表

5.1.3.3 初始化8259A

8259A PIC的初始化是通过向其写一系列“初始化命令字”(Initialization Command Word,简称ICW)来实现的。其中,ICW1必须写到Port A(0x20/0xA0)中,而ICW2、ICW3和ICW4则必须写到Port B(0x21/0xA1)中。此外,主、从8259A必须分别进行初始化。

ICW1的格式如下图5-5所示:

ICW2的格式如下图5-6所示:

在MCS-80/85模式下,A15-A8指定中断向量地址;而在80x86模式下,T7-T3用于指定中断向量地址,而bit[2:0]则可被设置为0。

ICW3(Master Device)的格式如下:

Si(0≤i≤7)为1则表示相应的IRi上级联了一个Slave 8259A PIC。

ICW3(Slave Device)的格式如下:

Bit[7:3]总为0,而ID2、ID1、ID0(即bit[2:0])用于标识Slave 8259A连接在Master 8259A的哪一根IRQ线上。例如:010就表示Slave 8259A是连接在Master 8259A的IR2上。

ICW4的格式如下:

5.1.3.4 控制8259A

可以向Port A或Port B写入“控制命令字”(Control Command Word,简称OCW)来控制8259A PIC。有三种类型的OCW,其中OCW1只能被写入到Port B中,OCW2和OCW3只能被写到Port A中。

OCW1(Interrupt Mask Register)的格式如下:

M7…M0就是IRQ7…IRQ0各自对应的屏蔽位。IMR寄存器可以通过向Port B写入OCW1来设置,它的当前值也可以通过读取Port B来得到。

OCW2的格式如下:

OCW3的格式如下:

§5.2 Linux对IDT的初始化

5.2.1 定义IDT的数据结构

Linux在include/asm-i386/Desc.h头文件中定义了数据结构desc_struct,用来描述i386 CPU中的各种描述符(均为8字节),如下:

struct desc_struct {

unsigned long a,b;

};

基于上述结构,Linux在arch/i386/kernel/traps.c文件中定义了数组idt_table[256],来表示中断描述符表IDT,其定义如下:

/*

* The IDT has to be page-aligned to simplify the Pentium

* F0 0F bug workaround.. We have a special link segment

* for this.

*/

struct desc_struct idt_table[256] __attribute__((__section__(".data.idt"))) = { {0, 0}, };

5.2.2 对门的操作函数

Linux在arch/i386/kernel/traps.c文件中定义了宏_set_gate(),用来设置一个描述符(即门)的具体值。由于Linux内核代码均在段选择子__KERNEL_CS所指向的内核段中,因此门中的Segment Selector字段总是等于__KERNEL_CS。宏_set_gate()有四个参数:(1)gate_addr:描述符desc_struct结构类型的指针,指定待操作的描述符,通常指向数组idt_table中的某个项。(2)type:描述符类型,对应于门格式中的Type字段。(3)dpl:该描述符的权限级别;(4)addr:中断处理程序入口地址的段内偏移量,由于内核段的起始地址总是为0,因此中断处理程序在内核段中的段内偏移量也就是中断处理程序的入口地址(即核心虚地址)。

宏_set_gate()的源码如下:

#define _set_gate(gate_addr,type,dpl,addr) \

do { \

int __d0, __d1; \

__asm__ __volatile__ ("movw %%dx,%%ax\n\t" \

"movw %4,%%dx\n\t" \

"movl %%eax,%0\n\t" \

"movl %%edx,%1" \

:"=m" (*((long *) (gate_addr))), \

"=m" (*(1+(long *) (gate_addr))), "=&a" (__d0), "=&d" (__d1) \

:"i" ((short) (0x8000+(dpl<<13)+(type<<8))), \

"3" ((char *) (addr)),"2" (__KERNEL_CS << 16)); \

} while (0)

由于不同的门其Type字段和DPL字段是固定的,因此Linux又在宏_set_gate()的基础上为不同类型的门分别封装了专用的操作宏,它们同样也在traps.c文件中:

(1)中断门的操作宏set_intr_gate(),其源码如下:

void set_intr_gate(unsigned int n, void *addr)

{

_set_gate(idt_table+n,14,0,addr); /* Type=1110(14),dpl=0(ring 0)*/

}

其中,参数n是中断向量(被用作IDT表的索引)。参数addr是中断处理程序的入口地址。

(2)陷阱门的操作宏set_trap_gate()。其源码如下:

static void __init set_trap_gate(unsigned int n, void *addr)

{

_set_gate(idt_table+n,15,0,addr); /* Type=1111(15),dpl=0(ring 0)*/

}

(3)系统门的操作宏set_system_gate()。Linux扩展了INTEL的术语,它将可编程异常所对应的中断描述符称为“系统门”。系统门的DPL字段为3(用户态),因此使得用户进程可以在用户态下(I386运行在ring 3级别)通过int指令或其它指令穿越系统门而进入内核态。

static void __init set_system_gate(unsigned int n, void *addr)

{

_set_gate(idt_table+n,15,3,addr); /* Type=1111(15),dpl=3(ring 3)*/

}

(4)调用门的操作宏set_call_gate(),其源码如下:

static void __init set_call_gate(void *a, void *addr)

{

_set_gate(a,12,3,addr); /* Type=1100(12),dpl=3(ring 3)*/

}

从上述这四个专用的宏操作实现也可以看出,Linux并没有使用i386 CPU的调用门和任务门,而是仅仅使用了中断门和陷阱门(Linux又将它细分为陷阱门和系统门)两种中断描述符。

5.2.3 对IDT表的初始化设置

Linux内核在初始阶段完成了对也是虚存管理机制的初始化后,便调用trap_init()函数和init_IRQ()函数对i386 CPU中断机制的核心——IDT进行初始化设置。如下:

asmlinkage void __init start_kernel(void)

{

……

trap_init();

init_IRQ();

……

}

其中,函数trap_init()用来对除外设中断(32-47)以外的所有处理器保留的中断向量进行初始化。而init_IRQ()函数则用来初始化对应于主、从8259A中断控制器的可屏蔽外设中断32-47。

函数trap_init()定义在arch/i386/kernel/traps.c文件中,其源码如下:

void __init trap_init(void)

{

#ifdef CONFIG_EISA

if (isa_readl(0x0FFFD9) == 'E'+('I'<<8)+('S'<<16)+('A'<<24))

EISA_bus = 1;

#endif

set_trap_gate(0,÷_error);

set_trap_gate(1,&debug);

set_intr_gate(2,&nmi);

set_system_gate(3,&int3); /* int3-5 can be called from all */

set_system_gate(4,&overflow);

set_system_gate(5,&bounds);

set_trap_gate(6,&invalid_op);

set_trap_gate(7,&device_not_available);

set_trap_gate(8,&double_fault);

set_trap_gate(9,&coprocessor_segment_overrun);

set_trap_gate(10,&invalid_TSS);

set_trap_gate(11,&segment_not_present);

set_trap_gate(12,&stack_segment);

set_trap_gate(13,&general_protection);

set_trap_gate(14,&page_fault);

set_trap_gate(15,&spurious_interrupt_bug);

set_trap_gate(16,&coprocessor_error);

set_trap_gate(17,&alignment_check);

set_trap_gate(18,&machine_check);

set_trap_gate(19,&simd_coprocessor_error);

set_system_gate(SYSCALL_VECTOR,&system_call);

/*

* default LDT is a single-entry callgate to lcall7 for iBCS

* and a callgate to lcall27 for Solaris/x86 binaries

*/

set_call_gate(&default_ldt[0],lcall7);

set_call_gate(&default_ldt[4],lcall27);

/*

* Should be a barrier for any external CPU state.

*/

cpu_init();

#ifdef CONFIG_X86_VISWS_APIC

superio_init();

lithium_init();

cobalt_init();

#endif

}

从上述代码可以看出,trap_init()函数的核心就是做两件事:(1)设置IDT的前20个表项,这是因为在0-31这32个CPU保留的异常中断向量中,Intel仅定义了前20个(0-19)中断向量,而中断向量20-31这12个中断向量则保留待以后扩展。(2)设置中断向量SYSCALL_VECTOR(0x80),以用于系统调用的实现。

函数init_IRQ()实现在arch/i386/kernel/i8259.c文件中。它负责初始化IDT表中的后224个中断描述符即中断向量32-256所对应的中断描述符(除了用于syscall的中断向量0x80)。其源码如下:

void __init init_IRQ(void)

{

int i;

#ifndef CONFIG_X86_VISWS_APIC

init_ISA_irqs();

#else

init_VISWS_APIC_irqs();

#endif

/*

* 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_IRQS; i++) {

int vector = FIRST_EXTERNAL_VECTOR + i;

if (vector != SYSCALL_VECTOR)

set_intr_gate(vector, interrupt);

}

#ifdef CONFIG_SMP

/*

* IRQ0 must be given a fixed assignment and initialized,

* because it's used before the IO-APIC is set up.

*/

set_intr_gate(FIRST_DEVICE_VECTOR, interrupt[0]);

/*

* The reschedule interrupt is a CPU-to-CPU reschedule-helper

* IPI, driven by wakeup.

*/

set_intr_gate(RESCHEDULE_VECTOR, reschedule_interrupt);

/* IPI for invalidation */

set_intr_gate(INVALIDATE_TLB_VECTOR, invalidate_interrupt);

/* IPI for generic function call */

set_intr_gate(CALL_FUNCTION_VECTOR, call_function_interrupt);

#endif

#ifdef CONFIG_X86_LOCAL_APIC

/* self generated IPI for local APIC timer */

set_intr_gate(LOCAL_TIMER_VECTOR, apic_timer_interrupt);

/* IPI vectors for APIC spurious and error interrupts */

set_intr_gate(SPURIOUS_APIC_VECTOR, spurious_interrupt);

set_intr_gate(ERROR_APIC_VECTOR, error_interrupt);

#endif

/*

* Set the clock to HZ Hz, we already have a valid

* vector now:

*/

outb_p(0x34,0x43); /* binary, mode 2, LSB/MSB, ch 0 */

outb_p(LATCH & 0xff , 0x40); /* LSB */

outb(LATCH >> 8 , 0x40); /* MSB */

#ifndef CONFIG_VISWS

setup_irq(2, &irq2);

#endif

/*

* External FPU? Set up irq13 if so, for

* original braindamaged IBM FERR coupling.

*/

if (boot_cpu_data.hard_math && !cpu_has_fpu)

setup_irq(13, &irq13);

}

该函数主要执行以下几个步骤:

1. 在没有配置80x86 APIC的情况下,调用init_ISA_irqs()函数来初始化中断向量32-256这后224个中断向量所对应的IRQ描述符。否则就调用init_VISWS_APIC_irqs()来完成这一点。PC体系结构中通常都没有配置APIC,因此后面将详细分析init_ISA_irqs()函数。

2. 接下来,用一个简单的for循环来初始化32-256这后224个中断向量(除了用于syscall之外的0x80中断向量)在IDT中对应的描述符。位于32-256之间的中断向量i所对应的中断处理程序入口地址由数组元素interrupt[i-32]。后面将会详细介绍这个数组interrupt[224]——一个被内核用来保存中断处理程序入口地址的数组。

3. 初始化系统时钟。

4. 如果没有定义CONFIG_VISWS配置选项,则调用setup_irq()函数将8259A中断控制器的IRQ2设置为用于级联。

5. 如果使用了FPU,则将IRQ13分配用于数学协处理器的错误报告中断。所以调用setup_irq()函数将IRQ13设置为用于FPU。

接下来将讨论内核对中断处理程序的构建,也即数组interrupt[224]的构建。

§5.3内核对中断服务程序的构建

由上一节对init_IRQ()函数的分析我们知道,函数指针数组interrupt[]中定义了中断向量32-256所对应的中断服务程序的入口地址。本节我们就来分析一下Linux内核是如何巧妙地为这224个中断向量构建ISR的。

函数指针数组interrupt[]定义在arch/i386/kernel/i8259.c文件中:

#define IRQ(x,y) \

IRQ##x##y##_interrupt

#define IRQLIST_16(x) \

IRQ(x,0), IRQ(x,1), IRQ(x,2), IRQ(x,3), \

IRQ(x,4), IRQ(x,5), IRQ(x,6), IRQ(x,7), \

IRQ(x,8), IRQ(x,9), IRQ(x,a), IRQ(x,b), \

IRQ(x,c), IRQ(x,d), IRQ(x,e), IRQ(x,f)

void (*interrupt[NR_IRQS])(void) = {

IRQLIST_16(0x0),

#ifdef CONFIG_X86_IO_APIC

IRQLIST_16(0x1), IRQLIST_16(0x2), IRQLIST_16(0x3),

IRQLIST_16(0x4), IRQLIST_16(0x5), IRQLIST_16(0x6), IRQLIST_16(0x7),

IRQLIST_16(0x8), IRQLIST_16(0x9), IRQLIST_16(0xa), IRQLIST_16(0xb),

IRQLIST_16(0xc), IRQLIST_16(0xd)

#endif

};

#undef IRQ

#undef IRQLIST_16

从上述定义可以看出,在没有定义CONFIG_X86_IO_APIC配置选项的单CPU体系结构中,interrupt[]数组中只有前16个数组元素中包含有有效的指针(也即对应于主、从8259A中断控制器的中断请求)。

先看宏IRQ()的定义。我们知道GCC预编译符号##的作用就是将字符串连接在一起。因此经过GCC的预编译处理后,宏IRQ(x,y)实际上就是符号IRQxy_interrupt。

在来看宏IRQLIST_16()。它的作用主要是为了避免重复的文字输入。因此但在interrupt[]数组的初始化中以参数0x0来调用宏IRQLIST_16()时,我们所得到的就是16个宏定义:IRQ(0x0,0) 、…、IRQ(0x0,f),将IRQ()宏继续展开,我们就可知道interrupt[]函数指针数组的前16项的值为:IRQ0x00_interrupt、…、IRQ0x0f_interrupt。而后208个数组元素则或者都是NULL指针(在没有APIC的情况下),或者分别是IRQ0x10_interrupt…IRQ0xdf_interrupt。

现在我们已经清楚地了解了interrupt[224]数组的定义以及它的初始值。很自然地我们会想到,函数IRQ0x00_interrupt到IRQ0xdf_interrupt这224个函数又是在哪定义的呢?请看i8259.c文件中另外几行宏定义与宏引用:

#define BI(x,y) \

BUILD_IRQ(x##y)

#define BUILD_16_IRQS(x) \

BI(x,0) BI(x,1) BI(x,2) BI(x,3) \

BI(x,4) BI(x,5) BI(x,6) BI(x,7) \

BI(x,8) BI(x,9) BI(x,a) BI(x,b) \

BI(x,c) BI(x,d) BI(x,e) BI(x,f)

/*

* ISA PIC or low IO-APIC triggered (INTA-cycle or APIC) interrupts:

* (these are usually mapped to vectors 0x20-0x2f)

*/

BUILD_16_IRQS(0x0)

显然,以参数0x0来引用BUILD_16_IRQ()宏在经过gcc预处理后,将展开成:BUILD_IRQ(0x00)、BUILD_IRQ(0x01)、…、BUILD_IRQ(0x0f)等共16个宏定义的引用。而宏BUILD_IRQ()则是定义在include/asm-i386/hw_irq.h头文件中定义的:

#define BUILD_IRQ(nr) \

asmlinkage void IRQ_NAME(nr); \

__asm__( \

"\n"__ALIGN_STR"\n" \

SYMBOL_NAME_STR(IRQ) #nr "_interrupt:\n\t" \

"pushl $"#nr"-256\n\t" \

"jmp common_interrupt");

上述代码中的IRQ_NAME()宏也是定义在include/asm-i386/hw_irq.h中:

#define IRQ_NAME2(nr) nr##_interrupt(void)

#define IRQ_NAME(nr) IRQ_NAME2(IRQ##nr)

所以,宏引用IRQ_NAME(nr)被展开后也就是一个类似于IRQ0x01_interrupt(void)这样的函数名。

因此,从BUILD_IRQ(0x00)到BUILD_IRQ(0x0f)这一共16个宏引用经过gcc预处理后,将展开成为一系列如下样式的代码:

amslinkage void IRQ0x01_interrupt(void);

__asm__( \

“\n” \

“IRQ0x01_interrupt:\n\t” \

“pushl $0x01-256 \n\t” \

“jmp common_interrupt”);

可以看出,Linux内核正是通过gcc的预处理功能巧妙地定义了从IRQ0x00_interrupt()到IRQ0x0f_interrupt()这16个中断处理函数。

下面在来看看中断处理函数IRQ0x00_interrupt()到IRQ0x0f_interrupt()本身的流程。这16个中断处理函数的执行过程都是一样的,它主要完成两件事:(1)将立即数(IRQ号-256)这样一个负数压入内核堆栈,以供中断处理流程中后面的函数获取中断号。比如对于中断向量0x20,它对应于注8259A中断控制器的IRQ0,因此其中断服务程序的入口地址应该是interrupt[0],也即函数IRQ0x00_interrupt(),该函数所做的第一件事情就是把负数-256压入内核堆栈中。这里之所以用复数来表示中断号,是因为正数已被用于标识0x80中断中的系统调用号。(2)所有的中断服务函数所做的第二件事情都相同——即跳转到一个公共的的程序common_interrupt中,并由该公共程序继续对中断请求进行服务。

5.3.1 公共的中断服务程序——common_interrupt

在源文件arch/i386/kernel/i8259.c中一开始就引用了宏BUILD_COMMON_IRQ(),其作用就是构建面向所有中断的公共服务程序common_interrup,如下所示:

36: BUILD_COMMON_IRQ()

宏BUILD_COMMON_IRQ()定义在头文件include/asm-i386/hw_irq.h中,如下所示:

#define BUILD_COMMON_IRQ() \

asmlinkage void call_do_IRQ(void); \

__asm__( \

"\n" __ALIGN_STR"\n" \

"common_interrupt:\n\t" \

SAVE_ALL \

"pushl $ret_from_intr\n\t" \

SYMBOL_NAME_STR(call_do_IRQ)":\n\t" \

"jmp "SYMBOL_NAME_STR(do_IRQ));

上述代码经过展开后,就成为如下汇编代码:

common_interrupt:

SAVE_ALL

call do_IRQ

jmp ret_from_intr

可以看出,公共服务程序common_interrupt主要做三件事:(1)首先,调用宏SAVE_ALL来保存CPU的执行现场;(2)然后,调用总控函数do_IRQ()对中断请求进行真正的服务;(3)当从do_IRQ()返回后,跳到函数ret_from_intr,进入中断返回操作。

用于保存现场的SAVE_ALL宏定义在arch/i386/kernel/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 $(__KERNEL_DS),%edx; \

movl %edx,%ds; \

movl %edx,%es;

说明几点:(1)用户态堆栈SS寄存器和ESP寄存器是在切换到内核态堆栈被压入的;(2)CPU在进入中断服务程序之前已经把EFLAGS寄存器和返回地址压入堆栈中;(3)段寄存器DS和ES被更改为内核态的段选择符__KERNEL_DS。因此,在执行SAVE_ALL宏之后内核态堆栈的内容应为如下图所示:

而Linux也根据上图中的关系在arch/i386/kernel/entry.S文件中定义了一些常数来表示个寄存器的内容相对于当前内核堆栈指针的偏移:

EBX = 0x00

ECX = 0x04

EDX = 0x08

ESI = 0x0C

EDI = 0x10

EBP = 0x14

EAX = 0x18

DS = 0x1C

ES = 0x20

ORIG_EAX = 0x24

EIP = 0x28

CS = 0x2C

EFLAGS = 0x30

OLDESP = 0x34

OLDSS = 0x38

真正对中断请求进行服务的do_IRQ()函数和中断返回函数ret_from_intr()将在下面介绍。在分析总控函数do_IRQ()之前,先来讨论一下中断请求描述符和中断服务队列。

§5.4 中断请求描述符和中断服务队列

我们都知道,在256个中断向量中,i386 CPU保留了0~31这前32个中断向量用于CPU异常,而剩余的224个中断向量则是可用于外设中断或软中断的可使用中断向量。由于不同体系结构的CPU所保留的中断向量不同,因此剩余的可使用中断向量数目也不同。所以,Linux定义了宏NR_IRQS来表示这个值。对于i386而言,该宏定义在include/asm-i386/irq.h头文件中:

#define TIME_IRQ 0 /* for i386,主8259A的IRQ0用于时钟中断 */

#define NR_IRQS 224

5.4.1 对中断控制器的抽象描述

在剩余的224个可用中断向量中,各中断向量所对应的中断类型也是不同的。比如对于PC机,中断向量0x20~0x2f则来自于主、从8259A PIC,其余中断向量则属于软中断(Linux仅仅使用了其中的0x80)。因此有必要对这些不同类型的中断向量进行区分。

另外,从中断控制器的角度看,尽管不同平台使用不同的PIC,但几乎所有的PIC都由相同的基本功能和工作方式。因此为了获得更好的跨平台兼容性,Linux对中断控制器进行了抽象描述。定义在头文件include/linux/irq.h头文件中的数据结构hw_interrupt_type描述了一个标准的中断控制器,如下所示:

/*

* Interrupt controller descriptor. This is all we need

* to describe about the low-level hardware.

*/

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, unsigned long mask);

};

typedef struct hw_interrupt_type hw_irq_controller;

在此基础上,Linux又在arch/i386/kernel/i8259.c文件中定义了全局变量i82559_irq_type,以用于所有来自主、从8259A中断控制器的中断请求(对应的中断向量为0x20~0x2f),如下:

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

};

而对于0x30~0xff子间的中断向量,Linux也在arch/i386/kernel/irq.c文件中定义了全局变量no_irq_type——一个虚拟的中断控制器,以表示这些中断向量的中断请求并不是来自于任何硬件PIC的中断,而是由软件编程指令产生的软中断。如下所示:

/* startup is the same as "enable", shutdown is same as "disable" */

#define shutdown_none disable_none

#define end_none enable_none

struct hw_interrupt_type no_irq_type = {

"none",

startup_none,

shutdown_none,

enable_none,

disable_none,

ack_none,

end_none

};

5.4.2 IRQ描述符

由上面的讨论可知,中断向量0x20~0xff所对应的中断请求是不同的,所以Linux在include/linux/irq.h头文件中定义了数据结构irq_desc_t来描述一个中断请求行为的属性。如下所示:

typedef struct {

unsigned int status; /* IRQ status */

hw_irq_controller *handler;

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

unsigned int depth; /* nested irq disables */

spinlock_t lock;

} ____cacheline_aligned irq_desc_t;

各成员的含义如下:

(1)status:表示中断请求的状态。它可以是下列值:

/*

* IRQ line status.

*/

#define IRQ_INPROGRESS 1 /* IRQ handler active - do not enter! */

#define IRQ_DISABLED 2 /* IRQ disabled - do not enter! */

#define IRQ_PENDING 4 /* IRQ pending - replay on enable */

#define IRQ_REPLAY 8 /* IRQ has been replayed but not acked yet */

#define IRQ_AUTODETECT 16 /* IRQ is being autodetected */

#define IRQ_WAITING 32 /* IRQ not yet seen - for autodetection */

#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 */

上述这些状态标志值也是定义在include/linux/irq.h头文件中。

(2)handler指针:指向这个中断请求所来自的中断控制器描述符。对于中断向量0x20~0x2f上的中断请求而言,该成员指针应该指向全局变量i8259_irq_type;而对于其余软中断的中断请求,该指针因该指向no_irq_type。

(3)action指针:指向服务这个中断请求的、由设备驱动程序注册的服务程序(ISR)队列。

(4)depth:表示action指针所指向的中断请求服务队列的长度。

(5)lock:对irq_desc_t结构中其他成员进行访问保护的自旋锁。这是因为内核中irq_desc_t类型的变量(如下面的irq_desc[]数组)都是全局变量,因此对它们的访问都必须是互斥地进行的。

在上述结构的基础上,Linux在arch/i386/kernel/irq.c文件中定义了数组irq_desc[224]来分别描述中断向量0x20~0xff所对应的中断请求:

/*

* Controller mappings for all interrupt sources:

*/

irq_desc_t irq_desc[NR_IRQS] __cacheline_aligned =

{ [0 ... NR_IRQS-1] = { 0, &no_irq_type, NULL, 0, SPIN_LOCK_UNLOCKED}};

5.4.3 中断请求服务队列

现代外设总线(如PCI)通常都允许外设共享一个IRQ线,因此一个中断向量可能会对应有多个由设备驱动程序提供的中断请求服务例程(ISR),所以Linux内核必须有效地将这个多个来自不同的device driver的ISR组织起来。为此,Linux通过数据结构irqaction来描述一个设备驱动程序对中断请求的服务行为,其定义如下所示(include/linux/interrupt.h):

struct irqaction {

void (*handler)(int, void *, struct pt_regs *);

unsigned long flags;

unsigned long mask;

const char *name;

void *dev_id;

struct irqaction *next;

};

各成员的含义如下:

(1)handler指针:指向驱动程序的ISR入口地址。

(2)flags:描述中断类型的属性标志,它可以是下列三个值的“或”:

l SA_SHIRQ:表示中断是共享的。

l SA_INTERRUPT:当执行handler函数时,屏蔽同级中断。

l SA_SAPLE_RANDOM。

(3)mask:屏蔽掩码。

(4)name指针:表示提供handler函数的设备名称。

(5)dev_id:一个唯一的设备标识符。注意!当flags属性中设置了SA_SHIRQ属性时,dev_id指针不能为NULL。

(6)next指针:指向同属于该中断请求的下一个服务行为。Linux就是通过next指针把同一个中断向量的中断请求的多个服务行为组织成为一条中断请求服务队列的。

下图5-14清晰地描述了与中断请求相关的几个数据结构之间的关系:

5.4.4 中断请求描述符数组的初始化

函数init_ISA_irqs()完成对中断请求描述符数组irq_desc[]中各元素的初始化,该函数定义在arch/i386/kernel/i8259.c文件中,如下所示:

void __init init_ISA_irqs (void)

{

int i;

init_8259A(0);

for (i = 0; i < NR_IRQS; i++) {

irq_desc[u].status = IRQ_DISABLED;

irq_desc[u].action = 0;

irq_desc[u].depth = 1;

if (i < 16) {

/*

* 16 old-style INTA-cycle interrupts:

*/

irq_desc[u].handler = &i8259A_irq_type;

} else {

/*

* 'high' PCI IRQs filled in on demand

*/

irq_desc[u].handler = &no_irq_type;

}

}

}

初始化的过程很简单:中断向量0x20~0x2f分别与中断描述符irq_desc[0]~irq_desc[15]相对应,因此它们的handler指针指向i8259A_irq_type,因为它们的中断请求来自于主、从8259A中断控制器。而其余软中断描述符的handler指针则指向no_irq_type这个虚拟中断控制器描述符。

5.4.5 注册驱动程序的ISR

Linux在include/linux/sched.h头文件中声明了函数接口request_irq()和free_irq(),以供设备驱动程序向内核注册和注销驱动程序所提供的ISR函数。这两个函数都是实现在文件arch/i386/kernel/irq.c中。

首先,我们来分析request_irq()函数。该函数有5个参数:(1)irq:外设所使用的IRQ线号。(2)handler函数指针:设备驱动程序所实现的ISR函数。(3)irqflags:设备驱动程序指定的中断请求类型标志,它可以是下列三个值的“或”:SA_SHIRQ、SA_INTERRUPT和SA_SAMPLE_RANDOM。(4)devname指针:设备名字字符串。(5)dev_id:指向全局唯一的设备标识ID,这是一个void类型的指针,可供设备驱动程序自行解释。该函数的源码如下:

/**

* request_irq - allocate an interrupt line

* @irq: Interrupt line to allocate

* @handler: Function to be called when the IRQ occurs

* @irqflags: Interrupt type flags

* @devname: An ascii name for the claiming device

* @dev_id: A cookie passed back to the handler function

*

* This call allocates interrupt resources and enables the

* interrupt line and IRQ handling. From the point this

* call is made your handler function may be invoked. Since

* your handler function must clear any interrupt the board

* raises, you must take care both to initialise your hardware

* and to set up the interrupt handler in the right order.

*

* Dev_id must be globally unique. Normally the address of the

* device data structure is used as the cookie. Since the handler

* receives this value it makes sense to use it.

*

* If your interrupt is shared you must pass a non NULL dev_id

* as this is required when freeing the interrupt.

*

* Flags:

*

* SA_SHIRQ Interrupt is shared

*

* SA_INTERRUPT Disable local interrupts while processing

*

* SA_SAMPLE_RANDOM The interrupt can be used for entropy

*

*/

int request_irq(unsigned int irq,

void (*handler)(int, void *, struct pt_regs *),

unsigned long irqflags,

const char * devname,

void *dev_id)

{

int retval;

struct irqaction * action;

#if 1

/*

* Sanity-check: shared interrupts should REALLY 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) {

if (!dev_id)

printk("Bad boy: %s (at 0x%x) called us without a dev_id!\n", devname, (&irq)[-1]);

}

#endif

if (irq >= NR_IRQS)

return -EINVAL;

if (!handler)

return -EINVAL;

action = (struct irqaction *)

kmalloc(sizeof(struct irqaction), GFP_KERNEL);

if (!action)

return -ENOMEM;

action->handler = handler;

action->flags = irqflags;

action->mask = 0;

action->name = devname;

action->next = NULL;

action->dev_id = dev_id;

retval = setup_irq(irq, action);

if (retval)

kfree(action);

return retval;

}

对该函数的NOTE如下:

(1)首先进行参数检查。①在指定了中断共享标志IRQ_SHIRQ标志时,参数dev_id必须有效,不能为NULL;②IRQ线号参数irq不能大于NR_IRQS;③handler指针不能为NULL。

(2)调用kmalloc()函数在SLAB分配器缓存中为结构类型irqaction分配内存,以构建一个中断服务描述符。如果分配失败,则返回-ENOMEM,表示系统内存不足。

(3)然后,根据参数相应地初始化刚刚构建的中断服务描述符中的各个成员。

(4)最后调用setup_irq()函数将上述构建号的中断服务描述符插入到参数irq所对应的中断服务队列中去。如果setup_irq()函数返回非0值,表示插入失败,因此调用kfree()函数将前面构建的中断服务描述符释放掉。

(5)最后,返回返回值retval。0表示成功,非0表示失败。

函数setup_irq()用来将一个已经构建好的中断服务描述符插入到相应的中断服务队列中。参数irq指定IRQ输入线号,它指定将中断描述符插到哪一个中断服务队列中去;参数new指针指向待插入的中断服务描述符。该函数的源码如下:

/* this was setup_x86_irq but it seems pretty generic */

int setup_irq(unsigned int irq, struct irqaction * new)

{

int shared = 0;

unsigned long flags;

struct irqaction *old, **p;

irq_desc_t *desc = irq_desc + irq;

/*

* Some drivers like serial.c use request_irq() heavily,

* so we have to be careful not to interfere with a

* running system.

*/

if (new->flags & SA_SAMPLE_RANDOM) {

/*

* This function might sleep, we want to call it first,

* outside of the atomic block.

* Yes, this might clear the entropy pool if the wrong

* driver is attempted to be loaded, without actually

* installing a new handler, but is this really a problem,

* only the sysadmin is able to do this.

*/

rand_initialize_irq(irq);

}

/*

* The following block of code has to be executed atomically

*/

spin_lock_irqsave(&desc->lock,flags);

p = &desc->action;

if ((old = *p) != NULL) {

/* Can't share interrupts unless both agree to */

if (!(old->flags & new->flags & SA_SHIRQ)) {

spin_unlock_irqrestore(&desc->lock,flags);

return -EBUSY;

}

/* add new interrupt at end of irq queue */

do {

p = &old->next;

old = *p;

} while (old);

shared = 1;

}

*p = new;

if (!shared) {

desc->depth = 0;

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

desc->handler->startup(irq);

}

spin_unlock_irqrestore(&desc->lock,flags);

register_irq_proc(irq);

return 0;

}

对该函数的NOTE如下:

(1)首先根据参数irq的值找到相应的中断请求描述符,也即irq_desc + irq,并让指针desc指向它。

(2)如果待插入中断服务描述符中的flags成员中设置了SA_SAMPLE_RANDOM标志,那就调用rand_initialize_irq()函数来引入一些随机性。一般设备驱动程序很少使用这个标志。

(3)因为接下来要对中断请求描述符desc进行访问,所以调用spin_lock_irqsave()函数对自旋锁desc->lock进行加锁,并同时关中断。

(4)将指针p指向desc->action成员。指针desc->action指向将要进行插入操作的中断服务队列的第一个元素。因此*p也就等于desc->action。此外,让指针old等于(*p)。因此old也就指向中断服务队列的第一个元素。

(5)然后判断(*p)所指向的中断服务队列是否为空。如果不为空:①判断中断服务队列的第一个中断服务描述符和new所指向的待插入中断服务描述符是否同时都设置了SA_SHIRQ标志。如果没有,返回错误值-EBUSY表示参数irq所指定的IRQ输入线已经被使用了。注意!必须二者都同意共享IRQ输入线才行。②通过一个do{}while循环来依次遍历整个中断服务队列,直到队列中的最后一个元素。当从do{}while循环退出时,指针p指向队列中最后一个中断服务描述符的next指针成员。③将局部变量shared置1,表示发生中断共享。

(6)让(*p)等于new指针,从而将new所指向的中断服务描述符插到中断服务队列的尾部。注意!如果原来的中断服务队列为空的话,new所指向的中断服务描述符将成为队列中唯一的一个元素。

(7)如果shared变量为0,说明new是中断服务队列中插入的第一个元素,因此对中断请求描述符desc进行相应的初始化设置,包括:将depth置0,清除IRQ_DISABLED、IRQ_AUTODETECT和IRQ_WAITING标志,以及调用中断控制器描述符的startup()函数来使能irq所指定的IRQ输入线。

(8)至此,插入操作结束,因此调用spin_unlock_irqrestore()函数进行解锁和开中断。

(9)最后,调用register_irq_proc()函数注册相应的/proc文件系统。

5.4.6 注销驱动程序的ISR

函数free_irq()用来注销驱动程序先前通过request_irq()函数所注册的ISR函数。其源码如下:

/**

* free_irq - free an interrupt

* @irq: Interrupt line to free

* @dev_id: Device identity to free

*

* Remove an interrupt handler. The handler is removed and if the

* interrupt line is no longer in use by any driver it is disabled.

* On a shared IRQ the caller must ensure the interrupt is disabled

* on the card it drives before calling this function. The function

* does not return until any executing interrupts for this IRQ

* have completed.

*

* This function may be called from interrupt context.

*

* Bugs: Attempting to free an irq in a handler for the same irq hangs

* the machine.

*/

void free_irq(unsigned int irq, void *dev_id)

{

irq_desc_t *desc;

struct irqaction **p;

unsigned long flags;

if (irq >= NR_IRQS)

return;

desc = irq_desc + irq;

spin_lock_irqsave(&desc->lock,flags);

p = &desc->action;

for (;;) {

struct irqaction * action = *p;

if (action) {

struct irqaction **pp = p;

p = &action->next;

if (action->dev_id != dev_id)

continue;

/* Found it - now remove it from the list of entries */

*pp = action->next;

if (!desc->action) {

desc->status |= IRQ_DISABLED;

desc->handler->shutdown(irq);

}

spin_unlock_irqrestore(&desc->lock,flags);

#ifdef CONFIG_SMP

/* Wait to make sure it's not being used on another CPU */

while (desc->status & IRQ_INPROGRESS)

barrier();

#endif

kfree(action);

return;

}

printk("Trying to free free IRQ%d\n",irq);

spin_unlock_irqrestore(&desc->lock,flags);

return;

}

}

对该函数的NOTE如下:

(1)为遍历中断服务队列做准备:首先,根据参数irq所指定的IRQ号找到相应的中断请求描述符desc(=irq_desc+irq)。然后调用spin_lock_irqsave()函数进行加锁和关中断。最后,让指针p指向当前被扫描队列元素的前一个元素的next指针成员,因此(*p)就指向队列中当前正被扫描的中断服务描述符。初始时,让指针p指向中断请求描述符desc的action指针成员,也即(*p)指向中断服务队列的第一个元素。

(2)用一个for死循环来遍历队列。循环体的执行步骤如下:

n 让action指针等于(*p),表示当前正被扫描的队列元素。

n 如果action指针不为空:①让指针pp等于p,因此pp也就指向当前被扫描队列元素的前一个元素的next指针成员。②更新指针p,让它指向当前被扫描的中断服务描述符action的next指针成员,因此(*p)也就指向下一个队列元素。③比较参数dev_id和当前中断服务描述符action的dev_id成员是否不相等,如果不相等,那就执行continue语句继续扫描队列。如果相等,则说明我们已经找到要删除的中断服务描述符,也即是当前被扫描的中断服务描述符action。④找到以后,先将所找到的中断服务描述符action从中断服务队列中摘除。语句*pp=action->next完成这一点,也即让前一个元素的next指针指向当前被扫描元素的下一个元素。⑤判断一下desc->action所指向的中断服务队列是否为空,因为中断服务队列中可能就只有一个元素,因此前一步的摘除操作又可能会使中断服务队列变为空。如果为空,那就在中断请求描述符的status中设置IRQ_DISABLED标志,并调用PIC描述符的shutdown()函数禁止这个IRQ输入线。⑥最后,解除自旋锁并开中断,接着调用kfree()函数释放内存后,函数就可以直接return了。

n 如果action指针为空,说明已经遍历到队列尾部,而此时我们还没有找到参数dev_id所指定的中断服务描述符,因此打印警告信息,然后解锁并开中断。函数以失败结束执行。

我的未来不是梦!

文章选项:

zhanrk

(journeyman)

03-02-14 12:08

[精华] Re: Linux内核的中断机制分析(continue...) [re: zhanrk]

§5.5 Linux对8259A中断控制器的编程实现

Linux对主、从8259A中断控制器的编程实现在源码文件arch/i386/kernel/i8259.c中,主要包含两部分:(1)对主、从8259A PIC的初始化;(2)对控制器描述符i8259A_irq_type中定义的各函数的实现。

5.5.1 对8259A的初始化

函数init_8259A()完成对主、从8259A中断控制器的初始化工作。如前所述,该函数是被init_ISA_irqs()函数所调用的(参见5.4.4节),其源码如下:

void __init init_8259A(int auto_eoi)

{

unsigned long flags;

spin_lock_irqsave(&i8259A_lock, flags);

outb(0xff, 0x21); /* mask all of 8259A-1 */

outb(0xff, 0xA1); /* mask all of 8259A-2 */

/*

* outb_p - this has to work on a wide range of PC hardware.

*/

outb_p(0x11, 0x20); /* ICW1: select 8259A-1 init */

outb_p(0x20 + 0, 0x21); /* ICW2: 8259A-1 IR0-7 mapped to 0x20-0x27 */

outb_p(0x04, 0x21); /* 8259A-1 (the master) has a slave on IR2 */

if (auto_eoi)

outb_p(0x03, 0x21); /* master does Auto EOI */

else

outb_p(0x01, 0x21); /* master expects normal EOI */

outb_p(0x11, 0xA0); /* ICW1: select 8259A-2 init */

outb_p(0x20 + 8, 0xA1); /* ICW2: 8259A-2 IR0-7 mapped to 0x28-0x2f */

outb_p(0x02, 0xA1); /* 8259A-2 is a slave on master's IR2 */

outb_p(0x01, 0xA1); /* (slave's support for AEOI in flat mode

is to be investigated) */

if (auto_eoi)

/*

* in AEOI mode we just have to mask the interrupt

* when acking.

*/

i8259A_irq_type.ack = disable_8259A_irq;

else

i8259A_irq_type.ack = mask_and_ack_8259A;

udelay(100); /* wait for 8259A to initialize */

outb(cached_21, 0x21); /* restore master IRQ mask */

outb(cached_A1, 0xA1); /* restore slave IRQ mask */

spin_unlock_irqrestore(&i8259A_lock, flags);

}

参数atuo_eoi决定是否让8259A工作在AEOI模式下。对该函数的注释如下:

(1)首先调用函数spin_lock_irqsave()对自旋锁i8259A_lock进行加锁。所有对主、从8259A进行编程的代码都首先必须进行这一步操作,以保证对主、从8259A的操作是互斥的。

(2)分别向主、从8259A写入屏蔽掩码0xff,以屏蔽所有的IRQ输入。

(3)通过向主8259A发送一些列ICW来初始化主8259A:①ICW1=0x11,也即:Edge Triggered Mode、AOI=0、Cascade Mode(ICW3 Needed)、ICW4 Needed;②ICW2=0x20,也即:IRQ0的中断向量为0x20;③ICW3=0x04,也即:主8259A通过IRQ2级连了一个从8259A;④如果参数auto_eoi非零,则让ICW4=0x03,也即:Auto EOI、80x86 mode;否则如果auto_eoi为0,则让ICW4=0x01,也即:Normal EOI、80x86 mode。

(4)通过向从8259A发送一系列ICW来初始化它:①ICW1与主8259A的相同;②ICW2=0x28,表示从8259A的IRQ0的中断向量为0x28;③ICW3=0x02,表示从8259A是连接在主8259A的IRQ2上;④ICW4=0x01,表示从8259A只工作在Normal EOI模式下。

这里需要注意的是,写端口时最好用outb_p()函数,以等待慢速的8259A中断控制器。

(5)如果参数auto_eoi非零,表示工作在“自动中断结束(AEOI)模式,因此就要将i8259A_irq_type的应答函数ack设置成disable_8259A_irq()函数;否则就将其设置成mask_and_ack_8259A()函数。

(6)至此,写初始化命令字的过程结束。然后调用udelay()函数延时100µs,以等待8259A中断控制器完成其自身的硬件初始化。

(7)最后,从全局变量cached_irq_mask中恢复主、从8259A的IRQ屏蔽掩码。关于cached_irq_mask这个全局屏蔽掩码下面马上会谈到。

5.5.2 对PIC描述符i8259A_irq_type的实现

(1)全局屏蔽掩码cached_irq_mask

Linux在arch/i386/kernel/i8259.c文件中定义了一个全局变量cached_irq_mask来表示8259A中断控制器的当前屏蔽状态。其中,bit[7:0]表示Master 8259A的屏蔽掩码,bit[15:8]表示Slave 8259A的屏蔽掩码。如下所示:

/*

* This contains the irq mask for both 8259A irq controllers,

*/

static unsigned int cached_irq_mask = 0xffff;

#define __byte(x,y) (((unsigned char *)&(y))[x])

#define cached_21 (__byte(0,cached_irq_mask))

#define cached_A1 (__byte(1,cached_irq_mask))

(2)禁止一个特定的IRQ

函数disable_8259A_irq()通过将8259A的IMR中的相应位设置成1来实现禁止某个特定的IRQ,其源码如下:

void disable_8259A_irq(unsigned int irq)

{

unsigned int mask = 1 << irq;

unsigned long flags;

spin_lock_irqsave(&i8259A_lock, flags);

cached_irq_mask |= mask;

if (irq & 8)

outb(cached_A1,0xA1);

else

outb(cached_21,0x21);

spin_unlock_irqrestore(&i8259A_lock, flags);

}

(1)参数irq取值范围为0~15,指定对哪一个IRQ线进行屏蔽。

(2)通过一个“或”操作将cached_irq_mask中的相应位设置为1。

(3)如果irq大于等于8(也即irq&8的结果非0),则说明这个IRQ来自于Slave 8259A,否则就应来自于Master 8259A。

(3)使能一个特定的IRQ

函数enable_8259A_irq()通过将8259A的IMR寄存器中的相应位清零来实现使能一个特定的IRQ。该函数与disable_8259A_irq()刚好相反。如下所示:

void enable_8259A_irq(unsigned int irq)

{

unsigned int mask = ~(1 << irq);

unsigned long flags;

spin_lock_irqsave(&i8259A_lock, flags);

cached_irq_mask &= mask;

if (irq & 8)

outb(cached_A1,0xA1);

else

outb(cached_21,0x21);

spin_unlock_irqrestore(&i8259A_lock, flags);

}

(4)8259A中断控制器的startup()函数和shutdown()函数

8259A中断控制器描述符i8259A_irq_type的shutdown()函数和startup()函数实际上就是其disable函数和enable函数。

#define shutdown_8259A_irq disable_8259A_irq

……

static unsigned int startup_8259A_irq(unsigned int irq)

{

enable_8259A_irq(irq);

return 0; /* never anything pending */

}

(5)end_8259A_irq()函数

8259A中断控制器描述符i8259_irq_type的end函数指针被定义成指向函数end_8259A_irq()。该函数的源码如下:

static void end_8259A_irq (unsigned int irq)

{

if (!(irq_desc[irq].status & (IRQ_DISABLED|IRQ_INPROGRESS)))

enable_8259A_irq(irq);

}

该函数的实现也是基于enable_8259A_irq()函数,它在相应IRQ描述符的status成员没有设置IRQ_DISABLED标志和IRQ_INPROGRESS标志时,通过调用enable_8259A_irq()函数来使能参数irq所指定的IRQ。

(6)应答函数

中断控制器描述符中的应答函数ack用来向PIC发送EOI命令,表示中断服务的结束。具体到8259A来讲,函数mask_and_ack_8259A()用来向主、从8259A发送EOI命令。向8259A应答因该遵循这样的顺序:首先,屏蔽相应的IRQ;然后,发送EOI命令。此外,如果IRQ来自于从8259A,还必须先向Slave 8259A发送EOI命令,再向Master 8259A发送EOI命令。如果IRQ来自于Master 8259A,则仅仅向Master 8259A发送EOI命令就可以了。

函数mask_and_ack_8259A()的源码如下所示:

/*

* Careful! The 8259A is a fragile beast, it pretty

* much _has_ to be done exactly like this (mask it

* first, _then_ send the EOI, and the order of EOI

* to the two 8259s is important!

*/

void mask_and_ack_8259A(unsigned int irq)

{

unsigned int irqmask = 1 << irq;

unsigned long flags;

spin_lock_irqsave(&i8259A_lock, flags);

/*

* Lightweight spurious IRQ detection. We do not want

* to overdo spurious IRQ handling - it's usually a sign

* of hardware problems, so we only do the checks we can

* do without slowing down good hardware unnecesserily.

*

* Note that IRQ7 and IRQ15 (the two spurious IRQs

* usually resulting from the 8259A-1|2 PICs) occur

* even if the IRQ is masked in the 8259A. Thus we

* can check spurious 8259A IRQs without doing the

* quite slow i8259A_irq_real() call for every IRQ.

* This does not cover 100% of spurious interrupts,

* but should be enough to warn the user that there

* is something bad going on ...

*/

if (cached_irq_mask & irqmask)

goto spurious_8259A_irq;

cached_irq_mask |= irqmask;

handle_real_irq:

if (irq & 8) {

inb(0xA1); /* DUMMY - (do we need this?) */

outb(cached_A1,0xA1);

outb(0x60+(irq&7),0xA0);/* 'Specific EOI' to slave */

outb(0x62,0x20); /* 'Specific EOI' to master-IRQ2 */

} else {

inb(0x21); /* DUMMY - (do we need this?) */

outb(cached_21,0x21);

outb(0x60+irq,0x20); /* 'Specific EOI' to master */

}

spin_unlock_irqrestore(&i8259A_lock, flags);

return;

spurious_8259A_irq:

/*

* this is the slow path - should happen rarely.

*/

if (i8259A_irq_real(irq))

/*

* oops, the IRQ _is_ in service according to the

* 8259A - not spurious, go handle it.

*/

goto handle_real_irq;

{

static int spurious_irq_mask;

/*

* At this point we can be sure the IRQ is spurious,

* lets ACK and report it. [once per IRQ]

*/

if (!(spurious_irq_mask & irqmask)) {

printk("spurious 8259A interrupt: IRQ%d.\n", irq);

spurious_irq_mask |= irqmask;

}

irq_err_count++;

/*

* Theoretically we do not have to handle this IRQ,

* but in Linux this does not cause problems and is

* simpler for us.

*/

goto handle_real_irq;

}

}

对该函数的NOTE如下:

①首先,通过全局变量cached_irq_mask判断参数irq所指定的IRQ线是否处于屏蔽状态。如果已经被屏蔽,则说明8259A在IMR寄存器中的相应位被置1的情况下仍然向CPU发出了相应的中断信号,因此这是一个假的中断,所以跳转到spurious_8259A_irq部分进行处理。有关这方面的详细描述请参见源码中的注释。

②如果cached_irq_mask中的相应位为0,也即相应的IRQ位被屏蔽,说明我们收到的是一个真实的中断请求,于是按照上面所述的步骤,先对这个IRQ进行屏蔽,因此将cached_irq_mask中的相应位设置为1。

③接下来进入真实的中断请求处理部分(handle_real_irq)。如果irq&8的结果非0,说明这个中断请求来自于Slave 8259A。否则这个中断请求来自于Master 8259A。

对于来自Slave 8259A的情形:①先将屏蔽字cached_irq_mask中的高字节cache_A1写入端口0xA1,以屏蔽相应的IRQ输入线。②向从8259A的端口0xA0写操作命令字OCW2,其值等于0x60+(irq&7)——Specific EOI、对irq&7这个IR进行操作。③向Master 8259A的端口0x20写入操作命令字OCW2,其值等于0x62——Specific EOI、对IR2进行操作(因为从8259A通过主8259A的IR2进行级连)。

对于来自Master 8259A的情形:①先将屏蔽字cached_irq_mask中的低字节cache_21写入端口0x21,以屏蔽相应的IRQ输入线。②向主8259A的端口0x20写操作命令字OCW2,其值等于0x60+irq——Specific EOI、对irq这个IR进行操作。

④执行完hadle_real_irq程序段后,对真实中断请求的应答过程就宣告结束。因此函数可以立即返回了。

⑤spurious_8259A_irq部分——对假的8259A 中断请求进行处理。首先,调用i8259A_irq_real()函数来根据8259A的ISR寄存器判断这是否是一个真实的由8259A发给CPU的中断请求。如果i8259A_irq_real()函数返回为1,则说明是一个真实的中断请求,于是跳转回handle_real_irq部分对其进行处理。否则,如果i8259A_irq_real()返回0值,说明我们的CPU真的是受到一个虚假的中断请求信号。

⑥对于真的受到一个虚假中断请求这种情形,分三个步骤来进行处理:①视需要打印一条内核信息向用户发出警告。②将全局变量irq_err_count的值加1(该变量定义在irq.c文件中)。③从理论上来讲,对于虚假的中断请求并不需要进行任何处理。但是在Linux中,将它按照真实的中断请求进行应答处理也不会引起任何问题,因此最后又跳转回handl_real_irq部分将它按照真实的中断请求进行应答处理。

函数i8259_real_irq()是定义在i8259.c文件中的一个内部函数。它根据8259A的ISR寄存器中的内容判断参数irq所指定的IRQ输入线上是否真的产生了一个中断请求。其源码如下所示:

/*

* This function assumes to be called rarely. Switching between

* 8259A registers is slow.

* This has to be protected by the irq controller spinlock

* before being called.

*/

static inline int i8259A_irq_real(unsigned int irq)

{

int value;

int irqmask = 1<<irq;

if (irq < 8) {

outb(0x0B,0x20); /* ISR register */

value = inb(0x20) & irqmask;

outb(0x0A,0x20); /* back to the IRR register */

return value;

}

outb(0x0B,0xA0); /* ISR register */

value = inb(0xA0) & (irqmask >> 8);

outb(0x0A,0xA0); /* back to the IRR register */

return value;

}

对该函数的注释如下:

由于端口0x20和0xA0对读指令默认的是IRR寄存器,因此要向端口0x20/0xA0写入操作命令字OCW3=0x0B,以切换到ISR寄存器。然后通过inb()函数读端口0x20/0xA0,以得到ISR寄存器的当前值,如果ISR&irqmask非0,说明指定的IRQ输入线上真的产生了一个中断请求;否则就没有。最后记得要将端口0x20/0xA0切换回IRR寄存器。

(7)函数i8259_irq_pending()

该函数并不是描述符i8259A_irq_type中定义的函数。该函数用来判断某个指定的IRQ是否未得到服务而一直处于pending状态。其源码如下:

int i8259A_irq_pending(unsigned int irq)

{

unsigned int mask = 1<<irq;

unsigned long flags;

int ret;

spin_lock_irqsave(&i8259A_lock, flags);

if (irq < 8)

ret = inb(0x20) & mask;

else

ret = inb(0xA0) & (mask >> 8);

spin_unlock_irqrestore(&i8259A_lock, flags);

return ret;

}

显然,端口0x20/0xA0对于读操作默认的是返回IRR寄存器的值。如果IRR寄存器中的某个位被置1,就表示相应的中断请求正等待CPU的服务(通过一个INTA-cycle),因此该中断请求也就处于pending状态。

§5.6 CPU的中断请求统计信息

Linux在头文件include/asm-i386/hardirq.h和include/linux/irq_cpustat.h中定义了和中断请求相关的CPU统计信息数据结构以及相应的操作宏。

首先,Linux在头文件hardirq.h中定义了数据结构irq_cpustat_t,以描述一个CPU对中断请求进行服务的历史统计信息。如下所示:

/* entry.S is sensitive to the offsets of these fields */

typedef struct {

unsigned int __softirq_active;

unsigned int __softirq_mask;

unsigned int __local_irq_count;

unsigned int __local_bh_count;

unsigned int __syscall_count;

unsigned int __nmi_count; /* arch dependent */

} ____cacheline_aligned irq_cpustat_t;

上述结构中各成员的命名已经清楚地表达了其各自的含义,这里就不详述。

在此基础上,Linux又在kernel/softirq.c文件中定义了全局数组irq_stat[NR_CPUS],以分别描述每个CPU各自的中断统计信息。这一点主要是从SMP体系结构来考虑的。该数组的定义如下所示:

/* No separate irq_stat for s390, it is part of PSA */

#if !defined(CONFIG_ARCH_S390)

irq_cpustat_t irq_stat[NR_CPUS];

#endif /* CONFIG_ARCH_S390 */

该数组的原型声明在头文件irq_cpustat.h中,如下:

extern irq_cpustat_t irq_stat[]; /* defined in asm/hardirq.h */

5.6.1 引用数据结构irq_cpustat_t中的成员

首先,Linux在头文件irq_cpustat.h中通过宏__IRQ_STAT来定义了一个引用数据结构irq_cpustat_t中各成员的操作模板,如下所示:

#ifdef CONFIG_SMP

#define __IRQ_STAT(cpu, member) (irq_stat[cpu].member)

#else

#define __IRQ_STAT(cpu, member) ((void)(cpu), irq_stat[0].member)

#endif

显然,在单CPU系统中,我们总是引用irq_stat[0]中的成员,此时参数cpu是一个无用的参数。

基于上述引用模板,Linux分别为irq_cpustat_t中各成员定义了显示的引用操作宏(irq_cpustat.h),如下所示:

/* arch independent irq_stat fields */

#define softirq_active(cpu) __IRQ_STAT((cpu), __softirq_active)

#define softirq_mask(cpu) __IRQ_STAT((cpu), __softirq_mask)

#define local_irq_count(cpu) __IRQ_STAT((cpu), __local_irq_count)

#define local_bh_count(cpu) __IRQ_STAT((cpu), __local_bh_count)

#define syscall_count(cpu) __IRQ_STAT((cpu), __syscall_count)

/* arch dependent irq_stat fields */

#define nmi_count(cpu) __IRQ_STAT((cpu), __nmi_count) /* i386, ia64 */

5.6.2 判断一个CPU是否处于中断上下文中

通过数据结构irq_cpustat_t的__local_irq_count成员和__local_bh_count成员的值,我们可以判断出当前CPU当前是否正处于中断服务上下文中。为此,Linux在hardirq.h头文件中定义了宏in_interrupt()和in_irq(),如下所示:

/*

* Are we in an interrupt context? Either doing bottom half

* or hardware interrupt processing?

*/

#define in_interrupt() ({ int __cpu = smp_processor_id(); \

(local_irq_count(__cpu) + local_bh_count(__cpu) != 0); })

#define in_irq() (local_irq_count(smp_processor_id()) != 0)

因此,只要当前CPU处于硬件中断处理或执行bottom half时,in_interrupt()宏就返回非零值。而对于in_irq()宏而言,则只有当CPU处于硬件中断处理时,才返回非0值。

5.6.3 增加/减少CPU的本地中断请求计数

当进入中断请求处理时,Linux必须将当前CPU的硬件中断请求计数值__local_irq_count加1(后面我们将会看到这一点)。而在退出硬件中断请求处理时将其减1。如下所示(hardirq.h):

#ifndef CONFIG_SMP

#define hardirq_trylock(cpu) (local_irq_count(cpu) == 0)

#define hardirq_endlock(cpu) do { } while (0)

#define irq_enter(cpu, irq) (local_irq_count(cpu)++)

#define irq_exit(cpu, irq) (local_irq_count(cpu)--)

#define synchronize_irq() barrier()

#else

#include <asm/atomic.h>

#include <asm/smp.h>

extern unsigned char global_irq_holder;

extern unsigned volatile long global_irq_lock; /* long for set_bit -RR */

static inline int irqs_running (void)

{

int i;

for (i = 0; i < smp_num_cpus; i++)

if (local_irq_count(i))

return 1;

return 0;

}

static inline void release_irqlock(int cpu)

{

/* if we didn't own the irq lock, just ignore.. */

if (global_irq_holder == (unsigned char) cpu) {

global_irq_holder = NO_PROC_ID;

clear_bit(0,&global_irq_lock);

}

}

static inline void irq_enter(int cpu, int irq)

{

++local_irq_count(cpu);

while (test_bit(0,&global_irq_lock)) {

/* nothing */;

}

}

static inline void irq_exit(int cpu, int irq)

{

--local_irq_count(cpu);

}

static inline int hardirq_trylock(int cpu)

{

return !local_irq_count(cpu) && !test_bit(0,&global_irq_lock);

}

#define hardirq_endlock(cpu) do { } while (0)

extern void synchronize_irq(void);

#endif /* CONFIG_SMP */

§5.7 中断服务的总控程序do_IRQ

在§5.3节我们谈到,由IDT表项所指向的中断服务程序IRQ0xYY_interrupt()全部都跳转到common_interrupt这个公共的中断服务程序,而common_interrupt函数只作三件事:(1)用SAVE_ALL宏来保存现场;(2)然后调用中断服务程序的总控函数do_IRQ对中断请求进行真正的服务;(3)从do_IRQ返回后,跳转到中断返回函数ret_from_intr以执行中断返回操作。

另外,在§5.4节中我们也谈到,PCI总线允许不同的外设共享同一个IRQ输入线号,每个设备驱动程序都通过request_irq()接口将驱动程序的ISR注册到相对应的中断服务对列中。因此do_IRQ()函数要真正地服务一个中断请求,就必须遍历相应的中断服务队列,以便让相对应正确的ISR得到执行(注意!通常每个设备驱动程序的ISR一进入就判断它所驱动的硬件是否真的产生了中断请求,如果不是它就立即返回,如果是它才继续向下执行,从而真正地服务中断请求)。下面我们就详细地分析一下do_IRQ()函数是如何遍历IRQ描述符中的中断服务队列的。

函数do_IRQ()以及其相关的底层支持函数全部都是现在arch/i386/kernel/irq.c文件中。函数do_IRQ()只有唯一的一个参数regs,它是一个pt_regs结构类型,该数据结构定义在include/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;

};

由上述pt_regs数据结构的内容可见,SAVE_ALL宏的作用不仅再与保存中断现场,它也为函数do_IRQ()模拟出了一个函数调用环境。

函数do_IRQ()的源码如下:

/*

* do_IRQ handles all normal device IRQ's (the special

* SMP cross-CPU interrupts have their own specific

* handlers).

*/

asmlinkage unsigned int do_IRQ(struct pt_regs regs)

{

/*

* We ack quickly, we don't want the irq controller

* thinking we're snobs just because some other CPU has

* disabled global interrupts (we have already done the

* INT_ACK cycles, it's too late to try to pretend to the

* controller that we aren't taking the interrupt).

*

* 0 return value means that this irq is already being

* handled by some other CPU. (or is disabled)

*/

int irq = regs.orig_eax & 0xff; /* high bits used in ret_from_ code */

int cpu = smp_processor_id();

irq_desc_t *desc = irq_desc + irq;

struct irqaction * action;

unsigned int status;

kstat.irqs[cpu][irq]++;

spin_lock(&desc->lock);

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

*/

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 (!(status & (IRQ_DISABLED | IRQ_INPROGRESS))) {

action = desc->action;

status &= ~IRQ_PENDING; /* we commit to handling */

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 (!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 (;;) {

spin_unlock(&desc->lock);

handle_IRQ_event(irq, ®s, action);

spin_lock(&desc->lock);

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

break;

desc->status &= ~IRQ_PENDING;

}

desc->status &= ~IRQ_INPROGRESS;

out:

/*

* The ->end() handler has to deal with interrupts which got

* disabled while the handler was running.

*/

desc->handler->end(irq);

spin_unlock(&desc->lock);

if (softirq_active(cpu) & softirq_mask(cpu))

do_softirq();

return 1;

}

对该函数的NOTE如下:

(1)首先,通过参数regs.orig_eax来获取此次中断请求的IRQ号。然后调用smp_processor_id()函数得到当前CPU的ID。指针desc指向相应于此次中断请求的IRQ描述符。

(2)将内核统计变量kstat.irqs数组中的相应元素kstat.irqs[cpu][irq]加1。并对中断请求描述符desc中的自旋锁成员lock进行加锁,以便使随后对中断服务队列的操作互斥。

(3)向中断控制器发出应答。具体到8259A来说,则是调用mask_and_ack_8259A()函数来向8259A发送EOI命令,表示中断结束,以便让8259A继续接收其它中断请求信号。注意,mask_and_ack_8259A函数同时还将屏蔽变量irq所对应的IRQ输入线。比如,当前服务的是IRQ3,那么在do_IRQ()执行期间,IRQ3是被8259A所屏蔽的。这一点需要特别注意。

(4)接下来,主要是设置中断请求的状态。先将IRQ描述符的status成员去掉IRQ_DISABLED标志位和IRQ_WAITING标志位(如果已经设置了的话)。然后在状态中设置IRQ_PENDING标志,表示这个中断请求的服务还未完成。注意!这里的IRQ_PENDING状态与前面所述的8259A IRQ pending状态是有区别的。8259A的IRQ pending是指在8259A已经向CPU发出了中断请求信号但CPU并没有向8259A发出中断应答(第一个INTA-cycle)时的状态,只要CPU向8259A发出了第一个INTA-cycle,8259A的IRQ pending状态即宣告结束。而这里的IRQ_PENDING标志的真正含义是指已经进入到do_IRQ()函数、但中断服务队列还未开始执行的意思。

(5)根据前面设置好的状态来确定中断服务队列。只有当IRQ状态中没有同时设置IRQ_DISABLED标志和IRQ_INPROGRESS标志时,才让action指针指向IRQ描述符中定义的中断服务队列(由desc->action所指向)。IEQ_DISABLED标志表示该中断请求已经被禁止,因此不需要被服务。而IRQ_INPROGRESS则表示中断请求正在被另一个CPU服务。因此当前CPU就不需要再对这个中断请求进行服务了。

在设置好action指针后,就清除中断请求状态中的IRQ_PENDING标志,同时在状态中设置IRQ_INPROGRESS标志,因为接下来马上就要开始服务这个中断请求了。最后,用刚刚设置好的状态值来更新IRQ描述符中的status值,以便让它对其它CPU可见。

(6)判断action指针是否为空,如果为空,说明没有任何中断服务行为等待执行,因此跳转到out部分,以便退出do_IRQ()函数。

(7)接下来的for死循环真正地遍历中断服务队列并调用其中的ISR来执行。循环体的步骤如下:

1. 首先,解除IRQ描述符中的自旋锁lock。这是因为在执行中断服务队列中的各个ISR时,ISR并不会访问IRQ描述符中的中断服务队列。

2. 调用handle_IRQ_event()函数来依次执行由action所指向的中断服务队列中的所有ISR。

3. 由于接下来又要访问IRQ描述符中的成员值,因此对IRQ描述符中的自旋锁lock进行加锁操作。

4. 最后测试IRQ描述符的status成员是否设置了IRQ_PENDING标志。如果没有,就执行break语句退出for循环。如果设置了IRQ_PENDING标志,则说明有一次中断请求未得到服务,于是首先清除IRQ_PENDING标志,然后继续for循环,以便为该中断请求补上一次中断服务。这个概念就成为“IRQ_REPLAY”。为什么需要这样呢?这是应为在SMP体系结构中,假设CPU1正在为一个IRQ执行do_IRQ()函数,且CPU1已经在相应IRQ描述符的status成员中设置了IRQ_INPROGRESS标志,而此时CPU2上又收到一次同级的中断请求,于是CPU2也执行do_IRQ()函数,但CPU2由于看到相应IRQ描述符的status成员中已经设置了IRQ_INPROGRESS标志,因此CPU2知道另一个CPU(它无需知道是哪个具体的CPU,反正不是他自己:-)已经正在为同级中断请求服务了,于是CPU2就简单地在相应IRQ描述符的status成员中设置一个IRQ_PENDING标志,然后就退出do_IRQ()函数,而把中断服务留给CPU1去执行。当CPU1从handle_IRQ_event()函数返回时,它看到设置了IRQ_PENDING标志,知道其它CPU把中断服务留给他来执行了,于是CPU1先清除IRQ_PENDING标志,然后继续for循环,以便补上一次中断服务。注意!这种机制可能会造成中断服务丢失现象的。

(6)从for循环退出后,CPU对硬件中断请求的服务也就宣告结束,因此,它清除相应IRQ描述符的status成员中的IRQ_INPROGRESS标志。

(7)out部分:接下来就是do_IRQ()函数的退出部分。为此,它首先调用中断控制器描述符中的end()函数。具体到8259A来讲,也就是调用end_8259A_irq()函数,该函数实际上就是通过enable_8259A_irq()函数来重新使能8259A的相应IRQ输入线(因为mask_and_ack_8259A()函数已经将它屏蔽,所以这里必须将其重新开启)。然后解除相应IRQ描述符中自旋锁lock。

(8)最后,在退出do_IRQ()函数之前,看看是否需要执行软中断。如果需要,就调用do_softirq()函数来执行软中断或设备驱动程序的bottom half函数。

5.7.1 函数handle_IRQ_event

函数handle_IRQ_event()用来执行一个中断服务队列中的所有ISR。它的实现相当简单,其核心思想就是用一个do{}while循环来依次执行中断服务队列中的各个ISR,之所以用do{}while循环而不用while循环,是因为:这个函数只被do_IRQ()函数所调用,而do_IRQ()在调用它时已经保证了中断服务队列不为空。该函数的源码如下:

int handle_IRQ_event(unsigned int irq, struct pt_regs * regs, struct irqaction * action)

{

int status;

int cpu = smp_processor_id();

irq_enter(cpu, irq);

status = 1; /* Force the "do bottom halves" bit */

if (!(action->flags & SA_INTERRUPT))

__sti();

do {

status |= action->flags;

action->handler(irq, action->dev_id, regs);

action = action->next;

} while (action);

if (status & SA_SAMPLE_RANDOM)

add_interrupt_randomness(irq);

__cli();

irq_exit(cpu, irq);

return status;

}

对这个函数请注意三点:

(1)如果中断服务队列中第一个中断服务描述符没有设置SA_INTERRUPT标志,那么整个队列中的ISR都是在CPU开中断的情况下执行的。但在整个队列都行完毕后,handle_IRQ_event()函数又重新用__cli()函数关闭中断。

(2)只要中断服务队列中有一个中断服务描述符中设置了随机标志,那么handle_IRQ_event()函数就将通过add_interrupt_randomness()函数为中断引入一些随机性。但是大多数driver都不使用这个随机标志。

(3)请注意这里对irq_enter()和irq_exit()函数对的使用。

§5.8 中断返回ret_from_intr

从总控程序do_IRQ()返回后,common_interrupt所作的第三件事情就是通过jmp指令跳转到ret_from_intr程序段,以执行中断返回过程。

程序段ret_from_intr定义在arch/i386/kernel/entry.S文件中,如下所示:

ENTRY(ret_from_intr)

GET_CURRENT(%ebx)

movl EFLAGS(%esp),%eax # mix EFLAGS and CS

movb CS(%esp),%al

testl $(VM_MASK | 3),%eax # return to VM86 mode or non-supervisor?

jne ret_with_reschedule

jmp restore_all

(1)首先,通过GET_CURRENT()宏将当前进程的task_struct结构的指针置入寄存器EBX中。

(2)然后,将内核堆栈中的EFLAGS寄存器的值放入EAX寄存器中,EFLAGS(%esp)表示地址为“堆栈指针%esp加上常数EFLAGS”处的内容,也即内核堆栈中的EFLAGS寄存器的值。

(3)将内核堆栈中CS寄存器的值置入al寄存器中。AL寄存器也就是EAX寄存器的低16位。这样,通过这两步也就把EFLAGS寄存器的高16位和CS寄存器的值平凑在一起,放到EAX寄存器中。这样做的目的有两个:

l 检查中断前夕CPU是否运行于VM86模式下。因为EFLAGS寄存器中的bit[16]表示CPU是否运行于VM86模式。

l 检查中断前夕CPU是否运行在用户台下。因为CS寄存器的bit[1:0]表示CPU的运行级别。只要CS的最低两位非0,就说明中断前夕CPU运行在用户台下。

(4)将EAX寄存器与立即数(VM_MASK|3)进行位测试,如果测试结果非0,则说明上述两种情况(VM86模式或用户态)之一发生,于是就跳转到ret_with_reschedule程序段。如果测试结果非0,则说明中断前夕CPU运行在内核态下,于是跳转到restore_all程序段。

5.8.1 返回到用户态

由于Linux不使用VM86模式,因此我们假定中断前夕CPU运行在用户态下。所以就先跳转到ret_with_reschedule程序段去执行。如下所示(entry.S):

ret_with_reschedule:

cmpl $0,need_resched(%ebx)

jne reschedule

cmpl $0,sigpending(%ebx)

jne signal_return

restore_all:

RESTORE_ALL

(1)由于EBX寄存器中的值是当前进程的task_struct结构的地址,因此表达式need_resched(%ebx)也就是当前进程的task_struct结构中的need_resched成员的值。因此第一个语句判断current->need_resched是否为0,如果不为0,表示需要重新调度,于是就跳转到reschedule程序段。该程序段如下所示:

reschedule:

call SYMBOL_NAME(schedule) # test

jmp ret_from_sys_call

(2)判断当前进程的task_struct结构中的sigpending成员是否为0,如果不为0,就表示有信号需要处理,于是跳转到signal_return程序段去处理当前进程的信号。

5.8.2 中断返回的总出口

不管是ret_from_syscall程序段还是signal_return程序段,它们最总都要跳回到restore_all程序段,它是一个总出口。该程序段只有一行代码,也即:执行宏RESTORE_ALL,以恢复内核堆栈到中断前夕的状态。宏RESTORE_ALL的定义如下(entry.S):

#define RESTORE_ALL \

popl %ebx; \

popl %ecx; \

popl %edx; \

popl %esi; \

popl %edi; \

popl %ebp; \

popl %eax; \

1: popl %ds; \

2: popl %es; \

addl $4,%esp; \

3: iret;

从上述代码可以看出,它是以和SAVE_ALL相反的顺序从内核堆栈中探出各个寄存器的值,直到ES寄存器,以恢复中断前夕的CPU现场。

由于内核堆栈中ES寄存器之上的是orig_eax,是用来保存IRQ号或syscall号的,而它现在已经没有任何用处,因此通过将内核堆栈指针%esp的加上常数4来简单地将orig_eax的内容从堆栈中丢弃。

最后,CPU执行iret指令,从而使得CPU从中断返回。

至此,整个Linux中断服务流程宣告结束。
[/u][/u][/u][/u][/u]
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: