您的位置:首页 > 运维架构 > Linux

深入理解Linux内核(4)---中断和异常(x86平台)

2016-01-03 15:26 381 查看
本文是ULK中断和异常这一章的笔记,讲的是Intel 80x86的中断,与硬件紧密相关,ARM与其有很大不同。
中断通常分为同步中断(synchronous)和异步中断(asynchronous):
同步中断:是当指令执行时,由CPU控制单元产生的,只有在一条指令终止执行后CPU才会发生中断。
异步中断:是由其他硬件设备依照CPU时钟信号随机产生的。
Intel微处理器手册中,把同步和异步中断分别称为异常(exception)和中断(interrupt)。
中断是由间隔定时器和I/O设备产生的,而异常是由程序的错误产生的,或是由内核必须处理的异常条件产生的。
1.中断信号的作用
中断或异常处理程序不是一个进程,而是一个内核控制路径,代表中断发生时正在运行的进程执行。中断处理是内核执行的最敏感的任务之一,必须满足以下约束:
①内核相应中断后的操作分两部分,关键的紧急的部分,内核立即执行,其余的推迟随后执行
②中断程序必须使内核控制路径能以嵌套的方式执行,当最后一个内核控制路径终止时,内核必须能恢复被中断进程的执行,或者如果中断信号已经导致了重新调度,内核能切换到另外的进程。
③尽管中断可以嵌套,但在临界区中,中断必须禁止。但内核必须尽可能限制这样的临界区,大部分时间应该以开中断的方式运行。
2.中断和异常
Intel文档把中断和异常分为以下几类
中断:可屏蔽中断(maskable interrupt)或非屏蔽中断。
异常:包括处理器探测异常(根据eip寄存器的值分为故障、陷阱、异常终止)、编程异常(由int或int3触发,也叫软中断)
每个中断和异常是由0~255之间的一个数来标识,Intel把这个8位的无符号整数叫做向量(vector),非屏蔽中断的向量和异常的向量是固定的,可屏蔽中断的向量可以通过控制器改变。
(1)IRQ和中断
每个能发出中断请求的硬件控制器都有一条IRQ输出线,所有IRQ线都与可编程中断控制器(Programmable
Interrupt Controller,PIC)的硬件电路输入引脚相连。PIC执行以下动作
①监视IRQ线,检查其信号,若有两条或两条以上的IRQ线上有信号,就选择引脚编号较小的IRQ线。
②如果一个信号在IRQ线上发生
a.把接收到的引发信号转换成相应的向量
b.把这个向量存放在PIC的一个I/O端口,从而允许CPU通过数据总线读取此向量。
c.把引发信号发送到处理器的INTR引脚,即产生一个中断。
d.等待,直到CPU通过把这个中断信号写进可编程中断控制器的一个I/O端口来确认它,当这个情况发生,清INTR线
③返回①
IRQ线从0开始顺序编号,第一条IRQ线IRQ0,与IRQn关联的Intel缺省向量是n+32.通过PIC可以修改IRQ和向量之间的映射。
可以通过修改PIC禁止IRQ,但禁止的中断是不会丢失的,它们一旦被激活,PIC就又把它们发送到CPU,这样CPU可以一次处理同一类型的IRQ,PIC禁止IRQ不同于屏蔽中断,屏蔽中断是被忽略的。
传统的PIC是由两片8259A芯片级联成的,可以支持15个IRQ线。
(2)高级可编程中断控制器
为了充分发挥SMP体系结构的并行性,能够把中断传递给系统中的每个CPU只管重要。基于此,Intel从Pentium
III开始引入了一种叫I/O APIC(I/O Advanced Programmable Interrupt Controller)的新组件,用来代替老式的8259A。



如图,一条APIC总线把“前端”I/O APIC链接到本地APIC,来自设备的IRQ线连接到I/O
APIC,因此相对于本地APIC,I/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个字节组成,因此最多需要256×8=2048字节来存放IDT.
IDT包含三种类型的描述符



