中断(interrupt)通常被定义为一个事件,该事件改变处理器执行的指令顺序。这样的事件与CPU芯片内外部硬件电路产生的电信号相对应。
中断通常分为同步(synchronous)中断和异步(asynchronous)中断:
1、同步中断是当指令执行时由CPU控制单元产生的,之所以称为同步,是因为只有在一条指令终止执行后CPU才会发出中断。
2、异步中断是由其他硬件设备依照CPU时钟信号随机产生的。
在Intel微处理器手册中,把同步和异步中断分别称为异常(exception)和中断(interrupt)。
中断是由间隔定时器和I/O设备产生的。异常是由程序的错误产生的,或者是由内核必须处理的异常条件产生的。第一种情况下,内核通过发送一个每个Unix程序员都熟悉的信号来处理异常。第二种情况下,内核执行恢复异常所需要的所有步骤,例如缺页,或对内核服务的一个请求(通过一条init或sysenter指令)。
一、中断信号的作用
中断信号提供了一种特殊的方式,使处理器转而去运行正常控制流之外的代码。当一个中断信号到时,CPU必须停止它当前正在做的事情,并且切换到一个新的活动。为了做到这一点,就要在内核态堆栈保存程序计数器的当前值(即eip和cs寄存器的内容),并把与中断类型相关的一个地址放进程序计数器。
中断处理与进程切换有一个明显的差异:由中断或异常处理程序执行的代码不是一个进程。更确切地说,它是一个内核控制路径,代表中断发生时正在运行的进程执行。作为一个内核控制路径,中断处理程序比一个进程要“轻”(light)(中断上下文很少,建立或终止中断处理需要的时间很少)。
中断处理是由内核执行的最敏感的任务之一,因此它必须满足下列约束:
1、当内核正打算去完成一些别的事情时,中断随时会到来。因此,内核的目标就是让中断尽可能快地处理完,尽其所能把更多的处理向后推迟。因此,内核响应中断后需要进行的操作分为两部分:关键而紧急的部分,内核立即执行;其余推迟的部分,内核随后执行。
2、因为中断随时会到来,所以内核可能正在处理其中的一个中断时,另一个中断(不同类型)又发生了。应该尽可能多地允许这种情况发生,因为这能维持更多的I/O设备处于忙状态。因此,中断处理程序必须编写成使相应的内核控制路径能以嵌套的方式执行。当最后一个内核控制路径终止时,内核必须能恢复被中断进程的执行,或者,如果中断信号已导致了重新调度,内核能切换到另外的进程。
3、尽管内核在处理前一个中断时可以接受一个新的中断,但在内核代码中还是存在一些临界区,在临界区中,中断必须被禁止。必须尽可能地限制这样的临界区,因为根据以前的要求,内核,尤其是中断处理程序,应该在大部分时间内以开中断的方式运行。
二、中断和异常
Intel文档把中断和异常分为以下几类:
中断:
1、可屏蔽中断(maskable interrupt):I/O设备发出的所有中断请求(IRQ)都能产生可屏蔽中断。可屏蔽中断可以处于两种状态:屏蔽的(masked)或非屏蔽的(unmasked);一个屏蔽的中断只要还是屏蔽的,控制单元就忽略它。
2、非屏蔽中断(nonmaskable interrupt):只有几个危急事件(如硬件故障)才引起非屏蔽故障。非屏蔽中断总是由CPU辨认。
异常
处理器探测异常(processor-detected exception):当CPU执行指令时探测到一个反常条件产生的异常。可以进一步分为三组,这取决于CPU控制单元产生异常时保存在内核态堆栈eip寄存器中的值。
故障(fault):
陷阱(trap):
异常终止(abort):
编程异常(programmed exception):
每个中断和异常是由0~255之间的一个数来标识。因为一些未知的原因,Intel把这个8位的无符号整数叫做一个向量(vector)。非屏蔽中断的向量和异常的向量是固定的,而可屏蔽中断的向量可以通过对中断控制器的编程来改变。
2.1、IRQ和中断
每个能够发出中断请求的硬件设备控制器都有一条名为IRQ(interrupt ReQuest)的输出线。所有现有的IRQ线(IRQ line)都与一个名为可编程中断控制器(Programmable Interrupt Controuer, PIC)的硬件电路的输入引脚相连,可编程中断控制器执行下列动作:
1、监视IRQ线,检查产生的信号(raised signal)。如果有条或者两条以上的IRQ线上产生信号,就选择引脚编号较小的IRQ线。
2、如果一个引发信号出现在IRQ线上:
a、把接收到的引发信号转换成对应的向量。
b、把这个向量存放在中断控制器的一个I/O端口,从而允许CPU通过数据总线读此向量。
c、把引发信号发送到处理器的INTR引脚,即产生一个中断。
d、等待,直到CPU通过把这个中断信号写进可编程中断控制器的一个I/O端口来确认它,当这种情况发生时,清INTR线。
3、返回到第1步。
2.1.1、高级可编程中断控制器
2.1.2、异常
2.2、中断描述符表
中断描述符表(Interrupt Descriptor Table,IDT)是一个系统表,它与每一个中断或异常向量相联系,每一个向量在页表中有相应的中断或异常处理程序的入口地址。内核在允许中断发生前,必须适当地初始化IDT。
描述符包括:
任务门(task gate):当中断信号发生时,必须取代当前进程的那个进程的TSS选择符存放在任务门中。
中断门(interrupt gate):包含段选择符和中断或异常处理程序的段内偏移量。当控制权转移到一个适当的段时,处理器清IF标志,从而关闭将来会发生的可屏蔽中断。
陷阱门(trap gate):与中断门相似,只是控制权传递到一个适当的段处理器不修改IF标志。
Linux利用中断门处理中断,利用陷阱门处理异常。
2.3、中断和异常的硬件处理
现在描述CPU控制单元如何处理中断和异常。假定内核已被初始化,因此CPU在保护模式下运行。
当执行了一条指令后,cs和eip这对寄存器包含下一条将要执行的指令的逻辑地址。在处理那条指令之前,控制单元会检查在运行前一条指令时是否已经发生了一个中断或异常。如果发生了一个中断或异常,那么控制单元执行下列操作:
1、确定与中断或异常关联的向量i(0<= i <=255)。
2、读由idtr寄存器指向的IDT表中的第i项。
3、从gdtr寄存器获得GDT的基地址,并在GDT中查找,以读取IDT表项中的选择符所标识的段选择符。这个描述符指定中断或异常处理程序所在段的基地址。
4、确信中断是由授权的(中断)发生源发出的。
5、检查是否发生了特权级的变化,也就是说,CPL是否不同于所选择的段描述符的DPL。如果是,控制单元必须开始使用与新的特权级相关的栈。通过执行一下步骤来做到这一点:
1) 读tr寄存器,以访问运行进程的TSS段。
2) 用与新特权级相关的栈段和栈指针的正确值装载ss和esp寄存器。这些值可以在TSS中找到。
3) 在新的栈中保存ss和esp以前的值,这些值定义了与旧特权级相关的栈的逻辑地址。
6、如果故障已发生,用引起异常的指令地址装载cs和eip寄存器,从而使得这条指令能再次被执行。
7、在栈中保存eflags、cs及eip的内容。
8、如果异常产生了一个硬件出错码,则将它保存在栈中。
9、装载cs和eip寄存器,其值分别时IDT表中的第i项门描述符的段选择符和偏移量字段。这些值给出了中断或者异常处理程序的第一条指令的逻辑地址。
控制单元所执行的最后一步就是跳转到中断或者异常处理程序。换句话说,处理完中断信号后,控制单元所执行的指令就是被选中处理程序的第一条指令。
中断或异常被处理完后,相应的处理程序必须产生一条iret指令,把控制权转交给被中断的进程,这将迫使控制单元:
1、用保存在栈中的值装载cs、eip或eflags寄存器。如果一个硬件出错码曾被压入栈中,并且在eip内容的下面,那么执行iret指令前必须先弹出这个硬件错误码。
2、检查处理程序的CPL是否等于cs中最低两位的值。如果是,iret终止执行;否则,转入下一步。
3、从栈中装载ss和esp寄存器。因此,返回到与旧特权级相关的栈。
4、检查ds、es、fs及gs段寄存器的内容,如果其中一个寄存器包含的选择符是一个段描述符,并且其DPL值小于CPL,那么,清相应的段寄存器。控制单元这么做是为了禁止用户态的程序(CPL=3)利用内核以前所用的段寄存器(DPL=0)。如果不清这些寄存器,怀有恶意的用户态程序就可能利用它们来访问内核地址空间。
三、中断和异常处理程序的嵌套执行
每个中断或异常都会引起一个内核控制路径,或者说代表当前进程在内核态执行单独的指令序列。例如:当I/O设备发出一个中断时,相应的内核控制路径的第一部分指令就是那些把寄存器的内容保存在内核堆栈的指令,而最后一部分指令就是恢复寄存器内容并让CPU返回到用户态的那些指令。
内核控制路径可以任意嵌套;一个中断处理程序可以被另一个中断处理程序“中断”,因此引起内核控制路径的嵌套执行。其结果是,对中断进行处理的内核控制路径,其最后一部分指令并不总能使当前进程返回到用户态:如果嵌套深度大于1,这些指令将执行上次被打断的内核控制路径,此时的CPU仍然运行在内核态。
中断处理程序运行期间不能发生进程切换。事实上,嵌套的内核控制路径恢复执行时需要的所有数据都存放在内核态堆栈中,这个栈毫无疑义的属于当前进程。
一个中断处理程序既可以抢占其他的中断处理程序,也可以抢占异常处理程序,相反,异常处理程序从不抢占中断处理程序。在内核态能触发的唯一异常就是刚刚描述的缺页异常。但是中断处理程序从不执行可以导致缺页(因此意味着进程切换)的操作。
基于以下两个主要原因,Linux交错执行内核控制路径:
1、为了提高可编程中断控制器和设备控制器的吞吐量。
2、为了实现一种没有优先级的中断模型。
在多处理器系统上,几个内核控制路径可以并发执行。此外,与异常相关的内核控制路径可以开始在一个CPU上执行,并且由于进程切换而移往另一个CPU上执行。
四、初始化中断描述符表
4.1、中断门、陷阱门及系统门
Intel提供了三种类型的中断描述符:任务门、中断门及陷阱门描述符。Linux使用与Intel稍有不同的细目分类和术语,把它们如下进行分类:
中断门(interrupt gate):用户态的进程不能访问的一个Intel中断门(门的DPL字段为0)。所有的Linux中断处理程序都通过中断门激活,并全部限制在内核态。
系统门(system gate):用户态的进程可以访问的一个Intel陷阱门(门的DPL字段为3)。通过系统门来激活三个Linux异常处理程序,它们的向量是4,5及128,因此,在用户态下,可以发布into、bound及int $0x80三条汇编语言指令。
系统中断门(system interrupt gate):能够被用户态进程访问的Intel中断门(门的DPL字段为3)。与向量3相关的异常处理程序是由系统中断门激活的,因此,在用户态可以使用汇编语言指令int3。
陷阱门(trap gate):用户态的进程不能访问的一个Intel陷阱门(门的DPL字段为0)。大部分Linux异常处理程序都通过陷阱门来激活。
任务门(task gate):不能被用户态进程访问的Intel任务门(门的DPL字段为0)。Linux对“Double fault”异常的处理程序是由任务门激活的。
4.2、IDT的初步初始化
当计算机还运行在实模式时,IDT被初始化并由BIOS例程使用。然而,一旦Linux接管,IDT就被移到移到RAM的另一个区域,并进行第二次初始化,因为Linux没有利用任何BIOS例程。
IDT存放在idt_table表中,有256个表项。6字节的idt_descr变量指定了IDT的大小和它的地址,只有当内核用lidt汇编指令初始化idtr寄存器时才用到这个变量。
用汇编语言写成的ignore_int()中断处理程序,可以看作一个空的处理程序,它执行下列动作:
1、在栈中保存一些寄存器的内容。
2、调用printk函数打印“Unknown interrupt”系统消息。
3、从栈恢复寄存器的内容。
4、执行iret指令以恢复被中断的程序。
ignore_int()处理程序应该从不被执行,在控制台或日志文件中出现的“Unknown interrupt”消息日志要么是出现了一个硬件的问题(一个I/O设备正在产生没有预料到的中断),要么就是出现了一个内核的问题(一个中断或异常未被适当地处理)。
紧接着这个预初始化,内核将在IDT中进行第二遍初始化,用有意义的陷阱和中断处理程序替换这个空处理程序。一旦这个过程完成,对控制单元产生的每个不同的异常,IDT都有一个专门的陷阱或系统门,而对于可编程中断控制器确认的每一个IRQ,IDT都将包含一个专门的中断门。
五、异常处理
CPU产生的大部分异常都由Linux解释为出错条件。当其中一个异常发生时,内核就向引起异常的进程发送一个信号向它通知一个反常条件。
异常处理程序有一个标准的结构,由以下三部分组成:
1、在内核堆栈中保存大多数寄存器的内容(这部分用汇编语言实现)。
2、用高级的C函数处理异常。
3、通过ret_from_exception()函数从异常处理程序退出。
5.1、为异常处理程序保存寄存器的值
5.2、进入和离开异常处理程序
执行异常处理程序的C函数名总是由do_前缀和处理程序名组成。其中的大部分函数把硬件出错码和异常向量保存在当前进程的描述符中,然后,向当前进程发送一个适当的信号。
六、中断处理
中断处理依赖于中断类型,我们将讨论三种主要的中断类型:
1、I/O中断:某些I/O设备需要关注;相应的中断处理程序必须查询设备以确定适当的操作过程。
2、时钟中断:某种时钟(或者是一个本地APIC时钟,或者是一个外部时钟)产生一个中断;这种中断告诉内核一个固定的时间间隔已经过去。这些中断大部分是作为I/O中断来处理的。
3、处理器间中断:多处理器系统中一个CPU对另一个CPU发出一个中断。
6.1、I/O中断处理
I/O中断处理程序必须足够灵活以给多个设备同时提供服务。
中断处理程序的灵活性是以两种不同方式实现的:
1、IRQ共享:中断处理程序执行多个中断服务例程(interrupt service routine, ISR)。每个ISR是一个与单独设备(共享IRQ线)相关的函数。因为不可能预先知道哪个特定的设备产生IRQ,因此每个ISR都被执行,以验证它的设备是否需要关注;如果是,当设备产生中断时,就执行需要执行的所有操作。
2、IRQ动态分配:一条IRQ线在可能的最后时刻才与一个设备驱动程序相关联;即使几个硬件设备并不共享IRQ线,同一个IRQ向量也可以由这几个设备在不同时刻使用。
当一个中断发生时,并不是所有的操作都具有相同的紧迫性。事实上,把所有的操作都放进中断处理程序本身并不合适。需要时间长的、非重要的操作应该推后,因为当一个中断处理程序正在运行时,相应的IRQ线上发出的信号就被暂时忽略。更重要的是,中断处理程序是代表进程执行的,它所代表的进程必须总处于TASK_RUNNING状态,否则,就可能出现系统僵死情形。因此,中断处理程序不能执行任何阻塞过程。因此,Linux把紧随中断要执行的操作分为三类:
1、紧急的(Critical):紧急操作要在一个中断处理程序内立即执行,而且是在禁止可屏蔽中断的情况下。
2、非紧急的(Noncritical):这样的操作如:修改那些只有处理器才会访问的数据结构。这些操作也要很快地完成,因此,它们由中断处理程序立即执行,但必须是在开中断的情况下。
3、非紧急可延迟的(Noncritical deferrable):这样的操作诸如:把缓冲区的内容拷贝到某个进程的地址空间。这些操作可能被延迟较长的时间间隔而不影响内核操作,有兴趣的进程将会等待数据。非紧急可延迟的操作由独立的函数来执行,比如“软中断及tasklet”。
所有的I/O中断处理程序都执行四个相同的基本操作:
1、在内核态堆栈中保存IRQ的值和寄存器的内容。
2、为正在给IRQ线服务的PIC发送一个应答,这将允许PIC进一步发出中断。
3、执行共享这个IRQ的所有设备的中断服务例程(ISR)。
4、跳到ret_frome_intr()的地址后终止。
6.1.1、中断向量
内核必须在启用中断前发现IRQ号与I/O设备之间的对应。IRQ号与I/O设备之间的对应是在初始化每个设备驱动程序时建立的。
6.1.2、IRQ数据结构
每个中断向量都有它自己的irq_desc_t描述符,所有这些描述符组织在一起形成irq_desc数组。
6.1.3、IRQ在多处理器系统上大的分发
Linux遵循对称多处理模型(SMP),这就意味着,内核从本质上对任何一个CPU都不应该有偏爱。因而,内核试图以轮转的方式把来自硬件设备的IRQ信号在所有CPU之间分发。因此,所有CPU服务于I/O中断的执行时间片几乎相同。
因为所有的任务优先级寄存器都包含相同的值,因此,所有CPU总是具有相同的优先级。为了突破这种约束,正如前面所解释的那样,多APIC系统使用本地APIC仲裁优先级寄存器中的值。因为这样的值在每次中断后都自动改变,因此,IRQ信号就公平地在所有CPU之间分发。
当硬件设备产生一个中断信号时,多APIC系统就选择其中的一个CPU,并把该信号传递给相应的本地APIC,本地APIC又依次中断它的CPU。这个事件不通报给其他所有的CPU。
kirqd内核线程周期性地执行do_irq_balance()函数,该函数跟踪在最近时间间隔内每个CPU接收的中断次数。如果该函数发现负荷最重的CPU和负荷最轻的CPU之间IRQ负载不平衡的问题太严重,它要么把IRQ从一个CPU转移到另一个CPU,要么让所有的IRQ在所有CPU之间“轮转”。
6.1.4、多种类型的内核栈
每个进程的thread_info描述符与thread_union结构中的内核栈紧邻,而根据内核编译时的选项不同,thread_union结构可能占一个页框或两个页框。如果thread_union结构的大小为8KB,那么当前进程的内核栈被用于所有类型的内核控制路径:异常、中断和可延迟的函数。相反,如果thread_union结构的大小为4KB,内核就使用三种类型的内核栈:
1、异常栈,用于处理异常(包括系统调用)。这个栈包含在每个进程的thread_union数据结构中,因此对系统中的每个进程,内核使用不同的异常栈。
2、硬中断请求栈,用于处理中断。系统中的每个CPU都有一个硬中断请求栈,而且每个栈占用一个单独的页框。
3、软中断请求栈,用于处理可延迟的函数(软中断或tasklet)。系统中的每个CPU都有一个软中断请求栈,而且每个栈占用一个单独的页框。
6.1.5、为中断处理程序保存寄存器的值
内核用负数表述所有的中断,用正数表示系统调用。
保存寄存器的值以后,栈顶的地址被存放到eax寄存器中,然后中断处理程序调用do_IRQ()函数。执行do_IRQ()的ret指令时(即函数结束时),控制转到ret_from_intr()。
6.1.6、do_IRQ()函数
6.1.7、__do_IRQ()函数
6.1.8、挽救丢失的中断
内核用来激活IRQ线的enable_irq()函数先检查是否发生了中断丢失,如果是,该函数就强迫硬件让丢失的中断再产生一次。
6.1.9、中断服务例程
每个中断服务例程在成功处理完中断后都返回1,也就是说,当中断服务例程所处理的硬件设备(而不是共享相同IRQ的其他设备)发出信号时,否则返回0。
6.1.10、IRQ线的动态分配
在激活一个准备利用IRQ线的设备之前,其相应的驱动程序调用request_irq()。这个函数建立一个新的irqaction描述符,并用参数值初始化它。然后调用setup_irq()函数把这个描述符插入到合适的IRQ链表。如果setup_irq()返回一个错误码,设备驱动程序中止操作,这意味着IRQ线已由另一个设备所使用,而这个设备不允许中断共享。当设备操作结束时,驱动程序调用free_irq()函数从IRQ链表中删除这个描述符,并释放相应的内存区。
6.2、处理器间中断处理
处理器间中断允许一个CPU向系统中的其他CPU发送中断信号。处理器间中断(IPI)不是通过IRQ线传输的,而是作为信号直接放在连接所有CPU本地APIC的总线上。
在多处理器系统上,Linux 定义了下列三种处理器间中断:
CALL_FUNCTION_VECTOR(向量0xfb):
发往所有的CPU(不包含发送者),强制这些CPU运行发送者传递过来的函数。相应的中断处理程序叫做call_function_interrupt()。
RESCHEDULE_VECTOR(向量0xfc):
当一个CPU接收这种类型的中断时,相应的处理程序(叫做reschedule_interrupt())限定自己来应答中断。当从中断返回时,所有的重新调度都自动进行。
INVALIDATE_TLB_VECTOR(向量0xfd):
发往所有的CPU(不包含发送者),强制它们的转换后援缓冲器(TLB)变为无效。相应的处理程序(叫做invalidate_interrupt())刷新处理器的某些TLB表项。
由于下列的一组函数,使得产生处理器间中断(IPI)变为一件容易的事:
send_IPI_all():发送一个IPI到所有的CPU(包括发送者)。
send_IPI_allbutself():发送一个IPI到所有的CPU(不包括发送者)。
send_IPI_self():发送一个IPI到发送者的CPU。
send_IPI_mask():发送一个IPI到位掩码指定的一组CPU。
七、软中断及tasklet
一个中断处理程序的几个中断服务例程之间是串行执行的,并且通常在一个中断的处理程序结束前,不应该再次出现这个中断。相反,可延迟中断可以在开中断的情况下执行。把可延迟中断从中断处理程序中抽出来有助于使内核保持较短的响应时间。这对于那些期望它们的中断能在几毫秒内得到处理的“紧迫”应用来说是非常重要的。
Linux2.6迎接这种挑战是通过两种非紧迫、可中断内核函数:所谓的可延迟函数(包括软中断与tasklets)和通过工作队列来执行的函数。
软中断和tasklet有密切的关系,tasklet是在软中断之上实现。事实上,出现在内核代码中断哦术语“软中断(softirq)”常常表示可延迟函数的所有种类。另外一种被广泛使用的术语是“中断上下文”:表示内核当前正在执行一个中断处理程序或一个可延迟的函数。
软中断的分配是静态的(即在编译时定义),而tasklet的分配和初始化可以在运行时进行。软中断(即便是同一种类型的软中断)可以并发地运行在多个CPU上。因此软中断是可重入函数而且必须明确地使用自旋锁保护其数据结构。tasklet不必担心这些问题,因为内核对tasklet的执行进行了更加严格的控制,相同类型的tasklet总是被串行地执行,换句话说就是:不能在两个CPU上同时运行相同类型的tasklet。但是类型不同的tasklet可以在几个CPU上并发执行。tasklet的串行化使tasklet函数不必是可重入的。
在可延迟函数上执行四种操作:
1、初始化(initialization):定义一个新的可延迟函数,这个操作通常在内核自身初始化或加载模块时运行。
2、激活(activation):标记一个可延迟函数为“挂起”。激活可以在任何时候进行(即使正在处理中断)。
3、屏蔽(masking):有选择地屏蔽一个可延迟函数,这样,即使它被激活,内核也不执行它。
4、执行(execution):执行一个挂起的可延迟函数和同类型的其他所有挂起的可延迟函数,执行是在特定的时间进行的。
由给定CPU激活的一个可延迟函数必须在同一个CPU上执行。
7.1、软中断
一个软中断的下标决定了它的优先级:低下标意味着高优先级,因为软中断函数将从下标0开始执行。
7.1.1、软中断所使用的数据结构
softirq_action数据结构包括两个字段:指向软中断函数的一个action指针和指向软中断函数需要的通用数据结构的data指针。
另外一个关键的字段是32位的preempt_count字段,用它来跟踪内核抢占和内核控制的嵌套,该字段存放在每个进程描述符的thread_info字段中。如下表,preempt_count字段的编码表示三个不同的计数器和一个标志。
preempt_count的字段:
位 描述
0~7 抢占计数器(max value = 255)
8~15 软中断计数器(max value = 255)
16~27 硬中断计数器(max value = 255)
28 PREEMPT_ACTIVE标志
第一个计数器记录显示禁用本地CPU内核抢占的次数,值等于0表示允许内核抢占。第二个计数器表示可延迟函数被禁用的程度(值为0表示可延迟函数处于激活状态)。第三个计数器表示在本地CPU上中断处理程序的嵌套数(irq_enter()宏递增它的值,irq_exit()宏递减它的值)。
给preempt_count字段起这个名字的理由很充分的:当内核代码明确不允许发生内核抢占(抢占计数器不等于0)或当内核正在中断上下文中运行时,必须禁用内核的抢占功能。因此,为了确定是否能够抢占当前进程,内核快速检查preempt_count字段中的相应值是否等于0.
7.1.2、处理软中断
open_softirq()函数处理软中断的初始化。
raise_softirq()函数用来激活软中断。
7.1.3、do_softirq()函数
如果在这样的一个检查点(local_softirq_pending()不为0)检测到挂起的软中断,内核就调用do_softirq()来处理它们。这个函数执行下面的操作:
1、如果in_interrupt()产生值1,则函数返回。这种情况说明要么在中断上下文中调用了do_softirq()函数,要么当前禁用软中断。
2、执行local_irq_save以保存IF标志的状态值,并禁用本地CPU上的中断。
3、如果thread_union的结构大小为4KB,那么在需要的情况下,它切换到软中断请求栈。
4、调用__do_softirq()函数。
5、如果在上面第3步成功切换到软中断请求栈,则把最初的栈指针恢复到esp寄存器中,这样就切换回到以前使用的异常栈。
6、执行local_irq_restore以恢复在第2步保存的IF标志(表示本地是关中断还是开中断)的状态值并返回。
7.1.4、__do_softirq()函数
下面简单描述__do_softirq()函数执行的操作:
1、把循环计数器的值初始化为10。
2、把本地CPU(被local_softirq_pending选中的)软中断的位掩码复制到局部变量pending中。
3、调用local_bh_disable()增加软中断计数器的值。在可延迟函数开始执行之前应该禁用它们,这似乎有点违反直觉,但确实极有意义。因为在绝大多数情况下可延迟函数是在开中断的状态下运行的,所以在执行__do_softirq()的过程中可能会产生新的中断。当do_IRQ()执行irq_exit()宏时,可能有另外一个__do_softirq()函数的实例开始执行。这种情况是应该避免的,因为可延迟函数必须以串行的方式在CPU上运行。因此,__do_softirq()函数的第一个实例禁止可延迟函数,以使每个新的函数实例将会在do_softirq()函数的第1步就退出。
4、清除本地CPU的软中断位图,以便可以激活新的软中断(在第2步,已经把位图保存在pending局部变量中)。
5、执行local_irq_enable()来激活本地中断。
6、根据局部变量pending每一位的设置,执行对应的软中断处理函数。回忆一下,下标为n的软中断函数的地址存放在softirq_vec[n]->action变量中。
7、执行local_irq_disable()以禁用本地中断。
8、把本地CPU的软中断位掩码复制到局部变量pending中,并且再次递减循环计数器。
9、如果pending不为0,那么从最后一次循环开始,至少有一个软中断被激活,而且循环计数器仍然是正数,跳转回到第4步。
10、如果还有更多的挂起软中断,则调用wakeup_softirqd()唤醒内核线程来处理本地CPU的软中断。
11、软中断计数器减1,因而重新激活可延迟函数。
7.1.5、ksoftirqd内核线程
7.2、tasklet
tasklet是I/O驱动程序中实现可延迟函数的首选方法。tasklet建立在两个叫做HI_SOFTIRQ和TASKLET_SOFTIRQ的软中断上。几个tasklet可以与同一个软中断相关联,每个tasklet执行自己的函数。两个软中断之间没有真正的区别,只不过do_softirq()先执行HI_SOFTIRQ的tasklet,后执行TASKLET_SOFTIRQ的tasklet。
tasklet描述符是一个tasklet_struct类型的数据结构,其字段表示如下:
字段名 描述
next 指向链表中下一个描述符的指针
state tasklet的状态
count 锁计数器
func 指向tasklet函数的指针
data 一个无符号长整数,可以由tasklet函数来使用
tasklet描述符的state字段含有两个标志:
TASKLET_STATE_SCHED:该标志被设置时,表示tasklet是挂起的(曾被调度执行);也意味着tasklet描述符被插入到tasklet_vec和tasklet_hi_vec数组的其中一个链表中。
TASKLET_STATE_RUN:该标志被设置时,表示tasklet正在被执行;在单处理器系统上不使用这个标志,因为没有必要检查特定的tasklet是否在运行。
为了激活tasklet,你应该根据自己tasklet需要的优先级,调用tasklet_schdedule()函数或tasklet_hi_schedule()函数。这两个函数非常类似,其中每个都执行下列操作:
1、检查TASKLET_STATE_SCHED标志;如果设置则返回(tasklet已经被调度)。
2、调用local_irq_save保存IF标志的状态并禁用本地中断。
3、在tasklet_vec[n]或tasklet_hi_vec[n]指向的链表的起始处增加tasklet描述符(n表示本地CPU的逻辑号)。
4、调用raise_softirq_irqoff()激活TASKLET_SOFTIRQ或HI_SOFTIRQ类型的软中断。(这个函数与raise_softirq()函数类似,只是raise_softirq_irqoff()函数假设已经禁用了本地中断。)
5、调用Local_irq_restore恢复IF标志的状态。
软中断函数一旦被激活,就由do_softirq()函数执行。与HI_SOFTIRQ软中断相关的软中断函数叫做tasklet_hiaction(),而与TASKLET_SOFTIRQ相关的函数叫做tasklet_action()。这两个函数非常相似,它们都执行下列操作:
1、禁用本地中断。
2、获得本地CPU的逻辑号n。
3、把tasklet_vec[n]或tasklet_hi_vec[n]指向的链表的地址存入局部变量list。
4、把tasklet_vec[n]或tasklet_hi_vec[n]的值赋为NULL,因此,已调度的tasklet描述符的链表被清空。
5、打开本地中断。
6、对于list指向的链表中的每个tasklet描述符:
a、在多处理器系统上,检查tasklet的TASKLET_STATE_RUN标志。
* 如果该标志被设置,说明同类型的一个tasklet正在另一个CPU上运行,因此,就把人物描述符重新插入到由tasklet_vec[n]或tasklet_hi_vec[n]指向的链表中,并再次激活TASKLET_SOFTIRQ或HI_SOFTIRQ软中断。这样,当同类型的其他tasklet在其他CPU上运行时,这个tasklet就被延迟。
* 如果TASKLET_STATE_RUN标志未被设置,tasklet就没有在其他CPU上运行,就需要设置这个标志,以便tasklet函数不能在其他CPU上执行。
b、通过查看tasklet描述符的count字段,检查tasklet是否被禁止。如果是,就清TASKLET_STATE_RUN标志,并把任务描述符重新插入到由tasklet_vec[n]或tasklet_hi_vec[n]指向的链表中,然后函数再次激活TASKLET_SOFTIRQ或HI_SOFTIRQ软中断。
c、如果tasklet被激活,清TASKLET_STATE_SCHED标志,并执行tasklet函数。
注意:除非tasklet函数重新激活自己,否则,tasklet的每次激活至多触发tasklet函数的一次执行。
八、工作队列
可延迟函数和工作队列主要区别在于:可延迟函数运行在中断上下文中,而工作队列中的函数运行在进程上下文中。执行可阻塞函数的唯一方式是在进程上下文中运行。因为在中断上下文中不可能发生进程切换。可延迟函数被执行时不可能有任何正在运行的进程。另一方面,工作队列中的函数是由内核线程来执行的,因此,根本不存在它要访问的用户态地址空间。
8.1、工作队列的数据结构
8.2、工作队列函数
create_workqueue("foo")函数接收一个字符串作为参数,返回新创建工作队列的workqueue_struct描述符的地址。该函数还创建n个工作者线程(n是当前系统中有效运行的CPU的个数),并根据传递给函数的字符串为工作者线程命名,如:foo/0,foo/1等等。create_singlethread_workqueue()函数与之相似,但不管系统中有多少个CPU,create_singlethread_workqueue()函数都只创建一个工作者线程。内核调用destroy_workqueue()函数撤销工作队列,它接收指向workqueue_struct数组指针作为参数。
queue_work()(封装在work_struct描述符中)把函数插入工作队列,它接收wq和work两个指针。wq指向workqueue_struct描述符,work指向work_struct描述符。queue_work()主要执行下面的步骤:
1、检查要插入的函数是否已经在工作队列中(work->pending字段等于1),如果是就结束。
2、把work_struct描述符加到工作队列链表中,然后把work_pending置为1。
3、如果工作者线程在本地CPU的cpu_workquueu_struct描述符的mor_work等待队列上睡眠,该函数唤醒这个线程。
queue_delayed_work()函数和queue_work()几乎是相同的,只是queue_delayed_work()函数多接收一个以系统滴答数来表示时间延迟的参数,它用于确保挂起函数在执行前的等待时间尽可能短。事实上,queue_delayed_work()依靠软地昂时期(work_struct描述的timer字段)把work_struct描述符插入工作队列链表的实际操作向后推迟了。如果相应的work_struct描述符还没有插入工作队列链表,cancel_delayed_work()就删除曾被调度过的工作队列函数。
8.3、预定义工作队列
九、从中断和异常返回
尽管终止阶段的主要目的很清楚,即恢复某个程序的执行,但是,在这样做之前,还需要考虑几个问题:
内核控制路径并发执行的数量
如果仅仅只有一个,那么CPU必须切换到用户态。
挂起进程的切换请求
如果有任何请求,内核就必须执行进程调度;否则,把控制权还给当前进程。
挂起的信号
如果一个信号发送给当前进程,就必须处理它。
单步执行模式
如果调试程序正在跟踪当前进程的执行,就必须在进程切换回到用户态之前恢复单步执行。
Virtual-8086模式
如果CPU处于virtual-8086模式,当前进程正在执行原来的实模式程序,因而必须以特殊的方式处理这种情况。
从技术上说,完成所有这些事情的内核汇编语言代码并不是一个函数,因为控制权从不返回到调用它的函数。它只是一个代码片段,有两个不同的入口点,分别叫做ret_frome_intr()和ret_from_exception()。正如其名所暗示的,中断处理程序结束时,内核进入ret_from_intr(),而当异常处理程序结束时,它进入ret_from_exception()。为了描述起来更容易一些,我们将把这两个入口点当作函数来讨论。
入口点ret_from_exception()和ret_from_intr()看起来非常相似,它们的唯一区别是:如果内核在编译时选择了支持内核抢占,那么从异常返回时要立刻禁用本地中断。
9.1、入口点
9.2、恢复内核控制路径
9.3、检查内核抢占
如果需要进行进程切换,就调用preempt_schedule_irq()函数,它设置preempt_count字段的PREEMPT_ACTIVE标志,把大内核锁计数器暂时置为-1,打开本地中断并调用schedule()以选择另一个进程来运行。当前面的进程要恢复时,preempt_schedule_irq()使大内核计数器的值恢复为以前的值,清楚PREEMPT_ACTIVE标志并禁用本地中断。但当前进程的TIF_NEED_RESCHED标志被设置,将继续调用schedule()函数。
9.4、恢复用户态程序
9.5、检测重调度标志
9.6、处理挂起信号、虚拟8086模式和单步执行
阅读(943) | 评论(0) | 转发(0) |