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.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
一、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