任务门(task gate):存放当中断信号发生时,必须取代当前进程的那个进程的TSS选择符
中断门(interrupt gate):包含段选择符和中断或异常处理程序段内偏移量。当控制权转移到一个适当的段时,处理器清IF标志,从而关闭将来会发生的可屏蔽中断。
陷阱门(Trap gate)与中断门相似,只是控制权传递到一个适当的段时处理器不修改IF标志。
Linux利用中断门处理中断,利用陷阱门处理异常。
(5)中断和异常的硬件处理
假定内核已被初始化,CPU在保护模式下运行,当执行一条指令后,cs和eip这对寄存器包含下一条将要执行的指令的逻辑地址。在处理这条指令之前,控制单元会检查允许前一条指令时,是否已经发生了一个中断或异常。若已发生,那么控制单元执行下列操作:
①确定与中断或异常关联的向量i(0<=i<=255)
②读由idtr寄存器指向的IDT表中的第i项
③从gdtr寄存器获得GDT的基地址,并在GDT中查找,以读取IDT表项中的选择符所标识的段描述符,这个描述符指定中断或异常处理程序所在段的基地址。
④确信中断是由授权的中断发生源发出的,中断处理程序的特权级不能低于引起中断的程序的特权。若是异常,进一步检查DPL,CPL这可以避免用户程序访问特殊的陷阱门或中断们。
⑤检查是否发生了特权级的变化,也就是检查CPL是否不同于所选择的段描述符的DPL。若不同必须使用心得特权级相关的栈。
a.读tr寄存器,以访问允许进程的TSS段
b.用与新特权级别相关的栈段和栈指针的正确值装载ss和esp寄存器。
c.在新的栈中保存ss和esp以前的值,这些值定义了与旧特权级相关的栈的逻辑地址。
⑥如果故障已发生,用引起异常的指令地址装载cs和eip寄存器,从而使得这条指令能再次被执行。
⑦在栈中保存flags、cs及eip的内容
⑧如果异常产生了一个硬件出错码,则将它保存在栈中
⑨装载cs和eip寄存器,其值分别是IDT表中第i项门描述符的段选择符和偏移量字段。这些值给出了中断或者异常处理程序的第一条指令的逻辑地址。
控制单元所执行的最后一步就是跳转到中断或异常处理程序,相应的处理程序必须产生一条iret指令,把控制权交给被中断的进程,这将迫使控制单元。
①用保存在栈中的值装载cs、eip或eflags寄存器,如果一个硬件出错码曾被压入栈中,并且在eip内容的上面,那么,执行iret指令前必须先弹出这个硬件出错码。
②检查处理程序的CPL是否等于cs中的最低两位,如果是,Iret终止执行,否则转入下一步。
③从栈中装载ss和esp寄存器,因此,返回到与旧特权级别相关的栈。
④检查ds、es、fs及gs段寄存器的内容,如果其中一个寄存器包含的选择符是一个段描述符,并且其DPL值小于CPL,那么清相应段寄存器,这是为了禁止用户态程序(CPL=3)利用内核以前所用的段寄存器(DPL=0),如果不清这些寄存器,恶意用户程序可以利用来访问内核地址空间。
3.中断和异常处理程序的嵌套
每个中断或异常都会引起一个内核控制路径的执行,当设备发出一个中断时,相应的内核控制路径的第一部分指令就是把那些寄存器的内容保存在内核堆栈,最后一部分就是恢复寄存器内容并让CPU返回到用户态的指令
内核控制路径可以任意嵌套,即一个中断处理程序可以被另一个中断处理程序“中断”,因此引起内核控制路径的嵌套执行。



允许内核控制路径嵌套执行必须付出代价,就是中断处理程序必须永不阻塞,即中断处理程序运行期间不能发生进程切换,事实上,嵌套的内核控制路径回复执行时需要的数据存放在内核态堆栈中,这个栈是属于当前进程的。
一个中断处理程序既可以抢占其他的中断处理程序,也可以抢占异常处理程序。相反,异常处理程序从不抢占中断处理程序。
Linux交错执行内核控制路径,因为:
①为了提高可编程中断控制器和设备控制器的吞吐量。
②为了实现一种没有优先级的中断模型。简化了内核代码,提高了内核可移植性。
在多处理器系统上,几个内核控制路径可以并发执行,此外,与异常相关的内核控制路径可以开始执行在一个CPU上,并且由于进程切换而移往另一个CPU上执行。
4.初始化中断描述符表
内核启用中断之前,必须把IDT表的初始地址装载idtr寄存器中,并初始化表中的每一项。
Linux与Intel稍有不同,把中断任务分为,中断门、系统门、系统中断门、陷阱门、人物门。
IDT存放在idt_table表中,有256个表项,6字节的变量idt_descr指定了IDT的大小和它的地址,只有当内核用lidt汇编指令初始化idtr寄存器时采用到这变量。
内核初始化过程中,setup_idt()汇编函数用同一个中断门(即指向ignore_int()中断处理程序)来填充所有这256个idt_table表项:

