结合源码分析 Linux Kernel 的中断系统原理 (基于 Intel 平台)

⚠ 转载请注明出处:作者:ZobinHuang,更新日期:May.25 2022


知识共享许可协议

    本作品ZobinHuang 采用 知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议 进行许可,在进行使用或分享前请查看权限要求。若发现侵权行为,会采取法律手段维护作者正当合法权益,谢谢配合。


目录

有特定需要的内容直接跳转到相关章节查看即可。

正在加载目录...

基本概念

    我们下面理清楚几个概念。

Interrupt 与 Exception

  • Hardware Interrupt: 我们将有外部硬件设备产生的中断信号称为硬件中断,由于它可以随时发生 (但仍然按照 CPU 时钟周期来产生),因此也被称为 Asynchronous Interrupt;
  • Software Interrupt: 软件中断由 CPU 执行的指令产生,由于它伴随着 CPU 指令的执行而产生,因此也被称为 Synchronous Interrupt。具体可以分为两种情况:
    1. Exception: 当处理器在执行指令的过程中发现异常事件 (e.g. 页表不存在, 除 0 错误等) 的时候被触发,由 CPU 控制单元发出;
    2. Sysenter: 程序可以通过执行一些指令来产生软件中断 (e.g. x86 平台下的 int 指令)

    其中,对于 Exception,Intel 平台下又有如下的分类 intel_document_volume_3a_c6:

  • Fault: 该类异常通常可以被纠正,纠正后程序可以继续运行。当 Fault 异常发生时,处理器会记录下产生 Fault 异常的指令的地址 (i.e. 当前 CSEIP 寄存器的值),作为异常处理程序的返回地址。在运行完异常处理程序后,处理器将返回原先产生 Fault 异常的指令并重新运行;
  • Trap: 该类异常通常是由于处理器运行了 Trapping Instruction 引起的,在运行完异常处理程序后,程序可以被继续运行。值得的注意的是此时异常程序返回后执行的是紧跟在 Trapping Instruction 后面的指令,这与 Fault 异常有所区别;
  • Abort: 该类异常代表着处理器无法精确地获取导致异常的指令的地址,因此产生 Abort 异常后,程序将无法恢复。
为了阐述方便,我们下面的行文中,将把上面介绍的 Interrupt 与 Exception 统称为中断。

Interrupt Service Routine

    当中断发生时,CPU 会转移到相关的中断处理函数处,我们将该处理函数称为 Interrupt Service Routine。为了定位各个中断对应的 ISRs,CPU 使用了一张 中断向量表 (Interrupt Vector Table),其中的每一条表项即 中断向量(Interrupt Vector),记录了各个 ISR 的 物理地址。CPU 会使用一个专用寄存器来存储中断向量表的位置。我们在 中断和异常的处理与抢占式多任务 一文曾经讨论过 x86 架构下这一部分的内容,此处不再进行赘述,我们在下文中将继续基于 x86 平台进行分析。

Vector Number

    在 Intel 处理器中,每一个中断都有一个全局唯一的硬件中断号,称为 Vector Number。当中断发生时,处理器就是通过对应的硬件中断号在中断向量表中找到对应的中断向量的。Intel 处理器限制硬件中断号的范围为 0~255 (8 bits),其中 0~31 (低 5 bits) 是 Intel 预留的处理器自身相关的 Interrupt 和 Exception 硬件中断号,剩余的 32~255 可用于用户自定义,通常被用于外部硬件中断。

    Intel 0~255 号中断分布具体如下所示:

Hardware Interrupt

APIC 架构

    Intel 平台的底层中断系统采用的是 Advanced Programmable Interrupt Controller (APIC) 架构,顶层架构设计如 apic_arch_overview 所示。对于每一个 CPU 核心来说,其都拥有一个 Local APIC,其负责将在总线上探测到的发送给当前 CPU 核心的中断信息,以及该 CPU 核心本地产生的中断信息发送给 CPU 核心,而在全局则有一个 I/O APIC,其接收来自外围设备的中断信号,然后将相关的中断信息转发给相应的 Local APIC 进行后续处理。

    下面是从 Intel 官方编程手册 intel_document_volume_3a_c10 中摘抄的 APIC 架构相关的说明图。

    可以发现,在 Intel 至强和奔腾处理器中,I/O APIC 和 Local APIC,以及 Local APIC 之间使用的是 System Bus (e.g. PCI 总线) 进行连接,而在 Intel P6 系列处理器中,则采用的是 3-Wire APIC 总线进行连接。

Local APIC 中断处理结构

    下面我们对 Local APIC 与中断处理相关的结构进行具体分析。

    如 local_apic_arch 所示,Local APIC 可以接收以下几种类型的中断信号,主要分为本地和外部两种类型:

中断类型 描述

本地中断源

CPU Core 本地连接的 I/O 设备产生的中断 APIC 架构允许 I/O 设备通过和处理器的 LINT[1:0] 引脚相连来产生本地中断信号,也可以在这两个引脚上连接 8259 等中断控制芯片来控制外部设备的中断
APIC 计时器产生的中断 本地的 APIC 计时器在计数器到达设置的值后将会触发中断信号
Performance monitoring counter 产生的中断 Intel P6,奔腾和至强处理器可以被设置为当 performance-monitoring 计数器溢出的时候,向处理器发送中断信号
温度传感器产生的中断
APIC Internal Error 产生的中断 当对 Local APIC 的操作产生错误 (e.g. 访问一个不存在的寄存器) 时产生的中断信号

