导航

Linux源代码阅读——中断

目录

  1. 为什么要有中断
  2. Linux 中断机制
  3. 中断处理过程
  4. 软中断、tasklet与工作队列

1 为什么要有中断

1.1 中断的作用

处理器的运算速度一般要比外部硬件快很多。以读取硬盘为例,如果是简单的顺序执行,CPU 必须等待很长时间,不停地轮询硬盘是否读取完毕,这会浪费很多 CPU 时间。中断提供了这样一种机制,使得读取硬盘这样的操作可以交给硬件来完成,CPU 挂起当前进程,将控制权转交给其他进程,待硬件处理完毕后通知 CPU,操作系统把当前进程设为活动的,从而允许该进程继续执行,处理读取硬盘的结果。

另一方面,有些事件不是程序本身可预见的,需要硬件以某种方式告诉进程。例如时钟中断为定时器提供了基础,如果没有时钟中断,程序只能每执行几条指令就检查一下当前系统时间,这在效率上是不可接受的。

从广义上说,中断是改变 CPU 处理指令顺序的硬件信号。分为两类:

1.2 中断的处理原则

中断处理的基本原则就是“快”。如果反应慢了,数据可能丢失或被覆盖。例如键盘按键中断,所按下的键的 keycode 放在 KBDR 寄存器中,如果在中断被处理之前用户又按了一个键,则 KBDR 的值被新按下的键的 keycode 覆盖,早先按下的键对应的数据就丢失了。

当一个中断信号到达时,CPU 必须停止当前所做的事,转而处理中断信号。为了尽快处理中断并为接收下一个中断做好准备,内核应尽快处理完一个中断,将更多的处理向后推迟。

为达到“快”这一目标,内核允许不同类型的中断嵌套发生,即在中断处理的临界区之外可以接受新的中断。这样,更多的 I/O 设备将处于忙状态。

2 Linux 中断机制

2.1 中断控制器

中断控制器是连接设备和 CPU 的桥梁,一个设备产生中断后,需要经过中断控制器的转发,才能最终到达 CPU。时代发展至今,中断控制器经历了 PIC(Programmable Interrupt Controller,可编程中断控制器) 和 APIC (Advanced Programmable Interrupt Controller,高级可编程中断控制器) 两个阶段。前者在 UP(Uni-processor,单处理器) 上威震四方,随着 SMP (Symmetric Multiple Processor,对称多处理器) 的流行,APIC 已广为流行并将最终取代 PIC。

8259A (PIC) 管脚图

上图中的管脚说明:

8259A 中的寄存器:

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 级联连接较多的设备,可以采用两种方式:

当然,APIC 是现代的解决方案。即使是 APIC,也需要使用 IRQ 共享。

I/O APIC 的组成为:一组 24 条 IRQ 线,一张 24 项的中断重定向表,可编程寄存器,通过 APIC 总线发送和接收 APIC 信息的一个信息单元。

与 8259A 不同,中断优先级不与引脚号相关联,中断重定向表中的每一项都可以被单独编程以指明中断向量和优先级、目标处理器和选择处理器的方式。

来自外部硬件设备的中断以两种方式在可用 CPU 之间分发:

2.2 中断描述符

Intel 提供了三种类型的中断描述符:任务门、中断门及陷阱门描述符。

Linux 使用与 Intel 稍有不同的分类,把中断描述符分为五类:

在 IDT 中插入门的函数定义在 include/asm-x86/desc.h 中。

这些函数以不同的参数调用内部函数 _set_gate()。_set_gate 调用两个内部函数

2.3 中断数据结构

在 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 中的主要数据成员。

handle_irq
handle_irq 是函数指针,指向 kernel/irq/chip.c 中的中断事件处理函数。

这个函数指针是由 kernel/irq/chip.c 中的 __set_irq_handler() 设置的。

chip
chip 是 irq_chip 结构体指针,include/linux/irq.h 中的 irq_chip 结构体定义了对每根中断线的底层硬件操作:
 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 };
action
action 是 irqaction 结构体指针,指向一个 irqaction 链表。irqaction 在 include/linux/interrupt.h 中定义,每个结构体描述一个中断处理程序。
 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 };
status
status 是描述 IRQ 线状态的一组标志。在同一文件中宏定义:
 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)

2.4 中断的初始化

中断机制的初始化分为三步:

  1. arch/x86/kernel/head_32.S 中 setup IDT,在内核引导分析报告中已经阐述。
  2. init/main.c 的 start_kernel() 中的 trap_init()
  3. init/main.c 的 start_kernel() 中的 init_IRQ()
trap_init()