点击(此处)折叠或打开

setup_idt:
lea ignore_int, %edx
movl $(_ _KERNEL_CS << 16), %eax
movw %dx, %ax /* selector = 0x0010 = cs */
movw $0x8e00, %dx /* interrupt gate, dpl=0, present */
lea idt_table, %edi
mov $256, %ecx
rp_sidt:
movl %eax, (%edi)
movl %edx, 4(%edi)
addl $8, %edi
dec %ecx
jne rp_sidt
ret

ignore_int()中断处理程序,可以看作一个空的处理程序,只调用printk打印“Unknown interrupt”系统消息。正常情况下,ignore_int()应该从不被执行。
紧接着这个预初始化,内核将在IDT中进程第二遍初始化,用有意义的陷阱和中断处理程序替换这个空程序。一旦这个过程完成,对于可编程中断控制器确认的每一个IRQ,IDT都将包含一个专门的中断门。
5.异常处理
(1)CPU产生的大部分异常都由Linux解释为出错条件,当一个异常发生时,内核就向引起异常的进程发送一个信号向它通知一个反常条件。
在两种情况下,Linux利用CPU异常更有效地管理硬件资源:①”Device not availeble”异常与cr0寄存器的TS标志一起用来把新值装入浮点寄存器②“Page
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()来终止当前进程。
当执行异常处理的C函数终止时,程序执行一条jmp指令以跳转到ret_from_exception()函数返回。
6.中断处理
异常处理中给当前进程发一个Unix信号处理的方式,对于中断来讲是毫无意义的。
中断处理依赖于中断类型,主要分三类:
I/O中断:中断处理程序必须查询设备已确定适当的操作过程。
时钟中断:某种时钟产生一个中断,告诉内核一个固定的时间间隔已经过去。
处理器间中断:多处理器系统中一个CPU对另一个CPU发出一个中断。
(1)I/O中断处理
①为了能给多个设备同时提供服务,中断处理程序有以下灵活性
IRQ共享:中断处理程序执行多个中断服务例程(interrupt service routine,ISR),每个ISR是一个与单独设备(共享IRQ线)相关的函数,每个ISR被执行时,都要验证它的设备是否是需要关注的设备。
IRQ动态分配:一条IRQ线在可能的最后时刻才与一个设备驱动程序相关联,这样即使几个硬件设备不共享IRQ线,同一个IRQ向量也可以由这几个设备在不同时刻使用。
②Linux吧中断要执行的操作分为三类:
紧急的(Critical):紧急操作要在一个中断处理程序内立即执行,而且是在禁止可屏蔽中断的情况下。
非紧急的(Noncritical):由中断处理程序立即执行,但必须是在开中断的情况下。
非紧急可延迟的(Noncritical deferrable):由独立函数来执行,比如tasklet。
③中断处理过程
a.在内核态堆栈中保存IRQ的值和寄存器内容
b.为正在给IRQ线服务的PIC发送一个应答,这将允许PIC进一步发出中断。
c.执行各项这个IRQ的所有设备的中断服务例程(ISR).
d.跳到ret_from_intr()的地址后终止



④中断向量
为IRQ可配置设备选择一条线有三种方式
a.设置一些硬件跳线器(仅适用于旧式设备卡)
b.安装设备是执行一个实用程序,该程序可以让用户选择一个可用的IRQ号,或者探测系统自身以确定一个可用的IRQ号。
c.在系统启动时执行一个硬件协议,以确定一个可用的IRQ号。比如,遵循外设部件互联标准(Peripheral Component Interconnect,PCI)的设备的驱动程序利用一组函数,如pci_read_config_byte()访问设备的配置空间。
内核必须在启用中断前发现IRQ号与I/O设备之间的对应,都则内核在不知道那个向量对于那个设备的情况下,无法处理来自该设备的信号。
一个例子