外部中断源

外部连接的 I/O 设备 外部连接的 I/O 设备会把它们产生的中断信号发送给 I/O APIC,后者会把中断信号转换为 Interrupt Message,通过 System Bus/APIC Bus 发送给选定 CPU Core 对应的 Local APIC
Interrupt-processor Interrupts (IPIs) Intel 处理器允许 CPU Core 在 System Bus 上将中断信息发送给其它 CPU Core(s)
Local Vector Table (LVT)

    针对本地中断,Local APIC 使用的是 Local Vector Table (LVT) 进行处理,具体如下所示。在收到对应的本地中断信号后,Local APIC 会在 LVT 中查询相关的后续动作,包括通告 CPU Core 的中断类型 (e.g. NMI, SMI, Fixed, etc.),中断 Vector Number (p.s. 如果需要的话) 等内容。

Interrupt Command Register (ICR)

    而针对外部中断,包括 IPI 和 外部 I/O 设备中断,它们是通过在系统总线上向 Local APIC 发送 Interrupt Message 来实现的中断通告。就 IPI 来说,CPU Core 可以通过向其本地的 Interrupt Command Register (ICR) 写入相应的信息,来实现 Interrupt Message 的发送。ICR 的具体格式如下所示:

什么是 NMI, SMI, Fixed, INIT 中断?

    在上面介绍的 LVT 和 ICR 中,我们可以看到与中断类型相关的 Delivery Mode 字段,其描述了最终向 CPU Core 发送的中断类型,下面我们对各种中断类型进行介绍:

  • NMI:
  • SMI:
  • Fixed:
  • INIT:
Interrupt Pending Resgiters

    当 Local APIC 接收 (receive) 并且接受 (accept) 了 Fixed 类型的中断时,它会将其 pend 在对应 CPU Core 的 Interrupt Pending Resgiters 中,其包含两个寄存器分别是 Interrupt Request Register (IRR)In-service Register (ISR),它们都是 256-bits 的寄存器,使用按位编码指代被 pending 的中断 Vector Number。当 Loal APIC 接收到中断源信号,并且确认中断类型为 Fixed 类型中断,获取了对应 Vector Number 后,它会置位 IRR/ISR 中对应的比特位,以代表对应 Vector Number 的中断处于 pending 状态,等待 CPU Core 运行相应的中断处理程序。当 CPU Core 准备好处理下一个中断时,IRR 中优先级最高 (i.e. Vector Number 最大) 的中断对应的比特位会被清除,ISR 中对应的比特位会被置位,然后 CPU 会运行 ISR 中标识的优先级最高的中断对应的处理程序。(优先级相关说明见 priority_registers)

    当 CPU 完成中断处理程序后,它会清除 ISR 中对应的比特位,并且向 End of Interrupt (EOI) 寄存器中执行写入操作,以告示中断处理的结束。

Interrupt/Processor 优先级寄存器

    Intel 处理器对中断的 Vector Number 进行了优先级的划分。上文说到 Intel 处理器一共支持 256 个中断,也即 8-bits。Vector Number 的 [7:4] 称为中断的 Interrupt-priority Class,是中断分级的大类; [3:0] 即中断在其 Interrupt-priority Class 中的相对位置,是中断分级的子类。值得注意的是,由于 Intel 把 0~31 号中断划分给 Intel 处理器自用,因此 Interrupt-priority Class 的合法取值范围为 2(0010)~15(1111),取值越大,优先级越高。

    在 Local APIC 中,有两个与中断优先级相关的寄存器 —— Task-Priority Register (TPR)Processor-Priority Register (PPR)

    TPR 用于被操作系统软件设置,其包含的值代表着当前操作系统允许被中断的 Vector Number 的下限,低于该值的中断将被操作系统暂时封锁不予处理。因此 TPR 是一个可写的寄存器。如 tpr 所示,TPR 寄存器中采用了和中断 Vector Number 一样的分级机制,分为 [7:4] 的 Task-Priority Class 和 [3:0] 的 Task-Priority Sub-Class。

    PPR 的值代表着当前处理器实际允许被中断的 Vector Number 的下限,其是基于 TPR 和 ISRV 的值产生的,其中 ISRV 的值指的是 ISR 寄存器中被置位的优先级最高的中断对应的 Vector,PPR 是一个只读寄存器,具体的值产生方法如下:

  • PPR[7:4]: 等于 TPR[7:4]ISRV[7:4] 中更大的那一个;
  • PPR[3:0] 产生规则如下:
    • 如果 TPR[7:4] > ISRV[7:4],那么 PPR[3:0] = TPR[3:0];
    • 如果 TPR[7:4] < ISRV[7:4],那么 PPR[3:0] = 0;
    • 如果 TPR[7:4] = ISRV[7:4],那么 PPR[3:0] 是 model-specific 的

    只有当中断 Vector 代表的优先级大于 PPR 中存储的值时,CPU Core 才会被中断并且对中断予以处理。当然,我们上面的说明针对的是 Delivery Mode 为 Fixed 的中断。

Local APIC 中断处理流程

    Local APIC 处理中断的流程如下所示:

中断相关内核定义

Generic IRQ Layer

    从计算机系统架构的角度来看,中断系统在底层的异构性主要体现在两方面:

  1. 中断控制器的异构性: 不同的平台上的中断控制器各有千秋,我们上面看到的 APIC 架构是 x86 下的中断控制器结构;
  2. 中断编号的异构性: 不同的底层平台会使用不同的编号方法,支持的中断数量也不尽相同;
  3. 中断线电平逻辑的异构性: 不同的外围设备的中断器件产生的中断电平逻辑各有千秋,比如有 Level (电平触发型), Edge (边缘触发型) 等等,软件对不同的中断电平逻辑的相应要有所不同,因此产生异构性

    由于底层系统的异构性,因此在 Linux 中提出了 Generic IRQ Layer 的概念,其基本目的是为了屏蔽底层系统的异构性,向内核的其它代码提供一个统一的中断接口,以保证内核代码的可移植性。我们下面从上述两个异构性出发,结合源码对内核底层中断的抽象进行阐述。

    interrupt_abstraction 展示了 Linux Generic IRQ Layer 对底层中断平台的抽象,包括中断控制器、中断编号和中断电平处理逻辑,我们下面分别进行说明。

对中断控制器的抽象

    针对不同的底层平台,内核有着不同的底层代码分支,用于具体实现 CPU 与中断控制器之间的交互 (e.g. 屏蔽/取消屏蔽某个中断,设置中断优先级,以及在 SMP 平台上的亲和度等)。为了兼容不同平台上的不同的中断控制器,内核使用了结构体 irq_chip 对中断控制器进行统一的描述,利用该结构体向内核的其它部分提供了操作中断控制器的接口。这个结构体是在 include/linux/irq.h linuxsrc_include_linux_irq_h 中定义的,其定义如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
/**
* struct irq_chip - hardware interrupt chip descriptor
*
* @parent_device: pointer to parent device for irqchip
* @name: name for /proc/interrupts
* @irq_startup: start up the interrupt (defaults to ->enable if NULL)
* @irq_shutdown: shut down the interrupt (defaults to ->disable if NULL)
* @irq_enable: enable the interrupt (defaults to chip->unmask if NULL)
* @irq_disable: disable the interrupt
* @irq_ack: start of a new interrupt
* @irq_mask: mask an interrupt source
* @irq_mask_ack: ack and mask an interrupt source
* @irq_unmask: unmask an interrupt source
* @irq_eoi: end of interrupt
* @irq_set_affinity: Set the CPU affinity on SMP machines. If the force
* argument is true, it tells the driver to
* unconditionally apply the affinity setting. Sanity
* checks against the supplied affinity mask are not
* required. This is used for CPU hotplug where the
* target CPU is not yet set in the cpu_online_mask.
* @irq_retrigger: resend an IRQ to the CPU
* @irq_set_type: set the flow type (IRQ_TYPE_LEVEL/etc.) of an IRQ
* @irq_set_wake: enable/disable power-management wake-on of an IRQ
* @irq_bus_lock: function to lock access to slow bus (i2c) chips
* @irq_bus_sync_unlock:function to sync and unlock slow bus (i2c) chips
* @irq_cpu_online: configure an interrupt source for a secondary CPU
* @irq_cpu_offline: un-configure an interrupt source for a secondary CPU
* @irq_suspend: function called from core code on suspend once per
* chip, when one or more interrupts are installed
* @irq_resume: function called from core code on resume once per chip,
* when one ore more interrupts are installed
* @irq_pm_shutdown: function called from core code on shutdown once per chip
* @irq_calc_mask: Optional function to set irq_data.mask for special cases
* @irq_print_chip: optional to print special chip info in show_interrupts
* @irq_request_resources: optional to request resources before calling
* any other callback related to this irq
* @irq_release_resources: optional to release resources acquired with
* irq_request_resources
* @irq_compose_msi_msg: optional to compose message content for MSI
* @irq_write_msi_msg: optional to write message content for MSI
* @irq_get_irqchip_state: return the internal state of an interrupt
* @irq_set_irqchip_state: set the internal state of a interrupt
* @irq_set_vcpu_affinity: optional to target a vCPU in a virtual machine
* @ipi_send_single: send a single IPI to destination cpus
* @ipi_send_mask: send an IPI to destination cpus in cpumask
* @irq_nmi_setup: function called from core code before enabling an NMI
* @irq_nmi_teardown: function called from core code after disabling an NMI
* @flags: chip specific flags
*/
struct irq_chip {
struct device *parent_device;
const char *name;
unsigned int (*irq_startup)(struct irq_data *data);
void (*irq_shutdown)(struct irq_data *data);
void (*irq_enable)(struct irq_data *data);
void (*irq_disable)(struct irq_data *data);

void (*irq_ack)(struct irq_data *data);
void (*irq_mask)(struct irq_data *data);
void (*irq_mask_ack)(struct irq_data *data);
void (*irq_unmask)(struct irq_data *data);
void (*irq_eoi)(struct irq_data *data);

int (*irq_set_affinity)(struct irq_data *data, const struct cpumask *dest, bool force);
int (*irq_retrigger)(struct irq_data *data);
int (*irq_set_type)(struct irq_data *data, unsigned int flow_type);
int (*irq_set_wake)(struct irq_data *data, unsigned int on);

void (*irq_bus_lock)(struct irq_data *data);
void (*irq_bus_sync_unlock)(struct irq_data *data);

void (*irq_cpu_online)(struct irq_data *data);
void (*irq_cpu_offline)(struct irq_data *data);

void (*irq_suspend)(struct irq_data *data);
void (*irq_resume)(struct irq_data *data);
void (*irq_pm_shutdown)(struct irq_data *data);

void (*irq_calc_mask)(struct irq_data *data);

void (*irq_print_chip)(struct irq_data *data, struct seq_file *p);
int (*irq_request_resources)(struct irq_data *data);
void (*irq_release_resources)(struct irq_data *data);

void (*irq_compose_msi_msg)(struct irq_data *data, struct msi_msg *msg);
void (*irq_write_msi_msg)(struct irq_data *data, struct msi_msg *msg);

int (*irq_get_irqchip_state)(struct irq_data *data, enum irqchip_irq_state which, bool *state);
int (*irq_set_irqchip_state)(struct irq_data *data, enum irqchip_irq_state which, bool state);

int (*irq_set_vcpu_affinity)(struct irq_data *data, void *vcpu_info);

void (*ipi_send_single)(struct irq_data *data, unsigned int cpu);
void (*ipi_send_mask)(struct irq_data *data, const struct cpumask *dest);

int (*irq_nmi_setup)(struct irq_data *data);
void (*irq_nmi_teardown)(struct irq_data *data);

unsigned long flags;
};

    可以看到在结构体 irq_chip 中,定义了很多函数指针。在不同的平台的代码实现中,会实现这个函数集合的子集。如下所示是 x86 多核平台下的 irq_chip 实现,分别有上文介绍的 I/O APIC 版本和 Local APIC 版本。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// arch/x86/kernel/apic/io_apic.c
static struct irq_chip ioapic_chip __read_mostly = {
.name = "IO-APIC",
.irq_startup = startup_ioapic_irq,
.irq_mask = mask_ioapic_irq,
.irq_unmask = unmask_ioapic_irq,
.irq_ack = irq_chip_ack_parent,
.irq_eoi = ioapic_ack_level,
.irq_set_affinity = ioapic_set_affinity,
.irq_retrigger = irq_chip_retrigger_hierarchy,
.irq_get_irqchip_state = ioapic_irq_get_chip_state,
.flags = IRQCHIP_SKIP_SET_WAKE,
};

static struct irq_chip lapic_chip __read_mostly = {
.name = "local-APIC",
.irq_mask = mask_lapic_irq,
.irq_unmask = unmask_lapic_irq,
.irq_ack = ack_lapic_irq,
};

对中断编号的抽象

    底层的中断控制器会用唯一的 Hardware IRQ 编号来标识各个中断 wikipedia_irq,我们在上面看到了 Intel 处理器编号的 256 个中断。在不同的底层平台上,Hardware IRQ 的数量和分配情况有所不同。为了实现 Linux 的可移植性,内核将底层平台的各个 Hardware IRQ 编号一一映射到 Linux IRQ 实例上去,所有的外部设备的驱动程序都是基于 Linux IRQ 来绑定和注册它们的中断处理程序的。Linux IRQ 的具体实现是结构体 struct irq_desc,它是在 include/linux/irqdesc.h linuxsrc_include_linux_irqdesc_h 中定义的,具体定义如下所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
/**
* struct irq_desc - interrupt descriptor
* @irq_common_data: per irq and chip data passed down to chip functions
* @kstat_irqs: irq stats per cpu
* @handle_irq: highlevel irq-events handler
* @action: the irq action chain
* @status_use_accessors: status information
* @core_internal_state__do_not_mess_with_it: core internal status information
* @depth: disable-depth, for nested irq_disable() calls
* @wake_depth: enable depth, for multiple irq_set_irq_wake() callers
* @tot_count: stats field for non-percpu irqs
* @irq_count: stats field to detect stalled irqs
* @last_unhandled: aging timer for unhandled count
* @irqs_unhandled: stats field for spurious unhandled interrupts
* @threads_handled: stats field for deferred spurious detection of threaded handlers
* @threads_handled_last: comparator field for deferred spurious detection of theraded handlers
* @lock: locking for SMP
* @affinity_hint: hint to user space for preferred irq affinity
* @affinity_notify: context for notification of affinity changes
* @pending_mask: pending rebalanced interrupts
* @threads_oneshot: bitfield to handle shared oneshot threads
* @threads_active: number of irqaction threads currently running
* @wait_for_threads: wait queue for sync_irq to wait for threaded handlers
* @nr_actions: number of installed actions on this descriptor
* @no_suspend_depth: number of irqactions on a irq descriptor with
* IRQF_NO_SUSPEND set
* @force_resume_depth: number of irqactions on a irq descriptor with
* IRQF_FORCE_RESUME set
* @rcu: rcu head for delayed free
* @kobj: kobject used to represent this struct in sysfs
* @request_mutex: mutex to protect request/free before locking desc->lock
* @dir: /proc/irq/ procfs entry
* @debugfs_file: dentry for the debugfs file
* @name: flow handler name for /proc/interrupts output
*/
struct irq_desc {
struct irq_common_data irq_common_data;
struct irq_data irq_data;
unsigned int __percpu *kstat_irqs;
irq_flow_handler_t handle_irq;
struct irqaction *action; /* IRQ action list */
unsigned int status_use_accessors;
unsigned int core_internal_state__do_not_mess_with_it;
unsigned int depth; /* nested irq disables */
unsigned int wake_depth; /* nested wake enables */
unsigned int tot_count;
unsigned int irq_count; /* For detecting broken IRQs */
unsigned long last_unhandled; /* Aging timer for unhandled count */
unsigned int irqs_unhandled;
atomic_t threads_handled;
int threads_handled_last;
raw_spinlock_t lock;
struct cpumask *percpu_enabled;
const struct cpumask *percpu_affinity;
#ifdef CONFIG_SMP
const struct cpumask *affinity_hint;
struct irq_affinity_notify *affinity_notify;
#ifdef CONFIG_GENERIC_PENDING_IRQ
cpumask_var_t pending_mask;
#endif
#endif
unsigned long threads_oneshot;
atomic_t threads_active;
wait_queue_head_t wait_for_threads;
#ifdef CONFIG_PM_SLEEP
unsigned int nr_actions;
unsigned int no_suspend_depth;
unsigned int cond_suspend_depth;
unsigned int force_resume_depth;
#endif
#ifdef CONFIG_PROC_FS
struct proc_dir_entry *dir;
#endif
#ifdef CONFIG_GENERIC_IRQ_DEBUGFS
struct dentry *debugfs_file;
const char *dev_name;
#endif
#ifdef CONFIG_SPARSE_IRQ
struct rcu_head rcu;
struct kobject kobj;
#endif
struct mutex request_mutex;
int parent_irq;
struct module *owner;
const char *name;
} ____cacheline_internodealigned_in_smp;

    内核在初始化的时候会为每一个中断源创建一个 irq_desc 实例,这些 irq_desc 实例会被存储在一个同样名为 irq_desc 的数组中,我们后面把这个数组称为 IRQ Decriptor Table

    在结构体 irq_desc 中,类型为 irq_data 的成员 irq_data 存储了描述该中断源的底层信息,包括 Linux IRQ 编号,Hardware IRQ 编号,以及指向存储着中断控制器操作的 irq_chip 实例的指针。它是在 include/linux/irq.h linuxsrc_include_linux_irq_h 中定义的,具体的定义如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/**
* struct irq_data - per irq chip data passed down to chip functions
* @mask: precomputed bitmask for accessing the chip registers
* @irq: interrupt number
* @hwirq: hardware interrupt number, local to the interrupt domain
* @common: point to data shared by all irqchips
* @chip: low level interrupt hardware access
* @domain: Interrupt translation domain; responsible for mapping
* between hwirq number and linux irq number.
* @parent_data: pointer to parent struct irq_data to support hierarchy
* irq_domain
* @chip_data: platform-specific per-chip private data for the chip
* methods, to allow shared chip implementations
*/
struct irq_data {
u32 mask;
unsigned int irq;
unsigned long hwirq;
struct irq_common_data *common;
struct irq_chip *chip;
struct irq_domain *domain;
#ifdef CONFIG_IRQ_DOMAIN_HIERARCHY
struct irq_data *parent_data;
#endif
void *chip_data;
};

中断处理逻辑

    对于不同的设备来说,它们可能有着不同的中断触发类型 (e.g. Level (电平触发型), Edge (边缘触发型), etc) kerneldoc_irq。因此,在中断处理逻辑中,我们有必要对所谓 Flow Management 加以区分。因此,对于每一个 Linux IRQ,内核使用 irq_desc 中类型为 irq_flow_handler_thandle_irq 成员来存储对应于该中断线上设备中断触发类型的 High-level 处理函数。内核在 kernel/irq/chip.c linuxsrc_kernel_irq_chip_c 中提供了以下几个对应与不同中断触发类型的 High-level 处理函数。这些函数针对不同的中断线电平逻辑有着不同的 High-level 中断处理逻辑,但是它们的核心都是调用中断处理函数。我们在后面将会看到。

1
2
3
4
5
6
7
handle_level_irq()      // For level-triggered interrupts
handle_edge_irq() // For edge-triggered interrupts
handle_fasteoi_irq() // For interrupts that only need an EOI at the end of the handler
handle_simple_irq() // For simple interrupts
handle_percpu_irq() // For per-CPU interrupts
handle_bad_irq() // For spurious interrupts
// ...

    在上面用于处理中断线电平的 High-level 的函数的包装下,当中断源发生中断时,由设备驱动注册在当前中断源上的中断处理函数需要被调用。irq_desc 中类型为 struct irqaction* 的成员 action 指向了一条存储着 struct irqaction 的链表。这些 struct irqaction 代表着中断描述符,各个中断描述符中存储着由各个设备驱动定义好并绑定在当前中断线上的中断处理逻辑。struct irqaction 是在 include/linux/interrupt.h linuxsrc_include_linux_interrupt_h 中定义的,其定义如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/**
* struct irqaction - per interrupt action descriptor
* @handler: interrupt handler function
* @name: name of the device
* @dev_id: cookie to identify the device
* @percpu_dev_id: cookie to identify the device
* @next: pointer to the next irqaction for shared interrupts
* @irq: interrupt number
* @flags: flags (see IRQF_* above)
* @thread_fn: interrupt handler function for threaded interrupts
* @thread: thread pointer for threaded interrupts
* @secondary: pointer to secondary irqaction (force threading)
* @thread_flags: flags related to @thread
* @thread_mask: bitmask for keeping track of @thread activity
* @dir: pointer to the proc/irq/NN/name entry
*/
struct irqaction {
irq_handler_t handler;
void *dev_id;
void __percpu *percpu_dev_id;
struct irqaction *next;
irq_handler_t thread_fn;
struct task_struct *thread;
struct irqaction *secondary;
unsigned int irq;
unsigned int flags;
unsigned long thread_flags;
unsigned long thread_mask;
const char *name;
struct proc_dir_entry *dir;
} ____cacheline_internodealigned_in_smp;

    值得注意的是,当我们在一个 irq_desc 的 action 链表上挂载多个 irqaction 时,此时中断线将是共享的,即 Shared IRQ,否则则是 Specified IRQ。如果是共享中断线,则在中断事件发生之后,在这条中断线上注册的中断处理函数将需要检查是否是自己的设备发生了中断。我们在后面将会看到相关的代码。

    现在让我们串起来。当某个中断源发生中断时,基于 Linux IRQ 编号,内核基于该下标可以在 irq_desc 数组中找到对应的 irq_desc 结构。此时 irq_desc 中的 handle_irq 指针指向的函数就会被调用,该函数会在正确处理对应中断线电平逻辑的情况下,调用在该中断线上的中断处理函数,这些中断处理函数是在中断描述符 irqaction 中被描述的。对于各条中断线,它们相应的中断处理函数被组织成一条链表的形式,挂在 irq_desc 结构的成员 action 上。

