Linux源代码阅读——中断

导航

  • 环境准备
  • 内核引导
  • 中断

Linux源代码阅读——中断

目录

  1. 为什么要有中断
    • 中断的作用
    • 中断的处理原则
  2. Linux 中断机制
    • 中断控制器
    • 中断描述符
    • 中断数据结构
    • 中断的初始化
    • 内核接口
  3. 中断处理过程
    • CPU 的中断处理流程
    • 保存中断信息
    • 处理中断
    • 从中断中返回
    • 编写中断处理程序
  4. 软中断、tasklet与工作队列
    • 上半部与下半部
    • 软中断
    • tasklet
    • 工作队列

1 为什么要有中断

1.1 中断的作用

处理器的运算速度一般要比外部硬件快很多。以读取硬盘为例,如果是简单的顺序执行,CPU 必须等待很长时间,不停地轮询硬盘是否读取完毕,这会浪费很多 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。

Linux源代码阅读——中断_第1张图片

8259A (PIC) 管脚图

上图中的管脚说明:

  • IR0~IR7 (Interrupt Request0~7,用于连接设备)
  • INT (连接 CPU,当有中断请求时,拉高该管脚以通知 CPU 中断的到来)
  • INTA (连接 CPU,CPU 通过该管脚应答中断请求,并通知 PIC 提交中断的 vector 到数据线)
  • CS (片选,用于将两个 8259A 串联成可连接 15 个设备的 PIC)

8259A 中的寄存器:

  • ICW: Initialization Command Word,初始化命令寄存器,用于初始化 8259A
  • OCW: Operation Command Word,操作命令字,用于控制 8259A
  • IRR: Interrupt Request Register,中断请求寄存器,共 8bit,对应 IR0~IR7 八个中断管脚。当某个管脚的中断请求到来后,若该管脚没有被屏蔽,IRR 中对应的 bit 被置1。表示 PIC 已经收到设备的中断请求,但还未提交给 CPU。
  • ISR: In Service Register,服务中寄存器,共 8bit,每 bit 意义同上。当 IRR 中的某个中断请求被发送给 CPU 后,ISR 中对应的 bit 被置1。表示中断已发送给 CPU,但 CPU 还未处理完。
  • IMR: Interrupt Mask Register,中断屏蔽寄存器,共 8bit,每 bit 意义同上。用于屏蔽中断。当某 bit 置1时,对应的中断管脚被屏蔽。

    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 发起中断的流程如下:

  • 一个或多个 IR 管脚上产生电平信号,若对应的中断没有被屏蔽,IRR 中相应的 bit 被置1。
  • PIC 拉高 INT 管脚通知 CPU 中断发生。
  • CPU 通过 INTA 管脚应答 PIC,表示中断请求收到。
  • PIC 收到 INTA 应答后,将 IRR 中具有最高优先级的 bit 清零,并设置 ISR 中对应的 bit。
  • CPU 通过 INTA 管脚第二次发出脉冲,PIC 收到后计算最高优先级中断的 vector,并将它提交到数据线上。
  • 等待 CPU 写 EOI (End of Interrupt)。收到 EOI 后,ISR 中最高优先级的 bit 被清零。如果 PIC 处于 AEOI 模式,当第二个 INTA 脉冲收到后,ISR 中最高优先级的 bit 自动清零。

PIC 还有优先级轮转模式,即 PIC 在服务完一个管脚之后将其优先级临时降低,并升高未服务管脚的优先级,以实现类似轮询的模式,避免一个管脚持续发出中断导致其他设备“饿死”。

下图是一个典型的 PIC 中断分配,管脚基本上都被古董级设备占据了。

Linux源代码阅读——中断_第2张图片

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 共享:中断处理程序执行多个中断服务程序(ISR),每个 ISR 是一个与共享 IRQ 线相关的函数。

    IRQ 共享需要满足两个条件:

    • 每个 ISR 都愿意共享 IRQ,即 request_irq() 时指定了 IRQF_SHARED
    • 所有 ISR 具有相同的触发条件(电平触发或边沿触发、高低电平或上下边沿)
  • IRQ 动态分配:在可能的最后时刻,才把 IRQ 线分配给一个设备。

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

Linux源代码阅读——中断_第3张图片

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

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

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

  • 静态分发
  • 动态分发

2.2 中断描述符

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