⑤IRQ数据结构
a.每个中断向量都有它自己的irq_desc_t描述符,所有的描述符组织在一起形成irq_desc数组。




点击(此处)折叠或打开

struct irq_desc {
       irq_flow_handler_t handle_irq;
       struct irq_chip *chip;
       struct msi_desc *msi_desc;
       void *handler_data;
       void *chip_data;
       struct irqaction *action; /* IRQ action list */

       unsigned int status; /* IRQ status */

       unsigned int depth; /* nested irq disables */
       unsigned int wake_depth; /* nested wake enables */
       unsigned int irq_count; /* For detecting broken
IRQs */
       unsigned int irqs_unhandled;
       spinlock_t lock;
#ifdef CONFIG_SMP
       cpumask_t affinity;
       unsigned int cpu;
#endif
#if defined(CONFIG_GENERIC_PENDING_IRQ) || defined(CONFIG_IRQBALANCE)
       cpumask_t pending_mask;
#endif
#ifdef CONFIG_PROC_FS
       struct proc_dir_entry *dir;
#endif
       const char *name;
} ____cacheline_aligned;

extern struct irq_desc irq_desc[NR_IRQS];

handle_irq:服务于IRQ线
action:标识当出现IRQ时要调用的中断服务例程,该字段指向IRQ的irqaction描述符链表的第一个元素。
Status:描述IRQ线状态的一组标志
Depth:如果IRQ线被激活,则显示0,若被禁止不止一次,则显示一个整数
irq_count:中断计数器,统计IRQ线上发生中断的次数
irqs_unhandled:IRQ线上无法处理的中断进行技术
内核把中断和意外中断总次数分别存放在irq_desc_t描述符的irq_count和irqs_unhandled字段中,当第100000次中断产生时,如果意外中断的次数超过99900内核禁止这条IRQ线。
b.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_DISABLED标志表示IRQ线是否被禁用,每次调用disable_irq()或disable_irq_nosync()函数,depth字段的值增加,如depth=0,函数禁用IRQ线并设置它的IRQ_DISABLED标志,每当调用enable_irq()函数,depth字段值减少,如depth变为0,函数激活IRQ线并清除IRQ_DISABLED标志。
在系统初始化期间,init_IRQ()函数把每个IRQ住描述符status字段设置成IRQ_DISABLED。

点击(此处)折叠或打开

for (i = 0; i < NR_IRQS; i++) 

    if (i+32 != 128) 

    set_intr_gate(i+32,interrupt[i]);

这段代码在Interrupt数组中找到用于建立中断门的中断处理程序地址,interrupt数组中的第n项中存放IRQn的中断处理程序的地址。

c. irqaction描述符的字段:多个设备能共享一个单独的IRQ,因此内核要维护多个irqaction描述符,其中每个描述符涉及一个特定的硬件设备和一个特定的中断。

点击(此处)折叠或打开

struct irqaction {
       irq_handler_t handler;//指向一个I/O设备的中断服务例程,这允许多个设备共享同一个IRQ的关键字段
       unsigned long flags;//描述IRQ和I/O设备之间的关系
       cpumask_t mask;//未用
       const char *name;//I/O设备名,cat /proc/interrupts看到的IRQ服务设备名
       void *dev_id;//I/O设备的私有字段
       struct irqaction *next;//指向irqaction描述符表的下一个元素,下一个共享同一IRQ的硬件设备
       int irq;//IRQ线
       struct proc_dir_entry *dir;//指向与IRQn相关的/proc/irq/n目录的描述符

};

Irqaction描述符的标志

SA_INTERRUPT:处理程序必须以禁止中断执行

SA_SHIRQ:设备允许它的IRQ线与其他设备共享

SA_SAMPLE_RANDOM:设备可以看作是事件随机的发生源

⑥IRQ在多处理系统上的分发

Linux遵循SMP,这意味着本质上内核不会对任何一个CPU有偏爱,内核试图以轮转的方式把来自硬件设备的IRQ信号在所有CPU之间分发,CPU服务于I/O中断的执行时间片几乎相同。

