Replies: 6 comments 16 replies
-
timer 接口与定义data structurepub struct VmmTimerEvent {
// task:注册 timerirq 的 task
task: CurrentTask,
// 当时钟到期时,触发的回调函数
timer_callback: Box<dyn FnOnce(TimeValue) + Send + 'static>,
}
#[percpu::def_percpu]
static TIMER_LIST: LazyInit<SpinNoIrq<TimerList<VmmTimerEvent>>> = LazyInit::new(); TimerList:一个小根堆,用于维护多个定时器及其回调函数。 TimerList 相关接口
timer 相关接口// deadline: ns,需要在各架构的 vcpu 实现中转成统一单位
// 注册一个时钟中断到 TIMER_LIST 中
pub fn register_timer(deadline: u64, handler: VmmTimerEvent) {
let timer_list = unsafe { TIMER_LIST.current_ref_mut_raw() };
let mut timers = timer_list.lock();
timers.set(TimeValue::from_nanos(deadline as u64), handler);
}
// 将 TIMER_LIST 中所有满足条件的中断全部删除
pub fn cancel_timer<F>(condition: F)
where
F: Fn(&VmmTimerEvent) -> bool, {
let timer_list = unsafe { TIMER_LIST.current_ref_mut_raw() };
let mut timers = timer_list.lock();
timers.cancel(condition);
}
// 触发 TIMER_LIST 中全部到期的时钟中断
pub fn check_events() {
loop {
let now = axhal::time::wall_time();
let timer_list = unsafe { TIMER_LIST.current_ref_mut_raw() };
let event = timer_list.lock().expire_one(now);
if let Some((_deadline, event)) = event {
// 处理时间中断
......
} else {
break;
}
}
}
// 设置下一次时钟中断到期的时间
pub fn scheduler_next_event() {
// info!("set deadline!!!");
let now_ns = axhal::time::monotonic_time_nanos();
let deadline = now_ns + PERIODIC_INTERVAL_NANOS;
axhal::time::set_oneshot_timer(deadline);
}
// 初始化时钟中断,需要重新注册 arceos 时钟中断
pub fn init() {
let timer_list = unsafe { TIMER_LIST.current_ref_mut_raw() };
timer_list.init_once(SpinNoIrq::new(TimerList::new()));
axhal::irq::register_handler(axhal::time::TIMER_IRQ_NUM, || {
check_events();
scheduler_next_event();
});
} situationcase1timer到期时,vcpu未运行(可能与timer_list在同一核也可能在不同核)。 调用 callback 处理。 case2timer到期时,vcpu 和timer_list在同一核。 调用 callback 处理。 case3timer到期时,vcpu正在运行,和timer_list在不同核。
case4arceos时钟中断。(调度) timer 处理的全流程
其他 timer 中断处理的接口arch_vcpu
axvcpu AxVCpuExitReason 里增加 : /// Register a timer
/// Because vmm is needed to register clock interrupts.
SetTimer { time: u64, callback: fn(TimeValue) },
/// A clock interrupt occurs
TimerIrq,
/// ipi
IPI, vcpu 增加接口 // 获取vcpu 正在运行的 cpu 的 id
pub fn get_cpu_id(self) -> i32; umhv 添加 timers.rs 用来处理 timer 相关接口(见上) 添加 IPI 处理接口 // vcpu 是目标 vcpu,irq 是 TimerIrq
pub fn inject_irq(vcpu, callback, irq) {
let to = vcpu.get_cpu_id();
if to < 0 || to == this_cpu_id() {
callback();
} else {
// 给 to 发送附带参数的 IPI
send_ipi(to, vcpu, callback, irq);
}
} |
Beta Was this translation helpful? Give feedback.
-
ARM 定时器中断通用定时器(Generic Timer)通用计时器为ARM的处理器核提供了一个标准化的计时器框架。通用计时器包含一个系统计数器(System Counter)和每个处理器核自己的计时器(per-core timer),如下图。图中的PE(Processor Element)代表处理器核。 系统计数器需要保证在芯片上电启动后一直保持工作,且以固定的时钟频率单调递增计数。系统计数器的数值需要广播给所有的处理器。在多核处理器架构中,即使某些处理器核出于某些原因(节省功耗,生成故障等等)关闭电源,系统计数器仍能为处于工作状态的处理器核提供计数值。 ARM建议系统计数器是56-64bit宽度,工作频率在1-50MHz。我们以最小宽度56bit和最高速度50MHz来计算,计数器溢出需要大概 $$ \frac{2^{56}}{5010^{6}606024*365} $$ ,约为45年。此处只是给出大概的计算,具体的计数器设计要根据实际需求确定。 不知道大家看到这里发现没有,系统计数器只是单调的递增,并不能反映真实物理世界的时间(年,月,日,时,分,秒)。也就是说,SoC还需要板级提供一个RTC(Real-Time Clock),以供给真实时间。 每个处理器有一组计时器。这些计时器本质上是比较器,软件可以设置这些处理器本地计时器的数值,并且与系统计数器广播来的值作比较。当计满后,触发中断(Interrupt)或者事件(Event)。 ARMv8-A中的处理器计时器如下表: 在实际硬件设计中,如何实现这种跨时钟域的数据传输?首先,系统计数器需要传输给处理器核,系统计数器工作在低频下(MHz),而处理器工作在高频下(GHz),如何在两个时钟频率下传输一组数值?这个问题可以通过二进制和格雷码转换来解决。系统计数器的数值转换成格雷码,以格雷码的形式在芯片中传播;在处理器端,先做跨时钟域采样,这样会保证采样不会采错,然后格雷码转换成二进制。 多核处理器芯片中如何保证时钟同步?系统计数器的值要传播给所有的处理器。这时,需要一定的机制保证该值同一时刻(此处不是绝对意义的分毫不差)到达每个处理器端。否则的话,可能会引发错误。例如,假设两个处理器A核B,处理器A端的系统计数器值更新快于处理器B端。处理器A以接收到的系统计数器值为时间戳,发送一个消息给处理器B;处理器B接收到消息后,看到的本地系统计数值如果早于消息中的时间戳,那就肯定不对了(不能接收来自未来的消息吧)。我在ARM的文档中没有找到ARM有什么推荐方案,个人感觉可以通过格雷码打多拍的方式在整个SoC中传播。这样可以保证从系统计数器传播到每个处理器入口端的延时一样,至于每个处理器内部跨时钟域转换造成的偏差,可以认为是系统计数器时钟的抖动(jitter),忽略不计。 多芯片时钟同步第三个问题,是多芯片间的同步问题。一个SMP可能由多个处理器芯片组成,每个处理器芯片有自己的系统计数器。但是SMP要求多芯片内的所有处理器时间保持一致(类似单芯片中的多核间需要一致)。ARM在其参考设计中提供了一个解决方案,使用两个芯片管脚SYNCREQ和SYNCACK实现一组握手协议。芯片间的同步通过这两个专用接口和CCIX消息来完成。 在以下情况下,通过CCIX将消息写入memory-mapped寄存器:
每个PE实现的通用定时器组件每个PE都包含两个定时器,一个是物理定时器(Physical Timer),另一个是虚拟定时器(Virtual Timer)。 物理定时器(Physical Timer)该物理定时器(Physical Timer)包含系统计数器(System Counter)的计数值,当实现了FEAT_ECV时,CNTPOFF_EL2寄存器保存了可选择设置的物理偏移量。
虚拟定时器(Virtual Timer)虚拟计数器等于物理计数器的值减去64 bits 的虚拟偏移量。CNTVOFF_EL2寄存器包含虚拟偏移量。CNTVCT_EL0寄存器保存着当前的虚拟计数器值。但是注意虚拟计数器和物理计数器一样,读取指令可以被乱序执行,需要使用内存屏障指令保证按序执行。 Timers每个实现的定时器的输出:
每一个定时器: 既可以作为一个 64 bits 的 CompareValue 来呈现,也可以作为 TimerValue 的形式呈现。不同点在于 CompareValue 是一个 64 位无符号的计数值,而 TimerValue 是一个 32 位有符号,并且是以倒计时的方式进行计数。除此之外,每一个定时器还有一个 32-bit 的控制寄存器。
CV 和 TV 的区别
寄存器映射
定时器中断可以将计时器配置为生成中断。来自某个PE定时器器的中断只能传递到该PE。这意味着一个PE的定时器器不能用来生成针对另一个核心的定时器。通过 CTL 寄存器控制中断的生成,使用以下字段: 中断的生成由 CTL 寄存器控制,使用以下字段:
要生成中断,软件必须将 ENABLE 设置为 1 并清除 IMASK。当定时器触发(CVAL <= System Count)时,向中断控制器发出中断信号。在 Armv8-A 系统中,中断控制器通常是通用中断控制器(GIC)。 每个定时器使用的中断 ID(INTID)由服务器基础体系结构(SBSA)定义,如下所示: 注意: 这些 INTID 在私有外围中断(PPI)范围内。这些 INTID 对于特定的PE,这意味着每个核心将其 EL1物理计时器视为 INTID 30。 定时器中断虚拟化在arm平台上的定时器虚拟化相对简单,因为构架强制Generic Timer必须实现,它足够操作系统使用的timekeeping 的需求。KVM使用类似bhyvearm64的方式:virtual timer给虚拟机用,需要注意的是timer产生的中断还是需要hypervisor注入,Physical timer由软件模拟,因为它被host使用了。 A. Generic TimerArmv8构架提供的定时器叫做Generic Timer. 实现上实际包括至少两个不同的timer, 最多到7个。一个系统可以有一个secure physical timer, 一个non secure physical timer, 通常简称为physical timer, 一个 virtual timer, physical和virtual non-secure EL2 timers, physical和virtual secure EL2 timers. 为虚拟化目的,我们聚焦在一般操作系统使用的timer上,也就是physical timer (它计数逝去的真实时间)和virtual timer(它计数带固定偏移的逝去时间)。
B. Virtual timer虚拟化Timer中断是极度时间敏感的。Timer中断以规律性的间隔到来(FreeBSD kernel配置为每1ms一个中断),因为它们这么频繁,因此花太多的时间服务这个中断是极不可取的。这对虚拟中断来说也是适用的:hypervisor在模拟timer上花的时间越少,下一个中断到来前,虚拟机可以利用的CPU时间越多。 中断天然地是异步的;它们可能在任何时候到来,不过处理器在执行什么程序。这也适用于virtual timer中断:一个虚拟timer中断可以在另一个host程序而不是在编程这个timer的虚拟机运行在CPU上时到来。Virtual Timer需要一个机制,在触发中断之前辨别是否是这个timer的虚拟机。 物理世界的时间(墙上时间)4ms里,每个vCPU各运行了2ms。如果我们设置vCPU0的比较器在T=0之后的3ms产生一个中断,那么你希望实际在哪个墙上时间点产生中断呢? 是vCPU0的虚拟时间的2ms,也就是墙上时间3ms那个点还是 vCPU0虚拟时间3ms的那个点? 在Arm体系结构中同时支持上述两种设置,这取决于你使用何种虚拟化方案。让我们看看这是如何实现的。 运行在vCPU上的软件可以访问如下两种时钟
EL1物理时钟会与系统计数器(System Conter)模块直接比较,使用的是绝对的墙上时间。而EL1虚拟时钟与虚拟计数器比较。虚拟计数器是在物理计数器的基础上减去一个偏移。 Hypervisor负责为当前调度运行的vCPU指定对应的偏移寄存器。这种方式使得虚拟时间只会覆盖vCPU实际运行的那部分时间。 在一个6ms的时段里,每个vCPU分别运行了3ms。Hypervisor可以使用偏移寄存器来将vCPU的时间调整为其实际运行的时间。 |
Beta Was this translation helpful? Give feedback.
-
串口中断管理在Rust-Shyper中,物理串口是默认分配给第一个管理VM,其他VM的串口挂载到第一个VM下,我们是不是可以使用toml文件来配置将串口中断分配给哪个VM,然后其他VM的串口虚拟到统一的VM中。 |
Beta Was this translation helpful? Give feedback.
-
虚拟中断控制器 接口与定义structure由于结构上的差异,各个架构的中断控制器可能不会共用数据结构,而是编写不同的struct,实现同一个trait。 如果需要的话,中断控制器连接中断源和目标,以下的结构应该是相似的: // 中断源相关
struct Source {
// 中断源的优先级
priority: u32,
// 中断源的等待处理标识
pending: bool,
}
// 目标(核心)相关
struct Target {
// 目标门限,当中断源优先级超过此门限时才有效
threshold: u32,
// 此目标对于每个中断源的使能
enable: [bool; SOURCE_NUM],
// 存放最优中断源
claim: u32,
}
interfacebasic基础接口。其中关于数据类型,发送中断信号的接口,是否定义claim/complete,以及不同数据长度的读写,有多种方案,需要做具体讨论。 trait InterruptController {
// 为设备提供发送中断信号的接口,包括连接哪个中断源(待定),和触发方式(电平/边缘)
fn send_irq(source_id: u32, level: bool);
// 写入,第一种实现
fn write_u32(addr: usize, val: u32);
fn write_u16(addr: usize, val: u16);
fn write_u8(addr: usize, val: u8);
...
// 读取,第一种实现
fn read_u32(addr: usize) -> u32;
fn read_u16(addr: usize) -> u16;
fn read_u8(addr: usize) -> u8;
...
// Claim过程,查看当前的最优中断源,可能会在read中使用而不需要对外暴露
fn claim() -> u32;
// Complete过程,告知中断控制器处理完成,可能会在write中使用而不需要对外暴露
fn complete(val: u32);
// get/set各种属性,可能并不需要对外暴露
// fn set_priority(source_id: u32, priority: u32);
// fn get_priority(source_id: u32) -> u32;
// fn set_enable(target_id: u32, enable: bool);
// fn get_enable(target_id: u32) -> bool;
...
}
读写实现:数据长度作为参数数据长度作为参数,使用最大变量(如u64)作为数据容器,在不同分支中做截断/扩展。 // 读取,第二种实现
fn read(addr: usize, len: usize) -> u64 {
match len {
8 => {
let res: u8 = ...;
return res as u64
}
16 => {
let res: u16 = ...;
return res as u64
}
32 => {...}
...
}
}
// 写入,第二种实现
fn write(addr: usize, val: u64, len: usize) -> u64 {
match len {
8 => {
let data: u8 = val as u8;
...
}
16 => {
let data: u16 = val as u16;
...
}
32 => {...}
...
}
} 读写实现:泛型使用泛型接口,并定义一个 trait InterruptController {
fn send_irq(source_id: u32, level: bool);
// 泛型的写接口
fn write<T>(&self, addr: usize, val: T)
where
Self: WriteRead<T>,
{
Self::write_impl(addr, val);
}
// 泛型的读接口
fn read<T>(&self, addr: usize) -> T
where
Self: WriteRead<T>,
{
Self::read_impl(addr)
}
}
trait WriteRead<T> {
fn write_impl(addr: usize, val: T);
fn read_impl(addr: usize) -> T;
} 例子: // 自定义控制器
pub struct myic {
base: u64,
size: u32,
data: [u32; 10]
}
// 控制器自有方法
impl myic {
pub fn new(base: u64, size: u32) -> Self {
myic{
base,
size,
data: [0; 10],
}
}
}
// 实现接口
impl InterruptController for myic {
fn send_irq(&self, source_id: u32, level: bool) {
println!("myic send irq");
}
...
}
// 实现对u8的读写
impl WriteRead<u8> for myic{
fn read_impl(addr: usize) -> u8 {
println!("myic read u8");
8
}
fn write_impl(addr: usize, val: u8) {
println!("myic write u8");
}
}
// 实现对u32的读写
impl WriteRead<u32> for myic {
fn read_impl(addr: usize) -> u32 {
println!("myic read u32");
32
}
fn write_impl(addr: usize, val: u32) {
println!("myic write u32");
}
}
// main.rs
fn main() {
let ic = myic;
ic.send_irq(1, true); // myic send irq
ic.write(0x100, 5 as u32); // myic write u32
ic.write::<u8>(0x100, 4); // myic write u8
ic.read::<u8>(0x100); // myic read u8
ic.read::<u32>(0x100); // myic read u32
} |
Beta Was this translation helpful? Give feedback.
-
更新的Timer设计文档
Guest 对 Timer 的访问移除 TimerList
Timer 对 Guest 的通知
在向 |
Beta Was this translation helpful? Give feedback.
-
如果不考虑
关于第1个需求: 保证 GUEST VM 的 不同
关于第2个需求: 能够支持
|
Beta Was this translation helpful? Give feedback.
-
一、GICv2介绍
通过上图可以确定,GIC 主要包含 3 部分:Distributor、CPU interfaces 和 Virtual CPU interfaces。Virtual CPU interfaces 包含 Virtual interface control 和 Virtual CPU interface。
中断进入 distributor,然后分发到 CPU interface
某个 CPU 触发中断后,读 GICC_IAR 拿到中断信息,处理完后写 GICC_EOIR 和 GICC_DIR(如果 GICC_CTLR.EOImodeNS 是 0,则 EOI 的同时也会 DI)
GICD、GICC 寄存器都是 MMIO 的,device tree 中会给出物理地址
中断类型
1. 软件生成中断(Software Generated Interrupts, SGI)
2. 私有外设中断(Private Peripheral Interrupts, PPI)
3. 共享外设中断(Shared Peripheral Interrupts, SPI)
中断号范围:32 到 1019(共 988 个中断号)
用途:用于处理系统中共享的外设中断,例如来自外部设备、网络接口、存储设备等的中断。
特点:这些中断是所有核心共享的,可以由任何一个核心处理,通常通过中断亲和性(affinity)来决定哪个核心处理该中断。
SPI默认发送vcpu 0上,同样将中断信号放到vcpu的ap_list字段排队,等待vcpu处理。
Distributor 作用
Distributor 主要作用为检测中断源、控制中断源行为和将中断源分发到指定 CPU 接口上(针对每个 CPU 将优先级最高的中断转发到该接口上)。
Distributor 对中断的控制包括:
全局启用中断转发到 CPU 接口
开启或关闭每一个中断
为每个中断设置优先级
为每个中断设置目标处理器列表
设置每个外设中断触发方式(电平触发、边缘触发)
为每个中断设置组
将 SGI 转发到一个或多个处理器
每个中断状态可见
提供软件设置或清除外设中断的挂起状态的一种机制
中断 ID
使用 ID 对中断源进行标识。每个 CPU 接口最多可以有 1020 个中断。SPI 和 PPI 中断为每个接口特定的,SPI 为为所有接口共用,因此多处理器系统中实际中断数大于 1020 个。
CPU Interface
CPU 接口提供一个处理器连接到 GIC 的接口。每一个 CPU 接口都提供一个编程接口:
二、中断处理状态机
GIC
为每个CPU
接口上每个受支持的中断维护一个状态机。下图显示了此状态机的实例,以及可能的状态转换。添加挂起状态(A1、A2)
对于一个 SGI,发生以下 2 种情况的 1 种:
对于一个 SPI 或 PPI,发生以下 2 种情况的 1 种:
外设发出一个中断请求信号
软件写 GICD_ISPENDRn 寄存器
删除挂起状态(B1、B2)
挂起到激活(C)
挂起到激活和挂起(D)
对于 SGI,这种转变发生在以下任一情况下:
对于 SPI 或 PPI,满足以下所有条件,则发生这种转换
删除激活状态(E1、E2)
三、中断虚拟化设计
中断虚拟化概要
Hypervisor interface (GICH)
struct vgic_cpu
的vgic_v2
字段,struct vgic_cpu
本身放在struct kvm_vcpu_arch
,每个 vCPU 一份vgic-v2-switch.S
中定义相关切换函数)vCPU interface (GICV, GICC in VM's view)
struct vgic_params vgic_v2_params
)保存了这个物理地址ioctl
设置该地址)映射到 GICV base 物理地址,然后把这个 GPA 作为 GICC base 在 device tree 中传给 VMVirtual distributor (GICD in VM's view)
struct vgic_dist
)struct vgic_dist
里的字段(在vgic-v2-emul.c
文件中)struct vgic_dist
放在struct kvm_arch
里VM's view
VGIC设计
主要以以下4中case进行讨论,其中case4涉及vCPU调度,其他情况不涉及调度:
VGIC Distributor设计
VGIC Distributor 主要模拟 nr_spis 个 spis 中断
VGIC初始化
- 对于case 1、2 和 3,不需要 IPI 通信。case 4 需要 IPI 通信。
- 这些寄存器保存 GIC 的一些属性和处理元素(PE)的数量。
- getenable:直接从结构中读取内容。
- setenable:根据 vtop 和 ptov 设置配置 GIC。对于情况 1、2 和 3,不需要 IPI 通信。情况 4 需要 IPI 通信。
- 其他 vgicd-emu 寄存器与 isenabler 类似。
多架构下的GIC路由,vint实现配置
在arm下,用户通过配置分发器的vgic_irq,就可以控制每个引脚的中断信息deliver到哪个CPU。考虑到需要兼容x86和riscv架构,需要设计一个通用的路由表vint_irq_routing_table。
vint_irq_routing_table
TODO:
vint_set_routing_entry(vm, entries, nr, ue)
中断响应回调,直接调用vgic_irqfd_set_irq将中断注入到指定的vCPU中。
SGI软件生成中断
SGI是一种特殊的中断,由软件生成,通常用于在多核系统中实现CPU间通信。SGI的目标CPU由发送者指定,并且SGI可以被路由到一个或多个核上。
在虚拟化环境下,由于多个vCPU可能共享同一个物理CPU,hypervisor需要对SGI进行虚拟化,以确保VM之间的隔离性和透明性。
Hypervisor对SGI的拦截
在虚拟化环境中,当VM试图发送SGI时,通常通过修改guest的GIC相关寄存器来触发。VM本身无法直接访问物理的GIC Distributor(GICD)寄存器,因此这些写操作会被hypervisor拦截。
SGI的处理与路由
在SGI虚拟化中,hypervisor负责以下操作:
虚拟GIC的支持
为了让VM能够像使用物理GIC一样处理中断,hypervisor会提供虚拟的GIC接口(vGIC)。vGIC负责模拟GICD和GICC(CPU接口)的寄存器操作,并将这些寄存器映射到VM的地址空间。
虚拟GIC支持VM的SGI管理,包括:
PPI 私有外设中断
PPI通常用于管理特定于处理器的外设中断。在GICv2中,每个核心都有其专属的PPI,通常包括定时器中断和其他本地外设中断。在虚拟化环境中,hypervisor需要虚拟化这些中断,以便每个VM能够透明地访问和使用它们。
VM发起PPI请求
当VM中的vCPU需要处理PPI时,通常是通过对GIC的寄存器进行操作。例如,vCPU可能会读取或清除某个PPI的状态,这一操作需要经过hypervisor的拦截。
Hypervisor拦截请求
PPI的路由和分发
目标vCPU处理中断
Hypervisor的清理工作
SPI 共享外设中断
在虚拟化环境中,SPI(Shared Peripheral Interrupt,共享外设中断)是一种用于处理多个处理器核心共享外设的中断。与SGI和PPI不同,SPI是针对共享设备的中断,允许多个CPU响应同一外设生成的中断。hypervisor在虚拟化SPI时需要确保VM之间的隔离,同时提供对共享外设的正确中断管理。
SPI通常用于系统中那些可以被多个处理器访问的外设,例如网络适配器、存储控制器等。在GICv2中,SPI由GIC的Distributor(GICD)管理,允许多个处理器核接收来自同一外设的中断。在虚拟化环境中,hypervisor需要将SPI虚拟化为适合多个VM使用的形式。
VM发起SPI请求
当外设生成中断时,它将通过物理GIC将SPI传递给相应的处理器核心。在虚拟化环境中,物理中断首先会传递到hypervisor。
Hypervisor的拦截和管理
SPI的路由和重定向
目标vCPU处理中断
Hypervisor的清理工作
List Register
对于有虚拟化扩展的 GIC,Hypervisor使用 List Registers 来维护高优先级虚拟中断的一些上下文信息。
KVM关于VGIC的设计
Beta Was this translation helpful? Give feedback.
All reactions