trap_init() 定义于 arch/x86/kernel/traps_32.c,作用是设置中断向量。

  1. 初始化 APIC 映射表
  2. 调用 set_trap_gate、set_intr_gate、set_task_gate、set_system_gate 等,初始化中断描述符表。
  3. 调用 set_system_gate,初始化系统调用
  4. 将已设置的中断向量置保留位
  5. 将已设置的系统调用置保留位
  6. 初始化 CPU 作为屏障
  7. 执行 trap_init 的钩子函数
init_IRQ
init_IRQ() 定义于 arch/x86/kernel/paravirt.c,由 paravirt_ops.init_IRQ() 和 native_init_IRQ() 二者组成。

native_init_IRQ() 定义于 arch/x86/kernel/i8259.c。该函数主要将 IDT 未初始化的各项初始化为中断门。

2.5 内核接口

中断处理程序不是编译内核时就完全确定的,因此要为开发者留下编程接口。

2.6.17 内核引入了 generic IRQ 机制,支持 i386、x86-64 和 ARM 三个体系结构。generic IRQ 层的引入,是为了剥离 IRQ flow 和 IRQ chip 过于紧密的耦合。为驱动开发者提供通用的 API 来 request/enable/disable/free 中断,而不需要知道任何底层的中断控制器细节。

这些中断 API 是在内核中用 EXPORT_SYMBOL 导出的。

请求中断
kernel/irq/manage.c 中的 request_irq:
536 int request_irq(unsigned int irq, irq_handler_t handler,
537                 unsigned long irqflags, const char *devname, void *dev_id)

参数

内部机制:

  1. 检查输入数据的合法性
  2. 为临时变量 irqaction 分配内存空间,初始化 irqaction 数据结构
  3. 如果是调试模式,测试是否运行正常
  4. 进入工作函数 setup_irq(unsigned int irq, struct irqaction *new)
    1. 如果是 IRQF_SAMPLE_RANDOM 模式,随机初始化 irq
    2. 上自旋锁
    3. 如果希望共享中断通道,所有中断处理程序需要有相同的触发特性标识、PERCPU 特性
    4. 把新 irqaction 结构体挂在链表尾部
    5. 如果设置了 IRQF_TRIGGER_MASK,初始化触发特性
    6. 初始化 irq 状态、嵌套深度
    7. 启动(enable)此 IRQ
    8. 释放自旋锁
    9. 调用 /kernel/irq/proc.c 中的 register_irq_proc() 和 register_handler_proc(),建立 /proc 文件系统中的相关数据结构
    10. 返回成功(0)
    11. 如果出错,输出内核调试信息,释放自旋锁,返回错误
  5. 释放 irqaction 的内存空间,返回 setup_irq 的返回值
清除中断
kernel/irq/manage.c 中的 free_irq:
435 void free_irq(unsigned int irq, void *dev_id)

参数

内部机制:

  1. 不能在中断上下文中调用
  2. 上自旋锁
  3. 循环,沿链表查找要删除的中断处理程序
    1. 如果发现是已经释放的,则输出内核调试信息,释放自旋锁
    2. 如果 dev_id 不对,沿着 irqaction 链表继续向下寻找
    3. 如果找到了,从链表中移除这个 irqaction
    4. 关闭此 IRQ,关闭硬件,释放自旋锁,从 /proc 文件系统中删除对应目录
    5. 同步 IRQ 以防正在其他 CPU 上运行
    6. 如果是调试模式,测试驱动程序是否知道此共享 IRQ 已移除
    7. 释放内存空间,返回
启用中断
kernel/irq/manage.c 中的 enable_irq:
153 static void __enable_irq(struct irq_desc *desc, unsigned int irq)

内部调用了 __enable_irq,首先上自旋锁,找到 irq_desc 结构体指针,判断嵌套深度,刷新 IRQ 状态,释放自旋锁。

参数

关闭中断
kernel/irq/manage.c 中的 disable_irq:
140 void disable_irq(unsigned int irq)

参数

关闭中断 (无等待)
disable_irq 会保证存在的 IRQ handler 完成操作,而 disable_irq_nosync 立即关中断并返回。事实上,disable_irq 首先调用 disable_irq_nosync,然后调用 synchronize_irq 同步。
111 void disable_irq_nosync(unsigned int irq)
同步中断 (多处理器)
 30 void synchronize_irq(unsigned int irq)
设置 IRQ 芯片
kernel/irq/chip.c: set_irq_chip()
 93 int set_irq_chip(unsigned int irq, struct irq_chip *chip)