Linux源代码阅读——中断_第4张图片

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

  • 中断门(interrupt gate):用户态的进程不能访问Intel中断门(门的DPL字段为0)。所有的Linux中断处理程序都通过中断门激活,并全部限制在内核态。
    set_intr_gate(n,addr)

    上述系统调用在 IDT 的第 n 个表项插入一个中断门。门中的段选择符设置成内核代码的段选择符,偏移量设置为中断处理程序的地址 addr,DPL 字段设置为0。

  • 系统门(system gate):用户态的进程可以访问Intel陷阱门(门的DPL字段为3)。通过系统门来激活三个Linux异常处理程序,它们的向量是4,5及128,因此,在用户态下,可以发布into、bound及int $0x80三条汇编语言指令。
    set_system_gate(n,addr)
  • 系统中断门(system interrupt gate):能够被用户态进程访问的Intel中断门(门的DPL字段为3)。与向量3相关的异常处理程序是由系统中断门激活的,因此,在用户态可以使用汇编语言指令int3。
    set_system_intr_gate(n,addr)
  • 陷阱门(trap gate):用户态的进程不能访问的一个Intel陷阱门(门的DPL字段为0)。大部分Linux异常处理程序都通过陷阱门来激活。
    set_trap_gate(n,addr)
  • 任务门(task gate):不能被用户态进程访问的Intel任务门(门的DPL字段为0)。Linux对“Double fault”异常的处理程序是由任务门激活的。
    set_task_gate(n,gdt)

    门中的段选择符中存放一个TSS的全局描述符表的指针,该TSS中包含要被激活的函数。

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

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

  • pack_gate: 设置门的数据结构:中断号、门类型、处理函数地址、DPL、ist、目录段寄存器
     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 }
    
  • write_idt_entry: 宏定义为 native_write_idt_entry,用 memcpy 将设置好的门写入 IDT。

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 中的中断事件处理函数。
  • handle_simple_irq
  • handle_level_irq
  • handle_fasteoi_irq
  • handle_edge_irq
  • handle_percpu_irq

这个函数指针是由 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 未初始化的各项初始化为中断门。

  • 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 };
    
  • 以上是 interrupt 数组的定义。在下面的代码中,## 是将字符串连接起来,这样宏定义的函数 IRQ(0x4,6) 就是 IRQ0x46_interrupt,生成 224 个这样的函数填入数组。
    // 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)
    
  • 那么这 224 个函数在哪里呢?通过下面的宏可以生成一个汇编函数,它调用了 common_interrupt 函数:
    // 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");
    
  • common_interrupt 是汇编函数,这个函数最终调用了 do_IRQ,这是我们下章要介绍的核心中断处理函数。
    // 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)
    
  • 看来,只需要调用 BUILD_IRQ 就能生成中断处理函数了。Linux Kernel 正是这样做的:
    // 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)
    
  • 不得不发表一下感慨,Linux Kernel 对 C 语言宏的运用真是炉火纯青。如果是我写代码,很可能不得不先写个代码生成器,用它生成 224 个函数的源码。但 Linux Kernel Source 里出现几百个几乎一模一样的函数太不优雅了,于是利用 C 语言的预处理机制,采用二级宏定义,尽可能降低代码量。

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)

参数

  • irq: 中断通道号,无符号整数
  • handler:中断处理程序的函数指针 (irq_return_t isr_func(int irq, void *dev_id))
  • irq_flags:标志位
    • IRQF_SHARED: 共享中断通道
    • IRQF_DISABLED: 中断处理程序执行时关中断
    • IRQF_SAMPLE_RANDOM: 随机发生中断,可用于产生随机数
    • IRQF_TRIGGER_LOW:2.6.26 中没有,低电平有效
    • IRQF_TRIGGER_HIGH: 2.6.26 中没有,高电平有效
    • IRQF_TRIGGER_RISING: 2.6.26 中没有,上升沿有效
    • IRQF_TRIGGER_FALLING: 2.6.26 中没有,下降沿有效
  • dev_name:名称,显示在 /proc/interrupts 中
  • dev_id:设备 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)

参数

  • irq: 中断通道号,无符号整数
  • dev_id: 请求中断时指定的设备 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 状态,释放自旋锁。

参数

  • desc: 指向 irq_desc 结构体的指针
  • irq: 中断通道号
关闭中断
kernel/irq/manage.c 中的 disable_irq:
140 void disable_irq(unsigned int irq)

参数

  • 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 从中断中返回

Linux源代码阅读——中断_第5张图片

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

  • 连续的低优先的中断可能持续占有 CPU, 而高优先的某些进程则无法获得 CPU
  • 中断处理的这几个阶段中不能调用可能导致睡眠的函数

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

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

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

3.5 编写中断处理程序

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

  1. catchirq.c
    #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");
    }
    
  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上并行执行,使得总的系统效率可以更高。特性是:

  • 产生后并不是马上可以执行,必须要等待内核的调度才能执行。软中断不能被自己打断,只能被硬件中断打断(上半部)。
  • 可以并发运行在多个CPU上(即使同一类型的也可以)。所以软中断必须设计为可重入的函数(允许多个CPU同时操作),因此也需要使用自旋锁来保护其数据结构。

4.3 tasklet

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

  • 一种特定类型的tasklet只能运行在一个CPU上,不能并行,只能串行执行。
  • 多个不同类型的tasklet可以并行在多个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 个已经被内核注册使用:

    • tasklet 使用的 HI_SOFTIRQ
    • tasklet 使用的 TASKLET_SOFTIRQ
    • 网络协议栈使用的 NET_TX_SOFTIRQ
    • 网络协议栈使用的 NET_RX_SOFTIRQ
    • SCSI 存储
    • 系统计时器

    其余的软中断描述符可以由内核开发者使用。

             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 因此具有了不同的特性。

4.4 工作队列

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

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

参考文献

  • 陈香兰老师《Linux内核源代码导读》讲义
  • Understanding the Linux Kernel, Third Edition
  • Interrupt in Linux (硬件篇)
  • Linux 中断处理浅析
  • 超强的 Linux 中断分析
  • 中断描述符表
  • 中断的硬件环境
  • 软中断/tasklet/工作队列



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

你可能感兴趣的:(linux内核)