简而言之,当硬件设备产生了一个中断信号时,多APIC系统就选择其中的一个CPU,并把该信号传递给相应的本地APIC,本地APIC又依次中断它的CPU,这个事件不通报给其他所有的CPU,所有这些都是由硬件自动完成,但有时硬件不能完全公平的分配中断,Linux2.6利用叫做kirqd的特殊内核线程来纠正对CPU进行的IRQ的自动分配。

⑦多种类型的内核栈

如果thread_union结构的大小为8K,那么当前进程的内核栈被用于所有类型的内核控制路径,异常、中断和可延迟函数。相反,如果thread_union结构的大小为4K,内核就使用三种类型的内核栈:

a.异常栈,用于处理异常(包括系统调用)。

b.硬中断请求线,用于处理中断。

c.软中断请求栈,用于处理可延迟函数(软中断或tasklet),系统中的每个CPU都有一个软中断请求栈,且每个栈占用一个单独的页框。

硬中断请求存放在hardirq_stack数组中,软中断请求放在softirq_stack数组中,每个数组元素都是跨越一个单独页框的irq_ctx类型的联合体。

与每个栈相连的thread_info结构不是与进程而是与CPU相关联的。

Hardirq_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_info描述符地址与存放在hardirq_ctx[smp_processor_id()]中的地址相比较,若想的,说明内核已经在使用硬中断请求站,因此跳转到第c步,这种情况发生在内核处理另外一个中断时又产生了中断请求的时候。

 b3.进一步切换内核栈

c.调用__do_IRQ()函数,把指针regs和regs->orig_eax字段中的中断号传递给该函数。

d. 如果上面已经成功切换到硬中断请求栈,函数把ebx寄存器中的原始栈指针拷贝到esp寄存器,从而回到以前在用的异常栈或软中断请求栈。

e.执行宏irq_exit(),该宏递减中断计数器并检查是否有可延迟函数正等待执行

f.转向ret_from_intr()函数。

__do_IRQ()函数:__do_IRQ()函数接受IRQ号(eax寄存器)和指向pt_regs结构的指针(edx寄存器)作为它的参数。

__do_IRQ()会检查是否必须真正地处理中断,在下列三种情况什么也不干:

a.IRQ_DISABLED被设置:即使相应IRQ线被禁止,CPU也可能执行__do_IRQ()函数。

b.IRQ_INPROGRESS被设置:同一中断由同一CPU执行,因为设备驱动的中断服务例程不必是可重入的(它们执行时串行的)。

c.irc_desc[irq].action为NULL:当没有相应中断服务例程时,一般只有内核正在探测一个硬件设备时才会发生。

假定这三种情况没有一种成立,中断必须被处理,__do_IRQ()设置IRQ_INPROGRESS标识并开始一个循环,调用handle_IRQ_event()执行中断服务例程。

假定CPU有一条激活的IRQ线,在CPU应答中断前,这条IRQ线被另外一个CPU屏蔽掉,结果IRQ_DISABLED标志被设置,那么这个中断就丢失了,不会得到响应。为解决这个问题,内核用来激活IRQ线的enable_irq()函数线检查是否发生了中断的丢失,然后hw_resend_irq()函数自我产生一个新中断,来达到响应该中断的目的。

⑨中断服务例程

Handle_IRQ_event()函数本质上执行如下步骤:

a.如果SA_INTERRUPT标志清0,就用sti汇编语言指令激活本地中断。

b.通过下列代码执行每个中断的中断服务例程

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线的动态分配

同一条IRQ线可以让几个硬件设备使用,即使想这些设备不允许IRQ共享,技巧是使这些硬件设备的活动串行化,以便一次只能有一个设备拥有这个IRQ线。

(2)处理器间中断(IPI)

处理器间中断允许一个CPU向系统中的其他CPU发送中断信号,IPI不是通过IRQ线传输的,而是作为信号直接放在连接所有CPU本地APIC的总线上。

在多处理器系统中,Linux定义了三种处理器间中断

①CALL_FUNCTION_VECTOR(向量0xfb)

发往除本身之外的所有CPU,强制这些CPU允许发送者传递过来的函数。

②RESCHEDULE_VECTOR(向量0xfc)

处理程序reschedule_interrupt()限定自己来应答中断,当从中断返回时,所有的重新调度都自动进程。

③INVALIDATE_TLB_VECTOR(向量0xfd)

