基于X86架构的时间管理虚拟化

作者:

Zachary Amsden <zamsden@redhat.com>

版权:
  1. 2010, Red Hat。保留所有权利。

1. 概述

X86平台最复杂的方面之一,特别是该平台的虚拟化,在于其计时设备种类繁多以及模拟这些设备的复杂性。此外,时间虚拟化带来了一系列新的挑战,因为它引入了一种超出客户机CPU控制范围的时间多路复用划分。

首先,我们将描述可用的各种时间管理硬件,然后提出一些出现的问题和可用的解决方案,并为特定类型的KVM客户机提供具体建议。

本文档的目的是收集与时间管理相关的数据和信息,这些信息可能在其他地方很难找到,特别是与KVM和基于硬件的虚拟化相关的信息。

2. 计时设备

首先,我们讨论可用的基本硬件设备。TSC和相关的KVM时钟足够特殊,值得进行全面阐述,它们将在下一节中描述。

2.1. i8254 - PIT

最早可用的定时器设备之一是可编程中断定时器(PIT)。PIT具有固定频率为 1.193182 MHz 的基准时钟和三个通道,可编程用于提供周期性或单次中断。这三个通道可以配置为不同模式,并具有独立的计数器。在最初的IBM PC中,通道1和2不适用于一般用途,历史上它们连接用于控制RAM刷新和PC扬声器。现在,PIT通常作为模拟芯片组的一部分集成,并且不使用单独的物理PIT。

PIT 使用 I/O 端口 0x40 - 0x43。对16位计数器的访问是通过对I/O端口进行单字节或多字节访问来完成的。有6种可用模式,但并非所有模式都适用于所有定时器,因为只有定时器2连接了门输入,这是模式1和5所必需的。门线由端口61h的第0位控制,如下图所示

--------------             ----------------
|            |           |                |
|  1.1932 MHz|---------->| CLOCK      OUT | ---------> IRQ 0
|    Clock   |   |       |                |
--------------   |    +->| GATE  TIMER 0  |
                 |        ----------------
                 |
                 |        ----------------
                 |       |                |
                 |------>| CLOCK      OUT | ---------> 66.3 KHZ DRAM
                 |       |                |            (aka /dev/null)
                 |    +->| GATE  TIMER 1  |
                 |        ----------------
                 |
                 |        ----------------
                 |       |                |
                 |------>| CLOCK      OUT | ---------> Port 61h, bit 5
                         |                |      |
Port 61h, bit 0 -------->| GATE  TIMER 2  |       \_.----   ____
                          ----------------         _|    )--|LPF|---Speaker
                                                  / *----   \___/
Port 61h, bit 1 ---------------------------------/

下面将描述定时器模式。

模式0:单次超时。

这是一种单次软件超时,当门为高电平(对定时器0和1始终为真)时开始倒计时。当计数达到零时,输出变为高电平。

模式1:触发单次。

输出最初设置为高电平。当门线设置为高电平时,开始倒计时(即使门线降低也不会停止),在此期间,输出设置为低电平。当计数达到零时,输出变为高电平。

模式2:频率发生器。

输出最初设置为高电平。当倒计时达到1时,输出会保持低电平一个计数周期,然后返回高电平。数值被重新加载,倒计时自动恢复。如果门线变为低电平,计数将停止。如果在门线降低时输出为低电平,输出会自动变为高电平(这只影响定时器2)。

模式3:方波。

这会生成高/低方波。计数决定脉冲的长度,当达到零时,脉冲在高电平和低电平之间交替。计数只在门为高电平时进行,并在达到零时自动重新加载。每次时钟都会将计数器减两次,以全周期速率生成一个完整的高/低周期。如果计数为偶数,时钟会保持高电平N/2个计数周期,低电平N/2个计数周期;如果计数为奇数,时钟会保持高电平(N+1)/2个计数周期,低电平(N-1)/2个计数周期。只有偶数值会被计数器锁存,因此读取时不会观察到奇数值。这是定时器2的预期模式,它通过对方波输出进行低通滤波来生成类正弦音。

模式4:软件选通。

编程此模式并加载计数器后,输出将保持高电平,直到计数器达到零。然后输出会持续低电平一个时钟周期并返回高电平。计数器不会重新加载。计数只在门为高电平时进行。

