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次,