http://os.chinaunix.net/a2008/0910/985/000000985664.shtml
Copyright © 2003 by 詹荣开
E-mail:zhanrk@sohu.com
Linux-2.4.0
Version 1.0.0,2003-2-14
摘要:本文主要从内核实现的角度分析了Linux 2.4.0内核的设备中断流程。本文是为那些想要了解Linux I/O子系统的读者和Linux驱动程序开发人员而写的。
关键词:Linux、中断、设备驱动程序
申明:这份文档是按照自由软件开放源代码的精神发布的,任何人可以免费获得、使用和重新发布,但是你没有限制别人重新发布你发布内容的权利。发布本文的目的是希望它能对读者有用,但没有任何担保,甚至没有适合特定目的的隐含的担保。更详细的情况请参阅GNU通用公共许可证(GPL),以及GNU自由文档协议(GFDL)。
你应该已经和文档一起收到一份GNU通用公共许可证(GPL)的副本。如果还没有,写信给:
The Free Software Foundation, Inc., 675 Mass Ave, Cambridge,MA02139, USA
欢迎各位指出文档中的错误与疑问。
§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> 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)
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 share