设置 IRQ 类型
kernel/irq/chip.c: set_irq_type()
122 int set_irq_type(unsigned int irq, unsigned int type)
设置 IRQ 数据
kernel/irq/chip.c: set_irq_data()
150 int set_irq_data(unsigned int irq, void *data)
设置 IRQ 芯片数据
kernel/irq/chip.c: set_irq_chip_data()
202 int set_irq_chip_data(unsigned int irq, void *data)

3 中断处理流程

3.1 CPU的中断处理流程

本节摘自参考文献之 中断的硬件环境

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

  1. 监视 IRQ 线,检查产生的信号。如果有两条以上的 IRQ 线上产生信号,就选择引脚编号较小的 IRQ 线。
  2. 如果一个引发信号出现在 IRQ 线上:
    1. 把接收到的引发信号转换成对应的向量号
    2. 把这个向量存放在中断控制器的一个 I/O 端口(0x20、0x21),从而允许 CPU 通过数据总线读此向量。
    3. 把引发信号发送到处理器的 INTR 引脚,即产生一个中断。
    4. 等待,直到 CPU 通过把这个中断信号写进可编程中断控制器的一个 I/O 端口来确认它;当这种情况发生时,清 INTR 线。
  3. 返回第1步。

当执行了一条指令后,CS和eip这对寄存器包含下一条将要执行的指令的逻辑地址。在处理那条指令之前,控制单元会检查在运行前一条指令时是否已经发生了一个中断或异常。如果发生了一个中断或异常,那么控制单元执行下列操作:

  1. 确定与中断或异常关联的向量i (0 ≤ i ≤ 255)。
  2. 读由idtr寄存器指向的 IDT表中的第i项(在下面的分析中,我们假定IDT表项中包含的是一个中断门或一个陷阱门)。
  3. 从gdtr寄存器获得GDT的基地址,并在GDT中查找,以读取IDT表项中的选择符所标识的段描述符。这个描述符指定中断或异常处理程序所在段的基地址。
  4. 确信中断是由授权的(中断)发生源发出的。首先将当前特权级CPL(存放在cs寄存器的低两位)与段描述符(存放在GDT中)的描述符特权级DPL比较,如果CPL小于DPL,就产生一个“General protection”异常,因为中断处理程序的特权不能低于引起中断的程序的特权。对于编程异常,则做进一步的安全检查:比较CPL与处于IDT中的门描述符的DPL,如果DPL小于CPL,就产生一个“General protection”异常。这最后一个检查可以避免用户应用程序访问特殊的陷阱门或中断门。
  5. 检查是否发生了特权级的变化,也就是说,CPL是否不同于所选择的段描述符的DPL。如果是,控制单元必须开始使用与新的特权级相关的栈。通过执行以下步骤来做到这点:
    1. 读tr寄存器,以访问运行进程的TSS段。
    2. 用与新特权级相关的栈段和栈指针的正确值装载ss和esp寄存器。这些值可以在TSS中找到(参见第三章的“任务状态段”一节)
    3. 在新的栈中保存ss和esp以前的值,这些值定义了与旧特权级相关的栈的逻辑地址。
  6. 如果故障已发生,用引起异常的指令地址装载CS和eip寄存器,从而使得这条指令能再次被执行。
  7. 在栈中保存eflags、CS及eip的内容。
  8. 如果异常产生了一个硬件出错码,则将它保存在栈中。
  9. 装载cs和eip寄存器,其值分别是IDT表中第i项门描述符的段选择符和偏移量字段。这些值给出了中断或者异常处理程序的第一条指令的逻辑地址。

控制单元所执行的最后一步就是跳转到中断或者异常处理程序。换句话说,处理完中断信号后,控制单元所执行的指令就是被选中处理程序的第一条指令。

中断或异常被处理完后,相应的处理程序必须产生一条iret指令,把控制权转交给被中断的进程,这将迫使控制单元:

  1. 用保存在栈中的值装载CS、eip或eflags寄存器。如果一个硬件出错码曾被压入栈中,并且在eip内容的上面,那么,执行iret指令前必须先弹出这个硬件出错码。
  2. 检查处理程序的CPL是否等于CS中最低两位的值(这意味着被中断的进程与处理程序运行在同一特权级)。如果是,iret终止执行;否则,转入下一步。
  3. 从栈中装载ss和esp寄存器,因此,返回到与旧特权级相关的栈。
  4. 检查ds、es、fs及gs段寄存器的内容,如果其中一个寄存器包含的选择符是一个段描述符,并且其DPL值小于CPL,那么,清相应的段寄存器。控制单元这么做是为了禁止用户态的程序(CPL=3)利用内核以前所用的段寄存器(DPL=0)。如果不清这些寄存器,怀有恶意的用户态程序就可能利用它们来访问内核地址空间。

