x86平台的TSC(TIME-STAMP COUNTER)

今天跟着Intel的开发手册,看看如何随着Intel对TSC不断的修改和增加新特性,让TSC从一个简单的性能计数器发展成当前Linux上x86平台最重要的时钟源之一。本文基本上可以看作是Intel® 64 and IA-32 Architectures Software Developer’s Manual Volume 3B: System Programming Guide, Part 217.15 TIME-STAMP COUNTER这章的翻译和总结。

在x86平台上,Linux系统里最常用的一个时钟源就是tsc,具体的,可以通过命令查看当前的时钟源和系统里可用的时钟源:

# cat /sys/devices/system/clocksource/clocksource0/available_clocksource
tsc hpet acpi_pm
# cat /sys/devices/system/clocksource/clocksource0/current_clocksource
tsc

那么TSC是个什么东西呢?我们可以跟着手册看一看。

TIME-STAMP COUNTER

从 Pentium 处理器开始,Intel 64 和 IA-32 架构定义了一种时间戳计数器机制(TSC),可以用于监控和识别处理器事件的相对时间。TSC包括以下组件:

- TSC flag:用于标识TSC是否可用,当`CPUID.1:EDX.TSC[bit 4] = 1`时,TSC可用
- IA32_TIME_STAMP_COUNTER MSR:对应TSC值的寄存器
- RDTSC指令:读取TSC寄存器值的指令
- TSD flag: 用于开关TSC功能,`CR4.TSD[bit 2] = 1`时开启TSC

TSC从第一次在CPU中实现到如今很多年了,所以不同CPU上的一些表现还是有些区别的,但主要的特点是通用的,首先TSC是一个64bit大小的计数器,当CPU重置时,这个计数器也会被重置成0,重置后,即使CPU因为执行了HLT指令进入idle状态,这个寄存器也会持续的增加。

但是TSC会按什么样的频率增加呢?这里不同的CPU差异就比较大了。在一些比较老的CPU上(大概是07-08年之前),TSC是跟着CPU的运行频率增加的,比如当前CPU跑在2.4GHz,那TSC就每秒增加24000000,但是针对这些老的CPU,超频、以及CPU自身的睿频都会对TSC的计数产生影响。这其实对用户是不太友好的,毕竟睿频是硬件层面的,这个计数器就不那么“稳定”了。

所以后续的新CPU,Intel将这个行为修改成了以固定频率增加,只是这个频率具体是多少得看具体的配置,具体的TSC频率如何检测,手册里有专门的一章进行解释,这里我们不过多涉及。

在新CPU里,TSC以固定频率增长,所以这其实是一个TSC的特性,叫做Constant TSC,有了这个特性之后,TSC频率相对就稳定了,不会随着CPU睿频而随意改变频率,所以他也就可以成为一个稳定的时钟源了。因此这个特性会一直在后续的CPU中提供。

用户可以使用RDTSC这个CPU指令获取TSC的值,正常情况下(计数器没有溢出),Intel的CPU可以保证每次通过RDTSC读取的值都是单调递增的,同时可以保证在10年内计数器不会溢出。但是需要注意的是,这个指令它不是有序的(也就说他是有可能被CPU乱序执行的,所以可能需要加上memory barrier)。另外由于TSC是一个MSR,所以其实这个寄存器是可以通过RDMSRWRMSR指令进行读写的,只是一些老CPU上只能写低32位(高32位此时会清0)。

Invariant TSC

前面提到TSC即使CPU处在halt状态,依然会持续的累加,但即使是这样,TSC依然不是100%可依赖最为一个时钟源的,为什么呢?因为halt状态只是CPU的C1状态,现代的CPU为了省电,引入了更多更深的C states,具体可以参考之前关于电源管理的内容再谈CPU的电源管理(如何做到稳定全核睿频?),当CPU进入到比较深的C states,比如C6,此时整个CPU的Core基本都被关闭了,那TSC自然也有可能不工作了。

为了解决这个问题呢,Intel又引入了一个新的特性增强,叫Invariant TSC,这个特性可以当CPUID.80000007H:EDX[8] == 1时,代表CPU支持这个特性。有这个特性的CPU,在任何的C states下,TSC都会持续运行,在Linux里,这个特性也会被称为NONSTOP_TSC(感觉这个更传神一些,不停止的TSC)。

可以看到引入Constant TSCInvariant TSC之后,CPU先后解决了P-States(CPU频率变化)和C-States(CPU电源状态)对TSC频率的影响,逐渐将TSC设计成符合时钟源要求的样子,这也算是软硬件协同发展,螺旋上升的正面例子吧。

IA32_TSC_AUX Register and RDTSCP Support

看起来到这里已经万事大吉了?其实并没有,前面提到,RDTSC指令并不是有序的,意味着CPU硬件有可能对这个执行乱序执行,这可能并不是软件所预期的结果,举个例子,假如想通过基于类似RDTSC;other insturctions;RDTSC这样的指令顺序来计算other insturctions消耗的时间,在真正执行的时候,有可能就按RDTSC;RDTSCother insturctions的顺序了,这显然不是所期望的结果。