发往除本身之外的所有CPU,强制他们的转换后缓冲器(TLB)变为无效。

7.软中断及tasklet

软中断:常常表示可延迟函数的所有种类,是静态的,在编译时定义。ISR须是可重入函数且必须明确地使用自旋锁保护其数据。

中断上下文:表示内核当前正在执行一个中断处理程序或一个可延迟的函数。

tasklet:是在软中断之上实现的,可以在运行时进行tasklet的分配和初始化,ISR可以是非重入函数,相同类型的tasklet总是被串行的执行,类型不同的tasklet可以在几个CPU上并发执行,但不能在两个CPU上同时运行相同类型的tasklet。

一般而言,在可延迟函数上可以执行四种操作:

①初始化(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_action包括两个字段,指向软中断函数的一个action指针和指向软中断函数需要的通用数据结构data指针。另一个关键字段是32位的preempt_count字段,用它来跟踪内核抢占和内核控制路径的嵌套,该字段存放在每个进程描述符的thread_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_cout字段的硬中断计数器和软中断计数器。

CPU的32位掩码,存放在irq_cpustat_t数据结构的__softirq_pending字段,内核使用宏local_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_VECTOR处理器间中断所触发函数时。

e.当一个特殊的ksoftirqd/n内核线程被唤醒时。

如果在一个检查点(local_softirq_pending()不为0)检测到挂起的软中断,内核就调用do_softirq()来处理它们。

④ksoftirqd内核线程

每个CPU都有自己的ksoftirqd/n内核线程(n为CPU的逻辑号),每个ksoftirqd/n内核线程都运行ksoftirqd()函数。

点击(此处)折叠或打开

for(;;) { 

        set_current_state(TASK_INTERRUPTIBLE ); 

        schedule( ); /* now in TASK_RUNNING
state */ 

        while (local_softirq_pending( )) { 

                preempt_disable(); 

                do_softirq( ); 

                preempt_enable(); 

                cond_resched( ); 

        } 

}

当内核线程被唤醒时,就检查local_softirq_pending()中的软中断掩码并在必要时调用do_softirq(),如果没有挂起的软中断,吧当前进程状态设置为TASK_INTERRUPTIBLE,随后如果当前进程需要就调用cond_resched()函数来实现进程切换。

软中断可以重新激活自己,实际上网络软中断和tasklet软中断都可以这么做。

(2)tasklet

Tasklet是I/O驱动程序中实现可延迟函数的首选,其建立在两个叫做HI_SOFTIRQ和TASKLET_SOFTIRQ的软中断之上。

点击(此处)折叠或打开

struct tasklet_struct

{

    struct tasklet_struct *next;//指向链表中下一个描述符的指针

    unsigned long state;//tasklet的状态

    atomic_t count;//锁计数器

    void (*func)(unsigned long);//指向tasklet函数的指针

    unsigned long data;//可以由tasklet函数使用

};

用tasklet_init()初始化tasklet_struct,用tasklet_schedule()或tasklet_hi_schedule()激活。

除非tasklet函数重新激活自己,否则tasklet的每次激活之多触发tasklet函数的一次执行。

8.工作队列

工作队列的数据机构workqueue_struct。

一个工作队列必须明确的在使用前创建,宏为:

点击(此处)折叠或打开

struct workqueue_struct *create_workqueue(const char *name);

struct workqueue_struct *create_singlethread_workqueue(const char *name);

Tasklet和工作队列的详细分析见http://blog.chinaunix.net/uid-24708340-id-3035286.html

9.从中断和异常返回

终止阶段主要目的是,恢复某个程序的执行,在这样做之前,需要考虑:

a.内核控制路径并发执行的数量:

如果只有一个,那么CPU必须切换到用户态。

b.挂起进程的切换请求:

如有任何请求,内核必须执行进程调度,否则,吧控制权还给当前进程。

c.挂起的信号:

如果一个信号发送到当前进程,就必须处理它。

d.单步执行模式:

如果掉是程序正在跟踪当前进程的执行,就必须在进程切换回到用户态之前恢复但不执行。

e.Virtual-8086模式

如果CPU处于virtual-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模式和单步执行。

from: http://v4.5-14833.htmlfyxspgpw.blog.chinaunix.net/uid-24708340-id-3323794.html
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: