中断简介
前言
最近计划学习一下cpu虚拟化相关内容,其中一个重要内容就是中断虚拟化。
在学习中断虚拟化之前,首先需要对中断有基本的了解认识,虽然之前在linux内核硬中断分析中有了解,但比较片面,这里对中断会进行更深入的学习
中断硬件
本质上,内核是操作系统控制硬件的接口,其逻辑实现紧密遵循硬件规范。这种硬件强相关的特性,使得研究cpu的中断硬件机制成为理解内核中断子系统的关键点。
一切的核心是CPU的IDT(Interrupt Descriptor Table)机制。其规定了每个中断需要有一个对应的中断向量号。CPU内部有一个专用的系统寄存器IDTR(Interrupt Descriptor Table Register),记录了IDT在内存中的地址。IDT最多有256个条目,每个条目描述了一个中断处理逻辑的地址,中断向量号就是IDT的索引。
因此CPU收到中断控制器发送的中断后,根据中断的中断向量号根据IDTR从IDT中找到该中断的回调函数的地址并执行,如下所示

而中断控制器则是将中断源触发的中断按照CPU的格式要求投递给CPU,从而触发上述IDT机制进行处理。下面介绍一下cpu的中断控制器的发展
pic(Programmable Interrupt Controller)
pic,也就是8259A,其是最早的cpu的中断管理芯片,其实物图和结构图如下所示

中断设置
按照8259A的手册,一般通过pio的方式完成ICW(Initialization Command Words)和OCW(Operation Command Words)寄存器的读取和设置,从而完成中断触发方式、中断向量基址、中断状态等8259A工作状态的查询和设置,这里不详细介绍了,感兴趣可以查看8259A的手册
中断投递
根据8259A的手册,一个典型的中断投递流程如下所示
- 一个或多个IR引脚发送中断信号(触发方式由ICW1配置),对应的IRR(Interrupt Request Register)中的相关bit位锁存请求状态(直到后续被清除)
- PR(Priority Resolver)根据中断优先级(OCW2配置)和IMR(Interrupt Mask Register)评估所有的中断请求。如果存在有效请求,并通过INT引脚向CPU发送中断请求信号投递中断
- CPU检测到INT信号后,在当前指令执行完毕后发送第一个INTA脉冲(8259A手册规定的电信号)
- 在第一个INTA周期内,8259A会在ISR(Interrupt Service Register)中将当前IRR中最高优先级的IR对应的位进行标记,并清除IRR中相应的bit位标志
- 在第二个INTA周期内,8259A会通过数据总线向CPU发送8位中断类型码(ICW2配置基地址+IRQ编号)
- 最后,根据ICW4配置的模式结束此次中断处理(AEOI模式会自动清除ISR对应的位;否则等到CPU发送EOI命令再清除ISR对应的位)
apic(Advanced Programmable Interrupt Controller)
前面提到的pic只适用于单处理器的中断投递,而对于当前的多CPU,则需要新的中断控制器,即apic。
apic由lapic(local apic)和ioapic(I/O apic)构成,其整体结构图如下所示
整体上,lapic类似于每个CPU的8259A,用于处理其所在的CPU的中断投递;而ioapic则是用来负责统一接受外部设备的中断请求,并重新分配给给不同CPU的lapic
lapic
lapic的主要功能就是接受中断消息或是自身/其他lapic产生的中断,然后投递到对应的CPU,其整体结构图如下所示
可以看到,其基本和8259A很相似,包括ISR、IRR等,但又新增了很多结构用来实现新增的功能,因此其设置和中断投递流程大体相似,但又有区别
中断设置
按照apic的手册,lapic的寄存器被默认被映射到起始地址为0xfee00000的连续4KB的物理内存中,可通过IA32_APIC_BASE MSR(Model Specific Register)更改基址。寄存器通过mmio方式进行读取和设置,具体的寄存器布局如下所示
中断处理
lapic可以投递三种中断:本地中断(诸如apic timer generated interrupts等)、IPI(Inter-Processor Interrupts)和ioapic发送的中断。lapic投递这些中断遵循下述流程
- 过滤中断目的地是自己的中断(本地中断目的地始终是自己,IPI和ioapic发送的中断需要根据中断的destination字段判断)
- 如果中断的delivery Mode是NMI(Non-Maskable Interrupt)、SMI(System Management Interrupt)、INIT、ExtINT和SIPI(Start-up IPI),则直接通过相关引脚将中断发送给CPU,完成此次中断投递
- 对于其他delivery Mode的中断,根据中断号将IRR对应的位进行标记(直到后续被清除)
- lapic基于PPR(Processor Priority Register)评估IRR中所有的中断请求。如果存在有效请求,清除IRR中相应的bit位,标记ISR中相应的bit位,并通过相关引脚将中断发送给CPU
- CPU在处理完中断后,向EOIR(End-Of-Interrupt Register)写入
- lapic清除ISR中相应的bit位,并根据SVR(Spurious Vector Register)内存可能会通过总线向所有ioapic广播EOI
ioapic
ioapic负责接收外部I/O设备的硬件中断,并将其转换成一定格式的消息,通过总线发送给一个或多个lapic,其相关示意图如下所示


中断设置
和lapic一样,ioapic有大量用于配置/读取工作状态的寄存器。但不同的是,ioapic没有将所有的寄存器直接映射到物理内存上;其仅仅映射了两个寄存器:位于0xfec0xy00(x和y可通过APICBASE寄存器配置)的IOREGSEL(I/O Register Select),用于要访问的ioapic的寄存器编号;位于0xfec0xy10的IOWIN(I/O Window Register),用于读写选择的寄存器内容
其中最重要的是IOREDTBL(I/O Redirection Table Registers),其是一张24项的中断重定向表,每项是一个64位的寄存器,对应着ioapic的一个中断引脚的中断重定向配置信息,包含中断向量、中断的destination、中断的delivery mode等内容
中断处理
ioapic的中断处理很简单,每当其收到外部I/O设备通过中断引脚发送的中断信号后,其会根据中断引脚对应的IOREDTBL中的重定向表项格式化出一条消息,并通过总线发送给destination字段指定的lapic
msi(Message Signaled Interrupt)
在早期的计算机架构中,外设硬件中断主要通过ioapic转发至各cpu的lapic。但随着pci/pcie设备呈指数级增长,传统中断机制暴露出诸多局限性。
为此,pci规范在2.2版本引入了msi(Message Signaled Interrupt)机制,其让pci设备通过pci总线存储器写事务,将特定的消息地址(message address)和消息数据(message data)直接写入内存映射的lapic寄存器中,从而绕过ioapic直接向lapic发送中断通知,其整体的架构图如下所示(具体硬件连线细节不一定准确,主要表现绕过ioapic)

中断设置
pci的msi中断的设置是通过pci的配置空间实现的,前面博客qemu的PCI设备中简单介绍过,其配置空间如下所示
其中,msi的配置通过msi capability/msix capability实现
msi capability
msi capability的结构如下所示

可以看到,其有多个字段,但重点是Message Address(Message Upper Address)和Message Data字段
其中Message Address的结构如下所示,其定义了pci设备发出中断时内存写TLP的目的地址
而Message Data则定义了写入的内容,用来描述中断的相关信息,其结构如下所示
msix capability
msix capability的结构如下所示

与msi capability不同的是,其使用一个数组存放Message Address字段和Message Data字段,数组结构如下所示

