Linux内核的中断机制.docx
- 文档编号:7541986
- 上传时间:2023-01-24
- 格式:DOCX
- 页数:56
- 大小:51.39KB
Linux内核的中断机制.docx
《Linux内核的中断机制.docx》由会员分享,可在线阅读,更多相关《Linux内核的中断机制.docx(56页珍藏版)》请在冰豆网上搜索。
Linux内核的中断机制
第五章Linux内核的中断机制
(By詹荣开,NUDT)
Copyright©2003by詹荣开
E-mail:
zhanrk@
Linux-2.4.0
Version1.0.0,2003-2-14
摘要:
本文主要从内核实现的角度分析了Linux2.4.0内核的设备中断流程。
本文是为那些想要了解LinuxI/O子系统的读者和Linux驱动程序开发人员而写的。
关键词:
Linux、中断、设备驱动程序
申明:
这份文档是按照自由软件开放源代码的精神发布的,任何人可以免费获得、使用和重新发布,但是你没有限制别人重新发布你发布内容的权利。
发布本文的目的是希望它能对读者有用,但没有任何担保,甚至没有适合特定目的的隐含的担保。
更详细的情况请参阅GNU通用公共许可证(GPL),以及GNU自由文档协议(GFDL)。
你应该已经和文档一起收到一份GNU通用公共许可证(GPL)的副本。
如果还没有,写信给:
TheFreeSoftwareFoundation,Inc.,675MassAve,Cambridge,MA02139,USA
欢迎各位指出文档中的错误与疑问。
§5.1I386的中断与异常
中断通常被分为“同步中断”和异步中断两大类。
同步中断是指当指令执行时由CPU控制单元产生的中断,之所以称为“同步中断”是因为只有在一条指令中止执行后CPU才会发出这类中断信号。
而异步中断则是指由其他硬件设备依照CPU时钟随机产生的中断信号。
在Intel80x86CPU手册中,同步中断和异步中断也被分别称为“异常(Exception)”和“中断(Interrupt)”。
Intel又详细地把中断和异常细分为以下几类:
(1)中断
1.可屏蔽中断(MaskableInterrupt):
这类中断被送到CPU的INTR引脚,通过清除eflag寄存器的IF标志可以关闭中断。
2.不可屏蔽中断(NonmaskableInterrupt):
被送到CPU的NMI引脚,通常只有几个危急的事件,如:
硬件故障等,才产生不可屏蔽中断信号。
寄存器eflag中的IF标志对这类中断不起作用。
(2)异常
1.处理器探测异常(Processor-detectedexception):
当CPU执行一条指令时所探测到的一个反常条件所产生的异常。
依据CPU控制单元产生异常时保存在内核态堆栈eip寄存器的值,这类异常又可以细分为三种:
n故障(Fault):
保存在eip中的值是引起故障的指令地址,因此但异常处理程序结束后,会重新执行那条指令。
“缺页故障”是这类异常的一个常见例子。
n陷阱(Trap):
保存在eip中的值是一个指令地址,但该指令在引起陷阱的指令地址之后。
只有当没有必要重新执行已执行过的指令时,才会触发trap,其主要用途是调试程序。
n异常中止(Abort):
当发生了一个严重的错误,致使CPU控制单元除了麻烦而不能在eip寄存器中保存有意义的值。
异常中止通常是由硬件故障或系统表中无效的值引起的。
由CPU控制单元发生的这个中断是一种紧急信号,用来把CPU的执行路径切换到异常中止的处理程序,而相应的ISR通常除了迫使受到影响的进程中止外,别无选择。
2.编程异常(ProgrammedException):
通常也称为“软中断(softwareinterrupt)”,是由编程者发出中断请求指令时发生的中断,如:
int指令和int3指令。
当into(检查溢出)和bound(检查地址越界)指令检查的条件不为真时,也引起编程异常。
CPU控制单元把编程异常当作Trap来处理,这类异常有两个典型的用途:
一、执行系统调用;二、给调试程序通报一个特定条件。
5.1.1中断向量
每个中断和异常都可以用一个0-255之间的无符号整数来标识,Intel称之为“中断向量(InterruptVector)”。
通常,不可屏蔽中断和异常的中断向量是固定的,而可屏蔽中断的中断向量则可以对中断控制器进行编程来改变。
I386CPU的256个中断向量是这样分配的:
1.从0-31这一共32个向量用于异常和不可屏蔽中断。
2.从32-47这一共16个向量用于可屏蔽中断,分别对应于主、从8259A中断控制器的IRQ输入线。
3.剩余的48-255用于标识软中断。
Linux全部使用了0-47之间的向量。
但对于48-255之间的软中断向量,Linux只使用了其中的一个,即用于实现系统调用的中断向量128(0x80)。
当用户态下的进程执行一条int0x80汇编指令时,CPU切换到内核态,以服务于系统调用。
Linux在头文件include/asm-i386/hw_irq.h中定义了宏FIRST_EXTERNAL_VECTOR来表示第一个外设中断(即8259A的IRQ0)所对应的中断向量,此外还定义了SYSCALL_VECTOR来表示用于系统调用的中断向量。
如下所示:
/*
*IDTvectorsusableforexternalinterruptsourcesstart
*at0x20:
*/
#defineFIRST_EXTERNAL_VECTOR0x20
#defineSYSCALL_VECTOR0x80
5.1.2I386的IDT
i386CPU的IDT表一共有256项,分别对应每一个中断向量。
每一个表项就是一个中断描述符,用以描述相对应的中断向量,中断向量就是该描述符在IDT中的索引,每一个中断描述符的大小都是8个字节。
根据INTEL的术语,中断描述符也称为“门(Gate)”。
中断描述符有下列4种类型:
(1)任务们(TaskGate):
包含了一个进程的TSS段选择符。
每当中断信号发生时,它被用来取代当前进程的那个TSS段选择符。
Linux并没有使用任务们。
任务们的格式如下:
(2)中断门(InterruptGate):
中断门中包含了一个段选择符和一个中断处理程序的段内偏移。
注意,当I386CPU穿越一个中断门进入相应的中断处理程序时,它会清除eflag寄存器中的IF标志,从而屏蔽接下来可能发生的可屏蔽中断。
(3)陷阱门(TrapGate):
与中断门类似,不同之处在于CPU通过陷阱门转入中断处理程序时不会清除IF标志。
(4)调用门(CallGate):
Linux并没有使用调用门。
这三种门的格式如图5-2所示。
5.1.3中断控制器8259A
我们都知道,PC机中都使用两个级联的8359APIC(ProgrammableInterruptController,可编程中断控制器,简称PIC)来管理来自系统外设的中断信号。
每个8259APIC提供8根IRQ(InterruptReQuest,中断请求,简称IRQ)输入线。
在级联方式中,Master8259APIC(第一个PIC)的中断信号输入线IR2用于级联Slave8259APIC(第二个PIC)的INT引脚,因此两个8259A一共可以提供15根可用的IRQ输入线。
如下图所示:
图5-3主、从8259A中断控制器的级联
5.1.3.18259APIC的基本原理
8259APIC芯片的基本逻辑块图如下所示:
“中断屏蔽寄存器”(InterruptMaskRegister,简称IMR)用于屏蔽8259A的中断信号输入,每一位对应一个输入。
当IMR中的bit[i](0≤i≤7)位被置1时,相对应的中断信号输入线IRi上的中断信号将被8259A所屏蔽,也即IRi被禁止。
当外设产生中断信号时(由低到高的跳变信号,80x86系统中的8259A是边缘触发的,EdgeTriggered),中断信号被输入到“中断请求寄存器”(InterruptRequestRegister,简称IRR),并同时看看IMR中的相应位是否已被设置。
如果没有被设置,则IRR中的相应位被设置为1,表示外设产生一个中断请求,等待CPU服务。
然后,8259A的优先级仲裁部分从IRR中选出一个优先级最高中断请求。
优先级仲裁之后,8259A就通过其INT引脚向CPU发出中断信号,以通知CPU有外设请求中断服务。
CPU在其当前指令执行完后就通过他的INTA引脚给8259A发出中断应答信号,以告诉8259A,CPU已经检测到有中断信号产生。
8259A在收到CPU的INTA信号后,将优先级最高的那个中断请求在ISR寄存器(In-ServiceRegister,简称ISR)中对应的bit置1,表示该中断请求已得到CPU的服务,同时IRR寄存器中的相应位被清零重置。
然后,CPU再向8259A发出一个INTA脉冲信号,8259A在收到CPU的第二个INTA信号后,将中断请求对应的中断向量放到数据总线上,以供CPU读取。
CPU读到中断向量后,就可以装入执行相应的中断处理程序。
如果8259A工作在AEOI(AutoEndOfInterrupt,简称AEOI)模式下,则当他收到CPU的第二个INTA信号时,它就自动重置ISR寄存器中的相应位。
否则,ISR寄存器中的相应位就一直保持为1,直到8259A显示地收到来自于CPU的EOI命令。
5.1.3.28259A的I/O端口
Master8259A的IO端口地址位0x20和0x21,Slave8259A的IO端口地址为0xA0和0xA1。
对这些IO端口进行读写操作时的功能如下表所示:
I/OPortAddrssRead/WriteFunction
PortA(0x20/0xA0)WInitializationCommandWord1(ICW1)
WOperationCommandWord2(OCW2)
WOperationCommandWord3(OCW3)
RInterruptRequestRegister(IRR)
RIn-ServiceRegister(ISR)
PortB(0x21/0xA1)WInitializationCommandWord2(ICW2)
WInitializationCommandWord3(ICW3)
WInitializationCommandWord4(ICW4)
WOperationCommandWord1(OCW1)
RInterruptMaskRegister(IMR)
表5-18259A的I/O端口地址列表
5.1.3.3初始化8259A
8259APIC的初始化是通过向其写一系列“初始化命令字”(InitializationCommandWord,简称ICW)来实现的。
其中,ICW1必须写到PortA(0x20/0xA0)中,而ICW2、ICW3和ICW4则必须写到PortB(0x21/0xA1)中。
此外,主、从8259A必须分别进行初始化。
ICW1的格式如下图5-5所示:
ICW2的格式如下图5-6所示:
在MCS-80/85模式下,A15-A8指定中断向量地址;而在80x86模式下,T7-T3用于指定中断向量地址,而bit[2:
0]则可被设置为0。
ICW3(MasterDevice)的格式如下:
Si(0≤i≤7)为1则表示相应的IRi上级联了一个Slave8259APIC。
ICW3(SlaveDevice)的格式如下:
Bit[7:
3]总为0,而ID2、ID1、ID0(即bit[2:
0])用于标识Slave8259A连接在Master8259A的哪一根IRQ线上。
例如:
010就表示Slave8259A是连接在Master8259A的IR2上。
ICW4的格式如下:
5.1.3.4控制8259A
可以向PortA或PortB写入“控制命令字”(ControlCommandWord,简称OCW)来控制8259APIC。
有三种类型的OCW,其中OCW1只能被写入到PortB中,OCW2和OCW3只能被写到PortA中。
OCW1(InterruptMaskRegister)的格式如下:
M7…M0就是IRQ7…IRQ0各自对应的屏蔽位。
IMR寄存器可以通过向PortB写入OCW1来设置,它的当前值也可以通过读取PortB来得到。
OCW2的格式如下:
OCW3的格式如下:
§5.2Linux对IDT的初始化
5.2.1定义IDT的数据结构
Linux在include/asm-i386/Desc.h头文件中定义了数据结构desc_struct,用来描述i386CPU中的各种描述符(均为8字节),如下:
structdesc_struct{
unsignedlonga,b;
};
基于上述结构,Linux在arch/i386/kernel/traps.c文件中定义了数组idt_table[256],来表示中断描述符表IDT,其定义如下:
/*
*TheIDThastobepage-alignedtosimplifythePentium
*F00Fbugworkaround..Wehaveaspeciallinksegment
*forthis.
*/
structdesc_structidt_table[256]__attribute__((__section__(".data.idt")))={{0,0},};
5.2.2对门的操作函数
Linux在arch/i386/kernel/traps.c文件中定义了宏_set_gate(),用来设置一个描述符(即门)的具体值。
由于Linux内核代码均在段选择子__KERNEL_CS所指向的内核段中,因此门中的SegmentSelector字段总是等于__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(),其源码如下:
voidset_intr_gate(unsignedintn,void*addr)
{
_set_gate(idt_table+n,14,0,addr);/*Type=1110(14),dpl=0(ring0)*/
}
其中,参数n是中断向量(被用作IDT表的索引)。
参数addr是中断处理程序的入口地址。
(2)陷阱门的操作宏set_trap_gate()。
其源码如下:
staticvoid__initset_trap_gate(unsignedintn,void*addr)
{
_set_gate(idt_table+n,15,0,addr);/*Type=1111(15),dpl=0(ring0)*/
}
(3)系统门的操作宏set_system_gate()。
Linux扩展了INTEL的术语,它将可编程异常所对应的中断描述符称为“系统门”。
系统门的DPL字段为3(用户态),因此使得用户进程可以在用户态下(I386运行在ring3级别)通过int指令或其它指令穿越系统门而进入内核态。
staticvoid__initset_system_gate(unsignedintn,void*addr)
{
_set_gate(idt_table+n,15,3,addr);/*Type=1111(15),dpl=3(ring3)*/
}
(4)调用门的操作宏set_call_gate(),其源码如下:
staticvoid__initset_call_gate(void*a,void*addr)
{
_set_gate(a,12,3,addr);/*Type=1100(12),dpl=3(ring3)*/
}
从上述这四个专用的宏操作实现也可以看出,Linux并没有使用i386CPU的调用门和任务门,而是仅仅使用了中断门和陷阱门(Linux又将它细分为陷阱门和系统门)两种中断描述符。
5.2.3对IDT表的初始化设置
Linux内核在初始阶段完成了对也是虚存管理机制的初始化后,便调用trap_init()函数和init_IRQ()函数对i386CPU中断机制的核心——IDT进行初始化设置。
如下:
asmlinkagevoid__initstart_kernel(void)
{
……
trap_init();
init_IRQ();
……
}
其中,函数trap_init()用来对除外设中断(32-47)以外的所有处理器保留的中断向量进行初始化。
而init_IRQ()函数则用来初始化对应于主、从8259A中断控制器的可屏蔽外设中断32-47。
函数trap_init()定义在arch/i386/kernel/traps.c文件中,其源码如下:
void__inittrap_init(void)
{
#ifdefCONFIG_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-5canbecalledfromall*/
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);
/*
*defaultLDTisasingle-entrycallgatetolcall7foriBCS
*andacallgatetolcall27forSolaris/x86binaries
*/
set_call_gate(&default_ldt[0],lcall7);
set_call_gate(&default_ldt[4],lcall27);
/*
*ShouldbeabarrierforanyexternalCPUstate.
*/
cpu_init();
#ifdefCONFIG_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__initinit_IRQ(void)
{
inti;
#ifndefCONFIG_X86_VISWS_APIC
init_ISA_irqs();
#else
init_VISWS_APIC_irqs();
#endif
/*
*Coverthewholevectorspace,novectorcanescape
*us.(someofthesewillbeoverriddenandbecome
*'special'SMPinterrupts)
*/
for(i=0;i intvector=FIRST_EXTERNA
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- Linux 内核 中断 机制