与中断相关的内核接口

    Generic IRQ Layer 提供了各种各样的接口,方便设备驱动用于实现与中断相关的操作。理解这些接口将有助于我们后续对设备驱动的学习,在本节中我们将对这些接口进行阐述。

注册中断处理函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/* include/linux/interrupt.h */
/**
* request_irq - Add a handler for an interrupt line
* @irq: The interrupt line to allocate
* @handler: Function to be called when the IRQ occurs.
* Primary handler for threaded interrupts
* If NULL, the default primary handler is installed
* @flags: Handling flags
* @name: Name of the device generating this interrupt
* @dev: A cookie passed to the handler function
*
* This call allocates an interrupt and establishes a handler; see
* the documentation for request_threaded_irq() for details.
*/
static inline int __must_check
request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags,
const char *name, void *dev)
{
return request_threaded_irq(irq, handler, NULL, flags, name, dev);
}

    request_irq 用于初始化一个 irqactioin 实例,并且绑定到由参数 irq 指定的 irq_desc 上去; 类型为 irq_handler_t 的参数 handler 指向了由设备驱动程序定义的中断处理函数; flags 是一个与中断管理相关的 bitmask,这个 bitmask 的各个位是在 include/linux/interrupt.h 中定义的,其中比较重要的位包括:

  • IRQF_SHARED: 表示当前注册的中断处理函数允许和其它中断处理函数一起共享中断线;
  • IRQF_TIMER: 标记当前中断是一个定时器中断;
  • IRQF_PERCPU: 标识当前中断是 per-CPU 的中断;
  • IRQF_NOBALANCING: 标识将当前注册的中断排除在 IRQ Balancing 策略之外;

    对于允许共享中断线的中断处理函数来说,在使用 request_irq 注册中断处理函数时,参数 dev 是必不可少的 (i.e. 不能设置为 NULL,可以指向驱动模块程序中的任意地址) ldd_sharedirq。其原因是,在共享中断线的情况下,在 irq_descaction 链表上将会存在多个 irqaction,这个 dev 值将被用作区分这些 irqaction 的唯一凭据。当共享中断线上发生中断信号时,这些 irqaction 所代表的中断处理函数都会被调用,因此允许共享中断线的中断处理函数必须首先检查是否是自己的设备发生了中断,若是,再继续后续的中断处理。那么这些中断处理函数找到自己设备的方法就是这个 dev 值。考虑这样一种情况,我们在一条中断线上启用了两个使用同个驱动程序的外围设备,那么这两个设备会在对应的 irq_descaction 链表上注册两个 irqaction,这两个 irqaction 的区别在于它们的 dev 值是有区别的,并且这个 dev 值是由驱动程序自己维护的。当中断来临时,本质上同一个中断处理程序会被调用两次,但是两次调用传入的 dev 值是不同的,因此前后两次处理的设备是不同的。

    当我们调用 request_irq 向一条中断线上注册一个允许共享中断的 irqaction 时,只有如下两个条件之一得到满足,才能注册成功 ldd_sharedirqrequest_irq 注册成功将返回 0 值。

  1. 这条中断线上没有注册任何 irqaction
  2. 这条中断线上注册的 irqaction 都允许共享中断线

    另外,传入 request_irq 的另一个重要参数是中断处理函数 handler。中断处理函数的类型被定义为 irqreturn_t,其函数原型如下:

1
typedef irqreturn_t (*irq_handler_t)(int, void *);

    这个函数会在中断到来的时候被调用。其中传入的第一个 int 参数是 Linux IRQ 编号,第二个参数是我们上面介绍过的 devirqreturn_t 的返回值是一个枚举类型,在 include/linux/irqreturn.h 中被定义,具体定义如下:

1
2
3
4
5
6
7
8
9
10
11
/**
* enum irqreturn
* @IRQ_NONE interrupt was not from this device or was not handled
* @IRQ_HANDLED interrupt was handled by this device
* @IRQ_WAKE_THREAD handler requests to wake the handler thread
*/
enum irqreturn {
IRQ_NONE = (0 << 0),
IRQ_HANDLED = (1 << 0),
IRQ_WAKE_THREAD = (1 << 1),
};

    当允许共享中断线的中断处理程序检测到不是自己的设备产生了中断时,它应该返回 IRQ_NONE; 如果中断被正确处理,它应该返回 IRQ_HANDLED; 如果中断程序尝试唤醒一个 中断处理线程 (Threaded Interrupt Handler),它应该返回 IRQ_WAKE_THREAD,我们将在 threaded_interrupt_handler 中对此部分进行介绍。

