中断是外部设备向操作系统发起请求,打断CPU正在执行的任务,转而处理特殊事件的操作。设备并不能直接连接到CPU,而是统一连接到中断控制器上,由中断控制器管理和分发设备中断。为了模拟一个完整的操作系统,虚拟化层也必须完成设备中断的模拟。虚拟机的中断控制器通过VMM创建,VMM可以利用虚拟机的中断控制器向其注入中断。
在x86_64架构下,中断控制器包括PIC和APIC两种类型。PIC控制器通过两块Intel8259芯片级联,支持15个中断。受到PIC中断引脚数量和不支持多CPU限制,Intel随后引入了APIC中断控制器。APIC中断控制器由I/OAPIC和LAPIC两部分组成,外部设备连接在I/OAPIC上,每个CPU内部都有LAPIC,I/OAPIC与LAPIC通过系统总线相连。当产生中断时,I/OAPIC可以将中断分发给对应的LAPIC,然后与LAPIC相关联的CPU开始执行中断处理例程。除了上述两种中断控制器,还有MSI/MSI-x的中断方式。它绕过了I/OAPIC,直接通过系统总线,将中断向量号写入对应CPU的LAPIC。使用MSI/MSI-x中断技术,将不再受管脚数量的约束,支持更多中断,减少中断延迟。
在aarch64架构下,中断控制器被称为GIC(GenericInterruptController),目前有v1~v4这四个版本。当前StratoVirt只支持GICv3版。同样的,aarch64也支持MSI/MSI-x中断方式。
INTx中断机制会在一些传统的老旧设备上使用。但实际上,在PCIe总线中,很多设备已经很少使用,甚至直接将该功能禁止了。所以,StratoVirt当前也不支持INTx中断机制。
创建中断芯片
由于中断控制器在KVM中模拟的性能更高,因此StratoVirt将中断芯片的具体创建过程和中断投递过程交给了KVM。在StratoVirt启动虚拟机之前,会具现化x86_64或aarch64的虚拟主板,即调用realize()函数,完成初始化。在这个阶段,就创建了中断控制器。其初始化代码如下:
fn realize(
vm: &Arc<Mutex<Self>>,
vm_config: &mut VmConfig,
is_migrate: bool,
) -> MachineResult<()> {
...
locked_vm.init_interrupt_controller(u64::from(vm_config.machine_config.nr_cpus))?;
...
}
StratoVirt提供了MachineOpstrait。无论是轻量化主板或者标准化主板,在x86_64和aarch64架构下都分别实现了init_interrupt_controller(),初始化中断控制器函数。
x86_64架构
上述调用了初始化中断控制器函数,在其内部的执行过程中,主要作用是调用create_irq_chip()函数,后者在vm_fd上调用ioctl(self,KVM_CREATE_IRQCHIP())系统调用,告诉内核需要在KVM模拟中断控制器。后续该系统调用进入了KVM模块,会同时创建PIC和APIC中断芯片,并生成默认的中断路由表。
fn init_interrupt_controller(&mut self, _vcpu_count: u64) -> MachineResult<()> {
...
KVM_FDS
.load()
.vm_fd
.as_ref()
.unwrap()
.create_irq_chip()
.chain_err(|| MachineErrorKind::CrtIrqchipErr)?;
...
}
aarch64架构
GIC中断控制器由四个组件组成:Distributor,CPUInterface,Redistributor,ITS。与x86_64类似,也需要在KVM创建中断控制器。但是不同的是,在创建过程中,需要提前告诉KVM模块,GIC组件在虚拟机内存布局的地址范围。通过dist_range,redist_region_ranges,its_range三个变量,向KVM传递了组件的内存地址。除此之外,内部仍然使用vm_fd,通过系统调用创建了vGICv3和vGICITS中断设备。
fn init_interrupt_controller(&mut self, vcpu_count: u64) -> Result<()> {
...
let intc_conf = InterruptControllerConfig {
version: kvm_bindings::kvm_device_type_KVM_DEV_TYPE_ARM_VGIC_V3,
vcpu_count,
max_irq: 192,
msi: true,
dist_range: MEM_LAYOUT[LayoutEntryType::GicDist as usize],
redist_region_ranges: vec![
MEM_LAYOUT[LayoutEntryType::GicRedist as usize],
MEM_LAYOUT[LayoutEntryType::HighGicRedist as usize],
],
its_range: Some(MEM_LAYOUT[LayoutEntryType::GicIts as usize]),
};
let irq_chip = InterruptController::new(&intc_conf)?;
self.irq_chip = Some(Arc::new(irq_chip));
self.irq_chip.as_ref().unwrap().realize()?;
...
}
创建MSI-x
在设计StratoVirt的VirtioPCI设备,使用MSI-x中断方式通知虚拟机。因此,使用MSI-x设备前,需要在VitioPCI设备具现化过程中调用init_msix(),进行相关的初始化。该函数的主要功能是在PCI设备的配置空间协商MSI相关信息。另外,具现化阶段提供了assign_interrupt_cb()函数,用来封装设备的中断回调函数。在VirtioPCI设备处理完I/O请求后,会调用中断回调,向KVM发送中断通知。
fn realize(mut self) -> PciResult<()> {
...
init_msix(
VIRTIO_PCI_MSIX_BAR_IDX as usize,
nvectors as u32,
&mut self.config,
self.dev_id.clone(),
)?;
self.assign_interrupt_cb();
...
}
管理中断路由表
上文提到,在KVM创建中断芯片时,会生成默认的中断路由表。但是某些设备(例如直通设备),需要向KVM添加额外的全局中断号,这时需要StratoVirt额外维护一份中断路由表,并向KVM同步。
在StratoVirt初始化中断控制器时,会创建中断路由表。内部统一调用init_irq_route_table()函数,但是架构不同,默认的中断路由表信息也不同。
除了可以生成默认的中断路由表,还需要向KVM同步。commit_irq_routing()函数提供了该功能,内部使用vm_fd的系统调用ioctl_with_ref(self,KVM_SET_GSI_ROUTING(),irq_routing),该系统调用将覆盖KVM模块内的中断路由表信息。
fn init_interrupt_controller(&mut self, vcpu_count: u64) -> Result<()> {
...
KVM_FDS
.load()
.irq_route_table
.lock()
.unwrap()
.init_irq_route_table();
KVM_FDS
.load()
.commit_irq_routing()
.chain_err(|| "Failed to commit irq routing for arm gic")?;
...
}
当设备需要动态申请或释放全局中断号时,StratoVirt提供了两个函数add_msi_route(),update_msi_route(),用于增加或修改中断路由表信息。
中断流程
对于模拟virtio设备,虚拟机通过触发VMExit退出到KVM。因为StratoVirt在起始阶段绑定了I/O地址空间与ioeventfd,并向KVM注册了这些信息。所以guestOS通知设备处理I/O的流程会从KVM直接返回到StratoVirt循环。接着由StratoVirt分发和处理I/O操作。当完成I/O请求或其他事件后,需要再次通知虚拟机继续往下执行,就通过注入中断的方式让虚拟机得到事件通知。
StratoVirt同时支持两种架构:microVM和standardVM,两种架构下使用的中断方式稍有不同。在microVM架构下,将一个evenetfd与一个全局中断号关联,并向KVM注册对应关系。当需要发送中断时,StratoVirt只需要向设备对应的eventfd发送信号,就会导致对应的中断被KVM模块注入到虚拟机。在standardVM架构,使用msixnotify()发起中断。经过一系列的函数调用,最后在vm_fd上调用ioctl_with_ref(self,KVM_SIGNAL_MSI(),&msi),向KVM发起中断通知,最终由KVM模块完成虚拟机的中断注入。
轻量化机型
在virtio设备激活阶段,将中断回调函数interrupt_cb,作为activate()函数的入参传入,保存在设备对应的IOhandler中。当需要发送中断时,会调用该中断回调函数。activate()函数声明如下:
fn activate(
&mut self,
mem_space: Arc<AddressSpace>,
interrupt_cb: Arc<VirtioInterrupt>,
queues: &[Arc<Mutex<Queue>>],
queue_evts: Vec<EventFd>,
) -> Result<()>;
轻量机型架构下的设备使用VirtioMMIO协议,处理完I/O请求后,会调用中断回调函数,发送中断。中断回调函数具体内容如下:
let cb = Arc::new(Box::new(
move |int_type: &VirtioInterruptType, _queue: Option<&Queue>| {
let status = match int_type {
VirtioInterruptType::Config => VIRTIO_MMIO_INT_CONFIG,
VirtioInterruptType::Vring => VIRTIO_MMIO_INT_VRING,
};
interrupt_status.fetch_or(status as u32, Ordering::SeqCst);
interrupt_evt
.write(1)
.chain_err(|| ErrorKind::EventFdWrite)?;
Ok(())
},
) as VirtioInterrupt);
在上面我们提到该eventfd和中断号信息已经告诉了KVM。中断回调通过向interrupt_evt写1,KVM就可以poll到相应事件,接着找到eventfd对应的全局中断号,注入到虚拟机中。
标准机型
与轻量机型不同,标准机型架构下实现的设备使用VirtioPCI协议。因此,中断方式也改为了MSI-x。与上面相同是,设备在激活阶段,都会保存中断回调函数。标准机型对应的中断回调函数如下:
let cb = Arc::new(Box::new(
move |int_type: &VirtioInterruptType, queue: Option<&Queue>| {
let vector = match int_type {
VirtioInterruptType::Config => cloned_common_cfg
.lock()
.unwrap()
.msix_config
.load(Ordering::SeqCst),
VirtioInterruptType::Vring => {
queue.map_or(0, |q| q.vring.get_queue_config().vector)
}
};
if let Some(msix) = &cloned_msix {
msix.lock().unwrap().notify(vector, dev_id);
} else {
bail!("Failed to send interrupt, msix does not exist");
}
Ok(())
},
) as VirtioInterrupt);
在中断回调函数中,获取中断向量号vector,然后使用notify()函数把中断信息发送给KVM。内部首先使用get_message()填充MSImessage结构的address和data成员。接着向KVM发送封装好的message。最后在内核KVM模块,根据中断路由表项,向虚拟机注入对应的中断。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。