前两天碰到一个基于QNX的虚拟化平台上的项目问题,同事反馈系统很卡,点击页面明显有延迟,卡顿严重。用top看了下Android系统的负载,还有20%左右的空闲,其他的如用户态、内核态以及中断的占用都比较正常,唯独有一个%host的占用特别高,最高能占到60%以上。这个host的占用是什么意思了?这篇文章,我们就基于这个问题,来详细阐述分析下虚拟化平台中host占用高的问题以及在虚拟化平台KVM是如何计算host占用的。
1 |
|
问题定位与排查
通过adb shell进到设备里top看下系统整体状态,可以发现系统内存还有不少空间,CPU的空闲只有不到30%,占大头的就是%host这一部分。
先尝试用DeepSeek问了下,top指令中的host占用到底是什么意思? DeepSeek很快给出了答案:
在Linux的top命令中,”host的CPU占用”(通常对应%host或st字段)是虚拟化环境特有的性能指标,用于反映虚拟机被宿主机(Hypervisor)抢占的CPU时间百分比
就是说,host过高,可能是由于虚拟化平台中宿主机有异常,比如处于高负载,CPU配置不合理(给guest系统的资源太少),导致了客户机Android系统一直无法抢占到CPU,处于挂起等待的状态。那么,究竟如何排查这类问题了?接着问下DeepSeek: 如何排查steal CPU占用过高的问题,DeepSeek给出了一些可能的解释:
顺着这个思路,进入设备继续查看,通过mpstat -P ALL查看整体的负载,观察到类似的情况, %steal这一栏显示,Android相当一部分负载都来自于等待宿主机上;这里多个核的%steal占比加起来就对应top中的%host:
基于这些数据,我们推测很可能宿主机QNX侧有问题(之前的版本没有异常),拉着开发人员对齐了下,确认了虚拟机上的CPU分配没有太大问题(系统只有一个客户机Android,可以访问所有的物理CPU),那么基本可以排除资源不足引起的问题;是不是最近有什么修改引入了这个问题?
开发反馈只有两个修改点: 一个是更新了车控相关的信号,一个是在QNX上新增了一个PPS节点。更新车控信号矩阵不应该对系统负载有太大的影响,而且从已有的数据看,Android的各个进程的负载并不高,况且在台架上也没有太多的车控信号需要传输,因此可以排除。那么,很可能是新增PPS节点导致了QNX侧的负载过高,从而引起Android侧无法拿到CPU。最后,开发排查了相关的进程,发现是某个PPS节点的配置异常,有一个进程在高频的写数据导致QNX侧的负载太高(QNX的idle已经接近0),因而客户机Android无法拿到足够的CPU资源。
PPS(Persistent Publish-Subscribe)是QNX用于跨进程通讯的一种协议
问题到这里也算告一段落。但是,为了对这类问题有更多的了解,后续碰到相似问题时时能够快速的定位分析,还是决定要深入代码层面来了解下虚拟化平台中steal time究竟是怎么来计算的。
虚拟化中的CPU steal-time
既然是‘偷来的时间’(steal-time),那么就说明不是客户机自己执行指令导致的CPU占用,而是等待宿主机分配资源所消耗的时间,此时宿主机可能是在处理其他客户机的请求,也有可能是在忙着处理内部的事务。
Steal time is the percentage of time a virtual CPU waits for a real CPU while the hypervisor is servicing another virtual processor (or host itself).
无论是top指令中展示的%host,还是mpstat中的%steal都是通过内核的/proc/stat获取到的CPU占用数据。查看Android的源码external/toybox/toys/posix/ps.c,CPU的steal-time就是/proc/stat的最后一列数据。我们继续查看下内核的代码。
1 |
|
内核中对应/proc/stat的状态显示在fs/proc/stat.c中实现的,可以看到steal-time的计算是通过一个per-cpu结构体变量kernel_cpustat中的cpustat数组对应的CPUTIME_STEAL索引获取到:
1 |
|
内核中统计各个维度的CPU占用数据在kernel/sched/cputime.c统一实现了相关的接口,只有开启了内核配置CONFIG_PARAVIRT的虚拟化平台中才会计算steam-time时间,其他的则直接返回0:通过函数paravirt_steal_clock获取对应CPU的客户机系统的steal时间。
1 |
|
函数paravirt_steal_clock在arch/arm64/include/asm/paravirt.h中定义,最终实际是通过一个结构体pv_time_ops中的函数steal_clock调用获取客户机的steal时间。
1 |
|
对于虚拟化平台来说,系统在初始化的时候,会通过pv_time_init函数对pv_ops进行初始化设置(arch/arm64/kernel/paravirt.c):
pv_time_init_stolen_time: 初始化存放steal时间相关的变量内存区域(用于宿主机hypervisor与客户机进行数据共享)pv_ops.time.steal_clock: 对提供给外部获取steal-time的接口进行赋值
1 |
|
函数pv_time_init_stolen_time注册一个CPU热插拔的回调函数,等CPU状态变为online收到回调后,调用stolen_time_cpu_online函数,初始化steal-time相关的配置:
1 |
|
可以看到,stolen_time_cpu_online主要用于客户机与hypervisor协商一块固定的内存区域用于交换steal-time的时间,主要有几个步骤:
- 首先通过一个
HVC异常陷入指令(用于切换不同的Exception Level-EL),让客户机从EL1(内核态)进入到EL2(虚拟机态),获取到宿主机用于保存steal-time的内存地址,并通过arm_smccc_res返回给客户机 - 内核将拿到的地址映射到一块内存,这样在内核中就可以通过函数访问到这块内存区域,从而读取到
steam-time
1 |
|
这里以Linux的虚拟化方案KVM(Kernel-based Virtual Machine)为例来说明,在虚拟机层面如何处理steal-time的计算,并给到客户机系统的。在KVM接收到HVC的指令ARM_SMCCC_HV_PV_TIME_ST后,最终会调用对应的异常处理函数kvm_hvc_call_handler(arch/arm64/kvm/hypercalls.c):
1 |
|
可以看到,在这里虚拟机hypervisor会调用kvm_init_stolen_time来初始化steam-time相关的配置:
- 通过客户机对应的
vcpu结构体kvm_vcpu获取到steal变量的基地址base(对应客户机的物理地址) - 通过
kvm_write_guest将一个pvclock_vcpu_stolen_time初始化值写入到base地址,正是在这个结构体中保存了客户机的steal-time
1 |
|
最后,KVM运行的时候,监听一个进程上下文切换的事件,在vcpu进程切换时主动调用kvm_update_stolen_time更新steal-time,从这里我们也可以看到,客户机的steal-time实际读取的是调度器的统计数据run_delay,就是vcpu调度的延迟-在调度队列里等待的时间。
1 |
|
总结
这篇文章,我们基于一个虚拟化项目的CPU占用高的问题,分析了排查的手段与思路;以KVM为例,深入分析了虚拟化平台CPU steal-time的计算与更新流程,这样在后续碰到类似的问题时,可以更加得心应手了。在平常的项目实践中,遇到了一些疑难问题,如果有知识盲点,花点时间深入研究下背后的原理与机制,学习的效果比单纯的理论研究要好很多。


