中断和异常(笔记

中断通常被定义为一个事件,该事件改变处理器执行的指令顺序。中断通常分为同步中断和异步中断:

  • 同步中断时当指令执行时由CPU控制单元产生的,只有在一条指令终止执行后CPU才会发生中断。(称为异常)
  • 异步中断时由其他硬件设备依照CPU始终信号随机产生的。(称为中断)

中断时由间隔定时器和I/O设备产生的。异常是由程序的错误产生的,或者是内核必须处理的异常条件产生的。

中断信号的作用

当一个中断信号到达时,CPU必须停止它当前正在做的事情,切换到一个新的活动。为了做到这点,内核态堆栈要保存程序计数器的当前值(eip和cs寄存器的内容),并把终端类型相关的一个地址放进程序计数器。由中断或异常处理程序执行的代码不是一个进程,是一个内核可能控制路径。中断处理程序比一个进程更“轻“,中断的上下文很少,建立或终止中断处理需要的时间很少。

中断处理时内核执行的最敏感的人物之一,必须满足下列约束:

  • 内核正打算完成一些别的事情时,中断随时会来,所以,内核目标是让中断尽可能快的完成,把更多的处理向后推迟。内核相应中断后要进行的操作分为两部分,关键而紧急的部分,立即执行;其余推迟部分,随后执行。
  • 内核可能处理其中一个中断,另一个中断又发生了。中断处理程序必须编写成使相应的内核控制路径能以嵌套的方式执行。
  • 尽管中断可以嵌套,但在内核代码中还是存在一个临界区,这个区间中,中断必须禁止。必须尽可能限制这样的临界区。
中断和异常

intel文档把中断和异常分为以下几类:

  • 中断:可屏蔽中断,I/O设备发出的所有中断请求(IRQ)都产生可屏蔽中断。非屏蔽中断,只有几个危急事件才引起非屏蔽中断。非屏蔽中断总是由CPU辨认。
  • 异常:处理器探测异常分为三组:故障,通常可以纠正;陷阱(trap),在陷阱指令执行后立即报告,内核把控制权返回给程序后就可以继续他的执行而不失连贯性;异常中止(abort),发生一个严重的错误。编程异常(programmed exception),在编程者发出请求时发生。
每个中断和异常是由0-255之间的一个数来标识。intel把这个8位的无符号整数叫做一个向量(vector)。非屏蔽中断的向量和异常的向量是固定的,而可屏蔽中断的向量可以通过对中断控制器的编程来改变

IRQ和中断

每个能发出中断请求的硬件设备控制器都有一条名为IRQ的输出线。所有现有的IRQ线,都与可编程中断控制器(PIC,Programmable Interupt Controuer)的硬件电路的输入引脚相连。可编程中断控制器执行下列动作:

  1. 监视IRQ线,检查产生的信号。如果多条产生信号,则选择引脚编号较小的IRQ线
  2. 把接受到的引发信号转换成对应的向量;把这个向量存放在中断控制器的一个I/O端口;把引发信号发送到处理器的INTR引脚,即产生一个中断;直到CPU通过这个中断信号写进可编程中断控制器的一个I/O端口来确认它。
  3. 返回第1步。

与IRQn相关联的intel的缺省向量是n+32.可以有选择的禁止每条IRQ线,通过PIC编程从而禁止IRQ。

当elfags寄存器的IF标志被清0时,由PIC发布的每个可屏蔽中断都由CPU暂时忽略。cli和sti汇编指令分别清楚和设置该标志

传统的PIC由两片8259风格的外部芯片以”级联“方式连在一起,每个芯片可以处理多达8个不同的IRQ输入线,因此,可用IRQ线的个数限制为15.

高级可编程中断控制器

如果系统中包含两个或多个CPU,需要高级可编程中断控制器。Intel从Pentinum III开始引入I/O高级可编程控制器(APIC)代替老式的8259A可编程中断控制器。

每个本地APIC都有32位的寄存器、一个内部时钟、一个本地定时设备及为本地APIC中断保留的两条额外的IRQ线LINT0和LINT1。所有本地的APIC都链接到一个外部的I/O APIC,形成一条多APIC的系统。从Pentium4开始,APIC总线通过系统总线实现。
I/O APIC的组成为:一组24条IRQ线、一张24项的中断重定向表、可编程寄存器,以及通过APIC总线发送和接收APIC信息的一个信息单元。中断重定向表的每一项都可以被单独编程以指明中断向量和优先级、目标处理器及选择处理器的方式。
来自外部硬件设备的中断请求以两种方式在可用CPU之间分发:
静态分发:IRQ信号传递给重定向表相应项中所列出的本地APIC。
动态分发:如果处理器正执行最低优先级的进程,IRQ信号就传递给处理器本地APIC,通过可编程任务优先级寄存器(task priority register, TPR,来计算当前运行进程的优先级;如果两个或多个CPU共享最低优先级,利用仲裁技术在CPU之间分配负荷;每当中断传递给一个CPU,相应的仲裁优先级置为0,其他每个CPU仲裁优先级都加1.
APIC系统还允许CPU产生处理器间中断(interprocessor interrupt)。 当一个CPU把中断发给另一个CPU时,就把自己本地APIC的中断指令寄存器(Interrupt Command Register, ICR)中存放这个中断向量和目标本地APIC的标识符。通过APIC总线向目标本地APIC发送一条消息。
处理器间中断(IPI)是SMP体系结构至关重要的组成部分。
异常
下面列出可以找到的异常的向量、名字、类型及描述:
0 - “Divide error(故障)": 当一个程序试图执行整数被0除操作时产生。divide_error()处理,信号SIGFPE
1 - ”Debug“ (陷阱或故障):a、设置eflags的TF标志时;b、一条指令或操作数的地址落在一个活动debug寄存器的范围之内。debug()处理,SIGTRAP信号。
2 - ”未用“: 为非屏蔽中断保留(利用NMI引脚的那些中断)。nmi()处理,信号None
3 - “Breakpoint”(陷阱):由int3指令引起。int3()处理,SIGTRAP信号。
4 - “Overflow” (陷阱):当eflags的OF标志被设置时,into(检查溢出)指令被执行。overflow()处理,SIGSEGV信号。
5 - “Bounds check” (故障):对于有效地址范围之外的操作数,bound指令被执行。bounds()处理,SIGSEGV信号。
6 - “Invalid opcode”(故障):CPU执行单元检测到一个无效的操作码。invalid_op()处理,SIGILL信号。
7 - “Device not available”(故障):device_not_available()处理,None信号。
8 - “Double fault”(异常中止):正常情况,处理一个异常时同时检测到另一个异常,可以串行处理。少数情况下,不能串行处理,产生这种异常。doublefault_fn()处理,None
9 - “Coprocessor segment overrun”(异常中止):因为外部的数学协处理器引起的问题。coprocessor_segment_overrun()处理,SIGFPE信号
10 - “Invalid TSS”(故障):CPU试图让一个上下文切换到有无效的TSS的进程。invalid_tss()处理,SIGSEGV信号。
11 - “Segment not present”(故障):引用一个不存在的内存段。segment_not_present()处理,SIGBUS信号。
12 - “Stack Segment fault”(故障):试图超过栈段界限的指令,或由ss标识的段不存在。stack_segment()处理,SIGBUS信号
13 - “General protection”(故障):寻址的页不在内存,相应的页表项为空,或违反了一种分页保护机制。general_protection()处理,SIGSEGV信号。
14 - “Page fault”(故障):寻址的页不在内存,相应的页表项为空,或违反了一种分页保护机制。page_fault()处理,SIGSEGV信号。
15 - 由Intel保留
16 - “Floating point error”(故障):集成到CPU芯片中的浮点单元用信号通知一个错误情形。coprocessor_error()处理,SIGFPE信号。
17 - “Alignment check”(故障):操作数的地址没有正确对齐。alignment_check()处理,SIGSEGV信号。
18 - “Machine check”(异常中止):机器检查机制检测到一个CPU错误或总线错误。machine_check(),None信号。
19 - “SIMD floating point exception”(故障):集成到CPU芯片中的SSE或SSE2单元对浮点操作信号通知一个错误。simd_coprocessor_error()处理,SIGFPE。
中断描述符表
Interupt Descriptor Table, IDT是一个系统表,每个中断或异常向量在表中有相应的处理程序入口。所以在允许中断发生前,必须适当初始化IDT。
IDT与GDT和LDT的格式相似。
idtr CPU寄存器使IDT可以位于内存的任何地方,允许中断之前,必须用lidt汇编指令初始化idtr。

中断和异常处理程序的嵌套执行
每个中断或异常都会引起一个内核控制路径。I/O设备发出一个中断时,相应的内核控制路径的第一条指令就是把寄存器的内容保存在内核堆栈,最后一条指令是恢复寄存器内容并让CPU返回到用户态。允许内核控制路径嵌套执行,必须是中断处理程序永不阻塞。也就是说,中断处理程序运行期间不能发生进程切换。缺页异常,是发生在内核态,即对不在RAM中的页进行寻址。处理这样的异常,内核是挂起当前进程,用另一个进程替代她,直到请求的页可以使用为止。
一个中断处理程序,既可以抢占其他的中断处理程序,也可以抢占异常处理程序。异常处理程序从不抢占中断处理程序,在内核态能触发的唯一异常就是缺页异常。中断程序从不执行可以导致缺页的操作,因为这样会导致进程切换。

linux内核交错执行内核控制路径,因为:
  • 为了提高可编程中断控制器和设备控制器的吞吐量。这样,内核即使在处理一个中断,也能发送应答。
  • 为了实现一种没有优先级的中断模型。因为每个中断处理程序都可以被另一个中断所延缓,所以,硬件设备之间没有必要建立预定义优先级。
在多处理器上,几个内核控制路径可以并发执行。与异常相关的内核控制路径可以开始在一个CPU执行,并且由于进程切换而移到另一个CPU上执行。

初始化中断描述符表
系统启用中断前,必须把IDT表的初始地址装到idtr寄存器,并初始化表中的每一项,这是在初始化系统时完成的。
int指令允许用户态进程发出一个中断信号,值可以是0-255之间,为了防止用户这么做,IDT的初始化通过把中断或陷阱门描述符的DPL字段设置为0,控制单元会检查出CPL和DPL字段有冲突,并产生一个“General protection”异常。
中断门、陷阱门和系统门
中断门(interrupt gate):用户态进程不能访问的一个intel中断门(门的DPL设为0),所有中断处理程序都通过中断门激活,并限制在内核态。
set_intr_gate(n, addr)在第n个表项插入一个中断门。门中的段选择符设为内核代码的段选择符,偏移量设为中断处理程序的地址addr,DPL为0.
系统门(system gate):用户态进程可以访问的一个intel陷阱门,可以激活三个linux异常处理程序,向量分别是4,5,128,所以,在用户态下,可以发布into、bound和int $0x80三条汇编指令。
set_system_gate(n, addr)同样,DPL设为3.
系统中断门(system interrupt gate):能被用户态进程访问的intel中断门,与向量3相关的 异常处理程序由这个门激活,用户态可以使用汇编指令int3。
set_system_intr_gate(n, addr)
陷阱门(trap gate):用户态进程不能访问的一个intel陷阱门,大部分异常处理程序都通过陷阱门激活。
set_trap_gate(n, addr)
任务门(task gate):不能被用户态进程访问的intel任务门,linux对“double fault”异常的处理是由任务门激活的。
set_task_gate(n, addr)


IDT的初步初始化

计算机还在实模式时,IDT被初始化并由BIOS例程使用。一旦linux接管,IDT就被移到RAM的另一个区域,进行第二次初始化。

IDT存放在idt_table中,有256个表项。

struct desc_struct idt_table[256] __attribute__((__section__(".data.idt"))) = { {0, 0}, };
struct desc_ptr idt_descr = { 256 * 16, (unsigned long) idt_table };
struct desc_ptr {                                                                   
    unsigned short size;                                                            
    unsigned long address;                                                          
} __attribute__((packed)) ;
6字节的idt_desc变量指定了IDT的大小和地址,只有内核使用lidt汇编指令初始化idtr寄存器时才用到这个变量。

setup_idt汇编语言用同一个中断门填充所有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中断处理程序,可以 看作是一个空的处理程序:

ignore_int:                                                                          
    cld                                                                              
    pushl %eax                                                                       
    pushl %ecx                                                                       
    pushl %edx                                                                       
    pushl %es                                                                        
    pushl %ds                                                                        
    movl $(__KERNEL_DS),%eax                                                         
    movl %eax,%ds                                                                    
    movl %eax,%es                                                                    
    pushl 16(%esp)                                                                   
    pushl 24(%esp)                                                                   
    pushl 32(%esp)                                                                   
    pushl 40(%esp)                                                                   
    pushl $int_msg                                                                   
    call printk                                                                      
    addl $(5*4),%esp                                                                 
    popl %ds                                                                         
    popl %es                                                                         
    popl %edx                                                                        
    popl %ecx                                                                        
    popl %eax                                                                        
    iret
主要执行:

  1. 在栈中保存一些寄存器的内容。
  2. 调用printk函数打印“Unknown interrupt”系统消息
  3. 从栈恢复寄存器的内容。
  4. 执行iret指令以恢复被中断的程序。
这个函数应该从不被执行,日志出现的“Unknown interrupt”要么是一个硬件问题,要么出现了一个内核问题。

这个预初始化后,内核进行第二边初始化,用有意义的陷阱和中断处理程序替换这个空处理程序。

异常处理

CPU大部分异常都由linux解释为出错条件。

异常处理程序有个标准的结构,由一下三部分组成:

  1. 在内核堆栈中保存大多数寄存器的内容
  2. 用高级的C函数处理异常
  3. 通过ret_from_excetion()函数从异常处理程序退出
为了利用异常,必须对IDT进行适当初始化。trap_init函数的工作是将一些最终值插入到IDT的非屏蔽中断及异常表项中。

void __init trap_init(void)                                                      
{                                                                                
#ifdef CONFIG_EISA                                                               
    void __iomem *p = ioremap(0x0FFFD9, 4);                                      
    if (readl(p) == 'E'+('I'<<8)+('S'<<16)+('A'<<24)) {                          
        EISA_bus = 1;                                                            
    }                                                                            
    iounmap(p);                                                                  
#endif                                                                           
                                                                                 
#ifdef CONFIG_X86_LOCAL_APIC                                                     
    init_apic_mappings();                                                        
#endif                                                                           
                                                                                 
    set_trap_gate(0,÷_error);                                              
    set_intr_gate(1,&debug);                                                     
    set_intr_gate(2,&nmi);                                                       
    set_system_intr_gate(3, &int3); /* int3-5 can be called from all */          
    set_system_gate(4,&overflow);                                                
    set_system_gate(5,&bounds);                                                      
    set_trap_gate(6,&invalid_op);                                                    
    set_trap_gate(7,&device_not_available);                                          
    set_task_gate(8,GDT_ENTRY_DOUBLEFAULT_TSS);                                      
    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_intr_gate(14,&page_fault);                                               
    set_trap_gate(15,&spurious_interrupt_bug);                                   
    set_trap_gate(16,&coprocessor_error);                                        
    set_trap_gate(17,&alignment_check);                                          
#ifdef CONFIG_X86_MCE                                                            
    set_trap_gate(18,&machine_check);                                            
#endif                                                                           
    set_trap_gate(19,&simd_coprocessor_error);                                   
                                                                                 
    set_system_gate(SYSCALL_VECTOR,&system_call);                                
                                                                                 
    /*                                                                           
     * Should be a barrier for any external CPU state.                           
     */                                                                          
    cpu_init();                                                                  
                                                                                 
    trap_init_hook();                                                            
}

由于“double fault”异常表示内核有严重的非法操作,其处理是通过任务门而不是陷阱门或系统门来完成的。

为异常处理程序保存寄存器的值

handler_name表示一个通用的异常处理程序的名字。每个异常处理程序都以下列的汇编指令开始:
handler_name:
       push $0 /* only for some exceptions,如果控制单元没有自动把一个硬件出错代码插入栈中,会在栈中垫上一个空值 */
       push $do_handler_name /* 高级C函数压入栈中,由异常处理程序名与do_前缀组成 */
       jump error_code

标号error_code的汇编语言对所有的异常处理程序都是相同的,除了“Device not available”这个异常。执行步骤如下:

  1. 把高级C函数可能用到的寄存器保存在栈中。
  2. 产生一条cld指令来清eflags的方向标志DF,以确保调用字符指令时会自动增加edi和esi寄存器的值。
  3. 把战中esp+36处的硬件出错码拷贝到edx中,给栈中这一位置存上值-1。
  4. 把保存在栈中esp+32位置的do_handler_name()高级C函数的地址装入edi寄存器中,在栈的这个位置写入es的值。
  5. 把内核的当前栈顶拷贝到eax寄存器。
  6. 把用户数据段的选择符拷贝到ds和es寄存器中。
  7. 调用地址在edi中的高级C函数。
进入和离开异常处理程序

大部分异常处理程序,把硬件出错码和异常向量保存在当前进程的描述符中,然后向当前进程发送一个适当的信号:

current->thread.error_code = error_code;

current->thread.trap_no = vector;

force_sig(sig_number, current);

异常处理程序总是检查是发生在用户态还是内核态,后一种情况,还要检查是否由系统调用的无效参数引起。出现在内核态的任何其他异常都是由于内核的bug引起,为了避免硬盘上的数据崩溃,处理程序调用die()函数,这种转储叫做kernel oops,并调用do_exit()终止的当前进程。

执行异常处理的C函数终止时,执行jmp指令跳到ret_from_exception()函数。

中断处理

中断处理依赖于中断类型,我们将讨论三种主要的中断类型:

I/O中断

相应的中断处理程序必须查询设备以确定适当的操作过程。

时钟中断

某些时钟产生一个中断,这些中断指示一个固定的时间已经过去,大部分是作为I/O中断来处理。

处理器间中断

多处理器系统中的一个CPU对另一个CPU发出的一个中断。

I/O中断处理

中断处理的灵活性是以两种不同的方式实现:

IRQ共享

中断处理程序执行多个中断服务例程(interrupt service routine, ISR),每个ISR是一个与单独设备相关的 函数。不可能 预先知道哪个特定的设备产生IRQ,每个ISR都被执行,验证它的设备是否需要关注,如是,就执行需要的所有操作。

IRQ动态分配

一条IRQ线在可能的最后一刻才与一个设备驱动程序相关联。

中断发生时,不是所有的操作都具有相同的急迫性。linux把紧随中断要执行的操作分为三类:

紧急的(Critical):他们必须被尽快地执行,紧急操作要在中断处理程序立即执行,而且是在禁止可屏蔽中断的情况下。

非紧急的(Noncritical):这些操作也要很快完成,也是立即执行的,但是必须在开中断的情况下。

非紧急可延迟的(Noncritical deferrable):诸如拷贝数据从缓冲区到某个地址空间。这样的操作由独立的函数执行。

不管引起中断的电路种类如何,所有的I/O中断处理程序都执行四个相同的操作:

  1. 在内核态堆栈中保存IRQ的值和寄存器的内容
  2. 为正在给IRQ线服务的PIC发送一个应答,允许PIC进一步发出中断
  3. 执行共享这个IRQ的所有设备的中断服务例程(ISR)
  4. 跳到ret_from_intr()的地址后终止。
中断向量

0~19(0x0 ~ 0x13) 非屏蔽中断和异常

20~31(0x14 ~ 0x1f)intel保留

32~127(0x20 ~ 0x7f)外部中断(IRQ)

128(0x80)用于系统调用的可编程异常

129~238(0x81 ~ 0xee) 外部中断(IRQ)

239(0xef)本地APIC时钟中断

240 (0xf0)本地APIC高温中断

241~250(0xf0 ~ 0xfa)由linux留作将来使用

251~253(0xfb ~ 0xff)处理器间中断

254(0xfe)本地APIC错误中断

255(0xff)本地APIC伪中断

IRQ与I/O设备之间的对应是在初始化每个设备驱动程序时建立的。

IRQ数据结构

typedef struct irq_desc {
    hw_irq_controller *handler; /* 指向PIC对象,服务于IRQ线 */                                                                                            
    void *handler_data; /* 指向PIC方法所使用的数据 */                                                       
    struct irqaction *action;   /* 标识当出现IRQ时要调用的中断服务例程 */
    unsigned int status;        /* 描述IRQ线状态的一组标志 */
    unsigned int depth;     /* 如果IRQ被激活,则显示0,如果IRQ线被禁止了不止一次,则显示一个正数 */
    unsigned int irq_count;     /* 中断计数器,统计IRQ线上发生中断的次数 */
    unsigned int irqs_unhandled; /* 对在IRQ线上发生的无法处理的中断进行计数 */
    spinlock_t lock; /* 用于串行访问IRQ描述符和PIC的自旋锁 */                           
} ____cacheline_aligned irq_desc_t;

如果一个中断内核没有处理,这个中断就是意外中断

内核不会在每检测到一个意外中断时就立刻禁用IRQ线,而是把中断和意外中断的总次数放在irq_count和irqs_unhandled字段中,当100000次中断产生时,如果意外中断次数超过99900,内核才禁用这条IRQ线。

描述IRQ线状态的一组标志:

#define IRQ_INPROGRESS  1   /* IRQ的一个处理程序在执行 */
#define IRQ_DISABLED    2   /* 由一个设备驱动程序故意地禁用IRQ线 */
#define IRQ_PENDING 4   /* 一个IRQ线已经出现在线上,也已经对PIC做出应答,但是没有为他提供服务 */
#define IRQ_REPLAY  8   /* IRQ线已经被禁用,但是前一个出现的IRQ还没有对PCI做出应答 */
#define IRQ_AUTODETECT  16  /* 内核在执行硬件设备探测时使用IRQ线 */
#define IRQ_WAITING 32  /* 内核在执行硬件设备探测时使用IRQ线,此外,相应的中断还没有产生 */
#define IRQ_LEVEL   64  /* 在80x86结构上没有使用 */
#define IRQ_MASKED  128 /* 未使用 */
#define IRQ_PER_CPU 256 /* 在80x86结构上没有使用 */
depth和IRQ_DISABLED标志表示IRQ线是否被禁用。每次调用disable_irq_nosync函数,depth字段的值增加,如果等于0,函数禁用IRQ线并设置它的IRQ_DISABLE标志。相反,enable_irq函数,depth字段值减少,如果为0,激活IRQ线并清除IRQ_DISABLE标志。

init_IRQ函数中,把每个IRQ主描述符的status字段设置为IRQ_DISABLED。通过替换由setup_idt所建立的中断门来更新IDT。实现如下:

for (i = 0; i < (NR_VECTORS - FIRST_EXTERNAL_VECTOR); i++) {                 
        int vector = FIRST_EXTERNAL_VECTOR + i;                                  
        if (i >= NR_IRQS)                                                        
            break;                                                               
        if (vector != SYSCALL_VECTOR)                                            
            set_intr_gate(vector, interrupt[i]);                                 
    }
为了以统一的方式处理所有的设备,linux用了“PIC对象”,由PIC名字和7个PIC标准方法组成。这种面向对象方法的优点是,驱动程序不必关注安装在系统中的PIC种类。定义PIC对象的数据结构叫做hw_interrupt_type。

struct hw_interrupt_type {
	const char * typename; /* PIC名字 */
	unsigned int (*startup)(unsigned int irq); /* 启动芯片的IRQ线 */
	void (*shutdown)(unsigned int irq); /*关闭芯片的IRQ线 */
	void (*enable)(unsigned int irq); /*启用和禁用IRQ线*/
	void (*disable)(unsigned int irq);
	void (*ack)(unsigned int irq); /* 把适当的字节发往I/O端口来应答所接收的IRQ */
	void (*end)(unsigned int irq); /* 在中断处理程序终止时被调用 */
	void (*set_affinity)(unsigned int irq, cpumask_t dest); /*用在多处理器系统中以声明特定IRQ所在CPU的“亲和力” */
};

多个设备能共享一个单独的IRQ。内核要维护多个irqaction描述符,结构如下:

struct irqaction {
    irqreturn_t (*handler)(int, void *, struct pt_regs *);/*指向一个I/O设备的中断服务例程*/
    unsigned long flags;/*描述IRQ和I/O设备之间的关系*/
    cpumask_t mask; //未使用
    const char *name; //IO设备名,通过/proc/interrupts文件,也可以看到设备名
    void *dev_id; //I/O设备的私有字段。
    struct irqaction *next; //指向irqaction描述符链表的下一个元素。
    int irq; //IRQ线
    struct proc_dir_entry *dir; //指向与IRQn相关的/proc/irq/n目录的描述符
};
irqaction描述符的标志:

SA_INTERRUPT:处理程序必须以进制中断执行

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

SA_SAMPLE_RANDOM:设备可以被看作是事件随机的发生源,内核可以用它做随机数产生器。

irq_cpustat_t irq_stat[NR_CPUS] ____cacheline_aligned;
typedef struct {
    unsigned int __softirq_pending; //表示挂起的软中断
    unsigned long idle_timestamp;  //CPU变为空闲的时间
    unsigned int __nmi_count;   /* arch dependent,NMI中断发生的次数 */
    unsigned int apic_timer_irqs;   /* arch dependent,本地APIC时钟中断发生的次数 */
} ____cacheline_aligned irq_cpustat_t;
irq_stat数组包含NR_CPUS个元素,系统中每个CPU对应一个元素,类型为irq_cpustat_t。

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

内核试图以轮转的方式把来自硬件设备的IRQ信号在所有CPU之间分发。所以,所有CPU服务于I/O中断的执行时间几乎相同。

系统启动的过程中,引导CPU执行setup_IO_APIC_irqs()来初始化I/O APIC芯片。系统初始化期间,所有的CPU都执行setup_local_APIC函数,处理本地APIC的初始化。每个芯片的任务优先级寄存器(TPR)都初始化为一个固定的值,意味着CPU愿意处理任何类型的IRQ信号,不管优先级。启动以后,再也不修改这个值。

有些情况下,硬件不能以公平的方式在微处理器之间成功的分配中断。必要的时候,linux2.6利用kirqd的特殊内核线程纠正对CPU进行的IRQ的自动分配。

内核线程为多APIC系统开发了一种优良特性,叫CPU的IRQ亲和力。通过修改I/O APIC的中断重定向表项,可以把中断信号发送到某个特定的CPU上。set_ioapic_affinity_irq函数用来实现这一功能,需要IRQ向量和一个32位掩码的参数。系统管理员通过向文件/proc/irq/n/smp_affinity中写入新的CPU位图掩码可以改变指定中断IRQ的亲和力。

kirqd周期地执行do_irq_balance函数,该函数跟踪在最近时间间隔内每个CPU接收的中断次数,如果发现负荷最重和最轻的CPU之间IRQ负荷不平衡的问题严重,要么把IRQ从一个CPU转到另一个CPU,要么让所有的IRQ在所有CPU之间“轮转”。

多种类型的内核栈

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

  • 异常栈,用于处理异常,这个栈包含在每个进程的thread_union数据结构中,对每个进程,使用不同的异常栈
  • 硬中断请求栈,用于处理中断,每个CPU都有一个硬中断请求栈,每个栈占用一个页框
  • 软中断请求栈,用于处理可延迟函数。同硬中断请求栈一样,占用一个页框。
所有的硬中断请求在hardirq_stack数组中,所有软中断请求存放在softirq_stack数组中。

static char softirq_stack[NR_CPUS * THREAD_SIZE]                                 
        __attribute__((__aligned__(THREAD_SIZE)));                               
                                                                                 
static char hardirq_stack[NR_CPUS * THREAD_SIZE]                                 
        __attribute__((__aligned__(THREAD_SIZE)));
每个数组元素都是跨越一个单独页框的irq_ctx类型的联合体。

hardirq_ctx和softirq_ctx数组使内核能迅速确定指定CPU的硬件中断请求栈和软中断请求栈。

static union irq_ctx *hardirq_ctx[NR_CPUS];                                      
static union irq_ctx *softirq_ctx[NR_CPUS];
union irq_ctx {
    struct thread_info      tinfo;
    u32                     stack[THREAD_SIZE/sizeof(u32)]; 
};

为中断处理程序保存寄存器的值

保存寄存器是中断处理程序做的第一件事情,通过entry.S中的汇编指令创建interrupt数组,包括NR_IRQS个元素,这里NR_IRQS产生的数为224(内核支持新近的I/O APIC芯片)或16(支持旧的8259A可编程控制芯片时)。数组中索引为n的元素存放下面两条汇编指令:

.rept NR_IRQS                                                                        
    ALIGN                                                                            
1:  pushl $vector-256                                                                
    jmp common_interrupt
把中断号减256保存在栈中,内核用负数表示所有的中断,正数表示系统调用。所有中断执行相同的代码,开始于标签common_interrupt处,

common_interrupt:                                                                
    SAVE_ALL                                                                     
    movl %esp,%eax                                                               
    call do_IRQ                                                                  
    jmp ret_from_intr
#define SAVE_ALL \                                                               
    cld; \                                                                       
    pushl %es; \                                                                 
    pushl %ds; \                                                                 
    pushl %eax; \                                                                
    pushl %ebp; \                                                                
    pushl %edi; \                                                                
    pushl %esi; \                                                                
    pushl %edx; \                                                                
    pushl %ecx; \                                                                
    pushl %ebx; \                                                                
    movl $(__USER_DS), %edx; \ 
    movl %edx, %ds; \                                                            
    movl %edx, %es;
SAVE_ALL可以在栈中保存中断处理程序可能会使用到的所有CPU寄存器, 但eflags、cs、eip、ss、esp除外。这几个寄存器由控制单元自动保存。保存寄存器的值后,栈顶的地址被存放到eax寄存器中。然后执行do_IRQ函数。

do_IRQ函数
这个函数的定义为:

fastcall unsigned int do_IRQ(struct pt_regs *regs)
#define fastcall    __attribute__((regparm(3)))

regpam表示函数到eax寄存器中找到参数regs的值,eax指向SAVE_ALL最后压入栈的那个寄存器在栈中的位置。

fastcall unsigned int do_IRQ(struct pt_regs *regs)
{	
	/* high bits used in ret_from_ code */
	int irq = regs->orig_eax & 0xff;
#ifdef CONFIG_4KSTACKS
	union irq_ctx *curctx, *irqctx;
	u32 *isp;
#endif

	irq_enter(); // 使中断处理程序嵌套数量的计数器增加。保存在当前进程的thread_info结构的preempt_count字段中
#ifdef CONFIG_DEBUG_STACKOVERFLOW
	/* Debugging check for stack overflow: is there less than 1KB free? */
	{
		long esp;

		__asm__ __volatile__("andl %%esp,%0" :
					"=r" (esp) : "0" (THREAD_SIZE - 1));
		if (unlikely(esp < (sizeof(struct thread_info) + STACK_WARN))) {
			printk("do_IRQ: stack overflow: %ld\n",
				esp - sizeof(struct thread_info));
			dump_stack();
		}
	}
#endif

#ifdef CONFIG_4KSTACKS //如果thread_union结构的大小为4KB,函数切换到硬中断请求栈

	curctx = (union irq_ctx *) current_thread_info(); //获取与内核栈相连的thread_info描述符的地址
	irqctx = hardirq_ctx[smp_processor_id()];

	/*
	 * this is where we switch to the IRQ stack. However, if we are
	 * already using the IRQ stack (because we interrupted a hardirq
	 * handler) we can't do that and just have to keep using the
	 * current stack (which is the irq stack already after all)
	 */
	if (curctx != irqctx) { //如果两个地址相等,说明内核已经在使用硬中断请求栈,直接执行__do_IRQ函数。
		int arg1, arg2, ebx;

		/* build the stack frame on the IRQ stack */
		isp = (u32*) ((char*)irqctx + sizeof(*irqctx));
		irqctx->tinfo.task = curctx->tinfo.task;//必须切换内核栈,保存task字段
		irqctx->tinfo.previous_esp = current_stack_pointer;//把CPU硬中断请求栈的栈顶装入esp寄存器;

		asm volatile(
			"       xchgl   %%ebx,%%esp      \n"
			"       call    __do_IRQ         \n"
			"       movl   %%ebx,%%esp      \n"
			: "=a" (arg1), "=d" (arg2), "=b" (ebx)
			:  "0" (irq),   "1" (regs),  "2" (isp)
			: "memory", "cc", "ecx"
		);
	} else
#endif
		__do_IRQ(irq, regs);//传递regs和中断号

	irq_exit();//减少中断计数器并检查是否有可延迟函数正等待执行;

	return 1;//结束,控制转向ret_from_intr函数;
}
__do_IRQ函数

fastcall unsigned int __do_IRQ(unsigned int irq, struct pt_regs *regs)//接收IRQ号和指向pt_regs结构的指针
{
	irq_desc_t *desc = irq_desc + irq;
	struct irqaction * action;
	unsigned int status;

	kstat_this_cpu.irqs[irq]++;
	if (desc->status & IRQ_PER_CPU) {
		irqreturn_t action_ret;

		/*
		 * No locking required for CPU-local interrupts:
		 */
		desc->handler->ack(irq);
		action_ret = handle_IRQ_event(irq, regs, desc->action);
		if (!noirqdebug)
			note_interrupt(irq, desc, action_ret);
		desc->handler->end(irq);
		return 1;
	}

	spin_lock(&desc->lock);//在访问主IRQ描述符前,获得相应的自旋锁,在多处理器上,没有自旋锁,主IRQ描述符会被几个CPU同时访问
	desc->handler->ack(irq);//如果使用旧的8259A PIC,相应的mask_and_ack_8259A应答PIC上的中断,禁用这条IRQ线,是禁用本地中断运行
	/*
	 * REPLAY is when Linux resends an IRQ that was dropped earlier
	 * WAITING is used by probe to mark irqs that are being tested
	 */
	status = desc->status & ~(IRQ_REPLAY | IRQ_WAITING);
	status |= IRQ_PENDING; /* we _want_ to handle it */

	/*
	 * If the IRQ is disabled for whatever reason, we cannot
	 * use the action we have.
	 */
	action = NULL;
	if (likely(!(status & (IRQ_DISABLED | IRQ_INPROGRESS)))) {
		action = desc->action;
		status &= ~IRQ_PENDING; /* we commit to handling */
		status |= IRQ_INPROGRESS; /* we are handling it */
	}
	desc->status = status;

	/*
	 * If there is no IRQ handler or it was disabled, exit early.
	 * Since we set PENDING, if another processor is handling
	 * a different instance of this same irq, the other processor
	 * will take care of it.
	 */
	if (unlikely(!action))
		goto out;

	/*
	 * Edge triggered interrupts need to remember
	 * pending events.
	 * This applies to any hw interrupts that allow a second
	 * instance of the same irq to arrive while we are in do_IRQ
	 * or in the handler. But the code here only handles the _second_
	 * instance of the irq, not the third or fourth. So it is mostly
	 * useful for irq hardware that does not mask cleanly in an
	 * SMP environment.
	 */
	for (;;) {
		irqreturn_t action_ret;

		spin_unlock(&desc->lock);

		action_ret = handle_IRQ_event(irq, regs, action);

		spin_lock(&desc->lock);
		if (!noirqdebug)
			note_interrupt(irq, desc, action_ret);
		if (likely(!(desc->status & IRQ_PENDING)))
			break;
		desc->status &= ~IRQ_PENDING;
	}
	desc->status &= ~IRQ_INPROGRESS;

out:
	/*
	 * The ->end() handler has to deal with interrupts which got
	 * disabled while the handler was running.
	 */
	desc->handler->end(irq);
	spin_unlock(&desc->lock);

	return 1;
}

__do_IRQ()检查是否必须真正的处理中断,下面三种情况什么也不做:

IRQ_DISABLED被设置:相应的IRQ线被禁止,CPU也可能执行__do_IRQ函数;即使PIC上的IRQ线被禁用,有问题的主板也可能产生伪中断。

IRQ_INPROGRESS被设置:设备驱动程序的中断服务例程不必是可重入的。

irq_desc[irq].action为NULL:当中断没有相关的中断服务例程时出现这种情况。

如果这三种情况都没有,设置IRQ_INPROGRESS开始循环,每次循环中,函数请IRQ_PENDING标志,释放中断自旋锁,并调用handle_IRQ_event()执行中断服务例程,执行完后,__do_IRQ再次获得自旋锁,检查IRQ_PENDING标志,如果清0,循环结束。相反,这个CPU正在执行handle_IRQ_event时,另一个CPU正在为这种中断执行do_IRQ函数。执行另一个循环,为新出现的中断提供服务。

最后__do_IRQ调用end方法,最后释放自旋锁。

挽救丢失的中断

内核用enable_irq()函数来检查是否发生了中断丢失。如果是,就强迫硬件让丢失的中断再发生一次。

void enable_irq(unsigned int irq)                                                
{                                                                                
    irq_desc_t *desc = irq_desc + irq;                                           
    unsigned long flags;                                                         
                                                                                 
    spin_lock_irqsave(&desc->lock, flags);                                       
    switch (desc->depth) {                                                       
    case 0:                                                                      
        WARN_ON(1);                                                              
        break;                                                                   
    case 1: {                                                                    
        unsigned int status = desc->status & ~IRQ_DISABLED;                      
                                                                                 
        desc->status = status;                                                   
        if ((status & (IRQ_PENDING | IRQ_REPLAY)) == IRQ_PENDING) {//检测IRQ_PENDING标志,检测到中断被丢失
            desc->status = status | IRQ_REPLAY;//这个标志确保只产生一个自我中断,__do_IRQ函数在开始处理中断时清楚这个标志
            hw_resend_irq(desc->handler,irq);//产生一个新的中断
        }
        desc->handler->enable(irq);                                              
        /* fall-through */                                                       
    }                                                                            
    default:                                                                     
        desc->depth--;                                                           
    }                                                                            
    spin_unlock_irqrestore(&desc->lock, flags);                                  
}

中断服务例程

ISR实现一种特定设备的操作,中断处理程序必须执行ISR时,调用handle_IRQ_event函数。

fastcall int handle_IRQ_event(unsigned int irq, struct pt_regs *regs,            
                struct irqaction *action)                                        
{                                                                                
    int ret, retval = 0, status = 0;                                             
                                                                                 
    if (!(action->flags & SA_INTERRUPT))//如果SA_INTERRUPT标志清0,就用sti汇编指令激活本地中断。
        local_irq_enable();
                                                                             
    do {//这个循环执行每个中断服务例程
        ret = action->handler(irq, action->dev_id, regs);                        
        if (ret == IRQ_HANDLED)                                                  
            status |= action->flags;                                             
        retval |= ret;                                                           
        action = action->next;                                                   
    } while (action);
                                                                                 
    if (status & SA_SAMPLE_RANDOM)                                               
        add_interrupt_randomness(irq);                                           
    local_irq_disable();//用cli汇编指令禁止本地中断
                                                                                
    return retval;//如果没有与中断对应的中断服务例程,返回0,否则返回1
}
#define local_irq_enable()  __asm__ __volatile__("sti": : :"memory")
#define local_irq_disable()     __asm__ __volatile__("cli": : :"memory")
所有中断服务例程都作用于相同的参数(分别通过eax, edx和ecx寄存器来传递)

irq:IRQ号,允许一个单独的ISR处理几条IRQ线

dev_id:设备标识符,允许一个单独的ISR照顾几个同类型的设备

regs:指向内核栈的pt_regs结构的指针,栈中含有中断发生后随即保存的寄存器。pt_regs包含15个字段:开始的9个字段是被SAVE_ALL压入寄存器的值;第10个为IRQ号编码,通过orig_eax字段被引用;其余字段对应由控制单元自动压入栈中的寄存器的值。允许ISR访问被中断的内核控制路径的执行上下文。

实际上,大多数ISR不使用这些参数。

IRQ线的动态分配

在激活一个准备利用IRQ线的设备之前,驱动程序调用request_irq,这个函数建立一个irqaction描述符,并用参数初始化。初始化硬件和注册中断处理程序的顺序必须正确,以防止中断处理程序在设备初始化完成之前就开始执行

int request_irq(unsigned int irq,                                                
        irqreturn_t (*handler)(int, void *, struct pt_regs *),                   
        unsigned long irqflags, const char * devname, void *dev_id)              
{                                                                                
    struct irqaction * action;                                                   
    int retval;                                                                  
                                                                                 
    /*                                                                           
     * Sanity-check: shared interrupts must pass in a real dev-ID,               
     * otherwise we'll have trouble later trying to figure out                   
     * which interrupt is which (messes up the interrupt freeing                 
     * logic etc).                                                               
     */                                                                          
    if ((irqflags & SA_SHIRQ) && !dev_id)                                        
        return -EINVAL;                                                          
    if (irq >= NR_IRQS)                                                          
        return -EINVAL;                                                          
    if (!handler)                                                                
        return -EINVAL;                                                          
                                                                                 
    action = kmalloc(sizeof(struct irqaction), GFP_ATOMIC);//kmalloc是可以睡眠的,所以,request_irq不能在中断上下文或其他不允许阻塞的代码中使用
    if (!action)                                                                 
        return -ENOMEM;                                                          
                                                                                 
    action->handler = handler;                                                   
    action->flags = irqflags;                                                    
    cpus_clear(action->mask);                                                    
    action->name = devname;                                                      
    action->next = NULL;                                                         
    action->dev_id = dev_id;                                                     
                                                                                 
    retval = setup_irq(irq, action);                                             
    if (retval)                                                                  
        kfree(action);                                                           
                                                                                 
    return retval;                                                               
}
在request_irq最后,会调用setup_irq把这个描述符插入到合适的IRQ链表。如果返回一个错误码,设备驱动程序中止操作,表示IRQ线已由另一个设备所使用,这个设备不允许中断共享。设备操作结束时,驱动程序会调用free_irq从IRQ链表中删除这个描述符,释放相应的内存区。

int setup_irq(unsigned int irq, struct irqaction * new)                          
{                                                                                
    struct irq_desc *desc = irq_desc + irq;                                      
    struct irqaction *old, **p;                                                  
    unsigned long flags;                                                         
    int shared = 0;                                                              
                                                                                 
    if (desc->handler == &no_irq_type)                                           
        return -ENOSYS;                                                          
    /*                                                                           
     * Some drivers like serial.c use request_irq() heavily,                     
     * so we have to be careful not to interfere with a                          
     * running system.                                                           
     */                                                                          
    if (new->flags & SA_SAMPLE_RANDOM) {                                         
        /*                                                                       
         * This function might sleep, we want to call it first,                  
         * outside of the atomic block.                                          
         * Yes, this might clear the entropy pool if the wrong                   
         * driver is attempted to be loaded, without actually                    
         * installing a new handler, but is this really a problem,               
         * only the sysadmin is able to do this.                                 
         */                                                                      
        rand_initialize_irq(irq);                                                
    }                                                                            
                                                                                 
    /*                                                                           
     * The following block of code has to be executed atomically                 
     */                                                                          
    spin_lock_irqsave(&desc->lock,flags);                                        
    p = &desc->action;                                                           
    if ((old = *p) != NULL) {                                                    
        /* Can't share interrupts unless both agree to */                        
        if (!(old->flags & new->flags & SA_SHIRQ)) {//检查两个设备的irqaction描述符中的SA_SHIRQ标志是否指定了IRQ线能被共享,如果不能,返回出错码
            spin_unlock_irqrestore(&desc->lock,flags);                           
            return -EBUSY;                                                       
        }                                                                        
                                                                                 
        /* add new interrupt at end of irq queue,把new指向链表的末尾 */
        do {                                                                     
            p = &old->next;
            old = *p;
        } while (old);
        shared = 1;                                                              
    }                                                                            
                                                                                 
    *p = new;
                                                                                 
    if (!shared) {//如果没有其他设备共享一个IRQ,清楚IRQ_DISABLED,IRQ_AUTODETECT, IRQ_WAITING和IRQ_INPROGRESS标志
        desc->depth = 0;
        desc->status &= ~(IRQ_DISABLED | IRQ_AUTODETECT |
                  IRQ_WAITING | IRQ_INPROGRESS);
        if (desc->handler->startup)//调用startup方法确保IRQ信号激活
            desc->handler->startup(irq);
        else
            desc->handler->enable(irq);
    }
    spin_unlock_irqrestore(&desc->lock,flags);                                   
                                                                                 
    new->irq = irq;                                                              
    register_irq_proc(irq);                                                      
    new->dir = NULL;                                                             
    register_handler_proc(irq, new);                                             
                                                                                 
    return 0;                                                                    
}

void free_irq(unsigned int irq, void *dev_id)//需要唯一的dev_id如果中断线是共享的,则仅删除dev_id对应的处理程序  
{
    struct irq_desc *desc;
    struct irqaction **p;
    unsigned long flags;
  
    if (irq >= NR_IRQS)        
        return;
 
    desc = irq_desc + irq;
    spin_lock_irqsave(&desc->lock,flags);
    p = &desc->action; 
    for (;;) {
        struct irqaction * action = *p;
 
        if (action) {
            struct irqaction **pp = p;
 
            p = &action->next;     
            if (action->dev_id != dev_id)
                continue;
 
            /* Found it - now remove it from the list of entries */
            *pp = action->next;
            if (!desc->action) {
                desc->status |= IRQ_DISABLED;
                if (desc->handler->shutdown)
                    desc->handler->shutdown(irq);
                else
                    desc->handler->disable(irq);
            }
            spin_unlock_irqrestore(&desc->lock,flags);
            unregister_handler_proc(irq, action);

            /* Make sure it's not being used on another CPU */
            synchronize_irq(irq);
            kfree(action);
            return;
        }
        printk(KERN_ERR "Trying to free free IRQ%d\n",irq);
        spin_unlock_irqrestore(&desc->lock,flags);
        return;
    }
}

处理器间中断处理

处理器间中断不是通过IRQ线传输的,而是作为信号直接放在连接所有CPU本地APIC的总线上

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

CALL_FUNCTION_VECTOR(向量0xfb)

发往所有CPU(不包括发送者),强者这些CPU运行发送者传递过来的函数。相应的中断处理程序叫做call_function_interrupt。但是,通过smp_call_function执行调用函数的CPU除外。

RESCHEDULE_VECTOR(向量0xfc)

当一个CPU接收这种类型的中断时,相应的处理程序(smp_reschedule_interrupt)限定自己来应答中断。

INVALIDATE_TLB_VECTOR(向量0xfd)

发往所有的CPU(不包括发送者),强制他们的转换后援缓冲器(TLB)变为无效。

处理器间中断处理程序的汇编语言代码由BUILD_INTERRUPT宏产生。

BUILD_INTERRUPT(reschedule_interrupt,RESCHEDULE_VECTOR)                          
BUILD_INTERRUPT(invalidate_interrupt,INVALIDATE_TLB_VECTOR)                      
BUILD_INTERRUPT(call_function_interrupt,CALL_FUNCTION_VECTOR)
#define BUILD_INTERRUPT(name, nr)   \                                                
ENTRY(name)             \                                                            
    pushl $nr-256;          \                                                        
    SAVE_ALL            \                                                            
    movl %esp,%eax;         \                                                        
    call smp_/**/name;      \                                                        
    jmp ret_from_intr;
这个宏从栈顶压入向量号减256的值,保存寄存器,然后调用高级C函数,其名字是低级处理程序的名字加前缀smp_。例如call_fucntion_interrupt,调用名为smp_call_function_interrupt的高级处理函数。

由于下列一组函数,使得产生处理器间中断(IPI)变得容易:

send_IPI_all():发送一个IPI到所有的CPU(包括发送者)

send_IPI_allbutself():发送一个IPI到所有CPU(不包括发送者)

send_IPI_self():发送一个IPI到发送者的CPU

send_IPI_mask(): 发送一个IPI到位掩码指定的一组CPU

软中断及tasklet

软中断和tasklet有密切联系,tasklet是在软中断之上实现的。内核术语“软中断”,常常表示可延迟函数的所有种类;术语”中断上下文“,表示内核当前正在执行一个中断处理程序或一个可延迟的函数。

软中断的分配是静态的(编译时定义),tasklet的分配和初始化可以在运行时进行。软中断可以并发的运行在多个CPU上,所以,其是可重入的函数而且必须明确的使用自旋锁保护其数据结构。tasklet不必担心,因为内核对tasklet的执行进行更严格的控制。相同类型的tasklet总是串行执行,也就是不能在两个CPU上同时运行同类型的tasklet,所以,tasklet不必是可重入的。

一般来说,可延迟函数上可以执行四种操作:

初始化(initialization)

定义一个新的可延迟函数,在内核自身初始化或加载模块时进行。

激活(activation)

标记一个可延迟函数为“挂起”,激活可以在任何时候进行。

屏蔽(masking)

有选择的屏蔽一个可延迟函数,即使被激活,内核也不执行它。

执行(execution)

执行一个挂起的可延迟函数和同类型的其他所有挂起的可延迟函数。

由给定CPU激活的一个可延迟函数必须在同一个CPU上执行。

软中断

linux2.6使用有限个软中断。目前,定义了六种软中断:

linux2.6使用的软中断
软中断下标(优先级)说明
HI_SOFTIRQ0处理高优先级的tasklet
TIMER_SOFTIRQ1和时钟中断相关的tasklet
NET_TX_SOFTIRQ2把数据包传送到网卡
NET_RX_SOFTIRQ3从网卡接收数据包
SCSI_SOFTIRQ4SCSI命令的后台中断处理
TASKLET_SOFTIRQ5处理常规的tasklet
低下标代表着高优先级,因为软中断函数从下标0开始执行。

软中断所使用的数据结构

表示软中断的主要数据结构是softirq_vec数组。

static struct softirq_action softirq_vec[32] __cacheline_aligned_in_smp;
只有数组的前6个元素被有效的使用。

struct softirq_action                                                            
{                                                                                
    void    (*action)(struct softirq_action *);//指向中断函数的指针
    void    *data;//指向软中断函数需要的通用数据结构data指针
};
另一个关键的字段是32位的preempt_count,用来跟踪内核抢占和内核控制路径的嵌套,保存在每个进程描述符的thread_info字段,这个字段的编码表示三个不同的计数器和一个标志。
0~7位:抢占计数器(max value 255),显示的禁用本地CPU内核抢占的次数,等于表示允许内核抢占

8~15位:软中断计数器(max value 255),表示可延迟函数被禁用的程度,0表示可延迟函数处于激活状态

16~27位:硬中断计数器(max value 4096),表示在本地CPU上中断处理程序的嵌套数,irq_enter宏递增它的值,irq_exit递减它的值。

28:PREEMPT_ACTIVE标志

宏in_interrupt宏检查preempt_count字段的硬中断计数器和软中断计数器,只要有一个为正数,该宏就产生一个非零值。

#define in_interrupt()      (irq_count())
#define irq_count() (preempt_count() & (HARDIRQ_MASK | SOFTIRQ_MASK)
#define preempt_count() (current_thread_info()->preempt_count)
 #define HARDIRQ_MASK    (__IRQ_MASK(HARDIRQ_BITS) << HARDIRQ_SHIFT)
#define __IRQ_MASK(x)   ((1UL << (x))-1)
#define HARDIRQ_BITS    12
如果内核使用多内核栈,该宏可能还要检查本地CPU的irq_ctx联合体中的thread_info描述符的preempt_count字段。

每个CPU都有个32位掩码(描述挂起的软中断),存放在irqstat_t数据结构的__softirq_pending字段中,为了获取或设置位掩码的值,内核使用宏local_softirq_pending(),选择本地CPU的软中断位掩码。

typedef struct {                                                                 
    unsigned int __softirq_pending;                                              
    unsigned long idle_timestamp;                                                
    unsigned int __nmi_count;   /* arch dependent */                             
    unsigned int apic_timer_irqs;   /* arch dependent */                         
} ____cacheline_aligned irq_cpustat_t;
#define local_softirq_pending() \                                                
    __IRQ_STAT(smp_processor_id(), __softirq_pending)
#define __IRQ_STAT(cpu, member) (irq_stat[cpu].member)
extern irq_cpustat_t irq_stat[];

处理软中断

open_softirq()处理软中断的初始化。

void open_softirq(int nr, void (*action)(struct softirq_action*), void *data)        
{
    softirq_vec[nr].data = data;
    softirq_vec[nr].action = action;                                                 
}
open_softirq限制自己初始化softirq_vec数组中适当的元素。

raise_softirq函数用来激活软中断。

void fastcall raise_softirq(unsigned int nr)                                         
{                                                                                    
    unsigned long flags;
 
    local_irq_save(flags);//保存eflags寄存器IF标志的状态值并禁用本地CPU上的中断
    raise_softirq_irqoff(nr);
    local_irq_restore(flags); //恢复刚开始保存的IF标志的状态值
}
inline fastcall void raise_softirq_irqoff(unsigned int nr)                       
{                                                                                
    raise_softirq_irqoff(nr);                                                  
                                                                              
    if (!in_interrupt())//如果in_interrupt产生为1,跳过wakeup_softirqd,1这种情况,要么已经在中断上下文调用raise_softirq,要么当前禁用软中断
        wakeup_softirqd();//唤醒本地CPU的ksoftirqd内核线程
}
#define local_irq_save(x)   __asm__ __volatile__("pushfl ; popl %0 ; cli":"=g" (x): /* no input */ :"memory")
#define local_irq_restore(x)    do { typecheck(unsigned long,x); __asm__ __volatile__("pushl %0 ; popfl": /* no output */ :"g" (x):"memory", "cc"); } while (0)
周期性的检查活动的软中断,检查是在内核代码的几个点上进行的:

  • 内核调用local_bh_enable函数,激活本地CPU的软中断时
  • do_IRQ完成了I/O中断的处理时或调用irq_exit宏时
  • 如果系统使用了I/O APIC,则当smp_apic_timer_interrupt函数处理本地定时器中断时
  • 在多处理器系统中,CPU处理完被CALL_FUNCTION_VECTOR处理器间中断所出发的函数时
  • 当一个特殊的ksoftirqd/n内核线程被唤醒时
do_softirq函数

如果检测到挂起的软中断,内核调用do_softirq函数处理它们。

asmlinkage void do_softirq(void)                                                 
{                                                                                
    __u32 pending;                                                               
    unsigned long flags;                                                         
                                                                                 
    if (in_interrupt())//说明要么在中断上下文调用了do_softirq,要么当前禁用软中断
        return;                                                                  
                                                                                
    local_irq_save(flags);//保存IF标志的状态值,禁用本地CPU上的中断
                                                                                 
    pending = local_softirq_pending();                                           
                                                                                 
    if (pending)                                                                 
        __do_softirq();                                                          
                                                                                 
    local_irq_restore(flags); //恢复之前保存的IF标志的状态值,并返回
}
asmlinkage void __do_softirq(void)                                               
{                                                                                
    struct softirq_action *h;                                                    
    __u32 pending;                                                               
    int max_restart = MAX_SOFTIRQ_RESTART; //把循环计数器初始化为10
    int cpu;                                                                     
                                                                                 
    pending = local_softirq_pending();//把本地CPU的软中断的位掩码复制到局部变量pending
                                                                                 
    local_bh_disable();//增加软中断计数器的值
    cpu = smp_processor_id();
restart:                                                                         
    /* Reset the pending bitmask before enabling irqs */                         
    local_softirq_pending() = 0;                                                 
                                                                                 
    local_irq_enable();//激活本地中断
                                                                                 
    h = softirq_vec;                                                             
                                                                                 
    do {//根据pending每一位的值,执行对应的软中断处理函数
        if (pending & 1) {                                                       
            h->action(h);                                                        
            rcu_bh_qsctr_inc(cpu);                                               
        }                                                                        
        h++;                                                                     
        pending >>= 1;                                                           
    } while (pending);                                                           
                                                                                 
    local_irq_disable();//禁用本地中断
                                                                                 
    pending = local_softirq_pending();//保存本地CPU的软中断位掩码到pending,再次递减循环计数器
    if (pending && --max_restart)                                                
        goto restart;                                                            
                                                                                 
    if (pending)//如果还有更多的挂起软中断,唤醒线程来处理本地CPU的软中断
        wakeup_softirqd();                                                       
                                                                                 
    __local_bh_enable();//软中断计数器减1,重新激活可延迟函数
}
为了保证可延迟函数的低延迟性,__do_softirq一直运行到执行完所有挂起的软中断。这样,会迫使__do_softirq运行很长时间,大大 延迟用户态进程的执行。因此,__do_softirq只做固定次数的循环,然后返回。

ksoftirqd内核线程

每个ksoftirqd/n内核线程都运行ksoftirqd()函数:

static int ksoftirqd(void * __bind_cpu)                                          
{                                                                                
    set_user_nice(current, 19);                                                  
    current->flags |= PF_NOFREEZE;                                               
                                                                                 
    set_current_state(TASK_INTERRUPTIBLE);                                       
                                                                                 
    while (!kthread_should_stop()) {                                             
        if (!local_softirq_pending())                                            
            schedule();                                                          
                                                                                 
        __set_current_state(TASK_RUNNING);                                       
                                                                                 
        while (local_softirq_pending()) {                                        
            /* Preempt disable stops cpu going offline.                          
               If already offline, we'll be on wrong CPU:                        
               don't process */                                                  
            preempt_disable();                                                   
            if (cpu_is_offline((long)__bind_cpu))                                
                goto wait_to_die;                                                
            do_softirq();                                                        
            preempt_enable();                                                    
            cond_resched();                                                      
        }                                                                        
                                                                                 
        set_current_state(TASK_INTERRUPTIBLE);                                   
    }                                                                            
    __set_current_state(TASK_RUNNING);                                           
    return 0;                                                                    
                                                                                 
wait_to_die:                                                                     
    preempt_enable();                                                            
    /* Wait for kthread_stop */                                                  
    set_current_state(TASK_INTERRUPTIBLE);                                       
    while (!kthread_should_stop()) {                                             
        schedule();                                                              
        set_current_state(TASK_INTERRUPTIBLE);                                   
    }                                                                            
    __set_current_state(TASK_RUNNING);                                           
    return 0;                                                                    
}

软中断函数可以重新激活自己;以及软中断的连续高流量可能产生问题。ksoftirqd就是为了解决这中平衡问题。

tasklet
tasklet是I/O驱动程序中实现可延迟函数的首选方法。tasklet和高优先级的tasklet分别存放在tasklet_vec数组和tasklet_hi_vec数组中。

static DEFINE_PER_CPU(struct tasklet_head, tasklet_vec) = { NULL };                  
static DEFINE_PER_CPU(struct tasklet_head, tasklet_hi_vec) = { NULL };
struct tasklet_head                                                              
{                                                                                
    struct tasklet_struct *list;
};
struct tasklet_struct                                                            
{                                                                                    
    struct tasklet_struct *next; //指向链表中下一个描述符的指针
    unsigned long state;//tasklet的状态
    atomic_t count;//锁计数器
    void (*func)(unsigned long);//指向tasklet函数的指针
    unsigned long data;//由tasklet函数来使用
}
state有两个标志:

TASKLET_STATE_SCHED

该标志被设置时,表示tasklet是挂起的,也意味着tasklet描述符被插入到tasklet_vec和tasklet_hi_vec数组的其中一个链表中

TASKLET_STATE_RUN

表示tasklet正在被执行,在单处理器上不使用,因为没有必要检查特定的tasklet是否在运行

使用tsklet,需要分配一个新的tasklet_struct数据结构,并用tasklet_init初始化

void tasklet_init(struct tasklet_struct *t,                                          
          void (*func)(unsigned long), unsigned long data)                           
{                                                                                    
    t->next = NULL;                                                                  
    t->state = 0;                                                                    
    atomic_set(&t->count, 0);                                                        
    t->func = func;                                                                  
    t->data = data;                                                                  
}
调用 tasklet_disable_nosync 和t asklet_disable可以有选择的禁止tasklet,都增加tasklet描述符的count字段,但是后一个只有在tasklet函数已经运行的实例结束后才返回,要重新激活tasklet,调用 tasklet_enable

static inline void tasklet_enable(struct tasklet_struct *t)                          
{                                                                                    
    smp_mb__before_atomic_dec();                                                     
    atomic_dec(&t->count);                                                           
} 
为了激活tasklet,要根据自己的tasklet需要的优先级,调用 tasklet_schedule函数或tasklet_hi_schedule函数。
static inline void tasklet_schedule(struct tasklet_struct *t)                        
{
    if (!test_and_set_bit(TASKLET_STATE_SCHED, &t->state))//检查这个标志,如果设置则返回
        __tasklet_schedule(t);
}
void fastcall __tasklet_schedule(struct tasklet_struct *t) 
{                                                                                    
    unsigned long flags;
 
    local_irq_save(flags);//保存IF标志的状态,并禁用本地中断
    t->next = __get_cpu_var(tasklet_vec).list;//在tasklet_vec[n]指向的链表的起始处增加tasklet描述符
    __get_cpu_var(tasklet_vec).list = t;
    raise_softirq_irqoff(TASKLET_SOFTIRQ);//激活TASKLET_SOFTIRQ类型的软中断
    local_irq_restore(flags); //恢复IF标志的状态
}
下面,看tasklet如果被执行,与HI_SOFTIRQ中断相关的软中断函数叫做tasklet_hi_action,与TASKLET_SOFTIRQ相关的 函数叫做tasklet_action()

static void tasklet_action(struct softirq_action *a)                                 
{                                                                                    
    struct tasklet_struct *list;                                                     
                                                                                     
    local_irq_disable();//禁用本地中断
    list = __get_cpu_var(tasklet_vec).list;//获取本地CPU的逻辑号,并把tasklet_vec[n]指向的链表的地址存入局部变量list
    __get_cpu_var(tasklet_vec).list = NULL;//已调度的tasklet描述符的链表被清空
    local_irq_enable();//打开本地中断   
                                                                                 
    while (list) {                                                               
        struct tasklet_struct *t = list;                                         
                                                                                 
        list = list->next;                                                       
                                                                                 
        if (tasklet_trylock(t)) {                                                
            if (!atomic_read(&t->count)) {                                       
                if (!test_and_clear_bit(TASKLET_STATE_SCHED, &t->state))         
                    BUG();                                                       
                t->func(t->data);                                                
                tasklet_unlock(t);                                               
                continue;                                                        
            }                                                                    
            tasklet_unlock(t);                                                   
        }                                                                        
                                                                                 
        local_irq_disable();                                                     
        t->next = __get_cpu_var(tasklet_vec).list;                               
        __get_cpu_var(tasklet_vec).list = t;                                     
        __raise_softirq_irqoff(TASKLET_SOFTIRQ);                                 
        local_irq_enable();                                                      
    }                                                                            
}


工作队列

linux2.6引入工作队列,代替2.4的任务队列。它允许内核线程被激活,而且由一种工作者线程(worker thread)的特殊内核线程滞后执行。

可延迟函数和工作队列区别主要在于:可延迟函数运行在中断上下文,工作队列的函数运行在进程上下文

工作队列的数据结构

主要数据结构是workqueue_struct的描述符

struct cpu_workqueue_struct {                                                    
                                                                                 
    spinlock_t lock;//保护该数据结构的自旋锁
    long remove_sequence;   /* Least-recently added (next to run),flush_workqueue使用的序列号 */
    long insert_sequence;   /* Next to add ,flush_workequeu使用的序列号 */
                                                                                 
    struct list_head worklist; //挂起链表的头结点,集中了工作队列中所有挂起的函数
    wait_queue_head_t more_work;//等待队列,其中的工作者线程因等待更多的工作处于睡眠状态
    wait_queue_head_t work_done;//等待队列,其中的进程由于等待工作队列被刷新处于睡眠状态
                                                                                 
    struct workqueue_struct *wq;//指向workqueue_struct结构的指针,其中包含该描述符
    task_t *thread;//指向结构中工作者线程的进程描述符指针
                                                                                 
    int run_depth;      /* Detect run_workqueue() recursion depth,run_workqueue当前的执行深度*/
} ____cacheline_aligned;                                                         
                                                                                 
/*                                                                               
 * The externally visible workqueue abstraction is an array of                   
 * per-CPU workqueues:                                                           
 */                                                                              
struct workqueue_struct {                                                        
    struct cpu_workqueue_struct cpu_wq[NR_CPUS];                                 
    const char *name;                                                            
    struct list_head list;  /* Empty if single thread */                         
};
work_struct 用于描述每一个挂起函数

struct work_struct {
    unsigned long pending;//如果函数已经在工作队列中,该字段为1否则为0
    struct list_head entry;//指向挂起函数链表前一个或后一个元素
    void (*func)(void *); //挂起函数的地址
    void *data;//传递给函数的参数
    void *wq_data;//通常指向cpu_workqueue_struct描述符的父节点的指针
    struct timer_list timer;//用于延迟挂起函数执行的软定时器
};
工作队列函数

create_workqueue("foo")函数接收 一个字符串作为参数,创建工作对列的workqueue_struct 描述符的地址,还创建n个线程,n表示CPU的个数。

#define create_workqueue(name) __create_workqueue((name), 0)
struct workqueue_struct *__create_workqueue(const char *name,                    
                        int singlethread)                                        
{                                                                                
    int cpu, destroy = 0;                                                        
    struct workqueue_struct *wq;                                                 
    struct task_struct *p;                                                       
                                                                                  
    BUG_ON(strlen(name) > 10);                                                   
                                                                                 
    wq = kmalloc(sizeof(*wq), GFP_KERNEL);                                       
    if (!wq)                                                                     
        return NULL;                                                             
    memset(wq, 0, sizeof(*wq));                                                  
                                                                                 
    wq->name = name;                                                             
    /* We don't need the distraction of CPUs appearing and vanishing. */         
    lock_cpu_hotplug();                                                          
    if (singlethread) {                                                          
        INIT_LIST_HEAD(&wq->list);                                               
        p = create_workqueue_thread(wq, 0);                                      
        if (!p)                                                                  
            destroy = 1;                                                         
        else                                                                     
            wake_up_process(p);                                                  
    } else {                                                                     
        spin_lock(&workqueue_lock);                                              
        list_add(&wq->list, &workqueues);                                        
        spin_unlock(&workqueue_lock);                                            
        for_each_online_cpu(cpu) {
            p = create_workqueue_thread(wq, cpu);                                
            if (p) {                                                             
                kthread_bind(p, cpu);                                            
                wake_up_process(p);                                              
            } else                                                               
                destroy = 1;                                                     
        }                                                                        
    }                                                                            
    unlock_cpu_hotplug();                                                        
                                                                                 
    /*                                                                           
     * Was there any error during startup? If yes then clean up:                 
     */                                                                          
    if (destroy) {                                                               
        destroy_workqueue(wq);                                                   
        wq = NULL;                                                               
    }                                                                            
    return wq;                                                                   
}
根据传递给函数的字符串为工作者线程命名,相反create_signlethread_workqueue只创建一个工作者线程。destroy_workqueue函数撤销工作对列,

void destroy_workqueue(struct workqueue_struct *wq)                              
{                                                                                
    int cpu;                                                                     
                                                                                 
    flush_workqueue(wq);                                                         
                                                                                 
    /* We don't need the distraction of CPUs appearing and vanishing. */         
    lock_cpu_hotplug();                                                          
    if (is_single_threaded(wq))                                                  
        cleanup_workqueue_thread(wq, 0);                                         
    else {                                                                       
        for_each_online_cpu(cpu)                                                 
            cleanup_workqueue_thread(wq, cpu);                                   
        spin_lock(&workqueue_lock);                                              
        list_del(&wq->list);                                                     
        spin_unlock(&workqueue_lock);                                            
    }                                                                            
    unlock_cpu_hotplug();                                                        
    kfree(wq);                                                                   
}
queue_work把函数装入工作队列

int fastcall queue_work(struct workqueue_struct *wq, struct work_struct *work)   
{                                                                                
    int ret = 0, cpu = get_cpu();                                                
                                                                                 
    if (!test_and_set_bit(0, &work->pending)) {//检查插入的函数是否在工作队列中,如果是,就结束
        if (unlikely(is_single_threaded(wq)))
            cpu = 0;
        BUG_ON(!list_empty(&work->entry));
        __queue_work(wq->cpu_wq + cpu, work);//把work_struct描述符加到工作队列链表中,然后设置 work->pending置为1
        ret = 1;                                                                 
    }                                                                            
    put_cpu();                                                                   
    return ret;                                                                  
}
queue_delay_work多接收一个系统滴答数表示时间延迟的参数,确保挂起函数在执行前的等待时间尽可能短。

每个工作者线程在work_thread()函数内部,不断的执行循环操作,工作线程一旦被唤醒就调用run_workqueue函数, 该函数从工作者线程的工作队列链表中删除所有work_struct描述符并执行相应的挂起函数。内核必须等待工作队列中所有挂起函数执行完毕。flush_workqueue函数,接收workqueue_struct描述符的地址,并且在工作对列中的所有挂起函数结束之前使调用进程一直处于阻塞状态。但是不会等待调用之后加入工作队列的挂起函数。

预定义工作队列

大多数情况下,为了运行一个函数而创建一个工作者线程开销太大,所以内核引入events的预定义工作队列,所有内核开发者可以随意使用它,它是一个包含不同内核层函数和I/O驱动程序的标准工作队列,存放在keventd_wq数组中。

为了使用预定义工作对列,提供了如下函数:

schedule_work(w)   等价于 queue_work(keventd_wq, w)

schedule_delayed_work(w, d) 等价于 queue_delayed_work(keventd_wq, w, d)

schedule_delayed_work_on(cpu, w, d) 等价于 queue_delayed_work(keventd_wq, w, d)

flush_scheduled_work() 等价于 flush_workqueue(keventd_wq)

不应该使预定义工作队列中执行的函数长时间处于阻塞状态,会影响其他用户的工作对列。

从中断和异常返回

thread_info描述符的flags字段中,存放中断和异常返回的标志:

TIF_SYSCALL_TRACE 正在跟踪系统调用

TIF_NOTIFY_RESUME 在80x86平台上不使用

TIF_SIGPENDING 进程有挂起信号

TIF_NEED_RESCHED 必须执行调度程序

TIF_SIGNLESTEP 临返回用户态前恢复单步执行

TIF_IRET 通过iret而不是sysexit从系统调用强行返回

TIF_SYSCALL_AUDIT 系统调用正在被审计

TIF_POLLING_NRFLAG 空闲进程正在轮询TIF_NEED_RESCHED 标志

TIF_MEMDIE 正在撤销进程以回收内存

=======================
中断处理程序结束时,内核进入ret_from_intr(), 而当异常处理程序结束时,进入ret_from_exception()。他们俩个的唯一区别是如果内核编译支持内核抢占,那么从异常返回时要立刻禁用本地中断。
入口点

恢复内核控制路径


  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值