虚拟化中的中断机制:X86与PIC 8259A探索(上)
本系列深入探讨虚拟化中断技术,从X86架构和PIC 8259A的基础,到IOAPIC和MSI的编程,再到MSIX技术与Broiler设备的实战应用,全面剖析中断虚拟化的前沿进展。
X86 中断机制
在计算机架构中,CPU 运行的速度远远大于外设运行的速度,在早期程序设计时,计算机如果要获得外部设备完成 IO 情况,计算机就不得不通过轮询来查询外设完成情况,因此往往做了很多无用的外设,从而导致计算机性能低下。为了解决这个问题引入了中断机制,中断 是为了解决外部设备完成某些工作之后通知 CPU 的一种机制,中断大大解放了 CPU, IO 操作效率大增. 在 X86 架构中,中断是一种电信号,由硬件外设产生,并直接送入中断控制器的输入引脚,然后由中断控制器下处理器发送相应的信号。处理器一旦检查到该信号,便会中断当前处理的工作转而处理中断。
X86 架构的 CPU 为中断提供了两条外接引脚: NMI 和 INTR. 其中 NMI 是不可屏蔽中断,通常用于电源掉电和物理存储器奇偶校验; INTR 是可屏蔽中断,可以通过设置中断屏蔽位来进行中断屏蔽,主要用于接受外部硬件的中断信号,这些信号由中断控制器传递给 CPU。常见的中断控制器有两种: 8259A PIC 和 APIC.
PIC 8259A
X86 架构中,传统的 PIC(Programmable Interrupt Controller) 可编程中断控制器由两片 8259A 外部芯片以级联的方式连接在一起,每个芯片可处理多达 8 个不同的 IRQ,Slave PIC 的 INT 输出线连接到 Master PIC 的 IRQ2 引脚,所以可用的 IRQ 线的个数达到 15 个. 中断引脚具有优先级,其中 IR0 优先级最高,IR7 优先级最低。PIC 内部具有三个重要的寄存器:
IRR(Interrupt Request Register) 中断请求寄存器, 一共 8 bit 对应 IR0 ~ IR7 8 个中断引脚. 某位置位代表收到对应引脚收到中断但还没有提交给 CPU. ISR(In Service Register) 服务中寄存器,一共 8 bit,某位置位代表对应引脚的中断已提交给 CPU 处理,但 CPU 还没有处理完. IMR(Interrupt Mask Register) 中断屏蔽寄存器,一个 8 bit,某位置位代表对应的引脚被屏蔽. 除此之外,PIC 还有一个 EOI 位,当 CPU 处理完一个中断时,通过写该 bit 告知 PIC 中断处理完毕。PIC 向 CPU 递交中断的流程如下:
1. 一个或多个 IR 引脚产生电平信号,若中断对应的 IRR bit 没有置位.
2. PIC 拉高 INT 引脚通知 CPU 中断发生
3. CPU 通过 INTA 引脚应答 PIC 表示中断请求收到
4. PIC 收到 INTA 应答之后,将 IRR 中具有高优先级的位清零,并置位 ISR 对应位
5. CPU 通过 INTA 引脚第二次发出脉冲,PIC 将最高优先级 Vector 送到数据线上.
6. 等待 CPU 写 EOI.
7. 收到 EIO 后,ISR 中最高优先级位清零.
APIC: Local APIC and I/O APIC
PIC 可以在 UP(单处理器)平台上工作,但无法用于 MP(多处理)平台,为此 APIC(Advanced Programmable Interrupt Controller) 应运而生。APIC 由位于 CPU 中的本地高级可编程中断控制器 LAPIC(Local Advanced Programmable Interrupt Controller) 和位于主板南桥中 I/O 高级可编程中断控制器 I/O APIC(I/O Advanced Programmable Interrupt Controller) 两部分构成,他们的关系如上图.
每个 Logical Processor 逻辑处理器都有自己的 Local APIC,每个 local APIC 包含了一组 Local APIC 寄存器,用于控制 kicak 和 external 中断的产生、发送和接受等,也用于产生和发送 IPI。Local APIC 寄存器组以 MMIO 形式映射到系统的存储域空间,因此可以像操作物理内存一样访问。Local APIC 寄存器在存储域的起始物理地址为 0xFEE0000; 在 x2APIC 模式的 Local APIC 寄存器映射到 MSR 寄存器组来代替,因此可以使用 RDMSR 和 WRMSR 指令来访问 Local APIC 寄存器。
Local APIC 由一组 LVT(Local vector table) 寄存器用来产生和接口 Local interrupt source. 由 LVT 的 LINT0 和 LINT1 寄存器对应着处理器 LINT0 和 LINT1 Pin, 它们可以直接接受外部 I/O 设备或连接 8259A 兼容类的外部中断控制器. 典型的 LINT0 作为处理器的 INTR Pin 接着外部的 8259 类的中断控制器的 INTR 输出端,LINT1 作为处理器的 NMI Pin 接外部设备的 NMI 请求.
IO APIC 通常有 24 个不具有优先级的引脚用于连接外部设备,当收到某个引脚的中断信号之后,IO APIC 根据软件设定的 PRT(Programmable Redirection Table) 表查找对应引脚的 RTE(Redirection Table Entry). 通过 PTE 的各个字段,格式化出一条包含该中断所有信息的中断信息,再由系统总线发送给特定的 CPU 的 Local APIC,Local APIC 收到该信息之后择机将中断递交给 CPU 处理. IO APIC 也有自己的寄存器,同样也是通过 MMIO 映射到存储域空间。在 APIC 系统中,中断发起大致流程如下:
1. IO APIC 收到某个引脚产生的中断信号
2. 查找 PRT 表获得引脚对应的 RTE
3. 根据 RTE 个字段格式化出一条中断信息,并确定发送给哪个 CPU 的 LAPIC
4. 通过系统总线发送中断信息
5. Local APIC 收到中断信息,判断是否由自己接受
6. Local APIC 确认接受,将 IRR 中对应的位置位,同时确认是否由 CPU 处理
7. 确认由 CPU 处理中断,从 IRR 中获得最高优先级中断,将 ISR 中对应的位置位,提交中断。对于边缘触发的中断,IRR 中对应的位此时清零.
8. CPU 处理完中断,软件写 EOI 寄存器告知中断处理完成. 对于电平触发中断,IRR 中对应的位清零. Local APIC 提交下一个中断.
在 MP(多处理器) 平台上,多个 CPU 要协同工作,处理器间中断 (Inter-processor Interrupt, IPI) 提供 CPU 之间相互通信的手段。CPU 可以通过 Local APIC 的 ICR(Interrupt Command Register, 中断命令寄存器) 向指定的一个/多个 CPU 发送中断. OS 通常使用 IPI 来完成诸如进程转移、中断平衡和 TLB 刷新等任务.
中断重要概念
中断可分为同步中断(Synchronous Interrupt) 和异步中断(Asynchronous Interrupt)。同步中断是当指令执行时由 CPU 控制单元产生,之所以称为同步,是因为只有在一条指令执行完毕后 CPU 才会发出中断,发生中断之后 CPU 立即处理,而不是发生在代码指令执行期间,比如系统调用; 异步中断是指由其他硬件设备依照 CPU 时钟信号随机产生,即意味着中断能够在指令之间发生,中断产生之后不能立即被 CPU 执行.
在 Intel X86 架构中,同步中断称为异常(exception), 异步中断称为中断(Interrupt), 中断可以分为可屏蔽中断(Maskable Interrupt) 和不可屏蔽中断(Nomaskable Interrupt). 异常可分为: 故障(Fault)、陷阱(Trap)和终止(abort)三类. 在 X86 架构中每个中断被赋予一个唯一的编号或者向量Vector(8 位无符号整数).
在 X86 架构保护模式下,系统使用中断描述表(Interrupt Descriptor Table, IDT) 表示中断向量表,总共 256 个描述符,IDT 的索引称为中断向量 Vector. IDT 表实际就是一个大数组,IDTR 寄存器指明了 IDT 在物理内存的位置以及长度,用于存放各种门(中断门、陷阱门、任务门),这些门是中断和异常通往各自处理函数的入口。
PIN/IRQ/GSI/VECTOR
PIN、IRQ、GSI 和 Vector 这几个概念容易搅浑,IRQ 是 PIC 时代的产物,由于 ISA 设备通常是连接到固定的 PIC 引脚,所以说一个设备的 IRQ 实际就是指它连接的 PIC 引脚号。IRQ 暗示着中断优先级,例如 IRQ0 比 IRQ1 有着更高的优先级。当进入到 APIC 时代,为了向前兼容,习惯用 IRQ 表示一个设备的中断号,但对于 16 以下的 IRQ,可能不再与 IOAPIC 的引脚对应. Pin 是引脚号,表示 IOAPIC 的引脚,PIC 时代类似的是 IRQ。Pin 的最大值受 IOAPIC 的引脚数限制,目前取值范围是 [0, 23]. GSI(Global System Interrupt) 是 APIC 时代引入的概念,它为系统中的每个中断源指定唯一的中断号.
上图中有 3 个 I/O APIC, IO-APIC0 具有 24 个引脚,其中 GSI Base 为 0, 每个 Pin 的 GSI=GSI_Base + Pin , 故 IO-APIC0 的 GSI 范围为 [0: 23]. IO-APIC1 具有 16 个引脚,GSI base 为 24,GSI 范围为 [24, 39], 以此类推。APIC 要求 ISA 的 16 个 IRQ 应该被 Identify map 到 GSI [0, 15]. Vector 是中断中 IDT 表中的索引,是一个 CPU 概念,每个 IRQ (或 GSI) 都对应一个 Vector。在 PIC 模式下,IRQ 对应的 vector = Start_Vector + IRQ; 在 APIC 模式下, IRQ/GSI 的 vector 由操作系统分配.
操作系统对中断/异常的处理流程
虽然各种操作系统对中断/异常处理的实现不同,但基本流程遵循如下顺序: 一个中断或异常发生,打断当前正在执行的任务
1. CPU 通过 vector 索引 IDT 表得到对应的门,并获得其处理函数的入口地址
2. 保存被打断任务的上下文,并跳转到中断处理函数进行执行
3. 如果是中断,处理完成后需要写 EOI 寄存器应答,异常不需要
4. 恢复被打断任务的上下文,准备返回
5. 从中断/异常的处理函数返回,恢复被打断的任务,使其继续执行
资料直通车:Linux内核源码技术学习路线+视频教程内核源码
学习直通车:Linux内核源码内存调优文件系统进程管理设备驱动/网络协议栈
X86 中断虚拟化
在虚拟化场景中,VMM 也需要为 Guest OS 展现一个与物理中断架构类似的虚拟中断架构。如上图展示虚拟机的中断架构,和物理平台一样,每个 VCPU 都对应一个虚拟 Local APIC 用于接收中断. 虚拟平台也包含了虚拟 I/O APIC 或者虚拟 PIC 用于发送中断。和 VCPU 一样,虚拟 Local APIC、虚拟 I/O APIC 和 虚拟 PIC 都是由 VMM 维护. 当虚拟设备需要发送中断时,虚拟设备会调用虚拟 I/O APIC 的接口发送中断,虚拟 I/O APIC 根据中断请求,挑选出相应的虚拟 Local APIC, 调用其接口发出中断请求,虚拟 Local APIC 进一步利用 VT-x 的事件注入机制将中断注入到相应的 VCPU. 由此可见中断虚拟化主要任务就是实现虚拟 PIC、虚拟 I/O APIC 和虚拟 Local APIC,并且实现虚拟中断的生成、采集和注入的过程。
PIC 8259A 虚拟化
IOAPIC 虚拟化
在 PCI/PCIe 设备上不仅支持 Line-Based PCI Interrupt Routing, 也支持更为现代的 PCI Message-Signalled Interrupt, 让设备支持超过 IOAPIC/PIC 更多的中断,MSI/MSIX 更好的服务 PCI Function,使中断直接送到指定的 LAPIC.
MSI 中断虚拟化
MSIX 中断虚拟化
当 Broiler 触发了 PIC/IOAPIC 中断,需要让虚拟机 VM-EXIT 之后再 VM-ENTRY,将需要注入的中断写入到 VMCS 的 VM_ENTRY_INTR_INFO_FIELD 域中,VM-ENTRY 的时候会检查该域是否有中断需要注入,如果有 VM-ENTRY 之后理解触发对应的中断, 当 Guest OS 处理完中断之后,需要写入 EOI,那么同样导致 VM-EXIT. 如果 Broiler 提出注入中断的请求之后,虚拟机正处于休眠时,那么 KVM 会模拟发送 IPI 中断,让虚拟机发送 VM-EXIT. 随机硬件功能的不断完善,开发者在考虑是否可以借助硬件,在不 VM-EXIT 的情况下进行中断注入,APICv 的映入很好的解决了这个问题,并且使用 Posted Interrupt 方式进行中断注入,使虚拟机在不发生 VM-EXIT 的请求下完成中断的注入和 EOI.
VM_ENTRY_INTR_INFO_FIELD 中断注入(TODO)
IPI 虚拟化(TODO)
APIv 虚拟化(TODO)
Posted Interrupt(TODO)
PIC 8259A 虚拟化
虚拟 PIC 本质上是一个虚拟设备,因此可以放在用户空间侧模拟(QEMU/Broiler),也可以放在内核侧 KVM 来模拟。根据 PIC 硬件规范,在软件上模拟出虚拟 PIC 与物理 PIC 一样的接口。Broiler 将 PIC 的设备模拟放到了 KVM 里实现,因此 vPIC 是一个 In-Kernel 设备,vPIC 虚拟了 8259A 对中断的基本处理模拟,另外还包括 Broiler 向 vPIC 提交一个中断,vPIC 根据中断优先级选择合适的中断进行注入,注入的过程会促使 VM 发送 VM-EXIT,并将需要注入的中断写入到虚拟机 VMCS 指定的域中,当虚拟机再次 VM-ENTRY 时检查到有中断注入,那么虚拟机运行时触发中断。本节用于全面介绍 vPIC 中断虚拟过程,并分作以下几个章节进行讲解:
- 8259A 中断控制器编程
- vPIC 创建
- Broiler vPIC 中断配置
- Broiler 设备使用 vPIC 中断
- vPIC 中断注入
8259A 中断控制器编程
x86 架构中 vPIC 通过模拟 8259A 中断控制器的逻辑来实现中断模拟,并侧重模拟 8259A 内部的寄存器处理逻辑,而 8259A 中断控制器编程侧重从操作系统对 8259A 的使用进行描述,那么本节用于介绍 8259A 的编程逻辑,这对 vPIC 的模拟有一定的帮助。根据 PIC 硬件规范,PIC 主要为软件提供了以下几个接口用于操作 PIC:
- 4 个初始化命令字(Initialization Command Words): ICW1/ICW2/ICW3/ICW4
- 3 个操作命令字(Operation Command Words): OCW1/OCW2/OCW3
ICW1 寄存器用于初始化 8259A 的连接方式和中断触发方式,其中 BIT0 用于指明 ICW4 是否启用,在 X86 架构该 BIT 必须置位. BIT1 用于指明系统中是单片 8259A 还是级联两块 8259A,置位表示单片,清零则表示级联 2 块 8259A. BIT3 用于指明中断的触发方式,置位表示电平,清零则表示边缘触发。BIT4 在 x86 架构下必须置位. ICW1 需要写入主 8259A 的 0x20 端口和从 8259A 的 0xA0 端口.
ICW2 寄存器用于设置初始中断向量,其中 BIT0-BIT0 用于指明中断号。ICW2 需要写入主 8259A 的 0x21 端口和从 8259A 的 0xA1 端口.
ICW3 寄存器用于指定主从 8259A 的级联引脚,在 x86 架构中,从 8259A 级联到主 8259A 的 IRQ2 引脚上,因此主 8259A 的 ICW3 寄存器值为 0x04, 从 8259A 的 ICW3 寄存器值为 0x02. ICW3 需要写入主 8259A 的 0x21 端口和从 8259A 的 0xa1 端口.
ICW4 寄存器用于初始化 8259A 数据连接方式和中断触发方式。在 x86 架构中 BIT0 uPM 必须置位; BIT1 位 AEOA,如果置位中断自动结束 AUTO EOI,如果清零则 8259A 需要收到 EOI 才算中断处理完成; BIT2-BIT3 用于指明缓存模式,BIT3 清零,那么非缓存模式,BIT3 置位则缓存模式; BIT2 用于指明主从 8259A 的缓存模式,置位表示主 8259A,清零则表示从 8259A. BIT4 用于设置嵌套模式,如果置位则表示特殊全嵌套模式,清零则全嵌套模式。ICW4 寄存器需要写入主 8259A 的 0x21 端口和从 8259A 的 0xA1 端口.
上图是简单的代码演示主从 8259A 的初始化.
OCW1 寄存器用于屏蔽连接在 8259A 上的中断源,某位置位则屏蔽对应的中断源,某个清零则不屏蔽对应的中断源. OCW1 寄存器需要写入主 8259A 的 0x20 端口和从 8259A 的 0xA0 端口.
OCW2 寄存器用于设置中断结束方式和优先级模式。BIT0-BIT2 用于指明中断优先级。BIT3-BIT4 在 X86 架构必须为 0. BIT5 EOI 手动结束中断时有意义,该位置位表示中断结束就清除 ISR 相应位, 改位清零则无意义. BIT6 SL 位,该位置位使用 BIT0-BIT2 指定需要结束的中断, 该位清零则结束正在处理的中断,并将 ISR 优先级最高的位清零. BIT7 R 位,该位置位则采用循环优先级方式,每个 IR 接口优先级在 0-7 循环,该位清零则采用固定优先级,IR 接口号越低优先级越高. OCW2 寄存器需要写入主 8259A 的 0x20 端口和从 8259A 的 0xA0 端口.
OCW3 寄存器用于设置特殊屏蔽方式以及查询方式。BIT0 RIS 标志位,该位置位则读取 ISR 寄存器的值,该位清零则读取 IRR 寄存器. BIT1 RR 标志位,该位置位则读取寄存器,该位清零则无意义。BIT2 P 标志位,该位置位则表示中断查询命令查询当前最高中断优先级,该位清零则无意义; BIT3-BIT4 在 X86 架构固定为 0x01; BIT5 SMM 标志位,该位置位表示工作于特殊屏蔽模式,该位清零则未工作于特殊屏蔽模式; BIT6 ESMM 标志位,该位置位则表示启用特殊屏蔽模拟, 该位清零则表示关闭特殊屏蔽模式. OCW3 需要写入主 8259A 0x20 端口和从 8259A 的 0xA0 端口.
vPIC 创建
vPIC 的创建主要分作两部分,第一个部分是在内核空间虚拟一个 vPIC 设备,其二是对默认引脚的模拟. Broiler 在初始化阶段通过调用 kvm_init() 函数,并使用 iotcl 方式向 KVM 传入命令 KVM_CREATE_IRQCHIP 用于创建 vPIC. KVM 的 kvm_arch_vm_ioctl() 函数收到了 Broiler 传递下来的命令,然后找到对应的处理分支 KVM_CREATE_IRQCHIP, 分支里有两个主要的函数分支,其中 kvm_pic_init() 函数用于在 KVM 内虚拟一个 vPIC 设备,用于模拟 vPIC 的 IO 端口; 另外一个函数是 kvm_setup_default_irq_routing() 函数,该函数用于对 vPIC 默认引脚的模拟.
KVM 使用 struct kvm_pic 数据结构描述一个 vPIC 设备,pics[] 成员表示 x86 架构中存在两块 8259A 芯片,其中 pics[0] 作为主 PIC,而 pics[1] 作为从 PIC,pics[] 使用 struct kvm_kpic_state 数据结构进行描述,该数据结构的成员用于模拟 PIC 设备内部的寄存器,例如 irr 对应 PIC 的 Interrupt request Register, imr 对应 PIC 的 Interrupt Mask Register, 以及 isr 对应 PIC 的 In Service Register 等. output 成员表示 vPIC 芯片是否有新中断需要注入,dev_master/dev_slave/dev_eclr 则是 KVM 模拟的三个设备,前两个分别模拟主 PIC 设备和从 PIC 设备。irq_states[] 数组则是对 vPIC 每个引脚状态的模拟.
回到 KVM 对 vPIC 设备模拟的调用逻辑,KVM 调用 kvm_pic_init() 函数对 vPIC 设备进行模拟,其通过调用 kvm_iodevce_init() 函数对主 vPIC 设备、从 vPIC 设备和 eclr 设备进行模拟,并为这些设备提供了 IO 端口读写进行模拟,IO 端口读操作最终通过 pcidev_read() 函数实现,IO 端口写操作最终通过 pcidev_write() 函数实现. 函数通过调用 kvm_io_bus_register_dev() 函数将 0x20/0x21/0xa0/0xa1 注册到 KVM,只要 Guest OS 访问这几个 IO 端口就会因为 EXIT_REASON_IO_INSTRUCTION 原因 VM-EXIT.
根据 8259A 芯片的逻辑,picdev_read() 函数实现了对 ICW/OCW 寄存器读模拟,可以看到对端口 0x20/0x21/0xa0/0xa1 的读取是根据地址推算出主从 vPIC,然后通过 pic_ioport_read() 函数从其对应的 struct kvm_kpic_state 数据接口中读取指定成员, 以此完成 IO 端口读模拟.
同理根据 8259A 芯片的逻辑,picdev_write() 函数实现了对 ICW/OCW 寄存器写模拟,可以看到对端口 0x20/0x21/0xa0/0xa1 的写是根据地址推算出主从 vPIC,然后通过 pic_ioport_write() 函数从其对应的 struct kvm_kpic_state 数据接口中写入指定成员,以此完成 IO 端口写模拟.
vPIC 创建的最后一步是对默认引脚的模拟,通过之前的分析可以知道 x86 架构存在两片级联的 8259A 芯片,其中一片做主片另外一片做从片。每个 8259A 包含 8 个引脚,其中主片的 Pin2 连接从片的 INT 引脚。KVM 对引脚的模拟通过调用 kvm_set_default_irq_routing() 函数实现,该函数包含了公共路径代码,对 vPIC 引脚的模拟主要在 setup_routing_entry() 函数中实现,其通过调用函数 kvm_set_routing_entry() 函数将 vPIC 对应的引脚中断模拟函数设置为 kvm_set_pic_irq() 函数,另外将模拟好的引脚加入到 hash 链表上。
KVM 使用 struct kvm_irq_routing_table 数据结构维护 vPIC 和 vIOAPIC 的引脚,其中 chip[] 数组存储了所有引脚信息,nr_rt_entries 成员则表示引脚的总数,另外使用 map[] 哈希链表维护了引脚和 Chip 的映射关系,即引脚是属于 vPIC 还是 vIOAPIC. KVM 使用 struct kvm_kernel_irq_routing_entry 数据结构描述一个 GSI,GSI 可以对应 vPIC 的一个引脚,也可以描述 vIOAPIC 的一个引脚,其成员 gsi 表示引脚的 GSI 号,set 回调函数则表示引脚的中断模拟,irqchip 成员则表示引脚对应的 CHIP 信息和引脚信息,最后 link 成员用于将模拟的引脚统一维护在 KVM struct kvm_irq_routing_table 数组结构 map[] 哈希链表上.
struct kvm_irq_routing_table 数据架构维护了 vPIC 和 vIOAPIC 引脚与 GSI 的映射关系,通过这个关系可以从设备的引脚推出 GSI 号,而 map[] 哈希链表则维护了 GSI 与引脚的关系,在 KVM 中模拟的 vPIC 和 vIOAPIC 的引脚是可以重叠的,因此一个 GSI 号可能对应 vPIC 或者 vIOAPIC 的引脚,或者同时对应 vPIC 和 vIOAPIC 的引脚,那么使用一个 map[] hash 链表,并使用 GSI 作为 key, struct kvm_kernel_irq_routing_entry 作为 hash 链表的成员, 最终形成了上图的逻辑结构.
KVM 默认支持的中断设备的引脚如上图,从 ROUTING_ENTRY2 宏的定义可以看出 vPIC 的引脚是 [0, 15], vIOAPIC 的引脚范围是 [0, 23],那么 vPIC 和 vIOAPIC 之间共享了 [0, 15] 的引脚.
Broiler vPIC 中断配置
KVM 在创建 vPIC 的时候,已经默认创建了一套 vPIC 和 vIOAPIC 引脚映射逻辑,虽然可以使用,但作为 HypV 软件,Broiler 还是可以自定义映射 vPIC 的引脚与 GSI 映射逻辑。如上图是 Broiler 虚拟的中断引脚连接逻辑,Broielr 规划的中断引脚逻辑是: 主 vPIC 的 8 个引脚独占 GSI0-GSI7, 从 vPIC 的前 4 个引脚独占 GSI8-GIS11, 后 4 个引脚与 vIOAPIC 前 12-15 引脚共享 GSI12-GSI15, vIOAPC 16-24 引脚独占 GSI16-GSI24.
Broiler 在 broiler_irq_init() 函数中对 GSI 进行了重映射,16-20 行用于映射 GSI0-GSI7 给 IRQCHIP_MASTER,33-25 行用于映射 GSI8-GSI15 给 IRQCHIP_SLAVE, 27-29 行用于映射 GSI12-GSI24 个 IRQCHIP_IOAPIC, 可以看出从 vPIC 和 vIOAPIC 共享了 GSI12-GSI15, 最后调用 ioctl 向 KVM 传入 KVM_SET_GSI_ROUTING 使映射生效.
vPIC 的模拟位于内核是一个 In-Kernel 的设备,当 Broiler 对中断映射有了更改,需要通过 ioctl() 函数传入 KVM_SET_GSI_ROUTING 命令来更新 vPIC 的中断引脚与 GSI 的映射。KVM 的 kvm_vm_ioctl() 函数收到 KVM_SET_GSI_ROUTING 命令之后,调用 kvm_set_irq_routing() 函数进行更新,更新核心通过 kvm_set_routing_entry() 函数,其会将主从 vPIC 的引脚的中断模拟函数设置为 kvm_set_pic_irq() 函数.
struct kvm_irq_routing_table 数据架构维护了 vPIC 引脚与 GSI 的映射关系,通过这个关系可以从设备的引脚推出 GSI 号,而 map[] 哈希链表则维护了 GSI 与引脚的关系,在 KVM 中模拟的 vPIC 的引脚是可以重叠的,因此一个 GSI 号可能对应 vPIC 或者 vIOAPIC 的引脚,或者同时对应 vPIC 和 vIOAPIC 的引脚,那么使用一个 map[] hash 链表,并使用 GSI 作为 key, struct kvm_kernel_irq_routing_entry 作为 hash 链表的成员, 映射完毕之后 Broiler 的 vPIC 引脚连接如上.
Broiler 设备使用 vPIC 中断
Broiler 配置完 vPIC 中断之后,那么接下来就是在设备中使用 vPIC 提供的中断,Broiler 目前支持的设备包括 PCI 设备和 IO 端口设备。对于 IO 端口设备 Broiler 提供了 irq_alloc_from_irqchip() 函数从 vPIC 中分配中断,而 PIC 设备如果使用 INTX 中断的话,Broiler 提供了 pci_assign_irq() 函数从 vPIC 中分配中断。对于分配的 vPIC 中断,中断的触发分为电平触发和边缘触发,Broiler 模拟的设备可以使用 broiler_irq_line() 函数进行电平触发,而使用 broiler_irq_trigger() 函数进行边缘触发。那么接下来以 Broiler 一个 IO 端口设备使用电平触发方式给 Guest OS 前端驱动发中断的案例进行讲解:
从虚拟机内部看到系统 IO 空间中,主 PIC 映射到端口 0x20-0x21,从 PIC 映射到端口 0xa0-0xa1. 接下来在 Broiler 虚拟一个设备,其包含一个异步 IO,对该 IO 进行读操作会触发一个中断(源码位置: foodstuff/Broiler-interrup-vPIC.c)
Broiler 模拟了一个设备,该设备包含了一段 IO 端口,其范围是 [0x6020, 0x6030], 里面包含了两个寄存器,第一个寄存器 IRQ_NUM_REG 用于获得设置使用的 IRQ,第二个寄存器是一个异步 IO,对该寄存器进行写操作会触发中断。Broiler 在 70 行调用 irq_alloc_from_irqchip() 函数从 vPIC 中分配一个 IRQ 号,然后调用 broiler_irq_line() 函数将 IRQ 的电平设置为低低电平。Broiler 在 74-103 行实现一个异步 IO, 如果 Guest OS 对该 IO 端口进行写操作,那么会唤醒 irq_threads 线程,线程的作用是向 Guest OS 注入一个 IRQ 中断,可以看到 45 行调用 broiler_irq_line() 函数拉高电平以便模拟高电平,进而产生中断,此时 KVM 会收到 ioctl 命令 KVM_IRQ_LINE, 然后 KVM 向 Guest OS 注入中断. 那么有了 Broiler 侧的模拟设备,接下来就是 Guest OS 内部驱动来处理中断,BiscuitOS 已经支持该驱动程序的部署,其部署逻辑如下:
# 切换到 BiscuitOS 目录
cd BiscuitOS
make linux-5.10-x86_64_defconfig
make menuconfig[*] Package --->[*] KVM --->[*] vInterrupt: Broiler vPIC interrupt# 保存配置并使配置生效
make# 进入 Broiler 目录
cd output/linux-5.10-x86_64/package/Broiler-vPIC-interrupt-default/
# 下载源码
make download
# 编译并运行源码
make
make install
make pack
# Broiler Rootfs 打包
cd output/linux-5.10-x86_64/package/BiscuitOS-Broiler-default/
make build
Broiler-vPIC-interrupt-default Gitee @link
Guest OS 驱动源码比较简单,首先通过 request_resource() 向 IO 空间注册了设备的 IO 端口,其 IO 端口域为 [0x6020, 0x6030], 驱动接着调用 ioport_map() 函数将 IO 端口映射到虚拟内存,接着在 52 行从设备的 0x6024 端口获得设备使用的中断号,接着在 53 行调用 request_irq() 进行中断处理函数注册,此时中断的触发方式设置为高电平触发,中断处理函数是 Broiler_irq_handler(),其内部用于在接收到中断之后打印中断号。最后驱动向端口 0x6020 进行写操作,那么这会触发设备向 CPU 发送一个中断,接下来在 BiscuitOS 实践该案例:
Broiler 启动 BiscuitOS 系统之后,加载驱动 Broiler-vPIC-interrupt-default.ko, 可以看到驱动加载成功,等待 5s 之后中断处理程序收到设备发来的中断,此时可以看到 IRQ 为 6,接着查看 IO 空间,可以看到端口 0x6020-0x6030 分配给 “Broiler PIO vPIC” 使用。最后可以查看 /proc/interrupts 节点获得中断映射关系,也就是开篇的图片可以看到 Broiler-PIO-vPIC 使用的中断.
vPIC 中断注入
vPIC 中断注入流程流程如上图,分作两个大的部分,第一部分是 Broiler 通过 ioctl() 函数向 KVM 传入 KVM_IRQ_LINE 命令,以此告诉 KVM 进行 vPIC 中断注入,此时 Broiler 和 KVM 是异步运行的,此时 Guest OS 在运行并没有 VM_EXIT. 当 KVM 的 vPIC 通过中断评估之后决定将某个中断注入到 Guesst,这时会向 KVM 发送 KVM_REQ_EVENT 请求,并发送 IPI 中断让虚拟机发生 VM-EXIT; 中断注入的第二阶段则是让虚拟机 VM-EXIT 之后再次 VM_ENTRY, 此时 KVM 会检查 VM_ENTRY_INTR_INFO_FIELD 域里是否注入了中断,如果注入那么虚拟机 VM-ENTRY 之后就触发中断,因此第二部分的任务就是在 VM-ENTRY 之前向 VM_ENTRY_INTR_INFO_FIELD 写入要注入的中断,KVM 通过在 vmx_inject_irq() 函数中调用 vmcs_write32() 函数向 VM_ENTRY_INTR_INFO_FIELD 域中写入了要注入的中断,最后虚拟机 VM-ENTRY 之后就收到中断,这个阶段 KVM 是同步运行的. 那么接下来对流程进行细节分析.
pic_set_irq1() 函数用于模拟中断产生之后,vPIC 的 Interrupt Request Register 收到中断之后将对应 bit 置位的模拟,从函数的实现可以看出 IRR 寄存器接收电平触发和边缘触发的中断,93-100 行处理电平触发的中断,如果模拟中断是电平触发,那么如果 level 为真,即中断引脚产生了一个高电平,那么 IRR 寄存器就将引脚对应的 bit 置位,并将 last_irr 寄存器对应的 bit 也置位; 反之如果是一个低电平,那么中断消除,于是将 IRR 寄存器对应的 bit 清零. 如果是边缘触发,那么函数使用 102-110 行进行中断边缘触发模拟,103-108 行如果此时 last_irr 对应引脚之前是低电平,那么此时中断引脚触发一个上升沿信号,那么此时 IRR 寄存器将引脚对应的 bit 置位; 反之只使用 last_irr 寄存器记录电平信号; 108-109 行产生一个低电平,并且下降沿不触发中断,那么只使用 last_irr 寄存器记录电平信息. 函数在 112 行检查 Interrupt mask register 寄存器是否已经屏蔽该中断,如果屏蔽直接返回 -1; 反之返回 1.
pic_get_irq() 函数用于中断评估,以此决定是否有中断送往 CPU 进行处理. 函数 137 行的作用是模拟通过 IRR 寄存器和 IMR 寄存器获得不被屏蔽可以处理的中断,函数 138 行则根据优先级策略获得最高优先级。函数接着在 146 行模拟从 ISR 寄存器获得当前正在处理的中断,然后获得对应的优先级,最后在 150-156 行的优先级判断该 PIC 是否有中断送往 CPU 处理.
pic_update_irq() 函数用于判断主从 vPIC 中是否有中断产生,如果有就进行标记,以便虚拟机再次 VM-ENTRY 时注入中断。函数在 167 行判断从 vPIC 是否有中断需要 CPU 处理,如果有则进行入 168 分支进行处理,函数在该分支将主 vPIC 的 IRQ2 置位,以此模拟从 vPIC 产生中断并切主 vPIC 的 2 号引脚接受该中断。函数接着在 175 行再次调用 pic_get_irq() 函数判断主 vPIC 是否有中断需要 CPU 处理,如果有则调用 pic_irq_request() 函数告诉 KVM 主 vPIC 有中断需要 CPU 处理.
pic_unlock() 函数的作用是向 KVM 提出 KVM_REQ_EVENT 需求,并让虚拟机 VM-EXIT. 函数在 62 行调用 kvm_make_request() 函数向 KVM 提出 KVM_REQ_EVENT 需求,KVM 在 VM-ENTRY 之间如果检查到有 KVM_REQ_EVENT 需求,那么其会检查 VM_ENTRY_INTR_INFO_FIELD 域是否有中断需要注入. 中断的注入只能在 VM-ENTRY 时才能注入,那么对于没有 VM-EXIT 的虚拟机,KVM 通过让 VCPU 发送 IPI 中断让虚拟机 VM-EXIT。至此 vPIC 的中断主动注入已经完成,接下来就是虚拟机再次 VM-ENTRY 之前将需要注入的中断写入 VM_ENTRY_INTR_INFO_FIELD.
虚拟机再次 VM-ENTRY 之前会调用 vcpu_enter_guest() 函数进行检查,此时函数在 8865 行发现有 KVM_REQ_EVENT 请求,那函数 8873 行调用 inject_pending_event() 函数进行中断注入.
在 inject_pending_event() 内部,函数在 8310 行通过 kvm_cpu_has_injectable_intr() 函数知道 vPIC 中断需要注入,那么函数进入 8311 分支进行处理,对于 vPIC 中断函数进入 8315 分支进行处理,函数首先调用 kvm_queue_interrupt() 函数获得需要注入中断的 vector,然后调用 vmx_inject_irq() 函数向 VMCS 的 VM_ENTRY_INTR_INFO_FIELD 域注入中断.
vmx_inject_irq() 函数用于最终的中断注入操作,从函数实现可以看到 4500 行获得需要注入的中断号,然后在 4519 行调用 vmcs_write32() 函数向 irq 在 VM_ENTRY_INTR_INFO_FIELD 域中对应的 bit 置位,至此 vPIC 的中断注入完结, 虚拟机 VM-ENTRY 检查到该域有置位,那么虚拟机 RESUME 之后就向虚拟机注入一个中断.
VM_ENTRY_INTR_INFO_FIELD 域是一个 32 位域,Vector 字段用于描述注入的中断向量号, Deliver err Code 域指明是否需要向 Guest 的堆栈中写入错误码,Valid 域用于表示需要 向 Guest OS 注入中断,VM-EXIT 时自动清除该域,Interrupt type 指明注入中断的类型>,具体支持如下:
- 0: External Interrupt
- 1: Reserved
- 2: NMI
- 3: Hardware exception (e.g. #PF)
- 4: Software interrupt (INT n)
- 5: Privileged software exception (INT 1)
- 6: Software exception (INT 3 or INTO)
- 7: Other event
原文作者:Linux内核之旅
相关文章:

虚拟化中的中断机制:X86与PIC 8259A探索(上)
本系列深入探讨虚拟化中断技术,从X86架构和PIC 8259A的基础,到IOAPIC和MSI的编程,再到MSIX技术与Broiler设备的实战应用,全面剖析中断虚拟化的前沿进展。 X86 中断机制 在计算机架构中,CPU 运行的速度远远大于外设…...

软件外包开发语言排行榜
软件开发语言的排行榜是一个动态的话题,而在未来的几年中,新的技术和语言可能会不断涌现,影响排名。然而以下是一些在过去几年中一直受欢迎并有前途的软件开发语言,如果是新入门软件开发行业在学习语言做选择,希望下面…...

BI技巧丨利用OFFSET计算同环比
微软最近更新了很多开窗函数,其内部参数对比以往的DAX函数来说,多了很多,这就导致学习的时间成本直线上升。 而且对于新增函数的应用场景,很多小伙伴也是一知半解的,本期我们就来聊一聊关于最近新增的开窗函数——OFF…...

整理mongodb文档:collation
文章连接 整理mongodb文档:collation 看前提示 对于mongodb的collation。个人主要用的范围是在createcollection,以及find的时候用,所以本片介绍的时候也是这两个地方入手,对新手个人觉得理解概念就好。不要求强制性掌握,但是要…...
【LangChain】Prompts之Prompt templates
Prompts 编程模型的新方法是通过提示(prompts)。 prompts是指模型的输入。该输入通常由多个组件构成。 LangChain 提供了多个类和函数,使构建和使用prompts变得容易。 Prompt templates(提示模板): 参数化模型输入Example selectors(选择器示例): 动态选择要包含在…...
【数字IC基础】时序违例的修复
时序违例的修复 建立时间违例保持时间违例Buffer 插入位置参考资料 建立时间违例 基本思路是减少数据线的延时、减少 Launch clock line 的延时、增加capture clock line的delay 加强约束,重新进行综合,对违规的路径进行进一步的优化,但是一…...

深度学习实战46-基于CNN的遥感卫星地图智能分类,模型训练与预测
大家好,我是微学AI,今天给大家介绍一下深度学习实战46-基于CNN的遥感卫星地图智能分类,模型训练与预测。随着遥感技术和卫星图像获取能力的快速发展,卫星图像分类任务成为了计算机视觉研究中一个重要的挑战。为了促进这一领域的研究进展,EuroSAT数据集应运而生。本文将详细…...
Node.js-fs模块文件创建、删除、重命名、文件内容的写入、读取以及文件夹的相关操作
一、写入文件操作 异步写入:writeFile() 同步写入:writeFileSync() const fs require("fs"); fs.writeFile("目标文件路径", "要写入的内容", err > {if(err){console.log(err);return;}console.log("写入成功&a…...

LIN协议总结
目录 一、LIN是什么1、LIN的概念2、扩展介绍一下同步通信和异步通信的区别3、LIN连接结构及节点构成 二、LIN的特点三、LIN协议层1、帧的结构2、帧的类型3、进度表4、状态机实现5、网络管理6、状态管理 四、帧收发的硬件实现1、组成2、硬件特点3、协议控制器4、总线收发器5、LI…...

Redis BigKey案例
面试题: 阿里广告平台,海量数据里查询某一固定前缀的key小红书,你如何生产上限制keys*/flushdb/flushall等危险命令以防止误删误用?美团,MEMORY USAGE命令你用过吗?BigKey问题,多大算big&#…...

ThinkPHP v6.0.8 CacheStore 反序列化漏洞
漏洞说明 1. 漏洞原理:ThinkPHP 6.0.8 CacheStore 会触发POP利用链子,造成任意命令执行 2. 组件描述: ThinkPHP是一个免费开源的,快速、简单的面向对象的轻量级PHP开发框架 3. 影响版本:V6.0.8 漏洞复现 1. 环境安…...

Spring 事务详解(注解方式)
目 录 序言 1、编程式事务 2、配置声明式事务 2.1 基于TransactionProxyFactoryBean的方式(不常用,因为要为每一个类配置TransactionProxyFactoryBean) 2.2 基于AspectJ的XML方式(常用,可配置在某些类下的所有子…...
华为云waf 使用场景
防护Web应用免受攻击就用华为云Web应用防火墙 Web应用防火墙(Web Application Firewall, WAF),通过对HTTP(S)请求进行检测,识别并阻断SQL注入、跨站脚本攻击、网页木马上传、命令/代码注入、文件包含、敏感文件访问、第…...
?.的写法 后缀修饰符
概览:处理后端返回的数据data,写法:data?.name。解决vue框架编译出现的报错Cannot read property name of undefined。出现问题的原因:这是因为我们试图访问对象中不在的 key 为 name 的属性,那么怎么解决呢ÿ…...

org.apache.hadoop.hive.ql.exec.DDLTask. show Locks LockManager not specified解决
Error while processing statement: FAILED: Execution Error, return code 1 from org.apache.hadoop.hive.ql.exec.DDLTask. show Locks LockManager not specified解决 当在Hive中执行show locks语句时,出现"LockManager not specified"错误通常是由于…...

Adaptive autosar 都有哪些模块?各有什么功能?
Adaptive autosar是一种用于高性能计算ECU的软件平台,它支持自适应应用程序的开发和运行。它由两部分组成:基础(Foundation)和服务(Service)。基础包括了操作系统接口、执行管理、网络管理、识别访问管理、加密、更新和配置管理等功能。服务包括了通信管理、RESTful、时间…...

C++ 动态内存分配
在C中动态内存的分配技术可以保证程序在允许过程中按照实际需要申请适量的内存,使用结束后还可以释放,这种在程序运行过程中申请和释放的存储单元也称为堆。 申请和释放过程一般称为建立和删除。 在C程序中,建立和删除堆对象使用两个运算符&…...
设计模式——面向对象的7大设计原则
1.单一职责原则 一个类中最好只放一种类型的方法,比如Dao中只有和数据库交互相关的代码。实现高内聚,低耦合。 2.开闭原则 对外拓展开放,对内修改关闭,有新的需求时不要修改已有代码,而是添加新的代码,比…...

智慧防汛,数字科技的力量
随着夏日的脚步临近,台风季节即将降临。对于那些居住在沿海地区的人们来说,台风是一种常见的自然灾害,其带来的风雨可能对生命和财产造成严重威胁。然而,随着数字科技的飞速发展,可视化技术为防汛抗台工作带来了全新的…...

“中国软件杯”飞桨赛道晋级决赛现场名单公布
“中国软件杯”大学生软件设计大赛是由国家工业和信息化部、教育部、江苏省人民政府共同主办,是全国软件行业规格最高、最具影响力的国家级一类赛事,为《全国普通高校竞赛排行榜》榜单内赛事。今年,组委会联合百度飞桨共同设立了“智能系统设…...

IDEA运行Tomcat出现乱码问题解决汇总
最近正值期末周,有很多同学在写期末Java web作业时,运行tomcat出现乱码问题,经过多次解决与研究,我做了如下整理: 原因: IDEA本身编码与tomcat的编码与Windows编码不同导致,Windows 系统控制台…...
Android Wi-Fi 连接失败日志分析
1. Android wifi 关键日志总结 (1) Wi-Fi 断开 (CTRL-EVENT-DISCONNECTED reason3) 日志相关部分: 06-05 10:48:40.987 943 943 I wpa_supplicant: wlan0: CTRL-EVENT-DISCONNECTED bssid44:9b:c1:57:a8:90 reason3 locally_generated1解析: CTR…...
线程与协程
1. 线程与协程 1.1. “函数调用级别”的切换、上下文切换 1. 函数调用级别的切换 “函数调用级别的切换”是指:像函数调用/返回一样轻量地完成任务切换。 举例说明: 当你在程序中写一个函数调用: funcA() 然后 funcA 执行完后返回&…...

linux arm系统烧录
1、打开瑞芯微程序 2、按住linux arm 的 recover按键 插入电源 3、当瑞芯微检测到有设备 4、松开recover按键 5、选择升级固件 6、点击固件选择本地刷机的linux arm 镜像 7、点击升级 (忘了有没有这步了 估计有) 刷机程序 和 镜像 就不提供了。要刷的时…...
GitHub 趋势日报 (2025年06月08日)
📊 由 TrendForge 系统生成 | 🌐 https://trendforge.devlive.org/ 🌐 本日报中的项目描述已自动翻译为中文 📈 今日获星趋势图 今日获星趋势图 884 cognee 566 dify 414 HumanSystemOptimization 414 omni-tools 321 note-gen …...
Python如何给视频添加音频和字幕
在Python中,给视频添加音频和字幕可以使用电影文件处理库MoviePy和字幕处理库Subtitles。下面将详细介绍如何使用这些库来实现视频的音频和字幕添加,包括必要的代码示例和详细解释。 环境准备 在开始之前,需要安装以下Python库:…...

消防一体化安全管控平台:构建消防“一张图”和APP统一管理
在城市的某个角落,一场突如其来的火灾打破了平静。熊熊烈火迅速蔓延,滚滚浓烟弥漫开来,周围群众的生命财产安全受到严重威胁。就在这千钧一发之际,消防救援队伍迅速行动,而豪越科技消防一体化安全管控平台构建的消防“…...
Electron简介(附电子书学习资料)
一、什么是Electron? Electron 是一个由 GitHub 开发的 开源框架,允许开发者使用 Web技术(HTML、CSS、JavaScript) 构建跨平台的桌面应用程序(Windows、macOS、Linux)。它将 Chromium浏览器内核 和 Node.j…...
mcts蒙特卡洛模拟树思想
您这个观察非常敏锐,而且在很大程度上是正确的!您已经洞察到了MCTS算法在不同阶段的两种不同行为模式。我们来把这个关系理得更清楚一些,您的理解其实离真相只有一步之遥。 您说的“select是在二次选择的时候起作用”,这个观察非…...
【R语言编程——数据调用】
这里写自定义目录标题 可用库及数据集外部数据导入方法查看数据集信息 在R语言中,有多个库支持调用内置数据集或外部数据,包括studentdata等教学或示例数据集。以下是常见的库和方法: 可用库及数据集 openintro库 该库包含多个教学数据集&a…...