解注册中断处理函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
/* kernel/irq/manage.c */
/**
* free_irq - free an interrupt allocated with request_irq
* @irq: Interrupt line to free
* @dev_id: Device identity to free
*
* Remove an interrupt handler. The handler is removed and if the
* interrupt line is no longer in use by any driver it is disabled.
* On a shared IRQ the caller must ensure the interrupt is disabled
* on the card it drives before calling this function. The function
* does not return until any executing interrupts for this IRQ
* have completed.
*
* This function must not be called from interrupt context.
*
* Returns the devname argument passed to request_irq.
*/
const void *free_irq(unsigned int irq, void *dev_id)
{
struct irq_desc *desc = irq_to_desc(irq);
struct irqaction *action;
const char *devname;

if (!desc || WARN_ON(irq_settings_is_per_cpu_devid(desc)))
return NULL;

#ifdef CONFIG_SMP
if (WARN_ON(desc->affinity_notify))
desc->affinity_notify = NULL;
#endif

action = __free_irq(desc, dev_id);

if (!action)
return NULL;

devname = action->name;
kfree(action);
return devname;
}
EXPORT_SYMBOL(free_irq);

    free_irq 用于释放一个由 request_irq 分配的 irqaction。其中传入的参数 irq 是 Linux IRQ 编号。如果释放的 irqaction 是一个共享中断线的中断处理程序,dev_id 用于指明具体取消的 irqaction 是哪一个。

注册 Threaded 中断处理线程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
/* include/linux/interrupt.h */
/**
* request_threaded_irq - allocate an interrupt line
* @irq: Interrupt line to allocate
* @handler: Function to be called when the IRQ occurs.
* Primary handler for threaded interrupts
* If NULL and thread_fn != NULL the default
* primary handler is installed
* @thread_fn: Function called from the irq handler thread
* If NULL, no irq thread is created
* @irqflags: Interrupt type flags
* @devname: An ascii name for the claiming device
* @dev_id: A cookie passed back to the handler function
*
* This call allocates interrupt resources and enables the
* interrupt line and IRQ handling. From the point this
* call is made your handler function may be invoked. Since
* your handler function must clear any interrupt the board
* raises, you must take care both to initialise your hardware
* and to set up the interrupt handler in the right order.
*
* If you want to set up a threaded irq handler for your device
* then you need to supply @handler and @thread_fn. @handler is
* still called in hard interrupt context and has to check
* whether the interrupt originates from the device. If yes it
* needs to disable the interrupt on the device and return
* IRQ_WAKE_THREAD which will wake up the handler thread and run
* @thread_fn. This split handler design is necessary to support
* shared interrupts.
*
* Dev_id must be globally unique. Normally the address of the
* device data structure is used as the cookie. Since the handler
* receives this value it makes sense to use it.
*
* If your interrupt is shared you must pass a non NULL dev_id
* as this is required when freeing the interrupt.
*
* Flags:
*
* IRQF_SHARED Interrupt is shared
* IRQF_TRIGGER_* Specify active edge(s) or level
*
*/
int request_threaded_irq(unsigned int irq, irq_handler_t handler,
irq_handler_t thread_fn, unsigned long irqflags,
const char *devname, void *dev_id)
{
struct irqaction *action;
struct irq_desc *desc;
int retval;

if (irq == IRQ_NOTCONNECTED)
return -ENOTCONN;

/*
* 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).
*
* Also IRQF_COND_SUSPEND only makes sense for shared interrupts and
* it cannot be set along with IRQF_NO_SUSPEND.
*/
if (((irqflags & IRQF_SHARED) && !dev_id) ||
(!(irqflags & IRQF_SHARED) && (irqflags & IRQF_COND_SUSPEND)) ||
((irqflags & IRQF_NO_SUSPEND) && (irqflags & IRQF_COND_SUSPEND)))
return -EINVAL;

desc = irq_to_desc(irq);
if (!desc)
return -EINVAL;

if (!irq_settings_can_request(desc) ||
WARN_ON(irq_settings_is_per_cpu_devid(desc)))
return -EINVAL;

if (!handler) {
if (!thread_fn)
return -EINVAL;
handler = irq_default_primary_handler;
}

action = kzalloc(sizeof(struct irqaction), GFP_KERNEL);
if (!action)
return -ENOMEM;

action->handler = handler;
action->thread_fn = thread_fn;
action->flags = irqflags;
action->name = devname;
action->dev_id = dev_id;

retval = irq_chip_pm_get(&desc->irq_data);
if (retval < 0) {
kfree(action);
return retval;
}

retval = __setup_irq(irq, desc, action);

if (retval) {
irq_chip_pm_put(&desc->irq_data);
kfree(action->secondary);
kfree(action);
}

#ifdef CONFIG_DEBUG_SHIRQ_FIXME
if (!retval && (irqflags & IRQF_SHARED)) {
/*
* It's a shared IRQ -- the driver ought to be prepared for it
* to happen immediately, so let's make sure....
* We disable the irq to make sure that a 'real' IRQ doesn't
* run in parallel with our fake.
*/
unsigned long flags;

disable_irq(irq);
local_irq_save(flags);

handler(irq, dev_id);

local_irq_restore(flags);
enable_irq(irq);
}
#endif
return retval;
}
EXPORT_SYMBOL(request_threaded_irq);

    由设备驱动通过 request_irq 注册的中断处理程序运行在 Hard IRQ Context 中: 当前 CPU 核心抢占和中断均被 Disabled。在这种环境下,CPU core 的运行将被中断处理程序完全接管,我们并不希望 CPU 长时间地运行在这种状态下,换句话说设备驱动中断处理程序的设计应该保证 原子性 (i.e. 不阻塞) 且 快速 (i.e. 不要做太多工作,尤其是耗时的工作)。然而,有一些设备 (e.g. 网卡) 产生的中断,系统要做的处理逻辑并不简单,这导致了中断处理程序的处理时间波动范围大的问题。内核应用了所谓 Split-handler 的设计思路,将中断处理逻辑拆分为 top halfbottom half 两部分,前者负责处理 interrupt critical 的操作,包括对外部设备的寄存器的读写、调度相关 bottom-half 处理逻辑等操作;后者负责处理 interrupt non-critical 和 deferrable work 的操作,比如处理由 top-half 产生的数据、与 Process Context 进行交互,以及访问用户地址空间等。内核提供了如 Softirq, TaskletsWorkqueue 三种 Deferred Work 的机制,我们在后面将进行介绍。

    除此之外,内核在中断系统中提供了一种可以在 Threaded Context 中运行中断处理程序的机制 —— Threaded Interrupt Handler。驱动程序可以使用 request_threaded_irq 接口来申请一个可 threaded 的中断处理程序,该接口的函数原型如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