3.2 保存中断信息

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

3.3 处理中断

前面汇编代码的实质是,以中断发生时寄存器的信息为参数,调用 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 函数流程:

  1. 保存寄存器上下文
  2. 调用 irq_enter:
    // 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)
    
  3. 如果可用空间不足 1KB,可能会引发栈溢出,输出内核错误信息
  4. 如果 thread_union 是 4KB 的,进行一些特殊处理
  5. 调用 desc->handle_irq(irq, desc),调用 __do_IRQ() (kernel/irq/handle.c)
    1. 取得中断号,获取对应的 irq_desc
    2. 如果是 CPU 内部中断,不需要上锁,简单处理完就返回了
    3. 上自旋锁
    4. 应答中断芯片,这样中断芯片就能开始接受新的中断了。
    5. 更新中断状态。

      IRQ_REPLAY:如果被禁止的中断管脚上产生了中断,这个中断是不会被处理的。当这个中断号被允许产生中断时,会将这个未被处理的中断转为 IRQ_REPLAY。

      IRQ_WAITING:探测用,探测时会将所有没有中断处理函数的中断号设为 IRQ_WAITING,只要这个中断管脚上有中断产生,就把这个状态去掉,从而知道哪些中断管脚上产生过中断。

      IRQ_PENDING、IRQ_INPROGRESS 是为了确保同一个中断号的处理程序不能重入,且不能丢失这个中断的下一个处理程序。具体地说,当内核在运行某个中断号对应的处理程序时,状态会设置成 IRQ_INPROGRESS。如果发现已经有另一实例在运行了,就将这下一个中断标注为 IRQ_PENDING 并返回。这个已在运行的实例结束的时候,会查看是否期间有同一中断发生了,是则再次执行一遍。

    6. 如果链表上没有中断处理程序,或者中断被禁止,或者已经有另一实例在运行,则进行收尾工作。
    7. 循环:
      1. 释放自旋锁
      2. 执行函数链:handle_IRQ_event()。其中主要是一个循环,依次执行中断处理程序链表上的函数,并根据返回值更新中断状态。如果愿意,可以参与随机数采样。中断处理程序执行期间,打开本地中断。
      3. 上自旋锁
      4. 如果当前中断已经处理完,则退出;不然取消中断的 PENDING 标志,继续循环。
    8. 取消中断的 INPROGRESS 标志
    9. 收尾工作:有的中断在处理过程中被关闭了,->end() 处理这种情况;释放自旋锁。
  6. 执行 irq_exit(),在 kernel/softirq.c 中:
    1. 递减中断计数器
    2. 检查是否有软中断在等待执行,若有则执行软中断。
    3. 如果使用了无滴答内核看是不是该休息了。
  7. 恢复寄存器上下文,跳转到 ret_from_intr (跳转点早在 common_interrupt 中就被指定了)

在中断处理过程中,我们反复看到对自旋锁的操作。在单处理器系统上,spinlock 是没有作用的;在多处理器系统上,由于同种类型的中断可能连续产生,同时被几个 CPU 处理(注意,应答中断芯片是紧接着获得自旋锁后,位于整个中断处理流程的前部,因此在中断处理流程的其余部分,中断芯片可以触发新的中断并被另一个 CPU 开始处理),如果没有自旋锁,多个 CPU 可能同时访问 IRQ 描述符,造成混乱。因此在访问 IRQ 描述符的过程中需要有 spinlock 保护。

3.4 从中断中返回

上面的中断处理流程中隐含了一个问题:整个处理过程是持续占有CPU的(除开中断情况下可能被新的中断打断外),这样

对于第一个问题,较新的 linux 内核增加了 ksoftirqd 内核线程,如果持续处理的软中断超过一定数量,则结束中断处理过程,唤醒 ksoftirqd,由它来继续处理。

对于第二个问题,linux 内核提供了 workqueue(工作队列)机制,定义一个 work 结构(包含了处理函数),然后在上述的中断处理的几个阶段的某一步中调用 schedule_work 函数,work 便被添加到 workqueue 中,等待处理。

工作队列有着自己的处理线程, 这些 work 被推迟到这些线程中去处理。处理过程只可能发生在这些工作线程中,不会发生在内核中断处理路径中,所以可以睡眠。下章将简要介绍这些中断机制。

3.5 编写中断处理程序

