本文是ULKIntel 80x86ARM中断通常分为同步中断(synchronous)(asynchronous)同步中断:CPUCPU异步中断:CPUIntel微处理器手册中,把同步和异步中断分别称为异常(exception)(interrupt)中断是由间隔定时器和I/O
1.中断信号的作用
中断或异常处理程序不是一个进程,而是一个内核控制路径,代表中断发生时正在运行的进程执行。中断处理是内核执行的最敏感的任务之一,必须满足以下约束:
①内核相应中断后的操作分两部分,关键的紧急的部分,内核立即执行,其余的推迟随后执行
②中断程序必须使内核控制路径能以嵌套的方式执行,当最后一个内核控制路径终止时,内核必须能恢复被中断进程的执行,或者如果中断信号已经导致了重新调度,内核能切换到另外的进程。
③尽管中断可以嵌套,但在临界区中,中断必须禁止。但内核必须尽可能限制这样的临界区,大部分时间应该以开中断的方式运行。
2.中断和异常
Intel文档把中断和异常分为以下几类
中断(maskable interrupt)异常(eip寄存器的值分为故障、陷阱、异常终止)、编程异常(int或int3触发,也叫软中断)
每个中断和异常是由0255之间的一个数来标识,Intel把这个8(vector)(1)IRQ和中断
每个能发出中断请求的硬件控制器都有一条IRQ输出线,所有IRQ(Programmable Interrupt Controller,PIC)的硬件电路输入引脚相连。PIC执行以下动作
①监视IRQIRQIRQ②如果一个信号在IRQa.把接收到的引发信号转换成相应的向量
b.把这个向量存放在PIC的一个I/OCPUc.把引发信号发送到处理器的INTR引脚,即产生一个中断。
d.等待,直到CPU通过把这个中断信号写进可编程中断控制器的一个I/O端口来确认它,当这个情况发生,清INTR线
③返回①
IRQ线从0开始顺序编号,第一条IRQIRQ0,与IRQn关联的Inteln+32.PIC可以修改IRQ和向量之间的映射。
可以通过修改PICIRQ,但禁止的中断是不会丢失的,它们一旦被激活,PIC就又把它们发送到CPUCPU可以一次处理同一类型的IRQ,PICIRQ不同于屏蔽中断,屏蔽中断是被忽略的。
传统的PIC8259A芯片级联成的,可以支持15个IRQ(2)高级可编程中断控制器
为了充分发挥SMPCPUIntelPentium IIII/O APIC(I/O Advanced Programmable Interrupt Controller)的新组件,用来代替老式的8259A
如图,一条APICI/O APICAPICIRQI/O APICAPICI/O APIC除了在处理器之间分发中断外,多APIC系统还允许CPU(interprocessor interrupt).
(3)异常
80x86发布了大约20中不同的异常,内核必须为每种异常提供一个专门的异常处理程序,对于某些异常,CPU在执行异常处理程序前会产生一个硬件出错码,并压入内核态堆栈。
0—“Divde error”0故障
1—“Debug”陷阱或故障
2—未用
3—“breakpoint”陷阱,由int3引起
…
(4)中断描述符表(Interrupt Descriptor Table,IDT)
IDT是一个系统表,它与每一个中断或异常向量相联系,每一个向量在表中有相应的中断或异常处理程序的入口地址。内核在允许中断发生前,必须适当地初始化IDT.
IDT表中每一项对应一个中断或异常向量,每个向量由8个字节组成,因此最多需要2568=2048IDT.
IDT包含三种类型的描述符
任务门(task gate):存放当中断信号发生时,必须取代当前进程的那个进程的TSS选择符
中断门(interrupt gate):IF标志,从而关闭将来会发生的可屏蔽中断。
陷阱门(Trap gate)与中断门相似,只是控制权传递到一个适当的段时处理器不修改IF标志。
Linux利用中断门处理中断,利用陷阱门处理异常。
(5)中断和异常的硬件处理
假定内核已被初始化,CPUcseip这对寄存器包含下一条将要执行的指令的逻辑地址。在处理这条指令之前,控制单元会检查允许前一天指令时是否已经发生了一个中断或异常,若发生,那么控制单元执行下列操作:
①确定与中断或异常关联的向量i(0<=i<=255)
②读由idtrIDTi项
③从gdtrGDTGDTIDT④确信中断是由授权的中断发生源发出的,中断处理程序的特权级不能低于引起中断的程序的特权。若是异常,进一步检查DPL,CPL这可以避免用户程序访问特殊的陷阱门或中断们。
⑤检查是否发生了特权级的变化,也就是检查CPL是否不同于所选择的段描述符的DPLa.读tr寄存器,以访问允许进程的TSSb.用与新特权级别相关的栈段和栈指针的正确值装载ss和espc.在新的栈中保存ss和esp⑥如果故障已发生,用引起异常的指令地址装载cs和eip⑦在栈中保存flagscs及eip的内容
⑧如果异常产生了一个硬件出错码,则将它保存在栈中
⑨装载cseip寄存器,其值分别是IDT表中第i控制单元所执行的最后一步就是跳转到中断或异常处理程序,相应的处理程序必须产生一条iret指令,把控制权交给被中断的进程,这将迫使控制单元。
①用保存在栈中的值装载cseip或eflags寄存器,如果一个硬件出错码曾被压入栈中,并且在eip内容的上面,那么,执行iret②检查处理程序的CPLcs中的最低两位,如果是,Iret终止执行,否则转入下一步。
③从栈中装载ssesp寄存器,因此,返回到与旧特权级别相关的栈。
④检查dses、fs及gsDPLCPL,那么清相应段寄存器,这是为了禁止用户态程序(CPL=3)利用内核以前所用的段寄存器(DPL=0),如果不清这些寄存器,恶意用户程序可以利用来访问内核地址空间。
3.中断和异常处理程序的嵌套
每个中断或异常都会引起一个内核控制路径的执行,当设备发出一个中断时,相应的内核控制路径的第一部分指令就是把那些寄存器的内容保存在内核堆栈,最后一部分就是恢复寄存器内容并让CPU返回到用户态的指令
内核控制路径可以任意嵌套,即一个中断处理程序可以被另一个中断处理程序“中断”,因此引起内核控制路径的嵌套执行。
允许内核控制路径嵌套执行必须付出代价,就是中断处理程序必须永不阻塞,即中断处理程序运行期间不能发生进程切换,事实上,嵌套的内核控制路径回复执行时需要的数据存放在内核态堆栈中,这个栈是属于当前进程的。
一个中断处理程序既可以抢占其他的中断处理程序,也可以抢占异常处理程序。相反,异常处理程序从不抢占中断处理程序。
Linux交错执行内核控制路径,因为:
①为了提高可编程中断控制器和设备控制器的吞吐量。
②为了实现一种没有优先级的中断模型。简化了内核代码,提高了内核可移植性。
在多处理器系统上,几个内核控制路径可以并发执行,此外,与异常相关的内核控制路径可以开始执行在一个CPU上,并且由于进程切换而移往另一个CPU上执行。
4.初始化中断描述符表
内核启用中断之前,必须把IDTidtrLinux与Intel稍有不同,把中断任务分为,中断门、系统门、系统中断门、陷阱门、人物门。
IDT存放在idt_table表中,有256个表项,6idt_descrIDT的大小和它的地址,只有当内核用lidt汇编指令初始化idtr内核初始化过程中,setup_idt()(ignore_int())256idt_table
点击(此处)折叠或打开
ignore_int()中断处理程序,可以看作一个空的处理程序,只调用printk打印“Unknown interrupt”系统消息。正常情况下,ignore_int()紧接着这个预初始化,内核将在IDTIRQ,IDT都将包含一个专门的中断门。
5.异常处理
(1)CPU产生的大部分异常都由Linux出错条件,当一个异常发生时,内核就向引起异常的进程发送一个信号向它通知一个反常条件。
在两种情况下,LinuxCPU异常更有效地管理硬件资源:①”Device not availeble”异常与cr0寄存器的TSPage Fault异常处理程序有一个标准的结构,包括三部分:
①在内核堆栈中保存大多数寄存器的内容(用汇编实现)
②用高级的C③通过ret_from_exception()(2)进入和离开异常处理程序
大部分的异常处理函数把硬件出错码和异常向量保存在当前进程的描述符中,然后向当前进程发送一个适当的信号。
current->thread.error_code = error_code; current->thread.trap_no = vector; force_sig(sig_number, current);异常处理程序刚一终止,当前进程就关注这个信号,该信号要么在用户态由进程自己的信号处理程序处理,要么由内核处理。
异常处理程序总是检查异常是发生在用户态还是在内核态,当内核异常是,为了避免硬盘上的数据崩溃,处理程序调用die()函数,该函数在控制台打印出所有CPU寄存器的内容(kernel oops)do_exit()当执行异常处理的Cjmpret_from_exception()6.中断处理
异常处理中给当前进程发一个Unix中断处理依赖于中断类型,主要分三类:
I/O中断:中断处理程序必须查询设备已确定适当的操作过程。
时钟中断:某种时钟产生一个中断,告诉内核一个固定的时间间隔已经过去。
处理器间中断:多处理器系统中一个CPU对另一个CPU(1)I/O中断处理
①为了能给多个设备同时提供服务,中断处理程序有以下灵活性
IRQ共享:中断处理程序执行多个中断服务例程(interrupt service routine,ISR),每个ISR是一个与单独设备(IRQ线)相关的函数,每个ISRIRQ动态分配:一条IRQ线在可能的最后时刻才与一个设备驱动程序相关联,这样即使几个硬件设备不共享IRQ线,同一个IRQ②Linux紧急的(Critical):非紧急的(Noncritical):非紧急可延迟的(Noncritical deferrable):由独立函数来执行,比如tasklet③中断处理过程
a.在内核态堆栈中保存IRQ的值和寄存器内容
b.为正在给IRQ线服务的PICPICc.执行各项这个IRQ的所有设备的中断服务例程(ISR).
d.跳到ret_from_intr()的地址后终止
④中断向量
为IRQa.设置一些硬件跳线器(仅适用于旧式设备卡)
b.安装设备是执行一个实用程序,该程序可以让用户选择一个可用的IRQ号,或者探测系统自身以确定一个可用的IRQ号。
c.在系统启动时执行一个硬件协议,以确定一个可用的IRQ号。比如,遵循外设部件互联标准(Peripheral Component Interconnect,PCI),pci_read_config_byte()内核必须在启用中断前发现IRQI/O设备之间的对应,都则内核在不知道那个向量对于那个设备的情况下,无法处理来自该设备的信号。
一个例子
⑤IRQ数据结构
a.每个中断向量都有它自己的irq_desc_t描述符,所有的描述符组织在一起形成irq_desc数组。
点击(此处)折叠或打开
handle_irq:服务于IRQ线
action:标识当出现IRQ时要调用的中断服务例程,该字段指向IRQ的irqactionStatus:描述IRQ线状态的一组标志
Depth:如果IRQ线被激活,则显示0irq_count:中断计数器,统计IRQ线上发生中断的次数
irqs_unhandledIRQ线上无法处理的中断进行技术
内核把中断和意外中断总次数分别存放在irq_desc_t描述符的irq_count和irqs_unhandled字段中,当第100000次中断产生时,如果意外中断的次数超过99900内核禁止这条IRQb.IRQ线状态的一组值:
Flag name |
Description |
IRQ_INPROGRESS |
A handler for the IRQ is being executed. |
IRQ_DISABLED |
The IRQ line has been deliberately disabled by a device driver. |
IRQ_PENDING |
An IRQ has occurred on the line; its occurrence has been acknowledged to the PIC, but it has not yet been serviced by the kernel. |
IRQ_REPLAY |
The IRQ line has been disabled but the previous IRQ occurrence has not yet been acknowledged to the PIC. |
IRQ_AUTODETECT |
The kernel is using the IRQ line while performing a hardware device probe. |
IRQ_WAITING |
The kernel is using the IRQ line while performing a hardware device probe; moreover, the corresponding interrupt has not been raised. |
IRQ_LEVEL |
Not used on the 80 x 86 architecture. |
IRQ_MASKED |
Not used. |
IRQ_PER_CPU |
Not used on the 80 x 86 architecture. |
Irq_desc_t描述符的depth字段和IRQ_DISABLEDIRQdisable_irq()disable_irq_nosync()depthdepth=0IRQIRQ_DISABLEDenable_irq()depthdepth0,函数激活IRQ线并清除IRQ_DISABLED在系统初始化期间,init_IRQ()IRQstatusIRQ_DISABLED
点击(此处)折叠或打开
这段代码在Interruptinterruptn项中存放IRQn的中断处理程序的地址。
c. irqaction描述符的字段:多个设备能共享一个单独的IRQ,因此内核要维护多个irqaction
点击(此处)折叠或打开
Irqaction描述符的标志
SA_INTERRUPT:处理程序必须以禁止中断执行
SA_SHIRQ:设备允许它的IRQ线与其他设备共享
SA_SAMPLE_RANDOM⑥IRQ在多处理系统上的分发
Linux遵循SMP,这意味着本质上内核不会对任何一个CPU有偏爱,内核试图以轮转的方式把来自硬件设备的IRQ信号在所有CPUCPUI/O中断的执行时间片几乎相同。
简而言之,当硬件设备产生了一个中断信号时,多APIC系统就选择其中的一个CPUAPIC,APIC又依次中断它的CPU,这个事件不通报给其他所有的CPULinux2.6kirqdCPUIRQ的自动分配。
⑦多种类型的内核栈
如果thread_union8K,thread_union结构的大小为4K,内核就使用三种类型的内核栈:
a.异常栈,用于处理异常(包括系统调用)b.硬中断请求线,用于处理中断。
c.软中断请求栈,用于处理可延迟函数(软中断或tasklet)CPU硬中断请求存放在hardirq_stacksoftirq_stackirq_ctx与每个栈相连的thread_infoCPUHardirq_cts和softirq_ctx数组是内核能快速确定指定CPU的硬中断请求栈和软中断请求栈,它们包含的指针分别指向相应的irq_ctx元素。
⑧do_IRQ()函数
保存寄存器的值后,栈顶地址被存放在eax寄存器中,然后中断处理程序调用do_IRQ()函数。
do_IRQ()函数执行下面的操作:
a.执行irq_enter()宏,它使表示中断处理程序嵌套数量的计数器递增,计数器保存在当前进程thread_info结构的preempt_count字段中。
b.如果thread_union结构大小为4KB,函数切换到硬中断请求栈,执行以下步骤
b1.current_thread_info()thread_info b2.thread_infohardirq_ctx[smp_processor_id()]c b3.c.调用__do_IRQ()函数,把指针regs和regs->orig_eaxd. 如果上面已经成功切换到硬中断请求栈,函数把ebx寄存器中的原始栈指针拷贝到espe.执行宏irq_exit(),该宏递减中断计数器并检查是否有可延迟函数正等待执行
f.转向ret_from_intr()函数。
__do_IRQ()函数:__do_IRQ()函数接受IRQ号(eax)和指向pt_regs结构的指针(edx寄存器)__do_IRQ()会检查是否必须真正地处理中断,在下列三种情况什么也不干:
a.IRQ_DISABLEDIRQCPU__do_IRQ()b.IRQ_INPROGRESSCPU()c.irc_desc[irq].actionNULL:当没有相应中断服务例程时,一般只有内核正在探测一个硬件设备时才会发生。
假定这三种情况没有一种成立,中断必须被处理,__do_IRQ()设置IRQ_INPROGRESS标识并开始一个循环,调用handle_IRQ_event()执行中断服务例程。
假定CPUIRQCPU应答中断前,这条IRQ线被另外一个CPUIRQ_DISABLEDIRQ线的enable_irq()hw_resend_irq()⑨中断服务例程
Handle_IRQ_event()a.如果SA_INTERRUPT标志清0,就用stib.通过下列代码执行每个中断的中断服务例程
retval = 0; do { retval |= action->handler(irq, action->dev_id, regs); action = action->next; } while (action);Action指向irqaction数据结构链表的开始,而irqaction表示接受中断后要采取的操作。
c.用cil指令禁止本地中断
d.通过返回局部变量retval的值而终止,也就是说,如果没有与中断对应的中断服务例程返回0,否则返回1.
中断服务例程参数(eax,edx,ecx)
irq: IRQ号
dev_id:设备标识符
regs:指向内核(异常)pt_regs⑩IRQ线的动态分配
同一条IRQIRQIRQ(2)处理器间中断(IPI)
处理器间中断允许一个CPUCPUIPIIRQCPUAPIC的总线上。
在多处理器系统中,Linux①CALL_FUNCTION_VECTOR(0xfb)
发往除本身之外的所有CPUCPU②RESCHEDULE_VECTOR(0xfc)
处理程序reschedule_interrupt()③INVALIDATE_TLB_VECTOR(0xfd)
发往除本身之外的所有CPU,(TLB)7.软中断及tasklet
软中断可延迟函数的所有种类,是静态的,在编译时定义。
中断上下文tasklet:是在软中断之上实现的,可以在运行时进行tasklet的分配和初始化,ISR可以是非重入函数,相同类型的tasklet总是被串行的执行,类型不同的tasklet可以在几个CPU上并发执行,但不能在两个CPUtasklet一般而言,在可延迟函数上可以执行四种操作:
①初始化(initialization):定义一个新的可延迟函数,通常在内核自身初始化或加载模块时进行
②激活(activation):标记一个可延迟函数的“挂起”(在可延迟的下一轮调度中执行),激活可以在任何时候进行,及时正在处理中断。
③屏蔽(masking):有选择地屏蔽一个可延迟函数,这样,及时它被激活,内核也不执行。
④执行(execution):执行一个挂起的可延迟函数,执行是在特定的时间进行的。
激活和执行总是捆绑在一起,由给定CPU激活的一个可延迟函数必须在同一个CPU上执行。
(1)软中断
①Linux 2.6
Table 4-9. Softirqs used in Linux 2.6 |
||
Softirq |
Index (priority) |
Description |
HI_SOFTIRQ |
0 |
Handles high priority tasklets |
TIMER_SOFTIRQ |
1 |
Tasklets related to timer interrupts |
NET_TX_SOFTIRQ |
2 |
Transmits packets to network cards |
NET_RX_SOFTIRQ |
3 |
Receives packets from network cards |
SCSI_SOFTIRQ |
4 |
Post-interrupt processing of SCSI commands |
TASKLET_SOFTIRQ |
5 |
Handles regular tasklets |
软中断的下标决定了它的优先级:第下标以为着高优先级,软中断函数从0开始执行。
②软中断使用的数据结构
Softirq_vec数组,该数组包含类型为softirq_action的32个元素。softirq_actionactiondata32preempt_countthread_info字段中,preempt_count字段的编码表示三个不同的计数器和一个标志。
Bits |
Description |
07 |
Preemption counter (max value = 255) |
815 |
Softirq counter (max value = 255). |
1627 |
Hardirq counter (max value = 4096) |
28 |
PREEMPT_ACTIVE flag |
当内核代码明确不允许发生抢占(0)宏in_interrupt()current_thread_info()->preempt_coutCPU的32位掩码,存放在irq_cpustat_t__softirq_pendinglocal_softirq_pending()CPU③处理软中断
Open_softirq()Raise_softirq()内核应该周期性地()(挂起)的软中断,在内核代码几个点上检查。
a.当内核调用local_bh_enable()函数记过本地CPU的软中断时
b.当do_IRQ()完成I/O中断处理或调用irq_exit()c.如果系统使用I/O APIC,则当smp_apic_timer_interrupt()函数处理完本地定时器中断时。
d.在多处理器系统中,当CPU处理完成CALL_FUNCTION_VECTORe.当一个特殊的ksoftirqd/n内核线程被唤醒时。
如果在一个检查点(local_softirq_pending()0)检测到挂起的软中断,内核就调用do_softirq()来处理它们。
④ksoftirqd每个CPUksoftirqd/n(n为CPU的逻辑号)ksoftirqd/nksoftirqd()
点击(此处)折叠或打开
当内核线程被唤醒时,就检查local_softirq_pending()do_softirq()TASK_INTERRUPTIBLEcond_resched()软中断可以重新激活自己,实际上网络软中断和tasklet软中断都可以这么做。
(2)tasklet
Tasklet是I/O驱动程序中实现可延迟函数的首选,其建立在两个叫做HI_SOFTIRQ和TASKLET_SOFTIRQ的软中断之上。
点击(此处)折叠或打开
用tasklet_init()tasklet_structtasklet_schedule()tasklet_hi_schedule()除非tasklettasklettasklet8.工作队列
工作队列的数据机构workqueue_struct。
一个工作队列必须明确的在使用前创建,宏为:
点击(此处)折叠或打开
Tasklet和工作队列的详细分析见http://blog.chinaunix.net/uid-24708340-id-3035286.html
9.从中断和异常返回
终止阶段主要目的是,恢复某个程序的执行,在这样做之前,需要考虑:
a.内核控制路径并发执行的数量:
如果只有一个,那么CPUb.挂起进程的切换请求:
如有任何请求,内核必须执行进程调度,否则,吧控制权还给当前进程。
c.挂起的信号:
如果一个信号发送到当前进程,就必须处理它。
d.单步执行模式:
如果掉是程序正在跟踪当前进程的执行,就必须在进程切换回到用户态之前恢复但不执行。
e.Virtual-8086如果CPUvirtual-8086① 需要使用一些标志来记录挂起进程切换的请求、挂起信号和单步执行,这些标志存放在thread_info描述符的flags字段中。
Table 4-15. The flags field of the thread_info descriptor (continues) |
|
Flag name |
Description |
TIF_SYSCALL_TRACE |
System calls are being traced |
TIF_NOTIFY_RESUME |
Not used in the 80 x 86 platform |
TIF_SIGPENDING |
The process has pending signals |
TIF_NEED_RESCHED |
Scheduling must be performed |
TIF_SINGLESTEP |
Restore single step execution on return to User Mode |
TIF_IRET |
Force return from system call via iret rather than sy***it |
TIF_SYSCALL_AUDIT |
System calls are being audited |
TIF_POLLING_NRFLAG |
The idle process is polling the TIF_NEED_RESCHED flag |
TIF_MEMDIE |
The process is being destroyed to reclaim memory (see the section "The Out of Memory Killer" in Chapter 17) |
ret_from_intr()中断处理时和ret_from_exception()异常处理结束时.
②恢复内核控制路径
③检查内核抢占
如需要进行进程切换,就调用preempt_schedule_irq()schedule()
④恢复用户态程序
⑤检查重调度标志
⑥处理挂起信号,虚拟8086管理员在2009年8月13日编辑了该文章文章。 -->
上一篇:深入理解Linux内核(3)---进程
下一篇:转来的,我每次看都有收获
lmnos2012-10-08 09:46:17
leon_yu2012-10-08 09:21:08
lmnos2012-10-07 17:00:53
是8259吧。不是8159。我的LMOS内核可以在初始化时自动检测是8259还是io-apic
并自动配置好其中一个中断控制器