Linux(16)之Time Stamp Counter
Author:Once Day Date:2023年5月30日
参考文档:
- 4. Environment Abstraction Layer — Data Plane Development Kit 23.03.0 documentation (dpdk.org)
- DPDK: lib/eal/include/generic/rte_cycles.h File Reference
- 测量CPU的利器 - TSC (Time Stamp Counter) - 知乎 (zhihu.com)
- 正确使用cpu提供的TSC - 知乎 (zhihu.com)
- x86 TSC使用的那些坑 - 爱你一万年123 - 博客园 (cnblogs.com)
- Time Stamp Counter (TSC) 知识点 - 简书 (jianshu.com)
- 细说RDTSC的坑 – Dreamer Thinker Doer (wangkaixuan.tech)
- 调整开发板arm的cpu频率_armv8 cpuinfo pll_思而后行之的博客-CSDN博客
- Pitfalls of TSC usage | Oliver Yang
- timestamp - Getting TSC rate from x86 kernel - Stack Overflow
- linux - rdtsc accuracy across CPU cores - Stack Overflow
- Porting x86架构的rdtsc函数到ARM64架构的方法 - 极术社区 - 连接开发者与智能计算生态 (aijishu.com)
- Arm Architecture Reference Manual for A-profile architecture
- 华为鲲鹏云 KBengine arm64编译问题实践报告-云社区-华为云 (huaweicloud.com)
文章目录
- Linux(16)之Time Stamp Counter
- 1. 概述
- 2. DPDK的两种时间戳计时器(TSC,HPET)
- 3. 时间戳计数器(TSC,Time Stamp Counter)详细总结
- 4. TSC发展历史
- 5. TSC多核时钟深入分析
- 6. ARM架构的“TSC”时钟
- 7. 实际代码演示
- 7.1 x86_64架构DPDK获取TSC频率和cycle代码
- 7.2 ARM64架构获取TSC(System Counter)
1. 概述
DPDK(数据包开发处理包,Data Plane Development Kit)是一套用于快速处理数据包的库和驱动程序。TSC(时间戳计数器,Time Stamp Counter)是一个高精度计时器,用于在CPU内核上测量时间。TSC是一个64位的寄存器,每个CPU内核都有一个TSC,它在每个时钟周期内递增。
在不同的CPU核上,TSC的周期可能相同,也可能不同。这取决于以下几个因素:
-
同步TSC:现代处理器实现了TSC的同步,即在所有内核上同时启动和递增TSC。这意味着在这些处理器上,所有内核上的TSC周期应该是相同的。要确定处理器是否支持同步TSC,可以检查处理器的规格文档,或者查询CPUID指令的相关字段。
-
动态调节频率(如Intel SpeedStep,AMD Cool’n’Quiet等):某些处理器可以根据负载动态调整CPU频率。这可能导致不同内核上的TSC以不同的速度增加,因为每个内核的运行频率可能不同。在这种情况下,不同内核上的TSC周期可能不同。要解决这个问题,可以将处理器设置为固定频率运行,或者使用不受动态频率调整影响的计时器,如HPET(高精度事件计时器,High Precision Event Timer)。
-
多处理器系统(如多个物理CPU的服务器):在具有多个物理处理器的系统中,每个处理器都有自己的TSC。这可能导致不同处理器上的内核具有不同的TSC周期。可以尝试使用软件技术,如RDTSC(读取时间戳计数器指令,Read Time Stamp Counter instruction)或者使用更适合多处理器系统的计时器,如HPET来解决这个问题。
总之,不同CPU核上的TSC周期可能相同,也可能不同。确定TSC周期是否相同通常需要考虑处理器架构、动态频率调整技术和多处理器系统等因素。在需要精确计时的场景中,可以使用其他计时器,如HPET,以避免潜在的问题。
2. DPDK的两种时间戳计时器(TSC,HPET)
在DPDK中,时间戳计数器(TSC,Time Stamp Counter)和高精度事件计时器(HPET,High Precision Event Timer)是两种用于测量时间的方法。它们之间的使用关系可以从以下几个方面进行总结:
-
时间测量精度和性能
- TSC:TSC是高精度的计时器,它在每个时钟周期内递增。由于TSC读取速度快,延迟低,因此在性能要求较高的场景中,DPDK可能优先使用TSC作为计时器。
- HPET:HPET也是一种高精度计时器,但相对于TSC,它的读取速度较慢,延迟较高。然而,HPET在多处理器系统和动态频率调整场景下表现更加稳定,因此在这些情况下,DPDK可能会选择使用HPET作为计时器。
-
TSC和HPET的选择
- DPDK在启动时会自动检测并选择合适的计时器。首选TSC,因为它具有较高的性能。然而,如果检测到TSC在多核处理器、多处理器系统或动态频率调整场景下可能存在不稳定性,DPDK会退回到使用HPET作为计时器。
-
定时器API
- DPDK提供了通用的定时器API,这些API抽象了底层的计时器实现(如TSC和HPET),使得DPDK应用程序可以在不关心底层计时器类型的情况下进行时间测量和调度。这意味着DPDK应用程序开发者不需要直接处理TSC和HPET之间的使用关系,而可以通过DPDK提供的API来实现所需的计时功能。
总之,在DPDK中,TSC和HPET是两种用于测量时间的方法。它们之间的使用关系主要取决于性能需求和特定场景下的稳定性。DPDK会自动选择合适的计时器,应用程序开发者可以通过DPDK提供的通用定时器API来实现计时功能,而无需直接处理TSC和HPET之间的关系。
3. 时间戳计数器(TSC,Time Stamp Counter)详细总结
(1)优点:
- 高精度:TSC是一个高精度计数器,每个CPU内核上的TSC寄存器在每个时钟周期内递增,因此可以提供非常精确的时间测量。
- 低延迟:相比其他计时器(如HPET),TSC读取速度更快,延迟更低。
- 广泛支持:绝大多数现代处理器支持TSC,使其成为一种通用的计时解决方案。
(2)缺点:
- 同步问题:在多核处理器和多处理器系统中,不同CPU核上的TSC可能不完全同步,导致时间测量不一致。
- 动态频率调整:动态调节CPU频率(如Intel SpeedStep,AMD Cool’n’Quiet等)可能导致TSC以不同速度增加,影响精确度。
- 虚拟化环境:在虚拟化环境中,TSC的行为可能受到虚拟机监视器(hypervisor)的影响,导致不准确的时间测量。
(3)使用方法:
- 读取TSC:可以通过执行RDTSC(读取时间戳计数器)指令来读取当前TSC值。在C/C++中,可以使用内联汇编或者使用编译器提供的内建函数(如
__rdtsc()
)来读取TSC。 - 计算时间差:通过在程序的不同点读取TSC,可以计算两个时间点之间的时钟周期数。然后,将时钟周期数除以CPU频率(单位为Hz),可以得到时间差(单位为秒)。
(4)注意事项:
- 确保同步:在使用TSC之前,应检查处理器是否支持同步TSC。可以查询CPUID指令的相关字段或处理器规格文档来获取这些信息。
- 考虑动态频率调整:在受到动态频率调整影响的处理器上使用TSC时,应将处理器设置为固定频率运行,或使用不受动态频率调整影响的计时器(如HPET)。
- 多处理器系统:在多处理器系统中,应使用适用于多处理器系统的计时器(如HPET),或使用软件技术来解决不同处理器上TSC不同步的问题。
- 考虑到CPU乱序执行的问题,rdtsc需要配合cpuid或lfence指令,以保证计这一刻流水线已排空,即rdtsc要测量的指令已执行完。后来的CPU提供了rdtscp指令,相当于cpuid + rdtsc,但cpuid指令本身的执行周期有波动,而rdtscp指令的执行更稳定。
- 多核系统:新的CPU支持了Invariant TSC特性,可以保证在默认情况下各核心看到的TSC是一致的,否则测量代码执行时不能调度至其它核心上。
- 时序测量容易被干扰(线程调度、抢占、系统中断、虚拟化等),要求测量的指令序列尽量短,并且需要进行多次测量
(5)常见问题:
- TSC不同步:多核处理器和多处理器系统中,不同CPU核上的TSC可能不完全同步。可以尝试使用软件技术进行校正,或使用其他计时器(如HPET)。
- 动态频率调整:动态调节CPU频率可能导致TSC以不同速度增加。可以将处理器设置为固定频率运行,或使用不受动态频率调整影响的计时器(如HPET)。
- 虚拟化环境:虚拟机中的TSC可能受到虚拟机监视器(hypervisor)的影响。在虚拟化环境中,建议使用虚拟化友好的计时器,如虚拟机监视器提供的虚拟化时钟(如KVM中的kvm-clock)。
4. TSC发展历史
参考文档:
- 正确使用cpu提供的TSC - 知乎 (zhihu.com)
- Pitfalls of TSC usage | Oliver Yang
最早CPU提供的TSC有很多弊端:
- 频率受cpu频率影响,进入C-state的某些深度级别甚至会停止工作(不再跳动)
- SMP架构下core间不同步,意味着一个core上的tsc与其它core上不一样,并且跳变的频率也不同。
后来Intel进行了增强(在CPU特性标识里面可以查看):
- constant_tsc:含义是以固定的频率跳动,与cpu当前的频率无关。
- nonstop_tsc:进入C-State也不会停止跳动。
基于这2个特性组合,称为 invariant tsc,即tsc是以理想中的恒定频率跳动,符合对时钟的假设。
SMP架构下不同步的问题,有内核来进行判定:
- Linux 内核启动时,探测tsc是否同步,采用尝试校准多个核心上的tsc以相同的频率和起始值启动运行。
- 通过写入MSR寄存器值来设置tsc的特性,需要cpu支持,目前仅仅intel的cpu才可能被认为是多核同步的。
TSC的频率有以下的获取方式:
- 通过CPUID中的一些寄存器值来计算,较新的cpu可以。
- 通过读取MSR寄存器的值来计算,需要跟进不同的CPU model来读取不同的寄存器。
- 通过读取内核export的符号 tsc_khz
如下是读取内核符号:
bpftrace -e 'BEGIN { printf("%u\n", *kaddr("tsc_khz")); exit(); }'
此外,内核计算和调整后的tsc freqency和经过硬件寄存器计算出来的不一定相同,因为内核会进行calibrate。
5. TSC多核时钟深入分析
参考文档:
- 细说RDTSC的坑 – Dreamer Thinker Doer (wangkaixuan.tech)
- linux - rdtsc accuracy across CPU cores - Stack Overflow
- Porting x86架构的rdtsc函数到ARM64架构的方法 - 极术社区 - 连接开发者与智能计算生态 (aijishu.com)
- x86 TSC使用的那些坑 - 爱你一万年123 - 博客园 (cnblogs.com)
在同一处理器的多个核心之间,以及不同处理器的不同核心之间,rdtsc的结果是否是同步的呢?如果不同步,那么取时的结果就不能用来相互比较。
关于这点,Intel的官方手册没有明说,如下:
The time stamp counter in newer processors may support an enhancement, referred to as invariant TSC. Processor’s support for invariant TSC is indicated by CPUID.80000007H:EDX[8].
The invariant TSC will run at a constant rate in all ACPI P-, C-. and T-states. This is the architectural behavior moving forward. On processors with invariant TSC support, the OS may use the TSC for wall clock timer services (instead of ACPI or HPET timers). TSC reads are much more efficient and do not incur the overhead associated with a ring transition or access to a platform resource.
只是说TSC能够在CPU处于任何(电源)状态下都能保证以标称速率递增,并没有明确说明TSC能够在多核甚至多处理器的情况下保持同步。
在Linux内核启动时,对TSC(时间戳计数器,Time Stamp Counter)时钟进行处理的过程可分为以下几个步骤:
-
检测TSC特性:内核首先使用CPUID指令来检测CPU是否支持TSC。如果CPU支持TSC,内核将继续检查其他TSC相关特性,例如:
- 是否支持不变TSC(Invariant TSC):不变TSC在所有内核和处理器之间同步,且不受CPU频率和电源管理事件影响。
- 是否支持恒定TSC(Constant TSC):恒定TSC在所有内核和处理器之间同步,但可能受到CPU频率调整的影响。
-
校准TSC:为了将TSC时钟周期转换为实际时间,内核需要知道CPU的时钟频率。在启动期间,内核将校准TSC,以便将其与实际时间对齐。校准过程通常涉及在一定时间间隔内计算TSC增量,然后根据这些增量推断出CPU时钟频率。
-
选择时钟源:Linux内核支持多种时钟源,例如TSC、HPET(高精度事件计时器,High Precision Event Timer)和ACPI Power Management Timer。在启动时,内核将根据可用时钟源的精度和性能选择最佳时钟源。如果TSC具有恒定或不变特性,并且表现出良好的性能和精度,内核可能会将其设置为默认时钟源。
-
同步多处理器系统中的TSC:在多处理器系统中,内核需要确保所有处理器上的TSC是同步的。内核将使用特定的同步算法(如校准时钟偏移)来尽量确保不同处理器上的TSC值保持一致。然而,这种同步并非总是完美的,因此在多处理器系统中使用TSC时需要小心。
-
初始化调度时钟:在内核启动过程中,它还需要初始化调度时钟,该时钟用于内核调度器来决定何时运行进程和线程。如果TSC被选为默认时钟源,内核将使用TSC来初始化和维护调度时钟。
内核检查TSC是否同步代码如下(X86结构):
/*
* Make an educated guess if the TSC is trustworthy and synchronized
* over all CPUs.
*/
int unsynchronized_tsc(void)
{
if (!boot_cpu_has(X86_FEATURE_TSC) || tsc_unstable)
return 1;
#ifdef CONFIG_SMP
if (apic_is_clustered_box())
return 1;
#endif
if (boot_cpu_has(X86_FEATURE_CONSTANT_TSC))
return 0;
if (tsc_clocksource_reliable)
return 0;
/*
* Intel systems are normally all synchronized.
* Exceptions must mark TSC as unstable:
*/
if (boot_cpu_data.x86_vendor != X86_VENDOR_INTEL) {
/* assume multi socket systems are not synchronized: */
if (num_possible_cpus() > 1)
return 1;
}
return 0;
}
从这段代码可以获取下面的信息:
- 如果你的cpuinfo里有constant_tsc的flag,那么无论在同一CPU不同核心之间,还是在不同CPU的不同核心之间,TSC都是同步的,可以随便用。
- 如果你用的是Intel的CPU,但是cpuinfo里没有constant_tsc的flag,那么在同一处理器的不同核心之间,TSC仍然是同步的,但是不同CPU的不同核心之间不同步,尽量不要用。
- 在Intel CPU下还有一个注释“assume multi socket systems are not synchronized”,即在多处理器系统上,不同CPU(处理器、socket、NUMA节点)之间的TSC是不同步的。
Non-intel x86 platform has different stories. Current Linux kernel treats all non-intel SMP system as non-sync TSC system. See unsynchronized_tsc code in tsc.c. LKML also has the AMD documents.
非英特尔x86平台有不同的情况。当前Linux内核将所有非intel SMP系统视为非同步TSC系统。
6. ARM架构的“TSC”时钟
参考文档:
- Porting x86架构的rdtsc函数到ARM64架构的方法 - 极术社区 - 连接开发者与智能计算生态 (aijishu.com)
- Arm Architecture Reference Manual for A-profile architecture
- 华为鲲鹏云 KBengine arm64编译问题实践报告-云社区-华为云 (huaweicloud.com)
- 【ARMv8】通用定时器总结_arm system counter_从善若水的博客-CSDN博客
ARM64架构(也称为ARMv8-A架构)引入了一种称为系统计数器(System Counter)的新组件,用于提供一个单调递增的计时器,以便在ARM64系统中实现精确的时间测量和调度。系统计数器在ARM64架构中相当重要,因为它为操作系统和应用程序提供了一个稳定、可靠的计时器。
系统计数器具有以下主要特点:
-
64位单调递增计数器:系统计数器是一个64位宽度的寄存器,它在每个时钟周期内递增。由于它是单调递增的,因此不会受到任何系统事件的影响,如电源管理事件或处理器休眠状态。
-
全局同步:系统计数器在所有处理器核心和处理器之间保持全局同步。这意味着,在多处理器系统中,不需要额外的同步机制以确保计数器的一致性。
-
基于架构的访问:ARM64架构提供了一组指令,以便操作系统和应用程序可以直接访问系统计数器。这些指令包括:
CNTFRQ_EL0
:用于读取系统计数器的频率,以便将计数器值转换为实际时间。CNTPCT_EL0
:用于读取系统计数器的当前计数值。
-
异常级别访问控制:ARM64架构中的异常级别(EL)机制允许操作系统控制应用程序和其他操作系统组件对系统计数器的访问。例如,操作系统可以允许用户级别应用程序(在EL0运行)访问系统计数器,也可以将其限制在内核级别(在EL1或更高级别运行)。
-
虚拟化支持:ARM64架构还为虚拟化环境提供了系统计数器支持。在虚拟化环境中,宿主操作系统可以为每个虚拟机配置虚拟系统计数器,从而使它们能够使用类似于物理计数器的计时功能。
Linux内核在ARM64架构上使用该定时器来实现"TSC时钟":
u64 rdtsc(void)
{
u64 val;
/*
* According to ARM DDI 0487F.c, from Armv8.0 to Armv8.5 inclusive, the
* system counter is at least 56 bits wide; from Armv8.6, the counter
* must be 64 bits wide. So the system counter could be less than 64
* bits wide and it is attributed with the flag 'cap_user_time_short'
* is true.
*/
asm volatile("mrs %0, cntvct_el0" : "=r" (val));
return val;
}
system counter的精度一般不会超过100MHz,一般是达不到CPU cycle级别的精度。
因此还可以借助PMU系列寄存器中的PMCCNTR_EL0(需要内核开启使能),读取此寄存器就可以知道当前CPU已运行了多少cycle。
7. 实际代码演示
7.1 x86_64架构DPDK获取TSC频率和cycle代码
- timestamp - Getting TSC rate from x86 kernel - Stack Overflow
最简单的方法是通过dmesg
消息来获取:
onceday->~:# dmesg |grep tsc
[ 0.000001] tsc: Detected 2995.199 MHz processor
其频率也和lscpu
里面的BogoMIPS
有关,是其二分之一:
BogoMIPS: 5990.39
其次也可以通过代码来获取,下面代码源自dpdk
:
/* SPDX-License-Identifier: BSD-3-Clause
* Copyright(c) 2017 Intel Corporation
*/
#include <stdio.h>
#include <stdint.h>
#include <fcntl.h>
#include <unistd.h>
#include <cpuid.h>
static unsigned int rte_cpu_get_model(uint32_t fam_mod_step)
{
uint32_t family, model, ext_model;
family = (fam_mod_step >> 8) & 0xf;
model = (fam_mod_step >> 4) & 0xf;
if (family == 6 || family == 15) {
ext_model = (fam_mod_step >> 16) & 0xf;
model += (ext_model << 4);
}
return model;
}
static int32_t rdmsr(int msr, uint64_t *val)
{
int fd;
int ret;
fd = open("/dev/cpu/0/msr", O_RDONLY);
if (fd < 0)
return fd;
ret = pread(fd, val, sizeof(uint64_t), msr);
close(fd);
return ret;
}
static uint32_t check_model_wsm_nhm(uint8_t model)
{
switch (model) {
/* Westmere */
case 0x25:
case 0x2C:
case 0x2F:
/* Nehalem */
case 0x1E:
case 0x1F:
case 0x1A:
case 0x2E:
return 1;
}
return 0;
}
static uint32_t check_model_gdm_dnv(uint8_t model)
{
switch (model) {
/* Goldmont */
case 0x5C:
/* Denverton */
case 0x5F:
return 1;
}
return 0;
}
uint64_t get_tsc_freq_arch(void)
{
uint64_t tsc_hz = 0;
uint32_t a, b, c, d, maxleaf;
uint8_t mult, model;
int32_t ret;
/*
* Time Stamp Counter and Nominal Core Crystal Clock
* Information Leaf
*/
maxleaf = __get_cpuid_max(0, NULL);
printf("maxleaf: %d\n", maxleaf);
if (maxleaf >= 0x15) {
__cpuid(0x15, a, b, c, d);
/* EBX : TSC/Crystal ratio, ECX : Crystal Hz */
if (b && c)
return c * (b / a);
}
__cpuid(0x1, a, b, c, d);
model = rte_cpu_get_model(a);
printf("model: %d\n", model);
if (check_model_wsm_nhm(model))
mult = 133;
else if ((c & bit_AVX) || check_model_gdm_dnv(model))
mult = 100;
else
return 0;
printf("mult: %d\n", mult);
ret = rdmsr(0xCE, &tsc_hz);
if (ret < 0)
return 0;
return ((tsc_hz >> 8) & 0xff) * mult * 1E6;
}
/** C extension macro for environments lacking C11 features. */
#if !defined(__STDC_VERSION__) || __STDC_VERSION__ < 201112L
#define RTE_STD_C11 __extension__
#else
#define RTE_STD_C11
#endif
uint64_t rte_rdtsc(void)
{
union {
uint64_t tsc_64;
// RTE_STD_C11
struct {
uint32_t lo_32;
uint32_t hi_32;
};
} tsc;
#ifdef RTE_LIBRTE_EAL_VMWARE_TSC_MAP_SUPPORT
if (unlikely(rte_cycles_vmware_tsc_map)) {
/* ecx = 0x10000 corresponds to the physical TSC for VMware */
asm volatile("rdpmc" : "=a"(tsc.lo_32), "=d"(tsc.hi_32) : "c"(0x10000));
return tsc.tsc_64;
}
#endif
asm volatile("rdtsc" : "=a"(tsc.lo_32), "=d"(tsc.hi_32));
return tsc.tsc_64;
}
uint64_t rte_get_tsc_cycles(void)
{
return rte_rdtsc();
}
/**
* Macro to align a value to the multiple of given value. The resultant
* value will be of the same type as the first parameter and will be no lower
* than the first parameter.
*/
#define RTE_ALIGN_MUL_CEIL(v, mul) \
((((v) + (typeof(v))(mul)-1) / ((typeof(v))(mul))) * (typeof(v))(mul))
/**
* Macro to align a value to the multiple of given value. The resultant
* value will be of the same type as the first parameter and will be no higher
* than the first parameter.
*/
#define RTE_ALIGN_MUL_FLOOR(v, mul) (((v) / ((typeof(v))(mul))) * (typeof(v))(mul))
/**
* Macro to align value to the nearest multiple of the given value.
* The resultant value might be greater than or less than the first parameter
* whichever difference is the lowest.
*/
#define RTE_ALIGN_MUL_NEAR(v, mul) \
({ \
typeof(v) ceil = RTE_ALIGN_MUL_CEIL(v, mul); \
typeof(v) floor = RTE_ALIGN_MUL_FLOOR(v, mul); \
(ceil - (v)) > ((v)-floor) ? floor : ceil; \
})
#define CYC_PER_10MHZ 1E7
int main(void)
{
uint64_t start, end, mhz;
uint64_t tsc_hz = get_tsc_freq_arch();
printf("tsc_hz: %lu\n", tsc_hz);
start = rte_get_tsc_cycles();
sleep(1);
end = rte_get_tsc_cycles();
printf("start_clock: %lu\n", start);
printf("end_clock: %lu\n", end);
printf("diff_clock: %lu\n", end - start);
/* Round up to 10Mhz. 1E7 ~ 10Mhz */
mhz = (end - start);
mhz = RTE_ALIGN_MUL_NEAR(mhz, CYC_PER_10MHZ);
printf("mhz: %lu Mhz\n", (uint64_t)(mhz/1E6));
return 0;
}
编译运行后,输出如下(获取频率失败,只能走手动测量):
ubuntu->performance:$ ./tsc.out
maxleaf: 13
model: 94
mult: 100
tsc_hz: 0
start_clock: 30482663960076192
end_clock: 30482687903183604
diff_clock: 23943107412
mhz: 2390 Mhz
7.2 ARM64架构获取TSC(System Counter)
获取cycle值和频率比较方便,直接使用汇编读取其值。
/** Read generic counter frequency */
static uint64_t __rte_arm64_cntfrq(void)
{
uint64_t freq;
asm volatile("mrs %0, cntfrq_el0" : "=r"(freq));
return freq;
}
/** Read generic counter */
static uint64_t __rte_arm64_cntvct(void)
{
uint64_t tsc;
asm volatile("mrs %0, cntvct_el0" : "=r"(tsc));
return tsc;
}
d_clock: 30482687903183604
diff_clock: 23943107412
mhz: 2390 Mhz