本节编写一个简单的中断处理程序 (catchirq) 作为内核模块,演示捕获网卡中断。

  1. catchirq.c
    #include <linux/module.h>
    #include <linux/kernel.h>
    #include <linux/init.h>  
    #include <linux/interrupt.h> 
    #include <linux/timer.h>
    
    #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");
    }
    
  2. Makefile(编写说明参见 Documentation/kbuild/)
    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
    
  3. 命令:make
  4. [boj@~/int]$ ls
    built-in.o  catchirq.c  catchirq.ko  catchirq.mod.c  catchirq.mod.o  catchirq.o  Makefile  modules.order  Module.symvers
    
  5. 查看 /proc/interrupts(前面章节已经贴出来了),获知我们想截获的网卡(eth0)是 21 号中断。通过 insmod 的 interface 和 irq 指定模块加载参数(源文件中的 module_params 指定的)
    sudo insmod catchirq.ko interface=eth1 irq=21
  6. 成功插入一个内核模块:
    [boj@~]$ lsmod | grep catchirq
    catchirq               12636  0 
    
  7. 我们看到,/proc/interrupts 的 21 号中断增加了一个中断处理程序:eth1
    [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
    
  8. dmesg 中可以看到大量如下形式的内核信息。这恰好是我们在源码中的 DEBUG 模式通过 printk 输出的。
    // [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
    
  9. 演示完毕,卸载内核模块:
    sudo rmmod catchirq
  10. 根据 dmesg,catchirq 模块输出了最后一句话,被正常卸载。从 /proc/interrupts 看到,中断处理程序表恢复原状。
    [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.
    

4 软中断、tasklet与工作队列

4.1 上半部与下半部

软中断、tasklet和工作队列并不是Linux内核中一直存在的机制,而是由更早版本的内核中的“下半部”(bottom half)演变而来。下半部的机制实际上包括五种,但2.6版本的内核中,下半部和任务队列的函数都消失了,只剩下了前三者。

上半部指的是中断处理程序,下半部则指的是一些虽然与中断有相关性但是可以延后执行的任务。举个例子:在网络传输中,网卡接收到数据包这个事件不一定需要马上被处理,适合用下半部去实现;但是用户敲击键盘这样的事件就必须马上被响应,应该用中断实现。

两者的主要区别在于:中断不能被相同类型的中断打断,而下半部依然可以被中断打断;中断对于时间非常敏感,而下半部基本上都是一些可以延迟的工作。由于二者的这种区别,所以对于一个工作是放在上半部还是放在下半部去执行,有一些参考标准:

4.2 软中断

软中断作为下半部机制的代表,是随着 SMP 的出现应运而生的,它也是tasklet实现的基础(tasklet实际上只是在软中断的基础上添加了一定的机制)。软中断一般是“可延迟函数”的总称,有时候也包括了tasklet(请读者在遇到的时候根据上下文推断是否包含tasklet)。它的出现就是因为要满足上面所提出的上半部和下半部的区别,使得对时间不敏感的任务延后执行,而且可以在多个CPU上并行执行,使得总的系统效率可以更高。特性是:

4.3 tasklet

由于软中断必须使用可重入函数,这就导致设计上的复杂度变高,作为设备驱动程序的开发者来说,增加了负担。而如果某种应用并不需要在多个CPU上并行执行,那么软中断其实是没有必要的。因此诞生了弥补以上两个要求的tasklet。它具有以下特性:

tasklet是在两种软中断类型的基础上实现的,因此如果不需要软中断的并行特性,tasklet就是最好的选择。

一般而言,在可延迟函数上可以执行四种操作:初始化/激活/执行/屏蔽。屏蔽我们这里不再叙述,前三个则比较重要。下面将软中断和tasklet的三个步骤分别进行对比介绍。

4.4 工作队列

上面的可延迟函数运行在中断上下文中(如上章所述,软中断的一个检查点就是 do_IRQ 退出的时候),于是导致了一些问题:软中断不能睡眠、不能阻塞。由于中断上下文出于内核态,没有进程切换,所以如果软中断一旦睡眠或者阻塞,将无法退出这种状态,导致内核会整个僵死。但可阻塞函数不能用在中断上下文中实现,必须要运行在进程上下文中,例如访问磁盘数据块的函数。因此,可阻塞函数不能用软中断来实现。但是它们往往又具有可延迟的特性。

因此在 2.6 版的内核中出现了在内核态运行的工作队列(替代了 2.4 内核中的任务队列)。它也具有一些可延迟函数的特点(需要被激活和延后执行),但是能够能够在不同的进程间切换,以完成不同的工作。

参考文献


Copyright © 2012 李博杰 PB10000603

This document is available from http://home.ustc.edu.cn/~boj/courses/linux_kernel/2_int.html