模式5:硬件选通。

编程并加载计数器后,输出将保持高电平。当门升高时,开始倒计时(即使门降低也不会停止)。当计数器达到零时,输出会持续低电平一个时钟周期,然后返回高电平。计数器不会重新加载。

除了正常的二进制计数,PIT还支持BCD计数。命令端口0x43用于设置三个定时器各自的计数器和模式。

发送到端口0x43的PIT命令,使用以下位编码

Bit 7-4: Command (See table below)
Bit 3-1: Mode (000 = Mode 0, 101 = Mode 5, 11X = undefined)
Bit 0  : Binary (0) / BCD (1)

命令表

0000 - Latch Timer 0 count for port 0x40
      sample and hold the count to be read in port 0x40;
      additional commands ignored until counter is read;
      mode bits ignored.

0001 - Set Timer 0 LSB mode for port 0x40
      set timer to read LSB only and force MSB to zero;
      mode bits set timer mode

0010 - Set Timer 0 MSB mode for port 0x40
      set timer to read MSB only and force LSB to zero;
      mode bits set timer mode

0011 - Set Timer 0 16-bit mode for port 0x40
      set timer to read / write LSB first, then MSB;
      mode bits set timer mode

0100 - Latch Timer 1 count for port 0x41 - as described above
0101 - Set Timer 1 LSB mode for port 0x41 - as described above
0110 - Set Timer 1 MSB mode for port 0x41 - as described above
0111 - Set Timer 1 16-bit mode for port 0x41 - as described above

1000 - Latch Timer 2 count for port 0x42 - as described above
1001 - Set Timer 2 LSB mode for port 0x42 - as described above
1010 - Set Timer 2 MSB mode for port 0x42 - as described above
1011 - Set Timer 2 16-bit mode for port 0x42 as described above

1101 - General counter latch
      Latch combination of counters into corresponding ports
      Bit 3 = Counter 2
      Bit 2 = Counter 1
      Bit 1 = Counter 0
      Bit 0 = Unused

1110 - Latch timer status
      Latch combination of counter mode into corresponding ports
      Bit 3 = Counter 2
      Bit 2 = Counter 1
      Bit 1 = Counter 0

      The output of ports 0x40-0x42 following this command will be:

      Bit 7 = Output pin
      Bit 6 = Count loaded (0 if timer has expired)
      Bit 5-4 = Read / Write mode
          01 = MSB only
          10 = LSB only
          11 = LSB / MSB (16-bit)
      Bit 3-1 = Mode
      Bit 0 = Binary (0) / BCD mode (1)

2.2. RTC

最初的PC中可用的第二个设备是MC146818实时时钟。原始设备现已过时,通常由系统芯片组模拟,有时通过HPET和一些拼凑的IRQ路由实现。

RTC通过CMOS变量访问,它使用一个索引寄存器来控制读取哪些字节。由于只有一个索引寄存器,读取CMOS和读取RTC都需要锁保护(此外,允许hwclock等用户空间工具直接访问RTC是危险的,因为它们可能会破坏内核对CMOS内存的读写)。

RTC会生成一个中断,通常路由到IRQ 8。该中断可以作为周期性定时器、每日一次的额外闹钟,并可以在MC146818完成CMOS寄存器更新后发出中断。中断类型在RTC状态寄存器中指示。

即使系统关闭,RTC也会通过电池供电更新当前时间字段。在更新进行时,不应读取当前时间字段,状态寄存器中对此有指示。

该时钟使用32.768kHz晶振,因此,如果RTC要计数秒数,寄存器A的位6-4应编程为32kHz分频器。

这是最初用于RTC/CMOS的RAM映射