/**
* request_threaded_irq - allocate an interrupt line
* @irq: Interrupt line to allocate
* @handler: Function to be called when the IRQ occurs.
* Primary handler for threaded interrupts
* If NULL and thread_fn != NULL the default
* primary handler is installed
* @thread_fn: Function called from the irq handler thread
* If NULL, no irq thread is created
* @irqflags: Interrupt type flags
* @devname: An ascii name for the claiming device
* @dev_id: A cookie passed back to the handler function
*
* This call allocates interrupt resources and enables the
* interrupt line and IRQ handling. From the point this
* call is made your handler function may be invoked. Since
* your handler function must clear any interrupt the board
* raises, you must take care both to initialise your hardware
* and to set up the interrupt handler in the right order.
*
* If you want to set up a threaded irq handler for your device
* then you need to supply @handler and @thread_fn. @handler is
* still called in hard interrupt context and has to check
* whether the interrupt originates from the device. If yes it
* needs to disable the interrupt on the device and return
* IRQ_WAKE_THREAD which will wake up the handler thread and run
* @thread_fn. This split handler design is necessary to support
* shared interrupts.
*
* Dev_id must be globally unique. Normally the address of the
* device data structure is used as the cookie. Since the handler
* receives this value it makes sense to use it.
*
* If your interrupt is shared you must pass a non NULL dev_id
* as this is required when freeing the interrupt.
*
* Flags:
*
* IRQF_SHARED Interrupt is shared
* IRQF_TRIGGER_* Specify active edge(s) or level
*
*/
int request_threaded_irq(unsigned int irq, irq_handler_t handler,
irq_handler_t thread_fn, unsigned long irqflags,
const char *devname, void *dev_id)

    在这个接口中,参数 handler 传入的是运行在 Interrupt Context 中的 Primary Interrupt Handler,参数 thread_fn 传入的是运行在 Threaded Context 的中断函数逻辑。按照这种方式注册的中断函数,当中断发生时,handler 所代表的 Primary Interrupt Handler 将首先被调用,在该函数返回 IRQ_WAKE_THREAD 后,thread_fn 所定义的后续中断处理逻辑将被调度,并在 Threaded Context 中被执行。

    Theaded Interrupt Handler 有两种玩法。一种是在 Primary Interrupt Handler 中完成 Interrupt-critical 的工作,然后唤醒 Threaded Handler 进行处理,这种处理方式与 Deferred Work 的机制类似,对应的中断源将在 Primary Interrupt Handler 执行完成后被手动 Unmasked 恢复;第二种是把所有的中断处理逻辑都放到 Threaded Handler 中,Primary Interrupt Handler 中只会对中断源进行确认 (i.e. 考虑 Shared IRQ 的情况) 以及唤醒 Threaded Handler,在这种情况下,对应的中断源需要被 Masked 直至 Threaded Handler 完成其处理逻辑。为了实现对中断源的持续 Masked,我们可以通过以下其中一种方式予以实现:

  • 在 Primary Interrupt Handler 中,在调度 Theaded Interrupt Handler 之前手动 Mask 掉中断,不要 Unmask 它;
  • 在使用 request_threaded_irq 注册时,在参数 irqflags 中使用 IRQF_ONESHOT 标记进行标识

控制接口

    Generic IRQ Layer 还提供了若干的用于控制各个 Linux 中断源的接口,下面进行介绍。

void disable_irq(unsigned int irq)

    disable_irq 关闭了指定 Linux IRQ 所代表的中断线的响应。这是一个阻塞的函数调用,这个函数会等待当前指定的中断线上正在运行的 Handler 运行结束后,再完成对中断线的屏蔽。实际上,disable_irq 底层调用了 irq_chip 结构体的 irq_disable 函数完成了对中断线的屏蔽,它实现的是全局的对某条中断线的屏蔽。

Deferred Work

    TODO