linux中断--中断嵌套&中断请求丢失
关于中断嵌套:
在linux内核里,如果驱动在申请注册中断的时候没有特别的指定,do_irq在做中断响应的时候,是开启中断的,如果在驱动的中断处理函数正在执行的过程中,出现同一设备的中断或者不同设备的中断,这时候新的中断会被立即处理,还是被pending,等当前中断处理完成后,再做处理。
在2.4和2.6内核里,关于这一块是否有什么不同。
一般申请中断的时候都允许开中断,即不使用SA_INTERRUPT标志。如果允许共享则加上 SA_SHIRQ,如果可以为内核熵池提供熵值(譬如你写的驱动是ide之类的驱动),则再加上 SA_SAMPLE_RANDOM标志。这是普通的中断请求过程。对于这种一般情况,只要发生中断,就可以抢占内核,即使内核正在执行其他中断函数。这里有两点说明:一是因为linux不支持 中断优先级,因此任何中断都可以抢占其他中断,但是同种类型的中断(即定义使用同一个 中断线的中断)不会发生抢占,他们会在执行本类型中断的时候依次被调用执行。二是所谓 “只要发生中断,就可以抢占内核”这句是有一定限制的,因为当中断发生的时候系统由中断门 进入时自动关中断(对于x86平台就是将eflags寄存器的if位置为0),只有当中断函数被执行 (handle_IRQ_event)的过程中开中断之后才能有抢占。 对于同种类型的中断,由于其使用同样的idt表项,通过其状态标志(IRQ_PENDING和 IRQ_INPROGRESS)可以防止同种类型的中断函数执行(注意:是防止handle_IRQ_event被重入, 而不是防止do_IRQ函数被重入),对于不同的中断,则可以自由的嵌套。因此,所谓中断嵌套, 对于不同的中断是可以自由嵌套的,而对于同种类型的中断,是不可以嵌套执行的。
以下简单解释一下如何利用状态标志来防止同种类型中断的重入:
当某种类型的中断第一次发生时,首先其idt表项的状态位上被赋予IRQ_PENDING标志,表示有待处理。 然后将中断处理函数action置为null,然后由于其状态没有IRQ_INPROGRESS标志(第一次),故将其状态置上IRQ_INPROGRESS并去处IRQ_PENDING标志,同时将action赋予相应的中断处理函数指针(这里是一个重点,linux很巧妙的用法,随后说明)。这样,后面就可以顺利执行handle_IRQ_event进行中断处理,当在handle_IRQ_event中开中断后,如果有同种类型的中断发生,则再次进入do_IRQ函数,然后其状态位上加上IRQ_PENDING标志,但是由于前一次中断处理中加上的IRQ_INPROGRESS没有被清除,因此这里无法清除IRQ_PENDING标志,因此action还是为null,这样就无法再次执行handle_IRQ_event函数。从而退出本次中断处理,返回上一次的中断处理函数中,即继续执行handle_IRQ_event函数。当handle_IRQ_event返回时检查IRQ_PENDING标志,发现存在这个标志,说明handle_IRQ_event执行过程中被中断过,存在未处理的同类中断,因此再次循环执行handle_IRQ_event函数。直到不存在IRQ_PENDING标志为止。
2.4和2.6的差别,就我来看,主要是在2.6中一进入do_IRQ,多了一个关闭内核抢占的动作,同时在处理中多了一种对IRQ_PER_CPU类型的中断的处理,其他没有什么太大的改变。这类IRQ_PER_CPU的中断主要用在smp环境下将中断绑定在某一个指定的cpu上。例如arch/ppc/syslib/open_pic.c中的openpic_init中初始化ipi中断的时候。
其实简单的说,中断可以嵌套,但是同种类型的中断是不可以嵌套的,因为在IRQ上发生中断,在中断响应的过程中,这个IRQ是屏蔽的,也就是这个IRQ的中断是不能被发现的。
同时在内核的临界区内,中断是被禁止的
关于do_IRQ可能会丢失中断请求:
do_IRQ函数是通过在执行完handle_IRQ_event函数之后判断status是否被设置了IRQ_PENDING标志来判断是否还有没有被处理的同一通道的中断请求。 但是这种方法只能判断是否有,而不能知道有多少个未处理的统一通道中断请求。也就是说,假如在第一个中断请求执行handle_IRQ_event函数的过程中来了同一通道的两个或更多中断请求,而这些中断不会再来,那么仅仅通过判断status是否设置了IRQ_PENDING标志不知道到底有多少个未处理的中断,handle_IRQ_event只会被再执行一次。
这算不算是个bug呢? 不算,只要知道有中断没有处理就OK了,知道1个和知道N个,本质上都是一样的。作为外设,应当能够处理自己中断未被处理的情况。
不可能丢失的,在每一个中断描述符的结构体内,都有一个链表,链表中存放着服务例程序
关于中断中使用的几个重要概念和关系:
一、基本概念
1.
产生的位置 | 发生的时刻 | 时序 | |
中断 | CPU外部 | 随机 | 异步 |
异常 | CPU正在执行的程序 | 一条指令终止执行后 | 同步 |
2.由中断或异常执行的代码不是一个进程,而是一个内核控制路径,代表中断发生时正在运行的进程的执行
中断处理程序与正在运行的程序无关
引起异常处理程序的进程正是异常处理程序运行时的当前进程
二、特点
1.(1)尽可能快
(2)能以嵌套的方式执行,但是同种类型的中断不可以嵌套
(3)尽可能地限制临界区,因为在临界区中,中断被禁止
2.大部分异常发生在用户态,缺页异常是唯一发生于内核态能触发的异常
缺页异常意味着进程切换,因此中断处理程序从不执行可以导致缺页的操作
3.中断处理程序运行于内核态
中断发生于用户态时,要把进程的用户空间堆栈切换到进程的系统空间堆栈,刚切换时,内核堆栈是空的
中断发生于内核态时, 不需要堆栈空间的切换
三、分类
1.中断的分类:可屏蔽中断、不可屏蔽中断
2.异常的分类:
分类 | 解决异常的方法 | 举例 |
故障 | 那条指令会被重新执行 | 缺页异常处理程序 |
陷阱 | 会从下一条指令开始执行 | 调试程序 |
异常中止 | 强制受影响的进程终止 | 发生了一个严重的错误 |
四、IRQ
1.硬件设备控制器通过IRQ线向CPU发出中断,可以通过禁用某条IRQ线来屏蔽中断。
2.被禁止的中断不会丢失,激活IRQ后,中断还会被发到CPU
3.激活/禁止IRQ线 != 可屏蔽中断的 全局屏蔽/非屏蔽
可以有选择地禁止每条IRQ线。因此,可以对PIC编程从而禁止IRQ,也就是说,可以告诉PIC停止对给定的IRQ线发布中断,或者激活它们。禁止的中断时丢失不了的,它们一旦被激活,PIC就又把它们发送到CPU。这个特点被大多数中断处理程序使用,因为这允许中断处理程序逐次地处理同一类型的IRQ
假定CPU有一条激活的IRQ线。一个硬件设备出现在这条IRQ线程上,且多APIC系统选择我们的CPU处理中断。在CPU应答中断前,这条IRQ线被另一个CPU屏蔽掉;结果,IRQ_DISABLED标志被设置。随后,我们的CPU开始处理挂起的中断;因此,do_IRQ()函数应答这个中断,然后返回,但没有执行中断服务例程,因为它发现IRQ_DISABLED标志被设置了,因此,在IRQ线禁用之前出现的中断丢失了。
为了应付这种局面,内核用来激活IRQ线的enable_irq()函数先检查是否发生了中断丢失,如果是,该函数就强迫硬件让丢失的中断再产生一次
它们最大的不同是上半部分不可中断,而下半部分可中断
五、中断描述符表IDT
1.基本概念
中断描述符表是一个系统表,它与每一个中断或异常向量相联系,每一个向量在表中有相应的中断或异常处理程序入口地址。
在允许发生中断以前,必须适当地初始化IDT
TSS只能位于GDT中,IDT能位于内存的任何的地方
2.中断描述符
硬件提供的中断描述符:
(1)任务门:中断信号发生时,必须取代当前进程的那个进程的TSS选择符存放在任务门中
(2)中断门:包含段选择符和中断处理程序的段内偏移
(3)陷阱门:与中断门的唯一区别是,通过中断门进入服务程序后,自动关中断,而通过陷阱门进入服务程序不自动关中断
Linux中使用的中断描述符:
中断描述符的类型 | 用户态能否访问 | 用户态的访问方式 | 能激活的程序 |
中断门 | 否 | 所有的Linux中断处理程序 | |
系统门 | 是 | into、bound、int $0x80 | 向量号为4,5,128的三个Linux异常处理程序 |
系统中断门 | 是 | int 3 | 与向量3相关的异常处理程序 |
陷阱门 | 否 | 大部分Linux异常处理程序 | |
任务门 | 否 | Linux对Double fault异常的处理程序 |
Linux中的系统门、系统中断门、陷阱门使用的都是硬件中的陷阱门
Linux利用中断门处理中断,利用陷阱门处理异常
Double fault是唯一用任务处理的异常
3.中断向量与中断和异常的关系
(1)每个中断和异常是由0-255之间的一个数来标识的,这个数就是1中的中断向量
(2)大约有20种异常,内核为每一个异常分配了一种中断/异常向量分别是0-19
(3)0x80是系统调用的中断向量
(4)32-255是内核为什么中断分配的中断向量。然而,224个中断向量显然不够,因此系统为每个中断向量设置一个队列,根据每个中断源所使用的中断向量,将其中断服务程序挂到相应的队列中。中断发生时,先执行与中断向量相对应的一段总服务程序,再根据具体的中断源设备号在其所属的队列找到特定的中断服务程序加以执行。
4.中断向量、与中断向量相对应的总服务程序、某个中断源的中断服务程序之间的关系如图所示:
(1)irq_desc是中断向量描述符队列(中断描述符是INT的一项,中断向量描述符是一个数据结构,用于描述与中断向量相关的服务程序)
(2)irq_desc_t是中断向量描述符的数据结构
(3)irqaction是挂在某个中断向量的具体的中断服务程序的描述符,组成一个队列
(4)hw_irq_controller是这个中断向量的总服务程序
六、IDT的初始化
1.两次初始化
运行模式 | 初始值 | 使用者 | |
第一次 | 实模式 | 空处理程序 | BIOS例程 |
第二次 | 保护模式 | 有意义的中断处理程序或异常处理程序 | Linux系统 |
2.在IDT表的初始化完成之初,每个中断处理队列都是空的,此时即使打开中断并且某个外设中断真的发生了,也得不到实际的服务,因为没有执行具体的中断处理程序。
真正的中断服务要到具体设备的初始化程序将其中断处理程序ISR挂入某个中断请求队列后才会发生
3.在允许发生中断以前,必须适当地初始化IDT
七、激活中断或异常(以下内容都是由硬件自动完成)
1.确定与中断或异常相关的中断向量号
中断:硬件设备控制器通过IRQ向CPU发出信号,中断管制器把接受到的信号转换为中断向量号i
异常:对于软件指令发出或产生的异常,CPU会差别归类错误的类别,这个类别号就是中断向量
2.IDT第i项 -----> 段选择符 ----->段描述符 -----> 段基址
3.IDT第i项 -----> 偏移量
4.段基址 + 偏移量 -----> 中断处理程序第一条指令的地址
5.在栈中保存EFLAGS、CS、EIP的内容
6.如果异常产生了一个出错码,把它保存在栈中
7.装载CS、EIP,其值分别是2-段选择符和4-偏移量,由这两个寄存器可得到中断或异常处理程序第一条指令的地址
八、找到中断或异常处理程序的第一条指令后,跳转这到这一指令的过程
1.中断
(1)在当前进程的内核堆栈中保存IRQ的值,为什么与系统调用号区分,保存的是-n
(2)在当前进程的内核堆栈中保存寄存器的值:SAVE_ALL
EFLAGS、CS、EIP、SS、ESP不包括在内,因为它们由控制单元自动保存(见七-7)
(3)把栈顶的地址存放到EAX中
(4)把用户段的选择符装到DS和ES中
(5)调用do_IRQ(),地址保存在CS、EIP中(见七-7)
(6)为正在给IRQ线服务的PIC(中断控制器)一个应答,这将允许PIC进一步发出中断
(7)执行共享这个IRQ的所有设备的ISR(总服务程序称为IRQ,某个设备的具体的服务程序称为ISR)
(8)跳到ret_from_intr()的地址后终止
(6)(7)(8)都是在(5)中被调用的,见十
2.异常
(1)如果异常发生时,控制单元没有自己把一个出错码压出栈中(见七-6),则压入一个空值。
这个“凑数”的出错码不在正常的出错码应该在的位置,以下-步是为了把它调整到它应该在的位置
(3)把异常处理程序的地址压入栈中
(4)把异常处理程度可能用到的寄存器保存到栈中
(5)把栈中位于ESP+36处的硬件出错码拷贝到EDX中,给栈中这一位置存上-1
(6)把保存在栈中ESP+32位置的异常处理程序的地址装入EDI中,给栈中的这一位置写入ES的值
(7)把栈当栈顶拷贝到EAX中
(8)把用户段的选择符装到DS和ES中
(9)调用地址在EDI中的异常处理程序
九、从中断或异常处理程序返回的过程
1.跳转到用于返回的代码的入口点
(1)中断ret_from_intr()
(2)异常:ret_from_exception()
2.把当前线程描述符的地址装载到EBP
3.根据栈中的CS和EFLAGS确定要返回到用户态还是内核态
4.如果有进程调度请求则调度
5.通过执行iret指令结束控制,被中断的程序重新开始执行
十、总的中服务务程序IRQ
1.为正在给IRQ线服务的PIC(中断控制器)一个应答,这将允许PIC进一步发出中断
2.发生以下任何一种情况,则返回
(1)相应的IRQ线被禁止
(2)另一个CPU正常处理这类中断
(3)没有相关的ISR
3.执行共享这个IRQ的所有设备的ISR
4.检查是否有可延迟函数在等待执行,如果有do_softirq()
5.ret_from_intr()
十一、中断服务例程
一个中断服务例程(ISR)实现一种特定设备的操作。当中断处理程序必须执行ISR时,它就调用hand_IRQ_event()函数。
十二、IRQ线的动态分配
在激活一个准备利用IRQ线的设备之前,其相应的驱动程序调用request_irq()。这个函数建立一个新的irqaction函数,并用参数值初始化它。然后调用setup_irq()函数把这个描述符插入到适合的IRQ链表。如果setup_irq()返回一个出错码,设备驱动程序中止操作,这意味着IRQ线已有另一个设备所使用,而这个设备不允许中断共享。当设备操作结束时,驱动程序调用free_irq()函数从IRQ链表中删除这个描述符,并释放相应的内存区。
request_irq()
free_irq()
1.Linux把紧随中断要执行的操作分为三类
特点 | 处理方法 | 举例 | |
第一类 | 紧急的 | 在禁止可屏蔽中断的情况下立即执行 | 修改设备和处理器同时访问的数据结构 |
第二类 | 非紧急的 | 在开中断的情况下立即执行 | 修改那些只有处理器才会访问的数据结构(例如,按下一个键后读扫描码) |
第三类 | 非紧急可延迟的 | 由独立的函数来执行 | 把缓冲区的内核拷贝到某个进程的地址空间 |
2.把可延迟中断从中断处理程序中抽出来,由独立的函数来执行,有助于使内核保持较短的响应时间
3.Linux2.6使用可延迟函数和工作队列来处理可延迟中断,这两个都是内核函数。
二、可延迟函数
1.可延迟函数包括软中断和tasklet,tasklet是在软中断之上实现的
tasklet是I/O驱动程序中实现可延迟函数的首选方法。
tasklet建立在HI_SOFTIRQ和TASKLET_SOFTIRQ这两个软中断之上
几个tasklet可以与同一软中断相关联,每个tasklet执行自己的函数
分配方式 | 并发性 | 可重入性 | |
软中断 | 编译时静态分配 | 可以并发地在多个CPU上运行 | 必须是可重入的,并明确地使用自旋锁保护其数据结构 |
tasklet | 运行时动态分配 | 相同类型的tasklet总是被串行地执行。不同类型的tasklet可以在几个CPU上并发地执行 | 不必是可重入的 |
2.由给定CPU激活的一个可延迟函数必须在同一个CPU上执行
可延迟函数执行时不允许内核抢占,因为从一个CPU移到另一个CPU的过程需要将进程挂起
3.中断与软中断所使用的数据结构比较
中断 | 软中断 | |
中断向量号描述符 | irq_desc[中断向量号] | softirq_vec[软中断号] |
中断请求寄存器 | 中断请求寄存器 | __softirq_active |
中断屏蔽寄存器 | 中断屏蔽寄存器 | __soft_mask |
中断处理程序 | do_handler_name | bh[]:32个 |
中断处理程序描述项 | irqaction | softirq_action |
中断机制的初始化 |
trap_init():异常初始化 init_IRQ():中断初始化 |
softirq_action |
中断请求队列初始化 | init_ISA_irqs() | open_softirq |
中断处理程序与中断请求队列相关联 | request_irq() | init_bh() |
执行中断 | do_IRQ() | do_softirq |
4.激活可延迟函数
(1)激活软中断
A.把软中断置为把挂起状态,并周期性地检查处于挂起状态的软中断。如果有,就调用do_softirq()
B.一般是在以下几个点来检查挂起的软中断的a.调用local_bh_enable()激活本地软中断时b.do_IRQ()完成I/O中断的处理即将退出时c.用于周期性检查挂起状态软中断的内核线程ksoftirqd被唤醒时d.else,我觉得不太重要
(2)激活tasklet
A.把自己定义的描述符加入到tasklet_vec指向的链表中即可。调用tasklet_action()时,依次处理队列中的每个tasklet描述符,然后清空tasklet_vec指向的链表。
B.tasklet的每次激活至多触发tasklet函数的一次执行,除非tasklet函数重新激活自己
三、工作队列
1.它们允许内核函数被激活,并稍后由一种叫做工作者线程的特殊内核线程来执行
如果系统有n个CPU,就会创建n个工作者,但是只会创建一个工作队列
2.工作队列与可延迟函数的区别:
可延迟函数运行中断上下文中,而工作队列运行在进程中下文中。
可延迟函数不能阻塞,工作队列可执行可阻塞函数
可延迟函数被执行时不可能有任何正在运行的进程,工作队列中的函数由内核线程来执行,不访问用户态地址空间
3.工作队列的激活
把要执行的函数的描述符插入到工作队列中。
工作者等待函数被插入队列,被唤醒后把函数描述符取下并执行。
由于工作队列函数可以阻塞,工作者线程可以睡觉,或移动另一个CPU
总而言之:
当发生一个中断时,