Location    Size    Description
------------------------------------------
00h         byte    Current second (BCD)
01h         byte    Seconds alarm (BCD)
02h         byte    Current minute (BCD)
03h         byte    Minutes alarm (BCD)
04h         byte    Current hour (BCD)
05h         byte    Hours alarm (BCD)
06h         byte    Current day of week (BCD)
07h         byte    Current day of month (BCD)
08h         byte    Current month (BCD)
09h         byte    Current year (BCD)
0Ah         byte    Register A
                     bit 7   = Update in progress
                     bit 6-4 = Divider for clock
                                000 = 4.194 MHz
                                001 = 1.049 MHz
                                010 = 32 kHz
                                10X = test modes
                                110 = reset / disable
                                111 = reset / disable
                     bit 3-0 = Rate selection for periodic interrupt
                                000 = periodic timer disabled
                                001 = 3.90625 uS
                                010 = 7.8125 uS
                                011 = .122070 mS
                                100 = .244141 mS
                                   ...
                               1101 = 125 mS
                               1110 = 250 mS
                               1111 = 500 mS
0Bh         byte    Register B
                     bit 7   = Run (0) / Halt (1)
                     bit 6   = Periodic interrupt enable
                     bit 5   = Alarm interrupt enable
                     bit 4   = Update-ended interrupt enable
                     bit 3   = Square wave interrupt enable
                     bit 2   = BCD calendar (0) / Binary (1)
                     bit 1   = 12-hour mode (0) / 24-hour mode (1)
                     bit 0   = 0 (DST off) / 1 (DST enabled)
OCh         byte    Register C (read only)
                     bit 7   = interrupt request flag (IRQF)
                     bit 6   = periodic interrupt flag (PF)
                     bit 5   = alarm interrupt flag (AF)
                     bit 4   = update interrupt flag (UF)
                     bit 3-0 = reserved
ODh         byte    Register D (read only)
                     bit 7   = RTC has power
                     bit 6-0 = reserved
32h         byte    Current century BCD (*)
(*) location vendor specific and now determined from ACPI global tables

2.3. APIC

在奔腾及更高版本的处理器上,每个CPU都有一个板载定时器,作为高级可编程中断控制器(APIC)的一部分。APIC通过内存映射寄存器访问,并为每个CPU提供中断服务,用于IPI和本地定时器中断。

尽管理论上APIC是本地中断的安全稳定来源,但实际上,由于APIC CPU本地内存映射硬件的特殊性,已经出现许多错误和故障。请注意,CPU勘误表可能会影响APIC的使用,并且可能需要采取变通措施。此外,其中一些变通措施对虚拟化带来了独特的限制——需要额外读取内存映射I/O而产生的额外开销,或者实现起来计算成本更高的额外功能。

由于APIC在Intel和AMD手册中有很好的文档记录,我们将在此避免重复细节。应该指出的是,APIC定时器通过LVT(本地向量定时器)寄存器编程,能够进行单次或周期性操作,并且基于总线时钟经过可编程分频器寄存器分频而来。

2.4. HPET

HPET相当复杂,最初旨在取代X86 PC的PIT/RTC支持。这种情况是否会发生还有待观察,因为PC硬件的事实标准是模拟这些旧设备。一些被指定为“无遗留设备”的系统可能只支持HPET作为硬件定时器设备。

HPET规范相当宽松和模糊,要求至少3个硬件定时器,但允许实现自由以支持更多。它也没有对定时器频率施加固定速率,但确实对频率、误差和偏移量施加了一些极值。

通常,HPET被推荐作为高精度(与PIT/RTC相比)的时间源,它不受本地变化的影响(因为在任何给定系统中只有一个HPET)。HPET也是内存映射的,并且其存在通过BIOS的ACPI表指示。

HPET的详细规范超出了本文档的当前范围,因为它在其他地方已有非常详细的文档记录。

2.5. 外部定时器

某些卡片,无论是专有的(看门狗板)还是常见的(e1000),都内置了计时芯片,这些芯片可能具有可供内核或用户驱动程序访问的寄存器。据作者所知,尚未有人尝试使用这些芯片为Linux或其他内核生成时钟源,并且通常不鼓励这样做,因为它不符合公认的游戏规则。这样的定时器设备需要额外的支持才能正确虚拟化,目前不被认为重要,因为没有已知的操作系统这样做。

3. TSC 硬件

TSC或时间戳计数器在理论上相对简单;它计算处理器发出的指令周期,可作为时间衡量标准。实际上,由于存在许多问题,它是最复杂的时间管理设备。

