虚拟化中的中断机制: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.开闭原则 对外拓展开放,对内修改关闭,有新的需求时不要修改已有代码,而是添加新的代码,比…...

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

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

MPNet:旋转机械轻量化故障诊断模型详解python代码复现
目录 一、问题背景与挑战 二、MPNet核心架构 2.1 多分支特征融合模块(MBFM) 2.2 残差注意力金字塔模块(RAPM) 2.2.1 空间金字塔注意力(SPA) 2.2.2 金字塔残差块(PRBlock) 2.3 分类器设计 三、关键技术突破 3.1 多尺度特征融合 3.2 轻量化设计策略 3.3 抗噪声…...
【网络】每天掌握一个Linux命令 - iftop
在Linux系统中,iftop是网络管理的得力助手,能实时监控网络流量、连接情况等,帮助排查网络异常。接下来从多方面详细介绍它。 目录 【网络】每天掌握一个Linux命令 - iftop工具概述安装方式核心功能基础用法进阶操作实战案例面试题场景生产场景…...

springboot 百货中心供应链管理系统小程序
一、前言 随着我国经济迅速发展,人们对手机的需求越来越大,各种手机软件也都在被广泛应用,但是对于手机进行数据信息管理,对于手机的各种软件也是备受用户的喜爱,百货中心供应链管理系统被用户普遍使用,为方…...
日语学习-日语知识点小记-构建基础-JLPT-N4阶段(33):にする
日语学习-日语知识点小记-构建基础-JLPT-N4阶段(33):にする 1、前言(1)情况说明(2)工程师的信仰2、知识点(1) にする1,接续:名词+にする2,接续:疑问词+にする3,(A)は(B)にする。(2)復習:(1)复习句子(2)ために & ように(3)そう(4)にする3、…...

3.3.1_1 检错编码(奇偶校验码)
从这节课开始,我们会探讨数据链路层的差错控制功能,差错控制功能的主要目标是要发现并且解决一个帧内部的位错误,我们需要使用特殊的编码技术去发现帧内部的位错误,当我们发现位错误之后,通常来说有两种解决方案。第一…...
Go 语言接口详解
Go 语言接口详解 核心概念 接口定义 在 Go 语言中,接口是一种抽象类型,它定义了一组方法的集合: // 定义接口 type Shape interface {Area() float64Perimeter() float64 } 接口实现 Go 接口的实现是隐式的: // 矩形结构体…...

关于iview组件中使用 table , 绑定序号分页后序号从1开始的解决方案
问题描述:iview使用table 中type: "index",分页之后 ,索引还是从1开始,试过绑定后台返回数据的id, 这种方法可行,就是后台返回数据的每个页面id都不完全是按照从1开始的升序,因此百度了下,找到了…...

智能在线客服平台:数字化时代企业连接用户的 AI 中枢
随着互联网技术的飞速发展,消费者期望能够随时随地与企业进行交流。在线客服平台作为连接企业与客户的重要桥梁,不仅优化了客户体验,还提升了企业的服务效率和市场竞争力。本文将探讨在线客服平台的重要性、技术进展、实际应用,并…...
【HTML-16】深入理解HTML中的块元素与行内元素
HTML元素根据其显示特性可以分为两大类:块元素(Block-level Elements)和行内元素(Inline Elements)。理解这两者的区别对于构建良好的网页布局至关重要。本文将全面解析这两种元素的特性、区别以及实际应用场景。 1. 块元素(Block-level Elements) 1.1 基本特性 …...
鸿蒙DevEco Studio HarmonyOS 5跑酷小游戏实现指南
1. 项目概述 本跑酷小游戏基于鸿蒙HarmonyOS 5开发,使用DevEco Studio作为开发工具,采用Java语言实现,包含角色控制、障碍物生成和分数计算系统。 2. 项目结构 /src/main/java/com/example/runner/├── MainAbilitySlice.java // 主界…...