中断处理
pci的中断处理遵循下述流程
- 当pci设备需要发起中断请求时,pci硬件会向capability中设置的Message Address地址写Message Data数据,其会以存储器写TLP的形式发送到RC(Root Complex)
- RC收到后,将其转换为interrupt message总线事务并广播,与ioapic操作类似
中断子系统
中断子系统负责统一管理硬件中断资源,其接收并路由硬件中断信号,将其转换为对应的虚拟中断号,并调用注册的中断服务例程
中断向量
中断向量,即CPU真正接受并进行处理的东西(x86架构下即前面中断硬件介绍的IDT机制)
不同架构下中断触发的CPU硬件行为是不一致的,因此其会有不一样的硬件中断管理机制,这里以x86平台为例进行分析
根据前面中断硬件的介绍,内核会在系统初始化阶段配置中断控制器的中断向量信息。当硬件发起中断时,中断控制器会按照设置投递对应的中断向量,处理器会接收包含中断对应中断向量的信号,并将该中断向量作为索引值,访问IDT中对应的门描述符从而进行处理。
中断子系统会在idt_setup_apic_and_irq_gates中初始化该IDT,如下所示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//#0 idt_setup_apic_and_irq_gates () at arch/x86/kernel/idt.c:282
//#1 0xffffffff832a7262 in native_init_IRQ () at arch/x86/kernel/irqinit.c:103
//#2 0xffffffff8329ab9b in start_kernel () at init/main.c:977
//#3 0xffffffff832a59a8 in x86_64_start_reservations (real_mode_data=real_mode_data@entry=0x14770 <entry_stack_storage+1904> <error: Cannot access memory at address 0x14770>) at arch/x86/kernel/head64.c:507
//#4 0xffffffff832a5ae6 in x86_64_start_kernel (real_mode_data=0x14770 <entry_stack_storage+1904> <error: Cannot access memory at address 0x14770>) at arch/x86/kernel/head64.c:488
//#5 0xffffffff810a96f6 in secondary_startup_64 () at arch/x86/kernel/head_64.S:420
//#6 0x0000000000000000 in ?? ()
void __init idt_setup_apic_and_irq_gates(void)
{
int i = FIRST_EXTERNAL_VECTOR;
void *entry;
idt_setup_from_table(idt_table, apic_idts, ARRAY_SIZE(apic_idts), true);
for_each_clear_bit_from(i, system_vectors, FIRST_SYSTEM_VECTOR) {
entry = irq_entries_start + IDT_ALIGN * (i - FIRST_EXTERNAL_VECTOR);
set_intr_gate(i, entry);
}
for_each_clear_bit_from(i, system_vectors, NR_VECTORS) {
/*
* Don't set the non assigned system vectors in the
* system_vectors bitmap. Otherwise they show up in
* /proc/interrupts.
*/
entry = spurious_entries_start + IDT_ALIGN * (i - FIRST_SYSTEM_VECTOR);
set_intr_gate(i, entry);
}
...
}
可以看到,其设置中断向量从FIRST_EXTERNAL_VECTOR到FIRST_SYSTEM_VECTOR之间的中断的入口函数为irq_entries_start所在的代码段中;FIRST_SYSTEM_VECTOR及以上的中断的入口函数为spurious_entries_start所在的代码段中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/*
* ASM code to emit the common vector entry stubs where each stub is
* packed into IDT_ALIGN bytes.
*
* Note, that the 'pushq imm8' is emitted via '.byte 0x6a, vector' because
* GCC treats the local vector variable as unsigned int and would expand
* all vectors above 0x7F to a 5 byte push. The original code did an
* adjustment of the vector number to be in the signed byte range to avoid
* this. While clever it's mindboggling counterintuitive and requires the
* odd conversion back to a real vector number in the C entry points. Using
* .byte achieves the same thing and the only fixup needed in the C entry
* point is to mask off the bits above bit 7 because the push is sign
* extending.
*/
.align IDT_ALIGN
SYM_CODE_START(irq_entries_start)
vector=FIRST_EXTERNAL_VECTOR
.rept NR_EXTERNAL_VECTORS
UNWIND_HINT_IRET_REGS
0 :
ENDBR
.byte 0x6a, vector
jmp asm_common_interrupt
/* Ensure that the above is IDT_ALIGN bytes max */
.fill 0b + IDT_ALIGN - ., 1, 0xcc
vector = vector+1
.endr
SYM_CODE_END(irq_entries_start)
.align IDT_ALIGN
SYM_CODE_START(spurious_entries_start)
vector=FIRST_SYSTEM_VECTOR
.rept NR_SYSTEM_VECTORS
UNWIND_HINT_IRET_REGS
0 :
ENDBR
.byte 0x6a, vector
jmp asm_spurious_interrupt
/* Ensure that the above is IDT_ALIGN bytes max */
.fill 0b + IDT_ALIGN - ., 1, 0xcc
vector = vector+1
.endr
SYM_CODE_END(spurious_entries_start)
这些代码段会push对应的中断向量,并调用asm_common_interrupt/asm_spurious_interrupt函数。
这里asm_common_inerrupt/asm_spurious_interrupt由DECLARE_IDTENTRY_IRQ宏在arch/x86/entry/entry_64.S文件中定义,如下所示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
109DECLARE_IDTENTRY_IRQ(X86_TRAP_OTHER, common_interrupt);
...
/* Entries for common/spurious (device) interrupts */
/*
* Interrupt entry/exit.
*
+ The interrupt stubs push (vector) onto the stack, which is the error_code
* position of idtentry exceptions, and jump to one of the two idtentry points
* (common/spurious).
*
* common_interrupt is a hotpath, align it to a cache line
*/
.macro idtentry_irq vector cfunc
.p2align CONFIG_X86_L1_CACHE_SHIFT
idtentry \vector asm_\cfunc \cfunc has_error_code=1
.endm
/**
* idtentry - Macro to generate entry stubs for simple IDT entries
* @vector: Vector number
* @asmsym: ASM symbol for the entry point
* @cfunc: C function to be called
* @has_error_code: Hardware pushed error code on stack
*
* The macro emits code to set up the kernel context for straight forward
* and simple IDT entries. No IST stack, no paranoid entry checks.
*/
.macro idtentry vector asmsym cfunc has_error_code:req
SYM_CODE_START(\asmsym)
.if \vector == X86_TRAP_BP
/* #BP advances %rip to the next instruction */
UNWIND_HINT_IRET_ENTRY offset=\has_error_code*8 signal=0
.else
UNWIND_HINT_IRET_ENTRY offset=\has_error_code*8
.endif
ENDBR
ASM_CLAC
cld
.if \has_error_code == 0
pushq $-1 /* ORIG_RAX: no syscall to restart */
.endif
.if \vector == X86_TRAP_BP
/*
* If coming from kernel space, create a 6-word gap to allow the
* int3 handler to emulate a call instruction.
*/
testb $3, CS-ORIG_RAX(%rsp)
jnz .Lfrom_usermode_no_gap_\@
.rept 6
pushq 5*8(%rsp)
.endr
UNWIND_HINT_IRET_REGS offset=8
.Lfrom_usermode_no_gap_\@:
.endif
idtentry_body \cfunc \has_error_code
_ASM_NOKPROBE(\asmsym)
SYM_CODE_END(\asmsym)
.endm
/**
* idtentry_body - Macro to emit code calling the C function
* @cfunc: C function to be called
* @has_error_code: Hardware pushed error code on stack
*/
.macro idtentry_body cfunc has_error_code:req
/*
* Call error_entry() and switch to the task stack if from userspace.
*
* When in XENPV, it is already in the task stack, and it can't fault
* for native_iret() nor native_load_gs_index() since XENPV uses its
* own pvops for IRET and load_gs_index(). And it doesn't need to
* switch the CR3. So it can skip invoking error_entry().
*/
ALTERNATIVE "call error_entry; movq %rax, %rsp", \
"call xen_error_entry", X86_FEATURE_XENPV
ENCODE_FRAME_POINTER
UNWIND_HINT_REGS
movq %rsp, %rdi /* pt_regs pointer into 1st argument*/
.if \has_error_code == 1
movq ORIG_RAX(%rsp), %rsi /* get error code into 2nd argument*/
movq $-1, ORIG_RAX(%rsp) /* no syscall to restart */
.endif
call \cfunc
/* For some configurations \cfunc ends up being a noreturn. */
REACHABLE
jmp error_return
.endm
...
在DECLARE_IDTENTRY_IRQ宏,其使用idtentry定义和实现asm_common_interrupt/asm_spurious_interrupt,最终会调用common_interrupt/spurious_interrupt进行处理。
而commont_interrupt/spurious_interrupt是由DEFINE_IDTENTRY_IRQ宏在arch/x86/kernel/irq.c中实现,如下所示
1 |
|
可以看到,实际上vector_irq中存储着每个硬件中断/中断向量对应的中断服务例程信息,即后面要提到的虚拟中断。
虚拟中断
上面只介绍了x86体系下的硬件中断资源的管理,实际上不同的体系架构有不同的处理方式。linux内核为了屏蔽这些不同,会以虚拟中断的形式向其他子系统或驱动提供服务,其中每一个硬件中断会分配一个全局唯一的虚拟中断,由struct irq_desc描述,统一存储在sparse_irqs变量中。
1 | static struct maple_tree sparse_irqs = MTREE_INIT_EXT(sparse_irqs, |
其中,handle_irq和action是struct irq_desc最重要的两个字段。handle_irq负责处理中断的硬件清理操作并触发驱动注册的中断处理程序;action则记录所有驱动注册的用于处理该中断的处理程序,如下所示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//#0 handle_edge_irq (desc=0xffff88810091dc00) at kernel/irq/chip.c:788
//#1 0xffffffff810af8fa in generic_handle_irq_desc (desc=<optimized out>)
// at ./include/linux/irqdesc.h:161
//#2 handle_irq (desc=<optimized out>, regs=<optimized out>)
// at arch/x86/kernel/irq.c:238
//#3 __common_interrupt (regs=<optimized out>, vector=0x20)
// at arch/x86/kernel/irq.c:257
//#4 0xffffffff81fd8eda in common_interrupt (regs=0xffffffff82a03d78,
// error_code=<optimized out>) at arch/x86/kernel/irq.c:247
//Backtrace stopped: Cannot access memory at address 0xffffc90000004010
static __always_inline void handle_irq(struct irq_desc *desc,
struct pt_regs *regs)
{
if (IS_ENABLED(CONFIG_X86_64))
generic_handle_irq_desc(desc);
else
__handle_irq(desc, regs);
}
/*
* Architectures call this to let the generic IRQ layer
* handle an interrupt.
*/
static inline void generic_handle_irq_desc(struct irq_desc *desc)
{
desc->handle_irq(desc);
}
中断路由
如前文中断硬件所述,系统通过 MMIO/PIO 按手册配置中断控制器的寄存器后,硬件产生的中断经控制器投递给CPU,最终触发回调函数。这里关键在于内核如何完成中断资源管理和配置,从而让配置后硬件发起的硬件中断能真正触发对应的虚拟中断的action:Linux 内核为此引入了 struct irq_data、struct irq_domain和struct irq_chip抽象层来描述中断控制器,从而优雅地适配了各异的硬件架构及复杂的中断路由关系。
数据结构
每一个中断控制器有不同的接口逻辑,因此内核中用struct irq_chip进行抽象描述,其提供了一系列中断控制器操作相关的回调函数,如下所示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/**
* struct irq_chip - hardware interrupt chip descriptor
*
* @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 {
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;
};
由于目前中断控制器往往有复杂的层次关系(例如x86架构下外设中断会经过ioapic中断控制器->lapic中断控制器->CPU的IDT),因此还需要协同管理多层中断控制器之间的中断资源,内核中使用struct irq_domain进行抽象描述,通过parent来描述中断控制器的层次关系,也提供了一系列的回调函数ops来实现具体的映射逻辑,如下所示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/**
* struct irq_domain - Hardware interrupt number translation object
* @link: Element in global irq_domain list.
* @name: Name of interrupt domain
* @ops: Pointer to irq_domain methods
* @host_data: Private data pointer for use by owner. Not touched by irq_domain
* core code.
* @flags: Per irq_domain flags
* @mapcount: The number of mapped interrupts
* @mutex: Domain lock, hierarchical domains use root domain's lock
* @root: Pointer to root domain, or containing structure if non-hierarchical
*
* Optional elements:
* @fwnode: Pointer to firmware node associated with the irq_domain. Pretty easy
* to swap it for the of_node via the irq_domain_get_of_node accessor
* @gc: Pointer to a list of generic chips. There is a helper function for
* setting up one or more generic chips for interrupt controllers
* drivers using the generic chip library which uses this pointer.
* @dev: Pointer to the device which instantiated the irqdomain
* With per device irq domains this is not necessarily the same
* as @pm_dev.
* @pm_dev: Pointer to a device that can be utilized for power management
* purposes related to the irq domain.
* @parent: Pointer to parent irq_domain to support hierarchy irq_domains
* @msi_parent_ops: Pointer to MSI parent domain methods for per device domain init
*
* Revmap data, used internally by the irq domain code:
* @revmap_size: Size of the linear map table @revmap[]
* @revmap_tree: Radix map tree for hwirqs that don't fit in the linear map
* @revmap: Linear table of irq_data pointers
*/
struct irq_domain {
struct list_head link;
const char *name;
const struct irq_domain_ops *ops;
void *host_data;
unsigned int flags;
unsigned int mapcount;
struct mutex mutex;
struct irq_domain *root;
/* Optional data */
struct fwnode_handle *fwnode;
enum irq_domain_bus_token bus_token;
struct irq_domain_chip_generic *gc;
struct device *dev;
struct device *pm_dev;
struct irq_domain *parent;
const struct msi_parent_ops *msi_parent_ops;
/* reverse map data. The linear map gets appended to the irq_domain */
irq_hw_number_t hwirq_max;
unsigned int revmap_size;
struct radix_tree_root revmap_tree;
struct irq_data __rcu *revmap[] __counted_by(revmap_size);
};
/**
* struct irq_domain_ops - Methods for irq_domain objects
* @match: Match an interrupt controller device node to a host, returns
* 1 on a match
* @map: Create or update a mapping between a virtual irq number and a hw
* irq number. This is called only once for a given mapping.
* @unmap: Dispose of such a mapping
* @xlate: Given a device tree node and interrupt specifier, decode
* the hardware irq number and linux irq type value.
*
* Functions below are provided by the driver and called whenever a new mapping
* is created or an old mapping is disposed. The driver can then proceed to
* whatever internal data structures management is required. It also needs
* to setup the irq_desc when returning from map().
*/
struct irq_domain_ops {
int (*match)(struct irq_domain *d, struct device_node *node,
enum irq_domain_bus_token bus_token);
int (*select)(struct irq_domain *d, struct irq_fwspec *fwspec,
enum irq_domain_bus_token bus_token);
int (*map)(struct irq_domain *d, unsigned int virq, irq_hw_number_t hw);
void (*unmap)(struct irq_domain *d, unsigned int virq);
int (*xlate)(struct irq_domain *d, struct device_node *node,
const u32 *intspec, unsigned int intsize,
unsigned long *out_hwirq, unsigned int *out_type);
/* extended V2 interfaces to support hierarchy irq_domains */
int (*alloc)(struct irq_domain *d, unsigned int virq,
unsigned int nr_irqs, void *arg);
void (*free)(struct irq_domain *d, unsigned int virq,
unsigned int nr_irqs);
int (*activate)(struct irq_domain *d, struct irq_data *irqd, bool reserve);
void (*deactivate)(struct irq_domain *d, struct irq_data *irq_data);
int (*translate)(struct irq_domain *d, struct irq_fwspec *fwspec,
unsigned long *out_hwirq, unsigned int *out_type);
void (*debug_show)(struct seq_file *m, struct irq_domain *d,
struct irq_data *irqd, int ind);
};
而每个虚拟中断都在struct irq_data中包含这两个数据结构,从而用于设置虚拟中断对应的硬件中断信息,如下所示
1 | /** |
x86架构
这里我们就以x86架构为例,详细分析一下virtio-net-pci设备中断的设置和路由。根据前面msi章节的介绍,pci的msi中断通过在msix capability空间中写入Message Address和Message Data数据来进行设置,从而pci硬件会向lapic以TLP消息形式投递Message Data中制定的中断向量,从而完成中断触发。这里实际上涉及到msi中断控制器以及lapic中断控制器
初始化
lapic
Linux内核中lapic中断控制器的irq_chip结构是lapic_controller,其是静态初始化的,如下所示
1 | static struct irq_chip lapic_controller = { |
其相关的操作函数的实际回调函数由apic_driver定义,会在apic_install_driver()进行设置,如下所示
1 | static struct apic apic_x2apic_phys __ro_after_init = { |
而其irq_domain结构则是x86_vector_domain,其在arch_early_irq_init()中初始化,如下所示
1 | static const struct irq_domain_ops x86_vector_domain_ops = { |
可以看到,其一方面初始化x86_vector_domain结构,后续用来和其他层次中断控制器进行交互;另一方面初始化vector_matrix,用来管理每个cpu的中断向量资源。
msix
类似的,Linux内核中msix中断控制器也有对应的irq_domain结构和irq_chip结构,但由于每个pci设备都会有一个msix中断控制器,是per-device的,因此内核是通过模板pci_msix_template进行初始化,如下所示
1 | static const struct msi_domain_template pci_msix_template = { |
可以看到,其基于pci_msix_template模板,通过msi_create_irq_domain()完成irq_domain的初始化。其对应的irq_domain_ops,即msi_domain_ops,其实际上最后会调用到模板中相关的info->ops。
而该domain对应的parent则是上个章节的x86_vector_domain,如下所示
1 | int pcibios_device_add(struct pci_dev *dev) |
而info->chip则是对应的irq_chip数据,其同样由pci_msix_template模板、parent->msi_parent_ops->init_dev_msi_info等共同初始化
1 | static const struct msi_parent_ops x86_vector_msi_parent_ops = { |
中断映射
Linux内核进行中断映射的一般流程如下所示
- 分配物理中断资源
- 分配中断剩余资源
- 激活中断
virtio-net-pci设备在vp_find_vqs_msix()中完成中断映射的流程
分配物理中断资源
virtio-net-pci设备会在msix_setup_msi_descs()中分配好物理的中断资源,如下所示
1 | //#0 msix_setup_msi_descs (dev=dev@entry=0xffff88810087d000, |
可以看到,其在msix_prepare_msi_desc()中准备好硬件中断所需的资源(例如msix capability基地址desc->pci.mask_base等),然后在msi_insert_msi_desc()中从de->msi.data->__domains[domid].store中进行实际分配
分配中断剩余资源
即需要分配中断建立所需要的其余所有资源,主要就是虚拟中断资源以及irq_domain层次所需资源。virtio-net-pci设备会在__msi_domain_alloc_irqs()中进行分配,如下所示
1 | //#0 __msi_domain_alloc_irqs (dev=0xffff88810087d0c0, |
首先,其会调用msi_domain_prepare_irqs(),准备分配msix虚拟中断资源所需的信息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
129
130int msi_domain_prepare_irqs(struct irq_domain *domain, struct device *dev,
int nvec, msi_alloc_info_t *arg)
{
struct msi_domain_info *info = domain->host_data;
struct msi_domain_ops *ops = info->ops;
return ops->msi_prepare(domain, dev, nvec, arg);
}
//#0 x86_msi_prepare (domain=0xffff88810c30f0c0, dev=0xffff88810087d0c0,
// nvec=0x3, alloc=0xffffc900000139b0) at arch/x86/kernel/apic/msi.c:177
//#1 0xffffffff81185f4a in msi_domain_prepare_irqs (domain=0xffff88810c30f0c0,
// dev=0xffff88810087d0c0, nvec=<optimized out>, arg=0xffffc900000139b0)
// at kernel/irq/msi.c:1097
//#2 __msi_domain_alloc_irqs (dev=0xffff88810087d0c0,
// domain=0xffff88810c30f0c0, ctrl=0xffffc90000013a38)
// at kernel/irq/msi.c:1289
//#3 0xffffffff81187636 in msi_domain_alloc_locked (dev=0xffff88810087d0c0,
// ctrl=0xffffc90000013a38) at kernel/irq/msi.c:1383
//#4 msi_domain_alloc_irqs_all_locked (dev=dev@entry=0xffff88810087d0c0,
// domid=domid@entry=0x0, nirqs=nirqs@entry=0x3) at kernel/irq/msi.c:1461
//#5 0xffffffff81611c36 in pci_msi_setup_msi_irqs (
// dev=dev@entry=0xffff88810087d000, nvec=nvec@entry=0x3,
// type=type@entry=0x11) at drivers/pci/msi/irqdomain.c:17
//#6 0xffffffff816112ef in msix_setup_interrupts (dev=<optimized out>,
// entries=<optimized out>, nvec=<optimized out>,
// affd=0x0 <fixed_percpu_data>) at drivers/pci/msi/msi.c:670
//#7 msix_capability_init (dev=0xffff88810087d000, entries=<optimized out>,
// nvec=<optimized out>, affd=<optimized out>) at drivers/pci/msi/msi.c:727
//#8 __pci_enable_msix_range (dev=dev@entry=0xffff88810087d000,
// entries=entries@entry=0x0 <fixed_percpu_data>, minvec=minvec@entry=0x3,
// maxvec=maxvec@entry=0x3, affd=affd@entry=0x0 <fixed_percpu_data>,
// flags=flags@entry=0x4) at drivers/pci/msi/msi.c:833
//#9 0xffffffff8160f957 in pci_alloc_irq_vectors_affinity (
// dev=0xffff88810087d000, min_vecs=min_vecs@entry=0x3,
// max_vecs=max_vecs@entry=0x3, flags=0x4,
// affd=affd@entry=0x0 <fixed_percpu_data>) at drivers/pci/msi/api.c:270
//#10 0xffffffff81695ee9 in vp_request_msix_vectors (vdev=0xffff88810080b800,
// nvectors=<optimized out>, per_vq_vectors=<optimized out>,
// desc=0x0 <fixed_percpu_data>) at drivers/virtio/virtio_pci_common.c:133
//#11 vp_find_vqs_msix (vdev=vdev@entry=0xffff88810080b800, nvqs=nvqs@entry=0x3,
// vqs=vqs@entry=0xffff88810c2ee200,
// callbacks=callbacks@entry=0xffff88810c2ee220,
// names=names@entry=0xffff88810c2ee240,
// per_vq_vectors=per_vq_vectors@entry=0x1, ctx=0xffff888100380c50,
// desc=0x0 <fixed_percpu_data>) at drivers/virtio/virtio_pci_common.c:312
//#12 0xffffffff81696319 in vp_find_vqs (vdev=vdev@entry=0xffff88810080b800,
// nvqs=0x3, vqs=0xffff88810c2ee200, callbacks=0xffff88810c2ee220,
// names=0xffff88810c2ee240, ctx=0xffff888100380c50,
// desc=0x0 <fixed_percpu_data>) at drivers/virtio/virtio_pci_common.c:408
//#13 0xffffffff81694826 in vp_modern_find_vqs (vdev=0xffff88810080b800,
// nvqs=<optimized out>, vqs=<optimized out>, callbacks=<optimized out>,
// names=<optimized out>, ctx=<optimized out>, desc=0x0 <fixed_percpu_data>)
// at drivers/virtio/virtio_pci_modern.c:604
//#14 0xffffffff819e3e34 in virtio_find_vqs_ctx (vdev=<optimized out>, nvqs=0x3,
// vqs=0xffff88810c2ee200, callbacks=0xffff88810c2ee220,
// names=0xffff88810c2ee240, ctx=0xffff888100380c50,
// desc=0x0 <fixed_percpu_data>) at ./include/linux/virtio_config.h:242
//#15 virtnet_find_vqs (vi=0xffff88810cdec920) at drivers/net/virtio_net.c:4389
//#16 init_vqs (vi=0xffff88810cdec920) at drivers/net/virtio_net.c:4478
//#17 0xffffffff819e4b86 in virtnet_probe (vdev=0xffff88810080b800)
// at drivers/net/virtio_net.c:4799
//#18 0xffffffff8168f3fd in virtio_dev_probe (_d=0xffff88810080b810)
// at drivers/virtio/virtio.c:311
//#19 0xffffffff81932472 in call_driver_probe (dev=0xffff88810080b810,
// drv=0xffffffff82c10280 <virtio_net_driver>) at drivers/base/dd.c:578
//#20 really_probe (dev=dev@entry=0xffff88810080b810,
// drv=drv@entry=0xffffffff82c10280 <virtio_net_driver>)
// at drivers/base/dd.c:656
//#21 0xffffffff8193268e in __driver_probe_device (
// drv=drv@entry=0xffffffff82c10280 <virtio_net_driver>,
// dev=dev@entry=0xffff88810080b810) at drivers/base/dd.c:798
//#22 0xffffffff8193272a in driver_probe_device (
// drv=drv@entry=0xffffffff82c10280 <virtio_net_driver>,
// dev=dev@entry=0xffff88810080b810) at drivers/base/dd.c:828
//#23 0xffffffff819329b3 in __driver_attach (dev=0xffff88810080b810,
// data=0xffffffff82c10280 <virtio_net_driver>) at drivers/base/dd.c:1214
//#24 __driver_attach (dev=0xffff88810080b810,
// data=0xffffffff82c10280 <virtio_net_driver>) at drivers/base/dd.c:1154
//#25 0xffffffff8193020a in bus_for_each_dev (bus=<optimized out>,
// start=start@entry=0x0 <fixed_percpu_data>,
// data=data@entry=0xffffffff82c10280 <virtio_net_driver>,
// fn=fn@entry=0xffffffff81932930 <__driver_attach>) at drivers/base/bus.c:368
//#26 0xffffffff81931f19 in driver_attach (
// drv=drv@entry=0xffffffff82c10280 <virtio_net_driver>)
// at drivers/base/dd.c:1231
//#27 0xffffffff81931637 in bus_add_driver (
// drv=drv@entry=0xffffffff82c10280 <virtio_net_driver>)
// at drivers/base/bus.c:673
//#28 0xffffffff81933a9d in driver_register (
// drv=drv@entry=0xffffffff82c10280 <virtio_net_driver>)
// at drivers/base/driver.c:246
//#29 0xffffffff8168ec0b in register_virtio_driver (
// driver=driver@entry=0xffffffff82c10280 <virtio_net_driver>)
// at drivers/virtio/virtio.c:370
//#30 0xffffffff832fb86c in virtio_net_driver_init ()
// at drivers/net/virtio_net.c:5050
//#31 0xffffffff810019e6 in do_one_initcall (
// fn=0xffffffff832fb800 <virtio_net_driver_init>) at init/main.c:1238
//#32 0xffffffff832aa0ae in do_initcall_level (level=0x6,
// command_line=0xffff8881002c1480 "rdinit") at init/main.c:1300
//#33 do_initcalls () at init/main.c:1316
//#34 do_basic_setup () at init/main.c:1335
//#35 kernel_init_freeable () at init/main.c:1548
//#36 0xffffffff81fddbd5 in kernel_init (unused=<optimized out>)
// at init/main.c:1437
//#37 0xffffffff810bb27f in ret_from_fork (prev=<optimized out>,
// regs=0xffffc90000013f58, fn=0xffffffff81fddbc0 <kernel_init>,
// fn_arg=0x0 <fixed_percpu_data>) at arch/x86/kernel/process.c:147
//#38 0xffffffff810022fa in ret_from_fork_asm () at arch/x86/entry/entry_64.S:243
//#39 0x0000000000000000 in ?? ()
static int x86_msi_prepare(struct irq_domain *domain, struct device *dev,
int nvec, msi_alloc_info_t *alloc)
{
struct msi_domain_info *info = domain->host_data;
init_irq_alloc_info(alloc, NULL);
switch (info->bus_token) {
case DOMAIN_BUS_PCI_DEVICE_MSI:
alloc->type = X86_IRQ_ALLOC_TYPE_PCI_MSI;
return 0;
case DOMAIN_BUS_PCI_DEVICE_MSIX:
case DOMAIN_BUS_PCI_DEVICE_IMS:
alloc->type = X86_IRQ_ALLOC_TYPE_PCI_MSIX;
return 0;
default:
return -EINVAL;
}
}
然后调用pci_msix_prepare_desc()准备虚拟中断对应的硬件中断所需的信息,这里在前面已经初始化过了,所以这里实际不会进行实际触发1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24static void pci_msix_prepare_desc(struct irq_domain *domain, msi_alloc_info_t *arg,
struct msi_desc *desc)
{
/* Don't fiddle with preallocated MSI descriptors */
if (!desc->pci.mask_base)
msix_prepare_msi_desc(to_pci_dev(desc->dev), desc);
}
void msix_prepare_msi_desc(struct pci_dev *dev, struct msi_desc *desc)
{
desc->nvec_used = 1;
desc->pci.msi_attrib.is_msix = 1;
desc->pci.msi_attrib.is_64 = 1;
desc->pci.msi_attrib.default_irq = dev->irq;
desc->pci.mask_base = dev->msix_base;
desc->pci.msi_attrib.can_mask = !pci_msi_ignore_mask &&
!desc->pci.msi_attrib.is_virtual;
if (desc->pci.msi_attrib.can_mask) {
void __iomem *addr = pci_msix_desc_addr(desc);
desc->pci.msix_ctrl = readl(addr + PCI_MSIX_ENTRY_VECTOR_CTRL);
}
}
接着调用pci_device_domain_set_desc()填充好msix虚拟中断所需的信息后,调用__irq_domain_alloc_irqs()好完成其余所有资源的分配
1 | //#0 pci_device_domain_set_desc (arg=0xffffc900000139b0, |
可以看到其会在irq_domain_alloc_descs中从spare_irqs中分配虚拟中断资源,如下所示
1 | int irq_domain_alloc_descs(int virq, unsigned int cnt, irq_hw_number_t hwirq, |
然后调用irq_domain_alloc_irq_data()给虚拟中断中每个irq_domain初始化irq_data资源,如下所示
1 | static int irq_domain_alloc_irq_data(struct irq_domain *domain, |
最后调用irq_domain_alloc_irqs_hierarchy()完成层次结构的irq_domain的分配回调函数的调用,填充irq_data等信息,如下所示
1 | int irq_domain_alloc_irqs_hierarchy(struct irq_domain *domain, |
激活
此时已经分配好了中断所需的所有资源,需要在request_irq()中激活中断,完成真正的中断映射,如下所示
1 | //#0 request_irq (irq=0x1b, handler=0xffffffff816956a0 <vp_config_changed>, |
可以看到,其在设置完irq_action后,会在__irq_domain_activate_irq()中分别调用所有层级irq_domain(即lapic层级和msix层级)来激活中断。
对于lapic层级,如前面章节所说,其会在对应irq_domain_ops的x86_vector_activate()中激活lapic层级的相关设置:即在irq_matrix_alloc()中更新每个cpu的中断向量资源vector_matrix,在apic_update_vector()中设置对应cpu中断向量服务服务例程vector_irq为对应的虚拟中断信息。
1 | //#0 assign_vector_locked (irqd=0xffff888100a23d00, |
而对于msix层级,如前面章节所说,其会在对应的irq_domain_ops的msi_domain_activate()中激活msix层级的相关设置:即在__irq_msi_compose_msg()中拼凑msi所需的Message信息,并在pci_write_msg_msix()中写入对应的msix capability指定的数组中
1 | static int msi_domain_activate(struct irq_domain *domain, |
中断下半部
根据前面中断硬件的介绍,在CPU处理中断的过程中会屏蔽相关中断,直到CPU完成中断处理并发送EOI命令。如果中断处理程序耗时过长,则会导致其他中断得不到有效处理,导致硬件中断丢失。为了避免这种情况,Linux内核把中断处理分为中断上半部和中断下半部:其中中断上半部在中断屏蔽期间执行,其主要快速响应硬件并调度中断下半部的执行;而中断下半部用于异步的处理耗时任务。
目前主要的中断下半部包括softirq(软中断)、tasklet和workqueue。其中tasklet基于softirq,都运行在软中断上下文中;而workqueue则运行在内核进程上下文中
softirq
softirq事件
softirq事件在Linux内核编译时就确定好的,每个软中断号对应一个事件1
2
3
4
5
6
7
8
9
10
11
12
13
14
15enum
{
HI_SOFTIRQ=0,
TIMER_SOFTIRQ,
NET_TX_SOFTIRQ,
NET_RX_SOFTIRQ,
BLOCK_SOFTIRQ,
IRQ_POLL_SOFTIRQ,
TASKLET_SOFTIRQ,
SCHED_SOFTIRQ,
HRTIMER_SOFTIRQ,
RCU_SOFTIRQ, /* Preferable RCU should always be the last softirq */
NR_SOFTIRQS
};
其内部使用数组softirq_vec管理事件的handler,并使用open_softirq()注册handler1
2
3
4
5
6static struct softirq_action softirq_vec[NR_SOFTIRQS] __cacheline_aligned_in_smp;
void open_softirq(int nr, void (*action)(struct softirq_action *))
{
softirq_vec[nr].action = action;
}
一般的,中断上半部中会调用raise_softirq()标记待处理的软中断事件,后续会在异步执行的中断下半部中调用__do_softirq()处理软中断事件
1 |
|
可以看到,其软中断处理逻辑还是很清晰。当中断上半部准备产生一个需要异步处理的软中断事件时,其会标记软中断事件号对应的比特。而当中断下半部准备处理软中断事件时,其会执行所有被标记的软中断事件的handler
触发时机
如前面所说,被标记的软中断任务会在中断下半部被异步地执行。其执行操作主要在以下两个时机被调度触发
- CPU调度到ksoftirqd内核线程
- 在中断上半部(硬件中断处理程序)退出时(irq_exit_rcu)
ksoftirqd
linux内核会在spawn_ksoftirqd()中为每一个CPU都初始化一个ksoftirqd内核线程,专门用于处理softirq事件1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17//#0 spawn_ksoftirqd () at kernel/softirq.c:972
//#1 0xffffffff81001a60 in do_one_initcall (fn=0xffffffff832c8940 <spawn_ksoftirqd>) at init/main.c:1238
//#2 0xffffffff8329e100 in do_pre_smp_initcalls () at init/main.c:1344
//#3 kernel_init_freeable () at init/main.c:1537
//#4 0xffffffff81fa3a85 in kernel_init (unused=<optimized out>) at init/main.c:1437
//#5 0xffffffff810cea7c in ret_from_fork (prev=<optimized out>, regs=0xffffc90000013f58, fn=0xffffffff81fa3a70 <kernel_init>, fn_arg=0x0 <fixed_percpu_data>) at arch/x86/kernel/process.c:147
//#6 0xffffffff8100244a in ret_from_fork_asm () at arch/x86/entry/entry_64.S:243
//#7 0x0000000000000000 in ?? ()
static __init int spawn_ksoftirqd(void)
{
cpuhp_setup_state_nocalls(CPUHP_SOFTIRQ_DEAD, "softirq:dead", NULL,
takeover_tasklets);
BUG_ON(smpboot_register_percpu_thread(&softirq_threads));
return 0;
}
early_initcall(spawn_ksoftirqd);
可以看到,其在内核初始化阶段,基于softirq_threads参数调用smpboot_register_percpu_thread()为每个CPU创建内核线程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
86static struct smp_hotplug_thread softirq_threads = {
.store = &ksoftirqd,
.thread_should_run = ksoftirqd_should_run,
.thread_fn = run_ksoftirqd,
.thread_comm = "ksoftirqd/%u",
};
/**
* smpboot_register_percpu_thread - Register a per_cpu thread related
* to hotplug
* @plug_thread: Hotplug thread descriptor
*
* Creates and starts the threads on all online cpus.
*/
int smpboot_register_percpu_thread(struct smp_hotplug_thread *plug_thread)
{
unsigned int cpu;
int ret = 0;
cpus_read_lock();
mutex_lock(&smpboot_threads_lock);
for_each_online_cpu(cpu) {
ret = __smpboot_create_thread(plug_thread, cpu);
if (ret) {
smpboot_destroy_threads(plug_thread);
goto out;
}
smpboot_unpark_thread(plug_thread, cpu);
}
list_add(&plug_thread->list, &hotplug_threads);
out:
mutex_unlock(&smpboot_threads_lock);
cpus_read_unlock();
return ret;
}
static int
__smpboot_create_thread(struct smp_hotplug_thread *ht, unsigned int cpu)
{
struct task_struct *tsk = *per_cpu_ptr(ht->store, cpu);
struct smpboot_thread_data *td;
if (tsk)
return 0;
td = kzalloc_node(sizeof(*td), GFP_KERNEL, cpu_to_node(cpu));
if (!td)
return -ENOMEM;
td->cpu = cpu;
td->ht = ht;
tsk = kthread_create_on_cpu(smpboot_thread_fn, td, cpu,
ht->thread_comm);
...
return 0;
}
/**
* smpboot_thread_fn - percpu hotplug thread loop function
* @data: thread data pointer
*
* Checks for thread stop and park conditions. Calls the necessary
* setup, cleanup, park and unpark functions for the registered
* thread.
*
* Returns 1 when the thread should exit, 0 otherwise.
*/
static int smpboot_thread_fn(void *data)
{
struct smpboot_thread_data *td = data;
struct smp_hotplug_thread *ht = td->ht;
while (1) {
set_current_state(TASK_INTERRUPTIBLE);
preempt_disable();
...
if (!ht->thread_should_run(td->cpu)) {
preempt_enable_no_resched();
schedule();
} else {
__set_current_state(TASK_RUNNING);
preempt_enable();
ht->thread_fn(td->cpu);
}
}
}
整体逻辑也很清晰,smpboot_register_percpu_thread()会给每个CPU创建一个执行smpboot_thread_fn()循环的内核线程。在循环里会调用回调函数run_ksoftirqd()函数,并最终调用__do_softirq()处理softirq事件
irq_exit_rcu
前面硬件中断小节介绍了,中断上半部的handler由DEFINE_IDTENTRY_IRQ宏定义,其在handler最后会调用到irq_exit_rcu(),并触发invoke_softirq()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
call_on_irqstack_cond(func, regs, ASM_CALL_IRQ, \
IRQ_CONSTRAINTS, regs, vector); \
}
/*
* Macro to invoke system vector and device interrupt C handlers.
*/
irq_enter_rcu(); \
func(c_args); \
irq_exit_rcu(); \
...
}
void irq_exit_rcu(void)
{
__irq_exit_rcu();
/* must be last! */
lockdep_hardirq_exit();
}
static inline void __irq_exit_rcu(void)
{
local_irq_disable();
lockdep_assert_irqs_disabled();
account_hardirq_exit(current);
preempt_count_sub(HARDIRQ_OFFSET);
if (!in_interrupt() && local_softirq_pending())
invoke_softirq();
tick_irq_exit();
}
//#0 invoke_softirq () at kernel/softirq.c:421
//#1 __irq_exit_rcu () at kernel/softirq.c:633
//#2 irq_exit_rcu () at kernel/softirq.c:645
//#3 0xffffffff81f8aabe in common_interrupt (regs=0xffffc9000009be38, error_code=<optimized out>) at arch/x86/kernel/irq.c:247
static inline void invoke_softirq(void)
{
if (!force_irqthreads() || !__this_cpu_read(ksoftirqd)) {
/*
* We can safely execute softirq on the current stack if
* it is the irq stack, because it should be near empty
* at this stage.
*/
__do_softirq();
/*
* Otherwise, irq_exit() is called on the task stack that can
* be potentially deep already. So call softirq in its own stack
* to prevent from any overrun.
*/
do_softirq_own_stack();
} else {
wakeup_softirqd();
}
}
而在invoke_softirq()中,其会直接调用__do_softirq()直接处理软中断时间或是唤醒ksoftirqd内核线程去处理
tasklet
由于softirq事件在内核编译时就确定了,如果想新添加还需要修改并重新编译内核,这是无法接受的。因此内核实现了一种更常用的中断下半部,即tasklet。tasklet构建在softirq之上,其实现基于两个softirq事件,TASKLET_SOFTIRQ和HI_SOFTIRQ
tasklet事件
tasklet用struct tasklet_struct表示每一个动态事件1
2
3
4
5
6
7
8
9
10
11
12struct tasklet_struct
{
struct tasklet_struct *next;
unsigned long state;
atomic_t count;
bool use_callback;
union {
void (*func)(unsigned long data);
void (*callback)(struct tasklet_struct *t);
};
unsigned long data;
};
其内部使用链表tasklet_hi_vec/tasklet_vec进行管理,其链表头是个per_cpu变量,即每个cpu一个链表1
2
3
4
5
6
7
8
9
10/*
* Tasklets
*/
struct tasklet_head {
struct tasklet_struct *head;
struct tasklet_struct **tail;
};
static DEFINE_PER_CPU(struct tasklet_head, tasklet_vec);
static DEFINE_PER_CPU(struct tasklet_head, tasklet_hi_vec);
内核其他子系统和内核驱动模块可以通过tasklet_setup()/tasklet_init()初始化动态创建的tasklet事件,或DECLARE_TASKLET()/DECLARE_TASKLET_OLD()静态创建tasklet事件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
34void tasklet_setup(struct tasklet_struct *t,
void (*callback)(struct tasklet_struct *))
{
t->next = NULL;
t->state = 0;
atomic_set(&t->count, 0);
t->callback = callback;
t->use_callback = true;
t->data = 0;
}
void tasklet_init(struct tasklet_struct *t,
void (*func)(unsigned long), unsigned long data)
{
t->next = NULL;
t->state = 0;
atomic_set(&t->count, 0);
t->func = func;
t->use_callback = false;
t->data = data;
}
一般的,中断上半部会调用tasklet_hi_schedule()/tasklet_schedule()将待处理的tasklet事件插入到tasklet_vec/tasklet_hi_vec队列队尾并标记对应的softirq事件,后续会在TASKLET_SOFTIRQ/HI_SOFTIRQ softirq事件中调用tasket_action()/tasklet_hi_action()处理tasklet事件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
37static inline void tasklet_schedule(struct tasklet_struct *t)
{
if (!test_and_set_bit(TASKLET_STATE_SCHED, &t->state))
__tasklet_schedule(t);
}
void __tasklet_schedule(struct tasklet_struct *t)
{
__tasklet_schedule_common(t, &tasklet_vec,
TASKLET_SOFTIRQ);
}
static inline void tasklet_hi_schedule(struct tasklet_struct *t)
{
if (!test_and_set_bit(TASKLET_STATE_SCHED, &t->state))
__tasklet_hi_schedule(t);
}
void __tasklet_hi_schedule(struct tasklet_struct *t)
{
__tasklet_schedule_common(t, &tasklet_hi_vec,
HI_SOFTIRQ);
}
static void __tasklet_schedule_common(struct tasklet_struct *t,
struct tasklet_head __percpu *headp,
unsigned int softirq_nr)
{
struct tasklet_head *head;
unsigned long flags;
local_irq_save(flags);
head = this_cpu_ptr(headp);
t->next = NULL;
*head->tail = t;
head->tail = &(t->next);
raise_softirq_irqoff(softirq_nr);
local_irq_restore(flags);
}
处理逻辑
根据前面softirq事件小节,内核在softirq_init()中注册TASKLET_SOFTIRQ事件和HI_SOFTIRQ事件的handler分别为tasklet_action()和tasklet_hi_action(),如下所示
1 | static __latent_entropy void tasklet_action(struct softirq_action *a) |
对于workqueue_softirq_action(),其用于workqueue的POOL_BH类型的worker处理work事件,这个在后面workqueue小节中再详细介绍
而tasklet_action_common()的逻辑很清晰,遍历per_cpu变量的tasklet_hi_vec/tasklet_vec链表来执行待处理tasklet事件。因为处理tasklet事件前会先获取锁,这保证了同一时刻同一个tasklet事件只会在一个cpu上执行
workqueue
由于softirq事件在内核编译时就确定了,如果想新添加还需要修改并重新编译内核,这是无法接受的。因此内核实现了一种支持动态创建事件的中断下半部,即workqueue:即workqueue使用者生产work事件,而基于内核线程的worker异步地消费work事件
内核中workqueue有多个版本的实现,目前最新版本的workqueue实现是CMWQ(Concurrency Managed Workqueue)。其整体思路为如下两点:
- worker由worker_pool进行管理,其会根据负载弹性扩缩worker数量
- 内核会静态/动态创建有限数量的worker_pool,workqueue公用这些worker_pool
worker_pool
CWMQ中最核心的概念就是worker_pool,其用于管理worker线程及其需要处理的work事件,由struct worker_pool结构体进行描述
1 | struct worker_pool { |
其中,worklist字段标记该pool所有待处理的work事件,其会被该pool的所有worker线程一起消费;而worker字段则标记该pool管理的所有worker线程,其会共同消费该pool管理的work事件队列,具体细节在下面章节具体介绍
内核中一共有两类worker_pool,即bound worker_pool和unbound worker_pool
其中bound worker_pool中所有的worker线程只能运行在指定CPU上。该类型的worker_pool是由内核基于DEFINE_PER_CPU_SHARED_ALIGNED()静态定义的,如下所示
1 | enum wq_internal_consts { |
可以看到,内核中一共定义了bh_pool_irq_works、bh_worker_pools和cpu_worker_pools三个bound worker_pools,并且每一个worker_pools在每个CPU上都有两个worker_pool,后一个(下标为1)的worker_pool其调度优先级会更高一些
而对于unbound worker_pool,其管理的worker可以在多个cpu上运行,内核会使用get_unbound_pool()来获取和创建
1 | /** |
可以看到,内核会使用unbound_pool_hash管理所有的unbound worker_pool,只有workqueue使用者需要当前特定的worker_pool并且unbound_pool_hash中没有记录时才会创建该worker_pool
work事件
前面worker_pool管理的work事件可以用struct work_struct进行描述
1 | struct work_struct { |
workqueue使用者可以通过INIT_WORK()初始化动态创建的work事件,或使用DECLARE_WORK()静态创建work事件
1 | static inline void __init_work(struct work_struct *work, int onstack) { } |
而所有的work事件会被以队列的形式管理在不同队列中(例如前面worker_pool中所有待处理事件被管理在struct worker_pool的worklist字段),struct work_struct的entry字段指向work事件所在的事件队列。kernel使用insert_work()将work事件插入到事件队列中
1 | /** |
worker进程
前面介绍的worker线程则由struct worker进行描述
1 | /* |
其node字段指向所在的worker_pool管理的所有worker列表
内核会调用create_worker()为worker_pool创建worker对应的内核线程
1 | /** |
可以看到,对于非POOL_BH类型,内核调用kthread_create_on_node函数,创建名为kworker/X的内核线程,该内核线程会执行worker_thread()线程函数来处理work事件
1 | /** |
其逻辑比较清晰,worker_thread()函数会循环执行获取事件并处理事件流程直到被标记WORKER_DIE。具体的,在每个循环中其使用assign_work()从struct worker_pool的worklist中获取事件,并根据情况将其添加到当前thread的worker或对应worker的scheduled字段的队列中;会使用process_scheduled_works()中持续调用process_one_work()处理所有的schedules事件队列
除此之外,worker_thread()中manage_workers()和worker_enter_idle()也实现了worker_pool中worker线程数量的弹性扩容和缩容
1 | /** |
可以看到,所有worker在准备处理work事件会调用manage_workers()来创建worker直到至少有一个idle worker,从而完成扩容
1 | static int init_worker_pool(struct worker_pool *pool) |
可以看到,在初始化每个struct worker_pool时会初始化一个timer(idle_timer)和idle_cull_work的work事件。当worker进程没有待处理的work事件要进入idle状态时,其会调用worker_enter_idle(),设置前面初始化的idle_timer定时器,当定时器超时后调用设置的处理函数idle_worker_timeout()来产生idle_cull_work的work事件,该事件会调用idle_cull_fn()来完成最终的worker删除,实现缩容
接口
而workqueue暴露给使用者的前端接口,则是struct workqueue_struct,如下所示
1 | /* |
前面介绍workqueue会公用所有的worker_pool,其使用struct pool_workqueue结构体来管理workqueue_struct和worker_pool的一一映射关系
1 | /* |
可以看到,pool_workqueue的wq字段和pool字段则分别为struct workqueue_struct和worker_pool,完成了一一映射
创建
内核使用alloc_workqueue()创建一个workqueue,如下所示
1 | __printf(1, 4) |
可以看到,其函数逻辑比较清晰,在完成相关数据结构分配和初始化后,其会调用alloc_and_link_pwqs(),根据flags参数完成对应worker_pool的选择和映射,如下所示
1 | static int alloc_and_link_pwqs(struct workqueue_struct *wq) |
可以看到,对于bound类型,其对应的per-cpu字段cpu_pwq会被分别初始化一个struct pool_workqueue实例。该实例将直接映射到内核静态定义好的per-cpu worker_pool,即bh_worker_pools/cpu_worker_pools,从而建立workqueue到指定per-cpu worker_pool的映射关系。
而对于unbound类型,则是使用apply_workqueue_attrs()完成worker_pool的选择和映射
1 | int apply_workqueue_attrs(struct workqueue_struct *wq, |
内核会将dfl_pwq字段和per-cpu字段的cpu_pwq都初始化一个struct pool_workqueue实例,但是这些实例都会映射同一个通过get_unbound_pool()获取/创建的worker_pool实例,从而确保不受特定cpu限制
work调度
内核使用queue_work()/queue_work_on()让workqueue使用者向workqueue提交work事件,其逻辑如下所示
1 | static inline bool queue_work(struct workqueue_struct *wq, |
其整体逻辑也很清晰,如果是bound类型的workqueue,则调用insert_work()将其添加到cpu_pwq字段对应的当前cpu的per_cpu的worker_pool中;如果是unbound类型的workqueue,则调用insert_work()将其添加到wq_select_unbound_cpu()选择的cpu的per_cpu的worker_pool中
在添加玩work事件后,还需要调用kick_pool()来唤醒worker_pool中的worker线程,如下所示
1 | /** |
这里可以看到,对于非POOL_BH类型的,其就是唤醒worker_pool中的idle worker进程即可。
而对于POOL_BH类型,前面worker进程小节中提到过,对于POOL_BH类型并不会创建worker进程,而是在tasklet的下半部的workqueue_softirq_action()中直接调用bh_worker()进行处理,如下所示
1 | void workqueue_softirq_action(bool highpri) |
可以看到,基本和worker_thread()逻辑类似,但是没有了睡眠和唤醒的逻辑,即执行完待处理的work事件后就退出,避免阻塞tasklet下半部的逻辑。因此workqueue只需要在kich_bh_pool()中在对应的cpu上生成HI_SOFTIRQ/TASKLET_SOFTIRQ事件即可,后续会在tasklet中断下半部完成work事件的处理
1 | static void kick_bh_pool(struct worker_pool *pool) |
参考
- 一文了解 OS-中断
- 计算机中断体系一:历史和原理
- 计算机中断体系二:中断处理
- 计算机中断体系三:中断路由
- 8259A PIC手册
- Intel® 64 and IA-32 Architectures Software Developer’s Manua-Volume 3A: System Programming Guide, Part 1-CHAPTER 11 ADVANCED PROGRAMMABLE INTERRUPT CONTROLLER (APIC)
- 再谈中断(APIC)
- 82093AA I/O ADVANCED PROGRAMMABLE INTERRUPT CONTROLLER (IOAPIC
- PCI/PCIe 总线概述(6)—MSI和MSI-X中断机制
- Linux kernel的中断子系统之(二):IRQ Domain介绍
- 【原创】Linux中断子系统(二)-通用框架处理
- 一文完全读懂 | Linux中断处理
- IRQ域层级结构
- Linux内核 | 中断机制
- 深度剖析Linux 网络中断下半部处理(看完秒懂)
- Linux 中断(IRQ/softirq)基础:原理及内核实现(2022)
- tasklet(linux kernel 中断下半部的实现机制)
- 扒开 Linux 中断的底裤之 workqueue
- Linux 的 workqueue 机制浅析
- IRQ Descriptor and irq_chip