TSC在内部表示为64位MSR,可以使用RDMSR、RDTSC或RDTSCP(如果可用)指令读取。过去,由于硬件限制,可以写入TSC,但通常在旧硬件上只能写入64位计数器的低32位,而高32位则被清除。然而,现在在Intel家族0Fh处理器(型号3、4和6)以及家族06h处理器(型号e和f)上,此限制已解除,所有64位均可写入。在AMD系统上,写入TSC MSR的能力并非架构保证。

TSC可从CPL-0访问,并且有条件地,对于CPL > 0的软件,通过CR4.TSD位进行访问,该位启用时会禁用CPL > 0的TSC访问。

一些供应商实现了一个额外的指令RDTSCP,它不仅原子地返回TSC,还返回一个与处理器编号对应的指示符。这可以用于索引TSC变量数组,以确定TSC未同步的SMP系统中的偏移信息。此指令的存在必须通过查询CPUID特征位来确定。

VMX和SVM都在虚拟化硬件中提供了扩展字段,允许客户机可见的TSC通过一个常量进行偏移。较新的实现承诺允许TSC额外进行缩放,但这种硬件尚未广泛可用。

3.1. TSC 同步

在大多数实现中,TSC是CPU本地时钟。这意味着在SMP平台上,不同CPU的TSC可能在不同时间启动,具体取决于CPU何时通电。通常,同一芯片上的CPU会共享相同的时钟,但并非总是如此。

BIOS可能会在开机过程中尝试重新同步TSC,操作系统或其他系统软件也可能尝试这样做。几个硬件限制使问题更糟——如果无法写入TSC的完整64位,则可能无法使新加入CPU的TSC与系统其余部分匹配,从而导致TSC不同步。这可以通过BIOS或系统软件完成,但实际上,除非所有值都从同一个时钟读取,否则不可能获得完全同步的TSC,这通常只在单插槽系统或具有特殊硬件支持的系统上才可能实现。

3.2. TSC 与 CPU 热插拔

如前所述,在系统引导时间之后加入的CPU的TSC值可能与系统其余部分不同步。系统软件、BIOS或SMM代码可能确实会尝试将TSC设置为与系统其余部分匹配的值,但完美匹配通常无法保证。这可能会导致系统从TSC同步的状态回到TSC同步缺陷(无论多么微小)可能暴露给操作系统和任何虚拟化环境的状态。

3.3. TSC 与多插槽 / NUMA

多插槽系统,特别是大型多插槽系统,可能拥有独立的时钟源,而不是单一的、普遍分布的时钟。由于这些时钟由不同的晶体驱动,它们的频率不会完全匹配,并且温度和电气变化会导致CPU时钟(以及TSC)随时间漂移。根据具体的时钟和总线设计,漂移可能在绝对误差上是固定或不固定的,并且可能随时间累积。

此外,非常大型的系统可能会故意偏移单个核心的时钟。这种技术被称为扩频时钟,它在时钟频率及其谐波处降低了EMI,这可能是通过电信和计算机设备的FCC标准所必需的。

由于这些原因,不建议在NUMA或多插槽系统上信任TSC保持同步。

3.4. TSC 与 C-state

C-state,或处理器空闲状态,特别是C1E和更深度的睡眠状态,也可能对TSC造成问题。在这种状态下,TSC可能停止前进,导致在恢复执行时,其TSC落后于其他CPU。操作系统必须根据CPU和芯片组标识检测并标记此类CPU。

在这种情况下,TSC可以通过追赶已知外部时钟源来校正。

3.5. TSC 频率变化 / P-state

更值得关注的是,一些CPU可能会改变频率。它们可能以相同速率运行TSC,也可能不以相同速率运行;而且由于频率变化可能是错开或偏移的,在某些时间点,TSC速率可能只知道在一个值范围内,而不能确定具体值。在这种情况下,TSC将不是一个稳定的时间源,必须根据已知、稳定的外部时钟进行校准才能成为可用的时间源。

TSC是否以恒定速率运行或随P-state缩放取决于型号,必须通过检查CPUID、芯片组或供应商特定的MSR字段来确定。

此外,一些供应商存在已知错误,即在正常操作期间P-state实际上得到了正确补偿,但当处理器不活动时,P-state可能会暂时提高,以服务于其他处理器的缓存未命中。在这种情况下,停止的CPU上的TSC可能比未停止的处理器上的TSC前进得更快。AMD Turion处理器已知存在此问题。

