导航
处理器的运算速度一般要比外部硬件快很多。以读取硬盘为例,如果是简单的顺序执行,CPU 必须等待很长时间,不停地轮询硬盘是否读取完毕,这会浪费很多 CPU 时间。中断提供了这样一种机制,使得读取硬盘这样的操作可以交给硬件来完成,CPU 挂起当前进程,将控制权转交给其他进程,待硬件处理完毕后通知 CPU,操作系统把当前进程设为活动的,从而允许该进程继续执行,处理读取硬盘的结果。
另一方面,有些事件不是程序本身可预见的,需要硬件以某种方式告诉进程。例如时钟中断为定时器提供了基础,如果没有时钟中断,程序只能每执行几条指令就检查一下当前系统时间,这在效率上是不可接受的。
从广义上说,中断是改变 CPU 处理指令顺序的硬件信号。分为两类:
中断处理的基本原则就是“快”。如果反应慢了,数据可能丢失或被覆盖。例如键盘按键中断,所按下的键的 keycode 放在 KBDR 寄存器中,如果在中断被处理之前用户又按了一个键,则 KBDR 的值被新按下的键的 keycode 覆盖,早先按下的键对应的数据就丢失了。
当一个中断信号到达时,CPU 必须停止当前所做的事,转而处理中断信号。为了尽快处理中断并为接收下一个中断做好准备,内核应尽快处理完一个中断,将更多的处理向后推迟。
为达到“快”这一目标,内核允许不同类型的中断嵌套发生,即在中断处理的临界区之外可以接受新的中断。这样,更多的 I/O 设备将处于忙状态。
中断控制器是连接设备和 CPU 的桥梁,一个设备产生中断后,需要经过中断控制器的转发,才能最终到达 CPU。时代发展至今,中断控制器经历了 PIC(Programmable Interrupt Controller,可编程中断控制器) 和 APIC (Advanced Programmable Interrupt Controller,高级可编程中断控制器) 两个阶段。前者在 UP(Uni-processor,单处理器) 上威震四方,随着 SMP (Symmetric Multiple Processor,对称多处理器) 的流行,APIC 已广为流行并将最终取代 PIC。
8259A (PIC) 管脚图
上图中的管脚说明:
8259A 中的寄存器:
arch/x86/kernel/i8259_32.c 中通过位运算来开启和关闭中断。
63 void disable_8259A_irq(unsigned int irq) 64 { 65 unsigned int mask = 1 << irq; 66 unsigned long flags; 67 68 spin_lock_irqsave(&i8259A_lock, flags); // 用 spinlock 锁住 69 cached_irq_mask |= mask; // 将 IRQ 的相应位置1,屏蔽中断 70 if (irq & 8) 71 outb(cached_slave_mask, PIC_SLAVE_IMR); // IR2 管脚负责 8259A 的级联(见下图),为0时使用主片,为1时使用从片 72 else 73 outb(cached_master_mask, PIC_MASTER_IMR); 74 spin_unlock_irqrestore(&i8259A_lock, flags); // 解开自旋锁 75 }
77 void enable_8259A_irq(unsigned int irq) 78 { 79 unsigned int mask = ~(1 << irq); 80 unsigned long flags; 81 82 spin_lock_irqsave(&i8259A_lock, flags); // 用 spinlock 锁住 83 cached_irq_mask &= mask; // 将 IRQ 的相应位置0,开启中断 84 if (irq & 8) 85 outb(cached_slave_mask, PIC_SLAVE_IMR); // IR2 管脚负责 8259A 的级联(见下图),为0时使用主片,为1时使用从片 86 else 87 outb(cached_master_mask, PIC_MASTER_IMR); 88 spin_unlock_irqrestore(&i8259A_lock, flags); // 解开自旋锁 89 }
PIC 的每个管脚具有优先级,连接号码较小的设备具有较高的中断优先级。
在 PIC 默认的 Full Nested 模式下,通过 PIC 发起中断的流程如下:
PIC 还有优先级轮转模式,即 PIC 在服务完一个管脚之后将其优先级临时降低,并升高未服务管脚的优先级,以实现类似轮询的模式,避免一个管脚持续发出中断导致其他设备“饿死”。
下图是一个典型的 PIC 中断分配,管脚基本上都被古董级设备占据了。
arch/x86/kernel/i8259_32.c 中 8259A 引脚的分配(function init_8259A)
292 outb_pic(0x11, PIC_MASTER_CMD); /* ICW1: select 8259A-1 init */ 293 outb_pic(0x20 + 0, PIC_MASTER_IMR); /* ICW2: 8259A-1 IR0-7 mapped to 0x20-0x27 */ 294 outb_pic(1U << PIC_CASCADE_IR, PIC_MASTER_IMR); /* 8259A-1 (the master) has a slave on IR2 */ 295 if (auto_eoi) /* master does Auto EOI */ 296 outb_pic(MASTER_ICW4_DEFAULT | PIC_ICW4_AEOI, PIC_MASTER_IMR); 297 else /* master expects normal EOI */ 298 outb_pic(MASTER_ICW4_DEFAULT, PIC_MASTER_IMR); 299 300 outb_pic(0x11, PIC_SLAVE_CMD); /* ICW1: select 8259A-2 init */ 301 outb_pic(0x20 + 8, PIC_SLAVE_IMR); /* ICW2: 8259A-2 IR0-7 mapped to 0x28-0x2f */ 302 outb_pic(PIC_CASCADE_IR, PIC_SLAVE_IMR); /* 8259A-2 is a slave on master's IR2 */ 303 outb_pic(SLAVE_ICW4_DEFAULT, PIC_SLAVE_IMR); /* (slave's support for AEOI in flat mode is to be investigated) */
从上图可见,PIC 能接的设备数量实在太少了,而且不支持多处理器。
为了使用 8259A 级联连接较多的设备,可以采用两种方式:
IRQ 共享需要满足两个条件:
IRQ 动态分配:在可能的最后时刻,才把 IRQ 线分配给一个设备。
当然,APIC 是现代的解决方案。即使是 APIC,也需要使用 IRQ 共享。
I/O APIC 的组成为:一组 24 条 IRQ 线,一张 24 项的中断重定向表,可编程寄存器,通过 APIC 总线发送和接收 APIC 信息的一个信息单元。
与 8259A 不同,中断优先级不与引脚号相关联,中断重定向表中的每一项都可以被单独编程以指明中断向量和优先级、目标处理器和选择处理器的方式。
来自外部硬件设备的中断以两种方式在可用 CPU 之间分发:
Intel 提供了三种类型的中断描述符:任务门、中断门及陷阱门描述符。
Linux 使用与 Intel 稍有不同的分类,把中断描述符分为五类:
set_intr_gate(n,addr)
上述系统调用在 IDT 的第 n 个表项插入一个中断门。门中的段选择符设置成内核代码的段选择符,偏移量设置为中断处理程序的地址 addr,DPL 字段设置为0。
set_system_gate(n,addr)
set_system_intr_gate(n,addr)
set_trap_gate(n,addr)
set_task_gate(n,gdt)
门中的段选择符中存放一个TSS的全局描述符表的指针,该TSS中包含要被激活的函数。
在 IDT 中插入门的函数定义在 include/asm-x86/desc.h 中。
这些函数以不同的参数调用内部函数 _set_gate()。_set_gate 调用两个内部函数
38 static inline void pack_gate(gate_desc *gate, unsigned type, unsigned long func, 39 unsigned dpl, unsigned ist, unsigned seg) 40 { 41 gate->offset_low = PTR_LOW(func); // 处理函数低内存偏移 42 gate->segment = __KERNEL_CS; // 内核代码段 43 gate->ist = ist; // ist 44 gate->p = 1; 45 gate->dpl = dpl; // DPL 46 gate->zero0 = 0; 47 gate->zero1 = 0; 48 gate->type = type; // 门类型(宏定义) 49 gate->offset_middle = PTR_MIDDLE(func); // 处理函数中内存偏移 50 gate->offset_high = PTR_HIGH(func); // 处理函数高内存偏移 51 }
在 Linux 中,中断描述符的核心数据结构是 include/linux/irq.h 中的 irq_desc 结构体。每个 irq_desc 实例描述一条中断线。
153 struct irq_desc { 154 irq_flow_handler_t handle_irq; // 中断事件处理函数,下面会介绍 155 struct irq_chip *chip; // irq_chip 指针,描述了一些硬件信息,下面会介绍 156 struct msi_desc *msi_desc; 157 void *handler_data; // chip 中使用的数据 158 void *chip_data; // chip 中使用的数据 159 struct irqaction *action; /* IRQ action list */ // irqaction 指针,下面会介绍 160 unsigned int status; /* IRQ status */ // IRQ 线状态标志 161 162 unsigned int depth; /* nested irq disables */ 163 unsigned int wake_depth; /* nested wake enables */ 164 unsigned int irq_count; /* For detecting broken IRQs */ // 中断计数 165 unsigned int irqs_unhandled; // 无法处理的中断计数 166 unsigned long last_unhandled; /* Aging timer for unhandled count */ 167 spinlock_t lock; // 自旋锁 168 #ifdef CONFIG_SMP 169 cpumask_t affinity; // 多处理器中的处理器亲和性 170 unsigned int cpu; 171 #endif 172 #if defined(CONFIG_GENERIC_PENDING_IRQ) || defined(CONFIG_IRQBALANCE) 173 cpumask_t pending_mask; 174 #endif 175 #ifdef CONFIG_PROC_FS 176 struct proc_dir_entry *dir; // 在 /proc 文件系统中的目录 177 #endif 178 const char *name; // 中断名称 179 } ____cacheline_internodealigned_in_smp;
irq_desc 在 kernel/irq/handle.c 中被使用,此文件是 IRQ 机制的核心入口,描述了各中断线。
50 struct irq_desc irq_desc[NR_IRQS] __cacheline_aligned_in_smp = { 51 [0 ... NR_IRQS-1] = { 52 .status = IRQ_DISABLED, // 默认屏蔽中断 53 .chip = &no_irq_chip, // 没有与 chip 相关联 // 未知(坏的)IRQ 处理程序,输出 IRQ 信息供调试,更新 CPU IRQ 次数计数器,回应 IRQ。 54 .handle_irq = handle_bad_irq, 55 .depth = 1, // 默认是第一层(没有嵌套中断) 56 .lock = __SPIN_LOCK_UNLOCKED(irq_desc->lock), // 还没有自旋锁 57 #ifdef CONFIG_SMP 58 .affinity = CPU_MASK_ALL // 处理器亲和性未定义 59 #endif 60 } 61 };
下面介绍 irq_desc 中的主要数据成员。
这个函数指针是由 kernel/irq/chip.c 中的 __set_irq_handler() 设置的。
99 struct irq_chip { 100 const char *name; // 中断线名称 101 unsigned int (*startup)(unsigned int irq); // 初始化中断的函数指针 102 void (*shutdown)(unsigned int irq); // 停止中断的函数指针 103 void (*enable)(unsigned int irq); // 启用中断的函数指针 104 void (*disable)(unsigned int irq); // 关闭中断的函数指针 105 106 void (*ack)(unsigned int irq); // 确认中断的函数指针 107 void (*mask)(unsigned int irq); // 屏蔽中断的函数指针 108 void (*mask_ack)(unsigned int irq); // 确认并屏蔽中断的函数指针 109 void (*unmask)(unsigned int irq); // 取消屏蔽中断的函数指针 110 void (*eoi)(unsigned int irq); // 中断处理结束的函数指针 111 112 void (*end)(unsigned int irq); 113 void (*set_affinity)(unsigned int irq, cpumask_t dest); // 设置处理器亲和性 114 int (*retrigger)(unsigned int irq); // 重新出发中断 // 设置中断触发类型,根据 IRQ_TYPE 宏定义,包括上边沿、下边沿、边沿、高电平、低电平等 115 int (*set_type)(unsigned int irq, unsigned int flow_type); 116 int (*set_wake)(unsigned int irq, unsigned int on); // 唤醒中断 117 118 /* Currently used only by UML, might disappear one day.*/ 119 #ifdef CONFIG_IRQ_RELEASE_METHOD 120 void (*release)(unsigned int irq, void *dev_id); 121 #endif 122 /* 123 * For compatibility, ->typename is copied into ->name. 124 * Will disappear. 125 */ 126 const char *typename; 127 };
60 struct irqaction { 61 irq_handler_t handler; // 中断处理程序的函数指针 62 unsigned long flags; 63 cpumask_t mask; // 处理器亲和性 64 const char *name; // 中断处理程序名称,显示在 /proc/interrupts 中 65 void *dev_id; // 设备 ID 66 struct irqaction *next; // 指向链表中的下一个 irqaction 结构体 67 int irq; // 中断通道号 68 struct proc_dir_entry *dir; // 在 /proc 文件系统中的目录 69 };
49 #define IRQ_INPROGRESS 0x00000100 /* IRQ handler active - do not enter! */ 50 #define IRQ_DISABLED 0x00000200 /* IRQ disabled - do not enter! */ 51 #define IRQ_PENDING 0x00000400 /* IRQ pending - replay on enable */ 52 #define IRQ_REPLAY 0x00000800 /* IRQ has been replayed but not acked yet */ 53 #define IRQ_AUTODETECT 0x00001000 /* IRQ is being autodetected */ 54 #define IRQ_WAITING 0x00002000 /* IRQ not yet seen - for autodetection */ 55 #define IRQ_LEVEL 0x00004000 /* IRQ level triggered */ 56 #define IRQ_MASKED 0x00008000 /* IRQ masked - shouldn't be seen again */ 57 #define IRQ_PER_CPU 0x00010000 /* IRQ is per CPU */
综上所述,内核中的中断描述符表是一个 irq_desc 数组,数组的每一项描述一根中断线的信息,包括芯片中断处理程序、底层硬件操作函数、注册的中断处理程序链表等。
中断向量表可以通过 /proc/interrupts 查看:
[boj@~]$ cat /proc/interrupts CPU0 CPU1 0: 3652701 2 IO-APIC-edge timer 1: 34517 0 IO-APIC-edge i8042 8: 1 0 IO-APIC-edge rtc0 9: 48512 19 IO-APIC-fasteoi acpi 12: 12 0 IO-APIC-edge i8042 14: 29337 0 IO-APIC-edge ata_piix 15: 38002 0 IO-APIC-edge ata_piix 16: 263352 1 IO-APIC-fasteoi uhci_hcd:usb5, yenta, i915 18: 0 0 IO-APIC-fasteoi uhci_hcd:usb4 19: 105769 0 IO-APIC-fasteoi uhci_hcd:usb3 21: 34677 0 IO-APIC-fasteoi eth0 22: 151 0 IO-APIC-fasteoi firewire_ohci 23: 2 0 IO-APIC-fasteoi ehci_hcd:usb1, uhci_hcd:usb2, mmc0 42: 360215 0 PCI-MSI-edge iwl3945 43: 656 0 PCI-MSI-edge hda_intel NMI: 0 0 Non-maskable interrupts LOC: 253429 2025163 Local timer interrupts SPU: 0 0 Spurious interrupts PMI: 0 0 Performance monitoring interrupts IWI: 0 0 IRQ work interrupts RES: 1063515 1286501 Rescheduling interrupts CAL: 3762 2967 Function call interrupts TLB: 13274 13115 TLB shootdowns TRM: 0 0 Thermal event interrupts THR: 0 0 Threshold APIC interrupts MCE: 0 0 Machine check exceptions MCP: 32 32 Machine check polls ERR: 0 MIS: 0
负责打印 /proc/interrupts 的代码位于 arch/x86/kernel/irq_32.c。
242 int show_interrupts(struct seq_file *p, void *v)
中断机制的初始化分为三步:
trap_init() 定义于 arch/x86/kernel/traps_32.c,作用是设置中断向量。
native_init_IRQ() 定义于 arch/x86/kernel/i8259.c。该函数主要将 IDT 未初始化的各项初始化为中断门。
pre_intr_init_hook() 调用 init_ISA_irqs(),初始化 irq_desc 数组、status、action、depth。
在循环中,对于所有在 FIRST_EXTERNAL_VECTOR(0x20) 与 NR_VECTOR(0x100)之间的、不是系统中断的 256 - 32 - 1 = 223 项,调用 set_intr_gate(),初始化为中断门。
现在我们关心的是,这些中断门的中断处理程序是什么?在 x86 体系结构下没找到 interrupt 数组的定义,因此使用 64 位体系结构的做说明:
// arch/x86/kernel/i8259_64.c 80 static void (*__initdata interrupt[NR_VECTORS - FIRST_EXTERNAL_VECTOR])(void) = { 81 IRQLIST_16(0x2), IRQLIST_16(0x3), 82 IRQLIST_16(0x4), IRQLIST_16(0x5), IRQLIST_16(0x6), IRQLIST_16(0x7), 83 IRQLIST_16(0x8), IRQLIST_16(0x9), IRQLIST_16(0xa), IRQLIST_16(0xb), 84 IRQLIST_16(0xc), IRQLIST_16(0xd), IRQLIST_16(0xe), IRQLIST_16(0xf) 85 };
// arch/x86/kernel/i8259_64.c 70 #define IRQ(x,y) \ 71 IRQ##x##y##_interrupt 72 73 #define IRQLIST_16(x) \ 74 IRQ(x,0), IRQ(x,1), IRQ(x,2), IRQ(x,3), \ 75 IRQ(x,4), IRQ(x,5), IRQ(x,6), IRQ(x,7), \ 76 IRQ(x,8), IRQ(x,9), IRQ(x,a), IRQ(x,b), \ 77 IRQ(x,c), IRQ(x,d), IRQ(x,e), IRQ(x,f)
// include/asm/hw_irq_64.h 155 #define IRQ_NAME2(nr) nr##_interrupt(void) 156 #define IRQ_NAME(nr) IRQ_NAME2(IRQ##nr) 162 #define BUILD_IRQ(nr) \ 163 asmlinkage void IRQ_NAME(nr); \ 164 asm("\n.p2align\n" \ 165 "IRQ" #nr "_interrupt:\n\t" \ 166 "push $~(" #nr ") ; " \ 167 "jmp common_interrupt");
// arch/x86/kernel/entry_32.S 613 common_interrupt: 614 SAVE_ALL 615 TRACE_IRQS_OFF 616 movl %esp,%eax 617 call do_IRQ 618 jmp ret_from_intr 619 ENDPROC(common_interrupt)
// arch/x86/kernel/i8259_64.c 37 #define BI(x,y) \ 38 BUILD_IRQ(x##y) 39 40 #define BUILD_16_IRQS(x) \ 41 BI(x,0) BI(x,1) BI(x,2) BI(x,3) \ 42 BI(x,4) BI(x,5) BI(x,6) BI(x,7) \ 43 BI(x,8) BI(x,9) BI(x,a) BI(x,b) \ 44 BI(x,c) BI(x,d) BI(x,e) BI(x,f) .................................................... 61 BUILD_16_IRQS(0x2) BUILD_16_IRQS(0x3) 62 BUILD_16_IRQS(0x4) BUILD_16_IRQS(0x5) BUILD_16_IRQS(0x6) BUILD_16_IRQS(0x7) 63 BUILD_16_IRQS(0x8) BUILD_16_IRQS(0x9) BUILD_16_IRQS(0xa) BUILD_16_IRQS(0xb) 64 BUILD_16_IRQS(0xc) BUILD_16_IRQS(0xd) BUILD_16_IRQS(0xe) BUILD_16_IRQS(0xf)
中断处理程序不是编译内核时就完全确定的,因此要为开发者留下编程接口。
2.6.17 内核引入了 generic IRQ 机制,支持 i386、x86-64 和 ARM 三个体系结构。generic IRQ 层的引入,是为了剥离 IRQ flow 和 IRQ chip 过于紧密的耦合。为驱动开发者提供通用的 API 来 request/enable/disable/free 中断,而不需要知道任何底层的中断控制器细节。
这些中断 API 是在内核中用 EXPORT_SYMBOL 导出的。
536 int request_irq(unsigned int irq, irq_handler_t handler, 537 unsigned long irqflags, const char *devname, void *dev_id)
参数
内部机制:
435 void free_irq(unsigned int irq, void *dev_id)
参数
内部机制:
153 static void __enable_irq(struct irq_desc *desc, unsigned int irq)
内部调用了 __enable_irq,首先上自旋锁,找到 irq_desc 结构体指针,判断嵌套深度,刷新 IRQ 状态,释放自旋锁。
参数
140 void disable_irq(unsigned int irq)
参数
111 void disable_irq_nosync(unsigned int irq)
30 void synchronize_irq(unsigned int irq)
93 int set_irq_chip(unsigned int irq, struct irq_chip *chip)
122 int set_irq_type(unsigned int irq, unsigned int type)
150 int set_irq_data(unsigned int irq, void *data)
202 int set_irq_chip_data(unsigned int irq, void *data)
本节摘自参考文献之 中断的硬件环境
每个能够发出中断请求的硬件设备控制器都有一条名为 IRQ 的输出线。所有现有的 IRQ 线都与一个名为可编程中断控制器(PIC)的硬件电路的输入引脚相连,可编程中断控制器执行下列动作:
当执行了一条指令后,CS和eip这对寄存器包含下一条将要执行的指令的逻辑地址。在处理那条指令之前,控制单元会检查在运行前一条指令时是否已经发生了一个中断或异常。如果发生了一个中断或异常,那么控制单元执行下列操作:
控制单元所执行的最后一步就是跳转到中断或者异常处理程序。换句话说,处理完中断信号后,控制单元所执行的指令就是被选中处理程序的第一条指令。
中断或异常被处理完后,相应的处理程序必须产生一条iret指令,把控制权转交给被中断的进程,这将迫使控制单元:
Linux 内核的中断处理机制自始至终贯穿着 “重要的事马上做,不重要的事推后做” 的思想。
中断处理程序首先要做:
显然, 这两步都是不可重入的。因此在进入中断服务程序时,CPU 已经自动禁止了本 CPU 上的中断响应。
上章中断初始化过程的分析中,已经介绍了 interrupt 数组的生成过程,其中索引为 n 的元素中存放着下列指令的地址:
pushl n-256 jmp common_interrupt
执行结果是将中断号 - 256 保存在栈中,这样栈中的中断都是负数,而正数用来表示系统调用。这样,系统调用和中断可以用一个有符号整数统一表示。
现在重述一下 common_interrupt 的定义:
// arch/x86/kernel/entry_32.S 613 common_interrupt: 614 SAVE_ALL 615 TRACE_IRQS_OFF 616 movl %esp,%eax # 将栈顶地址放入 eax,这样 do_IRQ 返回时控制转到 ret_from_intr() 617 call do_IRQ # 核心中断处理函数 618 jmp ret_from_intr # 跳转到 ret_from_intr()
其中 SAVE_ALL 宏将被展开成:
cld push %es # 保存除 eflags、cs、eip、ss、esp (已被 CPU 自动保存) 外的其他寄存器 push %ds pushl %eax pushl %ebp pushl %edi pushl %edx pushl %ecx pushl %ebx movl $ _ _USER_DS, %edx movl %edx, %ds # 将用户数据段选择符载入 ds、es movl %edx, %es
前面汇编代码的实质是,以中断发生时寄存器的信息为参数,调用 arch/x86/kernel/irq32.c 中的 do_IRQ 函数。
我们注意到 unlikely 和 unlikely 宏定义,它们的含义是
#define likely(x) __builtin_expect((x),1) #define unlikely(x) __builtin_expect((x),0)
__builtin_expect 是 GCC 的内部机制,意思是告诉编译器哪个分支条件更有可能发生。这使得编译器把更可能发生的分支条件与前面的代码顺序串接起来,更有效地利用 CPU 的指令流水线。
do_IRQ 函数流程:
// kernel/softirq.c 281 void irq_enter(void) 282 { 283 #ifdef CONFIG_NO_HZ // 无滴答内核,它将在需要调度新任务时执行计算并在这个时间设置一个时钟中断,允许处理器在更长的时间内(几秒钟)保持在最低功耗状态,从而减少了电能消耗。 284 int cpu = smp_processor_id(); 285 if (idle_cpu(cpu) && !in_interrupt()) 286 tick_nohz_stop_idle(cpu); // 如果空闲且不在中断中,则停止空闲,开始工作 287 #endif 288 __irq_enter(); 289 #ifdef CONFIG_NO_HZ 290 if (idle_cpu(cpu)) 291 tick_nohz_update_jiffies(); // 更新 jiffies 292 #endif 293 }
// include/linux/hardirq.h 135 #define __irq_enter() \ /* 在宏定义函数中,do { ... } while(0) 结构可以把语句块作为一个整体,就像函数调用,避免宏展开后出现问题 */ 136 do { \ 137 rcu_irq_enter(); \ 138 account_system_vtime(current); \ 139 add_preempt_count(HARDIRQ_OFFSET); \ /* 程序嵌套数量计数器递增1 */ 140 trace_hardirq_enter(); \ 141 } while (0)
IRQ_REPLAY:如果被禁止的中断管脚上产生了中断,这个中断是不会被处理的。当这个中断号被允许产生中断时,会将这个未被处理的中断转为 IRQ_REPLAY。
IRQ_WAITING:探测用,探测时会将所有没有中断处理函数的中断号设为 IRQ_WAITING,只要这个中断管脚上有中断产生,就把这个状态去掉,从而知道哪些中断管脚上产生过中断。
IRQ_PENDING、IRQ_INPROGRESS 是为了确保同一个中断号的处理程序不能重入,且不能丢失这个中断的下一个处理程序。具体地说,当内核在运行某个中断号对应的处理程序时,状态会设置成 IRQ_INPROGRESS。如果发现已经有另一实例在运行了,就将这下一个中断标注为 IRQ_PENDING 并返回。这个已在运行的实例结束的时候,会查看是否期间有同一中断发生了,是则再次执行一遍。
在中断处理过程中,我们反复看到对自旋锁的操作。在单处理器系统上,spinlock 是没有作用的;在多处理器系统上,由于同种类型的中断可能连续产生,同时被几个 CPU 处理(注意,应答中断芯片是紧接着获得自旋锁后,位于整个中断处理流程的前部,因此在中断处理流程的其余部分,中断芯片可以触发新的中断并被另一个 CPU 开始处理),如果没有自旋锁,多个 CPU 可能同时访问 IRQ 描述符,造成混乱。因此在访问 IRQ 描述符的过程中需要有 spinlock 保护。
上面的中断处理流程中隐含了一个问题:整个处理过程是持续占有CPU的(除开中断情况下可能被新的中断打断外),这样
对于第一个问题,较新的 linux 内核增加了 ksoftirqd 内核线程,如果持续处理的软中断超过一定数量,则结束中断处理过程,唤醒 ksoftirqd,由它来继续处理。
对于第二个问题,linux 内核提供了 workqueue(工作队列)机制,定义一个 work 结构(包含了处理函数),然后在上述的中断处理的几个阶段的某一步中调用 schedule_work 函数,work 便被添加到 workqueue 中,等待处理。
工作队列有着自己的处理线程, 这些 work 被推迟到这些线程中去处理。处理过程只可能发生在这些工作线程中,不会发生在内核中断处理路径中,所以可以睡眠。下章将简要介绍这些中断机制。
本节编写一个简单的中断处理程序 (catchirq) 作为内核模块,演示捕获网卡中断。
#include#include #include #include #include #define DEBUG #ifdef DEBUG #define MSG(message, args...) printk(KERN_DEBUG "catchirq: " message, ##args) #else #define MSG(message, args...) #endif MODULE_LICENSE("GPL"); MODULE_AUTHOR("boj"); int irq; char *interface; // module_param(name, type, perm) module_param(irq, int, 0644); module_param(interface, charp, 0644); int irq_handle_function(int irq, void *device_id) { static int count = 1; MSG("[%d] Receive IRQ at %ld\n", count, jiffies); count++; return IRQ_NONE; } int init_module() { if (request_irq(irq, irq_handle_function, IRQF_SHARED, interface, (void *)&irq)) { MSG("[FAILED] IRQ register failure.\n"); return -EIO; } MSG("[OK] Interface=%s IRQ=%d\n", interface, irq); return 0; } void cleanup_module() { free_irq(irq, &irq); MSG("IRQ is freed.\n"); }
obj-m := catchirq.o KERNELDIR := /lib/modules/$(shell uname -r)/build default: make -C $(KERNELDIR) M=$(shell pwd) clean: make -C $(KERNELDIR) M=$(shell pwd) clean
[boj@~/int]$ ls built-in.o catchirq.c catchirq.ko catchirq.mod.c catchirq.mod.o catchirq.o Makefile modules.order Module.symvers
sudo insmod catchirq.ko interface=eth1 irq=21
[boj@~]$ lsmod | grep catchirq catchirq 12636 0
[boj@~/int]$ cat /proc/interrupts CPU0 CPU1 0: 23443709 27 IO-APIC-edge timer 1: 205319 0 IO-APIC-edge i8042 8: 1 0 IO-APIC-edge rtc0 9: 170665 80 IO-APIC-fasteoi acpi 12: 12 0 IO-APIC-edge i8042 14: 135310 0 IO-APIC-edge ata_piix 15: 205712 1 IO-APIC-edge ata_piix 16: 1488409 29 IO-APIC-fasteoi uhci_hcd:usb5, yenta, i915 18: 0 0 IO-APIC-fasteoi uhci_hcd:usb4 19: 477290 5 IO-APIC-fasteoi uhci_hcd:usb3 21: 107049 0 IO-APIC-fasteoi eth0, eth1 22: 806 0 IO-APIC-fasteoi firewire_ohci 23: 2 0 IO-APIC-fasteoi ehci_hcd:usb1, uhci_hcd:usb2, mmc0 42: 1803270 2 PCI-MSI-edge iwl3945 43: 11783 0 PCI-MSI-edge hda_intel NMI: 0 0 Non-maskable interrupts LOC: 2013602 12644870 Local timer interrupts SPU: 0 0 Spurious interrupts PMI: 0 0 Performance monitoring interrupts IWI: 0 0 IRQ work interrupts RES: 6046340 7106551 Rescheduling interrupts CAL: 20110 14839 Function call interrupts TLB: 33385 36028 TLB shootdowns TRM: 0 0 Thermal event interrupts THR: 0 0 Threshold APIC interrupts MCE: 0 0 Machine check exceptions MCP: 172 172 Machine check polls ERR: 0 MIS: 0
// [Time] module_name: [count] Receive IRQ at jiffies [51837.231505] catchirq: [499] Receive IRQ at 12884307 [51837.232803] catchirq: [500] Receive IRQ at 12884308 [51837.232849] catchirq: [501] Receive IRQ at 12884308 [51837.269587] catchirq: [502] Receive IRQ at 12884317 [51844.585799] catchirq: [503] Receive IRQ at 12886146 [51844.586724] catchirq: [504] Receive IRQ at 12886146
sudo rmmod catchirq
[52413.797952] catchirq: [2245] Receive IRQ at 13028449 [52413.815899] catchirq: [2246] Receive IRQ at 13028453 [52413.815990] catchirq: [2247] Receive IRQ at 13028453 [52413.841763] catchirq: IRQ is freed.
软中断、tasklet和工作队列并不是Linux内核中一直存在的机制,而是由更早版本的内核中的“下半部”(bottom half)演变而来。下半部的机制实际上包括五种,但2.6版本的内核中,下半部和任务队列的函数都消失了,只剩下了前三者。
上半部指的是中断处理程序,下半部则指的是一些虽然与中断有相关性但是可以延后执行的任务。举个例子:在网络传输中,网卡接收到数据包这个事件不一定需要马上被处理,适合用下半部去实现;但是用户敲击键盘这样的事件就必须马上被响应,应该用中断实现。
两者的主要区别在于:中断不能被相同类型的中断打断,而下半部依然可以被中断打断;中断对于时间非常敏感,而下半部基本上都是一些可以延迟的工作。由于二者的这种区别,所以对于一个工作是放在上半部还是放在下半部去执行,有一些参考标准:
软中断作为下半部机制的代表,是随着 SMP 的出现应运而生的,它也是tasklet实现的基础(tasklet实际上只是在软中断的基础上添加了一定的机制)。软中断一般是“可延迟函数”的总称,有时候也包括了tasklet(请读者在遇到的时候根据上下文推断是否包含tasklet)。它的出现就是因为要满足上面所提出的上半部和下半部的区别,使得对时间不敏感的任务延后执行,而且可以在多个CPU上并行执行,使得总的系统效率可以更高。特性是:
由于软中断必须使用可重入函数,这就导致设计上的复杂度变高,作为设备驱动程序的开发者来说,增加了负担。而如果某种应用并不需要在多个CPU上并行执行,那么软中断其实是没有必要的。因此诞生了弥补以上两个要求的tasklet。它具有以下特性:
tasklet是在两种软中断类型的基础上实现的,因此如果不需要软中断的并行特性,tasklet就是最好的选择。
一般而言,在可延迟函数上可以执行四种操作:初始化/激活/执行/屏蔽。屏蔽我们这里不再叙述,前三个则比较重要。下面将软中断和tasklet的三个步骤分别进行对比介绍。
初始化
初始化是指在可延迟函数准备就绪之前所做的所有工作。一般包括两个大步骤:首先是向内核声明这个可延迟函数,以备内核在需要的时候调用;然后就是调用相应的初始化函数,用函数指针等初始化相应的描述符。
如果是软中断则在内核初始化时进行,其描述符定义如下:
struct softirq_action { void (*action)(struct softirq_action *); void *data; };
kernel/softirq.c 中的软中断描述符数组:static struct softirq_action softirq_vec[32]
前 6 个已经被内核注册使用:
其余的软中断描述符可以由内核开发者使用。
void open_softirq(int nr, void (*action)(struct softirq_action*), void *data)
例如网络子系统通过以下两个函数初始化软中断(net_tx_action/net_rx_action是两个函数):
open_softirq(NET_TX_SOFTIRQ, net_tx_action); open_softirq(NET_RX_SOFTIRQ, net_rx_action);
当内核中产生 NET_TX_SOFTIRQ 软中断之后,就会调用 net_tx_action 这个函数。
tasklet 则可以在运行时定义,例如加载模块时。定义方式有两种:
DECLARE_TASKET(name, func, data) DECLARE_TASKLET_DISABLED(name, func, data)
void tasklet_init(struct tasklet_struct *t, void (*func)(unsigned long), unsigned long data)
其参数分别为描述符,需要调用的函数和此函数的参数。初始化生成的就是一个实际的描述符,假设为 my_tasklet。
激活
激活:标记一个可延迟函数为挂起(pending)状态,表示内核可以调用这个可延迟函数。类似处于 TASK_RUNNING 状态的进程,处在这个状态的进程只是准备好了被 CPU 调度,但并不一定马上就会被调度。
软中断使用 raise_softirq() 函数激活,接收的参数就是上面初始化时用到的数组索引 nr。
tasklet 使用 tasklet_schedule() 激活,该函数接受 tasklet 的描述符作为参数,例如上面生成的 my_tasklet:
tasklet_schedule(&my_tasklet)
执行
执行就是内核运行可延迟函数的过程,但是执行只发生在某些特定的时刻。
每个CPU上都有一个32位的掩码__softirq_pending,表明此CPU上有哪些挂起(已被激活)的软中断。此掩码可以用local_softirq_pending()宏获得。所有的挂起的软中断需要用do_softirq()函数的一个循环来处理。
对于 tasklet,软中断初始化时设置了发生 TASKLET_SOFTIRQ 或 HI_SOFTIRQ 软中断时所执行的函数:
open_softirq(TASKLET_SOFTIRQ, tasklet_action, NULL); open_softirq(HI_SOFTIRQ, tasklet_hi_action, NULL);
tasklet_action 和 tasklet_hi_action 内部实现不同,软中断和 tasklet 因此具有了不同的特性。
上面的可延迟函数运行在中断上下文中(如上章所述,软中断的一个检查点就是 do_IRQ 退出的时候),于是导致了一些问题:软中断不能睡眠、不能阻塞。由于中断上下文出于内核态,没有进程切换,所以如果软中断一旦睡眠或者阻塞,将无法退出这种状态,导致内核会整个僵死。但可阻塞函数不能用在中断上下文中实现,必须要运行在进程上下文中,例如访问磁盘数据块的函数。因此,可阻塞函数不能用软中断来实现。但是它们往往又具有可延迟的特性。
因此在 2.6 版的内核中出现了在内核态运行的工作队列(替代了 2.4 内核中的任务队列)。它也具有一些可延迟函数的特点(需要被激活和延后执行),但是能够能够在不同的进程间切换,以完成不同的工作。