在 Linux 中计时并不简单,虚拟化带来了额外的挑战和机遇。在本文中,我将回顾 KVM、Xen 和 Hyper-V 相关的计时技术以及 Linux 内核的相应部分。
计时是记录某事物所需时间的过程或活动。我们需要“仪器”来测量时间。Linux 内核有几个抽象来表示这些设备
- Clocksource(时钟源)是一种可以在您需要时提供时间戳的设备。换句话说,Clocksource 是任何允许您获取其值的计时器。
- Clockevent device(时钟事件设备)是一种闹钟——您要求设备在未来某个时间发出信号(例如,“1 毫秒后叫醒我”),当闹钟触发时,您会收到信号。
- sched_clock() 函数类似于 clocksource,但这个特定的函数应该“廉价”读取(意味着可以快速获取其值),因为 sched_clock() 用于任务调度目的,而调度经常发生。我们准备牺牲准确性和其他特性来换取速度。
想象一下,您正在编写一个应用程序,需要获取当前日期和时间来进行时间戳记录,例如。您可能会想到这样的代码
#include <stdio.h>
#include <time.h>
int timestamp_function(...)
{
struct timespec tp;
int res = clock_gettime(CLOCK_REALTIME, &tp);
… do something with the timestamp …
}
什么是 CLOCK_REALTIME?我们还有哪些其他的时钟?man 2 clock_gettime 给出了答案
- CLOCK_REALTIME 时钟给出自 1970 年 1 月 1 日以来经过的时间。这个时钟受 NTP 调整的影响,当系统管理员调整系统时间时,可能会向前或向后跳跃。
- CLOCK_MONOTONIC 时钟给出自固定起点(通常是自您启动系统以来)的时间。这个时钟受 NTP 的影响,但它不能向后跳跃。
- CLOCK_MONOTONIC_RAW 时钟给出与 CLOCK_MONOTONIC 相同的时间,但这个时钟不受 NTP 调整的影响。
- CLOCK_REALTIME_COARSE 和 CLOCK_MONOTONIC_COARSE 是 CLOCK_REALTIME 和 CLOCK_MONOTONIC 的更快但精度较低的变体。
还有一些其他时钟与运行中的进程或线程相关,但我们现在跳过它们。
一些应用程序经常进行时间戳记录,每秒数千次,对于这些应用程序,我们必须确保 clock_gettime() 足够快。它在 Linux 中是如何工作的?算法是
- 从 vDSO(虚拟动态共享对象)调用 clock_gettime()。vDSO 是内核提供给每个应用程序的共享库,它包含可以在用户空间运行而无需切换到内核的代码。
- 对于 CLOCK_REALTIME_COARSE 和 CLOCK_MONOTONIC_COARSE,通过读取适当的 timekeeper 结构立即给出答案,内核提供该结构仅供用户空间读取。
- 对于 CLOCK_REALTIME 和 CLOCK_MONOTONIC,检查当前使用的 clocksource 是否可以从用户空间读取,如果可以,则使用其读取值来推断适当的 timekeeper 值。
- 如果正在使用的 clocksource 无法从用户空间读取,则通过执行系统调用切换到内核,并让内核读取当前的 clocksource 并推断适当的 timekeeper 值。
当直接从用户空间读取 clocksource 时,vDSO 优化非常重要。以下是我的测试程序的结果,该程序执行 1 亿次 clock_gettime() 读取。这些测试是在启用了和未启用 vDSO 优化的 KVM 虚拟机上执行的
kvmclock without vDSO:
# time ./clock_gettime_many
real 0m15.606s
user 0m2.684s
sys 0m12.916s
kvmclock with vDSO:
# time ./clock_gettime_many
real 0m2.365s
user 0m2.362s
sys 0m0.001s
纯用户空间方法比执行 syscall 快七倍。我们绝对需要这个。但是,是什么使 clocksource 适合如此快速的读取?
Clocksources(时钟源)
一般来说,我们要求 clocksource 满足以下条件
- 永不后退。
- 永不停顿。
- 避免“跳跃”。
- 具有良好的分辨率(频率)。
- 快速读取。
- 可用于用户空间代码。
PC 硬件有许多遗留的计时设备,但这些设备缺乏上述特性。具体来说
- PIT:仅适用于计数 jiffies(系统定时器中断);分辨率低。
- CMOS RTC:低分辨率(1 秒)日期和时间时钟,可选 32768Hz 定时器;无法从用户空间读取。
- ACPI (PM) 定时器:频繁溢出;读取速度慢;无法从用户空间访问。
- HPET:并非总是存在;不一定快速读取。
- LAPIC 定时器:未知频率;无法从用户空间读取。
所有现代 x86 虚拟机监控程序都虚拟化了此硬件,但所有访问的虚拟化成本太高,无法将这些设备用作 Linux 中可靠的 clocksource。
TSC
在裸机 x86 硬件上,当今最常用的 clocksource 是 TSC(时间戳计数器)。TSC 是一个特殊的自动递增 CPU 寄存器,与之前提到的使用传统硬件相比,它具有许多优势。它具有高精度,并且可以使用一条汇编指令 (rdtsc) 读取,即使从用户空间也是如此。然而,TSC 也有其自身的问题,包括
- 其频率未知,需要使用 PIT、CMOS 或 ACPI 定时器进行测量。
- 该寄存器是可写的,并且在不同的 CPU 上读取值可能不同。
- TSC 可以在处理器的某些低功耗 C 状态下停止。这通常不会发生在现代硬件上。
- 过去观察到,在一些大型 NUMA 系统上,TSC 会失去同步。幸运的是,这种系统的数量有限。
- SMI 处理程序可能会重置计数器。
虚拟化带来了额外的挑战。当虚拟机迁移到另一台主机时,它的 TSC 值会不同,因此我们会在该值中看到“跳跃”。此外,我们在启动时测量的频率不再是实际的 TSC 频率。引入了两种类似的方法来解决这些问题:Xen 和 KVM 虚拟机监控程序客户机的 pvclock(准虚拟化时钟)和 Hyper-V 客户机的 TSC page(TSC 页面)。这些都是固定频率的时钟,因此除了读取 TSC 值之外,我们还需要进行一些数学运算才能获得读取值。
pvclock
Xen 和 KVM 虚拟机监控程序提出了所谓的 pvclock 协议来增强 TSC 并使其适用于虚拟化客户机。该协议基于一个简单的每 CPU 结构,该结构在主机和客户机之间共享
struct pvclock_vcpu_time_info {
u32 version;
u32 pad0;
u64 tsc_timestamp;
u64 system_time;
u32 tsc_to_system_mul;
s8 tsc_shift;
u8 flags;
u8 pad[2];
};
要获得当前的 TSC 读数,客户机必须进行以下数学运算
PerCPUTime = ((RDTSC() - tsc_timestamp) >> tsc_shift) * tsc_to_system_mul + system_time
flags 字段指示我们是否可以信任该读数,即使我们在不同的 CPU 上进行后续调用,也能保持单调性的承诺,这决定了我们从 vDSO 使用 clocksource 的能力。如果无法保证单调性,Linux 需要跟踪上次读取的值,以确保即使从一个 CPU 迁移到另一个 CPU,也没有应用程序会看到时间倒退。幸运的是,这种情况在现代硬件上并不经常发生,而且我们的读数速度很快。
Hyper-V TSC 页面
Microsoft 使用他们自己的 TSC page 协议重新发明了 pv_clock 协议,该协议类似于 pv_clock,但有一个显着的区别。TSC page 是每个虚拟机一个结构——而不是每个 CPU 一个结构——因此它无法补偿 TSC 在多个 CPU 上失去同步的情况。我们不能确定,但猜测是,在这种情况下,虚拟机监控程序将尝试同步 TSC 或完全禁用 TSC page 机制。
TSC 页面的协议是
struct ms_hyperv_tsc_page {
volatile u32 tsc_sequence;
u32 reserved1;
volatile u64 tsc_scale;
volatile s64 tsc_offset;
u64 reserved2[509];
};
要获得当前的 TSC 读数,客户机必须进行以下数学运算
PerVMTime = ((VirtualTsc * tsc_scale) >> 64) + tsc_offset
tsc_sequence 字段中的特殊值 0 指示该方法已被禁用,我们应该回退到从 Hyper-V 提供的另一个虚拟化 MSR(特定于模型的寄存器)读取值。这对于用户空间代码来说是不可能的,并且通常速度要慢得多。
用于虚拟化 TSC 的硬件扩展
自从硬件辅助虚拟化早期以来,英特尔一直在提供一个选项,用于在硬件中为虚拟客户机执行 TSC 偏移,这意味着客户机的 rdtsc 读数将返回主机的 TSC 值 + 偏移量。不幸的是,这不足以支持在不同主机之间的迁移,因为 TSC 频率可能不同,因此引入了 pvclock 和 TSC page 协议。在 2015 年末,英特尔推出了 TSC 缩放 功能(该功能已经在 AMD 处理器中存在了几年),理论上,这是一个改变游戏规则的功能,使 pvclock 和 TSC page 协议变得多余。然而,立即切换到使用纯 TSC 作为虚拟化客户机的 clocksource 似乎是不切实际的;必须确保所有潜在的迁移接收主机都支持该功能,但该功能尚未广泛使用。还必须进行广泛的测试,以确保从准虚拟化协议切换没有缺点。
主机范围的时间同步
因此,我们在 KVM 上运行虚拟化客户机并使用 kvmclock(它实现了 pvclock 协议),或者我们正在运行 Hyper-V 客户机并将 Hyper-V TSC page 作为 clocksource。我们的日期和时间是否在客户机和主机(或,在同一主机上的不同客户机之间)同步?我们正在读取相同的 TSC 值,因此生成的时间应该相同,对吗?好吧,不完全是。主机和客户机的 CLOCK_REALTIME 时钟都受到 NTP 调整的影响,并且可能会随着时间的推移而发散。
为了解决这个问题,Linux-4.11引入了一个解决方案:KVM和Hyper-V的PTP设备。这些设备实际上与PTP时间同步协议无关,也不与网络设备一起工作,但它们将自己呈现为PTP(/dev/ptp*)设备,因此可以被现有的时间同步软件使用。
要启用与主机的时间同步,我们必须执行以下操作
- 对于KVM客户机,我们需要加载 ptp_kvm 模块。为了使其在重启后加载,我们可以这样做
# echo ptp_kvm > /etc/modules-load.d/ptp_kvm.conf
(在Fedora/RHEL7上)。Hyper-V客户机不需要这样做,因为实现该设备的模块会自动加载。
- 将 /dev/ptp0 作为参考时钟添加到 NTP 守护进程配置中。对于chrony,它将是
# echo "refclock PHC /dev/ptp0 poll 3 dpoll -2 offset 0" >> /etc/chrony.conf
- 重启NTP服务器
systemctl restart chronyd
- 检查时间同步状态
# chronyc sources | grep PHC0
众所周知,KVM客户机比Hyper-V客户机产生更好的结果,因为设备背后的机制非常不同。Hyper-V主机每五秒向其客户机发送时间样本,而KVM客户机可以选择直接对hypervisor进行hypercall以获取其时间。
KVM上的测试结果(空闲主机,单个客户机)
# for f in `seq 1 5`; do chronyc sources | grep PHC0 ; sleep 10s; done #* PHC0 0 3 377 4 -24ns[ -166ns] +/- 37ns #* PHC0 0 3 377 6 +13ns[ +49ns] +/- 32ns #* PHC0 0 3 377 8 +49ns[ +182ns] +/- 28ns #* PHC0 0 3 377 11 -43ns[ -113ns] +/- 24ns #* PHC0 0 3 377 4 +60ns[ +152ns] +/- 18ns
Hyper-V上的测试结果(空闲主机,单个客户机)
# for f in `seq 1 5`; do chronyc sources | grep PHC0 ; sleep 10s; done #* PHC0 0 3 377 7 +287ns[+2659ns] +/- 131ns #* PHC0 0 3 377 7 +119ns[-3852ns] +/- 130ns #* PHC0 0 3 377 9 +1648ns[+2449ns] +/- 156ns #* PHC0 0 3 377 5 +898ns[ +613ns] +/- 142ns #* PHC0 0 3 377 7 +288ns[ -403ns] +/- 98ns
虽然Hyper-V PTP设备不如KVM准确,但与NTP相比,它仍然非常准确。从上面可以看出,客户机的系统时间通常与主机的时间保持在10微秒以内,这是一个很好的结果。
Vitaly Kuznetsov 将于 2017 年 6 月 20 日在北京的LinuxCon ContainerCon CloudOpen China上谈论 Linux 虚拟机中的时间管理。
评论已关闭。