3.6. TSC 与 STPCLK / T-state

给处理器的外部信号也可能导致TSC停止。这通常是为了热紧急电源控制以防止过热情况发生,并且通常无法检测到这种情况。

3.7. TSC 虚拟化 - VMX

VMX提供了对RDTSC、RDMSR、WRMSR和RDTSCP指令的条件捕获,这足以以任何方式完全虚拟化TSC。此外,VMX允许透传主机TSC以及VMCS中指定的额外TSC_OFFSET字段。必须使用特殊指令来读写VMCS字段。

3.8. TSC 虚拟化 - SVM

SVM提供了对RDTSC、RDMSR、WRMSR和RDTSCP指令的条件捕获,这足以以任何方式完全虚拟化TSC。此外,SVM允许透传主机TSC以及SVM控制块中指定的额外偏移字段。

3.9. Linux中的TSC特性位

总之,除非架构明确保证,否则无法保证TSC保持完美同步。即使如此,多插槽或NUMA系统中的TSC仍可能独立运行,尽管它们在本地是一致的。

Linux使用以下特性位来指示各种TSC属性,但它们仅对UP或单节点系统有意义。

X86_FEATURE_TSC

硬件中提供TSC

X86_FEATURE_RDTSCP

提供RDTSCP指令

X86_FEATURE_CONSTANT_TSC

TSC速率不随P-state变化

X86_FEATURE_NONSTOP_TSC

TSC在C-state下不停止

X86_FEATURE_TSC_RELIABLE

跳过TSC同步检查 (VMware)

4. 虚拟化问题

时间管理对于虚拟化来说尤其成问题,因为它带来了许多挑战。最明显的问题是时间现在由宿主机和(可能)多个虚拟机共享。因此,虚拟操作系统并未以100%的CPU利用率运行,尽管它很可能做了这样的假设。它可能期望在中断源禁用时仍能严格遵守精确的界限,但实际上只有其虚拟中断源被禁用,机器仍可能随时被抢占。这导致了问题,因为真实时间的流逝、机器中断的注入以及相关的时钟源不再与真实时间完全同步。

同样的问题在原生硬件上也可能在一定程度上发生,因为当BIOS使用SMM模式时,SMM模式可能会从X86系统(自然开启SMM)中窃取周期,但程度不会如此极端。然而,SMM模式可能导致与虚拟化类似的问题,这使得在裸机上解决其中许多问题有了很好的理由。

4.1. 中断计时

遗留操作系统最直接的问题之一是,系统时间管理例程通常通过计数周期性中断来跟踪时间。这些中断可能来自PIT或RTC,但问题是一样的:宿主机虚拟化引擎可能无法每秒提供正确数量的中断,因此客户机时间可能会落后。如果选择了高中断速率,例如1000 HZ(不幸的是,这是许多Linux客户机的默认设置),这尤其成问题。

解决这个问题有三种方法;首先,可能可以直接忽略它。具有独立时间源来跟踪“挂钟时间”或“实时”的客户机可能不需要调整其中断来维持正确的时间。如果这还不够,可能需要向客户机注入额外中断以提高有效中断速率。这种方法在极端条件下会导致复杂性,例如宿主机负载过高或客户机延迟过大而无法补偿,因此出现了另一种解决方案:客户机可能需要意识到丢失的节拍并进行内部补偿。尽管理论上很有前景,但此策略在Linux中的实现极易出错,并且许多带有bug的丢失节拍补偿变体分布在常用的Linux系统中。

Windows使用周期性RTC计时作为内部时间保持手段,因此需要中断偏移来保持正确时间。然而,它使用的速率足够低(编者注:是18.2 Hz吗?),以至于在实践中尚未成为问题。

4.2. TSC 采样与序列化

作为可用的最高精度时间源,CPU的周期计数器引起了开发人员的极大兴趣。如上所述,该定时器作为本地的、可能不稳定且可能不同步的源,具有许多独特的问题。一个并非TSC独有但因其高精度特性而突出的问题是采样延迟。根据定义,计数器一旦被读取就已过时。然而,计数器也可能在实际使用结果之前就被读取。这是指令流超标量执行的结果,指令可能乱序执行。这种执行被称为非序列化的。强制序列化执行对于使用TSC进行精确测量是必要的,并且需要一个序列化指令,例如CPUID或MSR读取。

