X86-64架构下一个网卡中断的处理流程

Table of Contents

本文以一个网卡中断的处理过程来研究X86-64的中断管理,这个调用流程如下:

=> e1000_intr
=> __handle_irq_event_percpu
=> handle_irq_event
=> handle_fasteoi_irq
=> __common_interrupt
=> common_interrupt
=> asm_common_interrupt
=> e1000_clean_rx_irq
=> e1000_clean
=> __napi_poll
=> net_rx_action
=> handle_softirqs
=> __irq_exit_rcu
=> common_interrupt
=> asm_common_interrupt
=> finish_task_switch.isra.0
=> __schedule
=> schedule
=> worker_thread
=> kthread
=> ret_from_fork
=> ret_from_fork_asm

1. 中断向量表初始化

X86-64的外部中断通过idt_setup_apic_and_irq_gates函数来注册,但是外部中断表本身的内容通过irq_entries_start符号描述。下面分这两方面来介绍X86-64的初始化。

1.1. 外部中断向量表构造

下面的汇编代码描述了外部中断向量表的内容:

	.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)

这里NR_EXTERNAL_VECTORS以及FIRST_EXTERNAL_VECTOR被如下方式定义:

/*
 * Posted interrupt notification vector for all device MSIs delivered to
 * the host kernel.
 */
#define POSTED_MSI_NOTIFICATION_VECTOR	0xeb

/*
 * IDT vectors usable for external interrupt sources start at 0x20.
 * (0x80 is the syscall vector, 0x30-0x3f are for ISA)
 */
#define FIRST_EXTERNAL_VECTOR		0x20

#ifdef CONFIG_X86_LOCAL_APIC
#define FIRST_SYSTEM_VECTOR		POSTED_MSI_NOTIFICATION_VECTOR
#else
#define FIRST_SYSTEM_VECTOR		NR_VECTORS
#endif
#define NR_EXTERNAL_VECTORS		(FIRST_SYSTEM_VECTOR - FIRST_EXTERNAL_VECTOR)

X86-64架构对于中断号的使用是划分了范围的,0x20是外部中断向量的起始位置,所谓外部中断源是指硬件设备(如键盘、网卡、定时器等)通过中断控制器(如 PIC、APIC或Local APIC)触发的中断,这些外部设备都需要通过中断控制器再中断CPU。

现代x86-64架构一般都配置了CONFIG_X86_LOCAL_APIC,所以FIRST_SYSTEM_VECTOR就是POSTED_MSI_NOTIFICATION_VECTOR,POSTED_MSI_NOTIFICATION_VECTOR(向量号 0xEB)主要用在中断虚拟机化里,当设备通过 MSI(Message Signaled Interrupts)触发中断时,如果设备的中断目标是虚拟机(vCPU),并且这个vCPU正在运行,且它支持APICv,APIC会直接通过PI机制将中断注入到目标虚拟机。如果目标虚拟机vCPU未运行(例如调度到宿主机的其他线程上),则会触发POSTED_MSI_NOTIFICATION_VECTOR(0xEB)来通知宿主机,传统中断处理中,当设备向虚拟机发送中断时,通常需要以下步骤,首先触发中断导致虚拟机VM-Exit。然后宿主机内核处理中断后,再注入到虚拟机。这种方法会带来大量的性能开销,尤其是I/O密集型工作负载(如网卡或存储设备)会产生频繁的中断。使用POSTED_MSI_NOTIFICATION_VECTOR和PI机制后,如果目标vCPU正在运行,则直接将中断注入虚拟机,完全避免VM-Exit。如果目标vCPU未运行,仅在必要时通知宿主机处理,这大幅减少了VM-Exit的次数。

所以[0x20, 0xeb)共计203个向量号用于外部设备中断,低于0x20的中断号一般用于处理CPU内部异常,比如除0错误,调试以及Page Fault等,NMI使用的向量号也低于0x20。

使用.byte硬编码push指令的方式

最后反汇编vmlinux查看被编译出来的irq_entries_start符号处的指令,就类似下面的模式:

ffffffff81e00230 <irq_entries_start>:
ffffffff81e00230:       f3 0f 1e fa             endbr64
ffffffff81e00234:       6a 20                   push   $0x20
ffffffff81e00236:       e9 05 13 00 00          jmp    ffffffff81e01540 <asm_common_interrupt>
ffffffff81e0023b:       cc                      int3
ffffffff81e0023c:       cc                      int3
ffffffff81e0023d:       cc                      int3
ffffffff81e0023e:       cc                      int3
ffffffff81e0023f:       cc                      int3
ffffffff81e00240:       f3 0f 1e fa             endbr64
ffffffff81e00244:       6a 21                   push   $0x21
ffffffff81e00246:       e9 f5 12 00 00          jmp    ffffffff81e01540 <asm_common_interrupt>
ffffffff81e0024b:       cc                      int3
ffffffff81e0024c:       cc                      int3
ffffffff81e0024d:       cc                      int3
ffffffff81e0024e:       cc                      int3
ffffffff81e0024f:       cc                      int3

每个中断句柄入口的代码都是相似的几条指令,唯一的不同就是push到栈上的向量号不一样。注意每个中断句柄的入口,其第一条指令都是endbr64,这是因为内核开启了X86_KERNEL_IBT配置的缘故,该指令的作用是标记合法的间接跳转目标,确保控制流的安全性。所谓间接跳转,比如间接调用或中断处理程序入口(因为硬件会自动往中断句柄跳)。如果跳转到没有endbr64的地址,处理器会触发异常(#CP: Control Protection Exception),从而防御攻击。

.align IDT_ALIGN指明了接下来的汇编符号(代码)要对齐到某个字节,现代Intel处理器一般启用了IBT(Intel CET,Control-flow Enforcement Technology),这是一种安全机制,用于防范间接分支跳转攻击,这些攻击会劫持程序的控制流,跳转到恶意代码或利用程序中合法代码片段进行恶意行为,启用了这个配置,就会对齐到16字节处。

.rept和.endr宏指令表示在它们之间的指令需要重复编出NR_EXTERNAL_VECTORS次,

1.2. 注册外部中断向量表

2. 一个网卡中断的触发与执行

Author: Cauchy(pqy7172@gmail.com)

Created: 2025-01-22 Wed 22:13

Validate