所以Intel针对这个问题,又增加了一个新的指令RDTSCP,使得可以原子地读取TSC。当执行RDTSCP指令时,会同时读取TSC和IA32_TSC_AUX两个寄存器的值。并且这个操作是原子的,不会出现上下文切换的问题。不过需要注意的是,只有当CPUID.80000001H:EDX[27] == 1时,CPU才支持RDTSCP指令。

针对这个乱序的问题,其实Linux内核里也是做了相应的处理的,我们可以从内核读取TSC的源码看出来,源码里的注释也是非常的详细:

static __always_inline unsigned long long rdtsc_ordered(void)
{
	DECLARE_ARGS(val, low, high);

	/*
	 * The RDTSC instruction is not ordered relative to memory
	 * access.  The Intel SDM and the AMD APM are both vague on this
	 * point, but empirically an RDTSC instruction can be
	 * speculatively executed before prior loads.  An RDTSC
	 * immediately after an appropriate barrier appears to be
	 * ordered as a normal load, that is, it provides the same
	 * ordering guarantees as reading from a global memory location
	 * that some other imaginary CPU is updating continuously with a
	 * time stamp.
	 *
	 * Thus, use the preferred barrier on the respective CPU, aiming for
	 * RDTSCP as the default.
	 */
     //优先使用rdtscp,如果不支持,先执行lfence再执行rdtsc
	asm volatile(ALTERNATIVE_2("rdtsc",
				   "lfence; rdtsc", X86_FEATURE_LFENCE_RDTSC,
				   "rdtscp", X86_FEATURE_RDTSCP)
			: EAX_EDX_RET(val, low, high)
			/* RDTSCP clobbers ECX with MSR_TSC_AUX. */
			:: "ecx");

	return EAX_EDX_VAL(val, low, high);
}

Time-Stamp Counter Adjustment

最后的最后,还有一个问题需要解决,上面其实也提到了,TSC本质上是个MSR(IA32_TIME_STAMP_COUNTER MSR 地址10H),而这个MSR是可写的!这会存在一个问题,对于现代的多核系统,每个核都有自己的TSC MSR,如果某个核的MSR被修改了,这个修改怎么同步到其他核上去呢?很显然,不管是想计算出来被修改的核心的TSC的变化值,以及将这个值同步到其他的核上,都是不现实的。因为没办法在同一时刻在所有核上执行相同的指令。

但是多核之间同步TSC需求又是客观存在的,怎么办呢?Intel提供了一个新的MSR:IA32_TSC_ADJUST(地址3BH)来解决这个问题。首先和TSC一样每个核都有自己独立的IA32_TSC_ADJUST,当处理器重置时,IA32_TSC_ADJUST也会被置为0,当对IA32_TIME_STAMP_COUNTER进行写入时,比如加上(或者)一个X的值,那么对应核的IA32_TSC_ADJUST也会有对应的X值被加上(或者减去)。因此有了这个MSR之后,想计算某个核TSC的变化值,直接读取IA32_TSC_ADJUST里的值就行了,如果要把这个值同步到其他的核,就只需要把这个值写入到其他核的IA32_TSC_ADJUST里就行了。这就直接解决了多核之间TSC同步的问题,不过这个特性也不是所有CPU都支持,只有当CPUID.(EAX=07H, ECX=0H):EBX.TSC_ADJUST == 1时才支持。

到这里,TSC就变得真正可依赖了,首先有了Constant TSC,确保TSC按固定频率运行,然后有了Invariant TSC确保TSC一直运行,还有IA32_TSC_ADJUST确保当TSC被修改后依然能被同步回来。有了这些特性,TSC就可以成为系统中可信赖的时钟源。我们也可以通过Linux内核里的代码,看看内核是如何针对这种场景进行适配的:

static void __init check_system_tsc_reliable(void)
{
    // ...
	/*
	 * Disable the clocksource watchdog when the system has:
	 *  - TSC running at constant frequency
	 *  - TSC which does not stop in C-States
	 *  - the TSC_ADJUST register which allows to detect even minimal
	 *    modifications
	 *  - not more than two sockets. As the number of sockets cannot be
	 *    evaluated at the early boot stage where this has to be
	 *    invoked, check the number of online memory nodes as a
	 *    fallback solution which is an reasonable estimate.
	 */
    // 默认情况下Kernel假设TSC不稳定,所以会有个watchdog进行检测,当满足下面几个条件时,TSC足够稳定,watchdog也不需要运行了。
	if (boot_cpu_has(X86_FEATURE_CONSTANT_TSC) &&
	    boot_cpu_has(X86_FEATURE_NONSTOP_TSC) &&
	    boot_cpu_has(X86_FEATURE_TSC_ADJUST) &&
	    nr_online_nodes <= 4)
		tsc_disable_clocksource_watchdog();
}

总结

好了,跟着文档的节奏,其实也可以看到TSC的发展历程,硬件不断的做出一些变化从而满足软件层面的需求,确定经历了相当长的时间。其实除此之外,TSC还有一些其他相关的特性,主要是和虚拟化相关,也是硬件为了更好的实现虚拟化做出的适配,这里就暂时不说了,期望下次可以继续聊聊虚拟化场景下TSC的一些特性。