由于CPUID实际上可能通过捕获和模拟机制进行虚拟化,因此这种序列化可能对硬件虚拟化造成性能问题。因此,准确的时间戳计数器读取可能并非总是可用,并且在实现中可能需要防范从其他CPU角度来看TSC的“倒退”读取,即使在其他方面完美同步的系统中也是如此。

4.3. Timespec 混叠

此外,TSC的这种非序列化特性在使用TSC结果与另一个时间源进行测量时带来了另一个挑战。由于TSC的精度更高,当另一个时钟仍表示相同值时,可能会读取到TSC的许多可能值。

也就是说,当外部时钟C保持相同值时,您可能会读取到(T, T+10)。由于非序列化读取,您实际可能得到一个波动的范围——从(T-1.. T+10)。因此,任何从TSC计算但根据外部值校准的时间可能具有一个有效值范围。重新校准此计算可能会导致校准后计算的时间相对于校准前计算的时间倒退。

这个问题在Linux内部时间源,即内核时间方面尤为突出,它以理论上的高分辨率timespec表示,但其前进是以大得多的粒度间隔进行的,有时以jiffies的速率,在追赶模式下可能以大得多的步长。

这种混叠需要在计算和重新校准kvmclock以及从TSC计算得出的任何其他值(例如TSC虚拟化本身)时特别小心。

4.4. 迁移

虚拟机的迁移在时间管理方面带来了两个问题。首先,迁移本身可能需要时间,在此期间无法传递中断,之后客户机时间可能需要追赶。NTP可能在一定程度上有所帮助,因为所需的时钟校正通常足够小,可以落在NTP可校正的窗口内。

另一个问题是,基于TSC(或HPET,如果原始总线时钟暴露)的定时器现在可能以不同的速率运行,需要在虚拟机监控程序中通过虚拟化这些定时器进行某种补偿。此外,迁移到更快的机器可能会排除使用直通TSC,因为更快的时钟如果对客户机可见,可能会导致时间比平时更快地前进。较慢的时钟问题较小,因为它总是可以追赶到原始速率。KVM时钟通过简单地存储相对于TSC的乘数和偏移量来避免这些问题,供客户机转换回纳秒分辨率值。

4.5. 调度

由于调度可能基于精确的计时和中断触发,操作系统的调度算法可能会受到虚拟化的不利影响。理论上,这种影响是随机的并且应该普遍分布,但在人为设计和真实场景(客户机设备访问、虚拟化退出原因、可能的上下文切换)中,情况可能并非总是如此。这种影响尚未得到充分研究。

为了解决这个问题,一些实现提供了半虚拟化调度器时钟,它揭示了虚拟机实际运行的CPU时间量。

4.6. 看门狗

看门狗定时器,例如Linux中的锁检测器,在硬件虚拟化下运行时可能会由于定时器中断延迟或对实时流逝的误解而意外触发。通常,这些警告是虚假的,可以忽略,但在某些情况下可能需要禁用此类检测。

4.7. 延迟与精确计时

精确计时和延迟在虚拟化系统中可能无法实现。如果系统控制物理硬件,或者为了补偿设备缓慢的I/O而引入延迟,就可能发生这种情况。第一个问题对于虚拟化系统通常无法解决;硬件控制软件在没有完整的实时操作系统的情况下无法充分虚拟化,这将需要一个RT感知的虚拟化平台。

第二个问题可能会导致性能问题,但这不太可能是一个重大问题。在许多情况下,这些延迟可以通过配置或半虚拟化来消除。

4.8. 隐蔽信道与信息泄露

除了上述问题,除了虚拟化时间的完美实现之外,时间信息将不可避免地泄露给客户机关于宿主机的信息。这可能允许客户机推断虚拟机监控程序的存在(如红丸检测),并且可能通过使用CPU利用率本身作为信令通道,导致信息在客户机之间泄露。防止此类问题将需要完全隔离的虚拟时间,这种时间可能不再跟踪实时。这在某些安全或QA环境中可能有用,但通常不推荐用于实际部署场景。