在x86内核中获取TSC速率

Pau*_*cco 5 linux-kernel tsc

我有一个在Atom上运行的嵌入式Linux系统,这是一个足够新的CPU,具有不变的TSC(时间戳记计数器),内核在启动时测量其频率。我在自己的代码中使用TSC来节省时间(避免内核调用),而我的启动代码会测量TSC速率,但我只想使用内核的度量值。有什么办法可以从内核中检索到它吗?它不在/ proc / cpuinfo中。

max*_*zig 8

跟踪

作为 root,您可以使用 bpftrace 检索内核的 TSC 速率:

# bpftrace -e 'BEGIN { printf("%u\n", *kaddr("tsc_khz")); exit(); }' | tail -n
Run Code Online (Sandbox Code Playgroud)

(在 CentOS 7 和 Fedora 29 上测试过)

这是在arch/x86/kernel/tsc.c 中定义、导出和维护/校准的

广交会

或者,作为 root,您也可以从 读取它/proc/kcore,例如:

# gdb /dev/null /proc/kcore -ex 'x/uw 0x'$(grep '\<tsc_khz\>' /proc/kallsyms \
    | cut -d' ' -f1) -batch 2>/dev/null | tail -n 1 | cut -f2
Run Code Online (Sandbox Code Playgroud)

(在 CentOS 7 和 Fedora 29 上测试过)

系统抽头

如果系统没有 bpftrace 或 gdb 可用但 SystemTap 你可以这样得到它(作为 root):

# cat tsc_khz.stp 
#!/usr/bin/stap -g

function get_tsc_khz() %{ /* pure */
    THIS->__retvalue = tsc_khz;
%}
probe oneshot {
    printf("%u\n", get_tsc_khz());
}
# ./tsc_khz.stp
Run Code Online (Sandbox Code Playgroud)

当然,您也可以编写一个小的内核模块,tsc_khz通过/sys伪文件系统提供访问。更好的是,有人已经这样做了,并且tsc_freq_khz 模块在 GitHub 上可用。有了这个,以下应该工作:

# modprobe tsc_freq_khz
$ cat /sys/devices/system/cpu/cpu0/tsc_freq_khz
Run Code Online (Sandbox Code Playgroud)

(在 Fedora 29 上测试,读取 sysfs 文件不需要 root)

内核消息

如果以上都不是一个选项,您可以从内核日志中解析 TSC 速率。但这很快就会变得丑陋,因为您会在不同的硬件和内核上看到不同类型的消息,例如在 Fedora 29 i7 系统上:

$ journalctl --boot | grep 'kernel: tsc:' -i | cut -d' ' -f5-
kernel: tsc: Detected 2800.000 MHz processor
kernel: tsc: Detected 2808.000 MHz TSC
Run Code Online (Sandbox Code Playgroud)

但是在 Fedora 29 Intel Atom 上:

kernel: tsc: Detected 2200.000 MHz processor
Run Code Online (Sandbox Code Playgroud)

在 CentOS 7 i5 系统上:

kernel: tsc: Fast TSC calibration using PIT
kernel: tsc: Detected 1895.542 MHz processor
kernel: tsc: Refined TSC clocksource calibration: 1895.614 MHz
Run Code Online (Sandbox Code Playgroud)

性能值

Linux 内核尚未提供用于读取 TSC 速率的 API。但它确实提供了一种用于获取可用于将 TSC 计数转换为纳秒的multshift值。这些值来自tsc_khz- 也在arch/x86/kernel/tsc.c 中-tsc_khz初始化和校准的地方。它们与用户空间共享。

使用 perf API 并访问共享页面的示例程序:

#include <asm/unistd.h>
#include <inttypes.h>
#include <linux/perf_event.h>
#include <stdio.h>
#include <sys/mman.h>
#include <unistd.h>

static long perf_event_open(struct perf_event_attr *hw_event, pid_t pid,
           int cpu, int group_fd, unsigned long flags)
{
    return syscall(__NR_perf_event_open, hw_event, pid, cpu, group_fd, flags);
}
Run Code Online (Sandbox Code Playgroud)

实际代码:

int main(int argc, char **argv)
{
    struct perf_event_attr pe = {
        .type = PERF_TYPE_HARDWARE,
        .size = sizeof(struct perf_event_attr),
        .config = PERF_COUNT_HW_INSTRUCTIONS,
        .disabled = 1,
        .exclude_kernel = 1,
        .exclude_hv = 1
    };
    int fd = perf_event_open(&pe, 0, -1, -1, 0);
    if (fd == -1) {
        perror("perf_event_open failed");
        return 1;
    }
    void *addr = mmap(NULL, 4*1024, PROT_READ, MAP_SHARED, fd, 0);
    if (!addr) {
        perror("mmap failed");
        return 1;
    }
    struct perf_event_mmap_page *pc = addr;
    if (pc->cap_user_time != 1) {
        fprintf(stderr, "Perf system doesn't support user time\n");
        return 1;
    }
    printf("%16s   %5s\n", "mult", "shift");
    printf("%16" PRIu32 "   %5" PRIu16 "\n", pc->time_mult, pc->time_shift);
    close(fd);
}
Run Code Online (Sandbox Code Playgroud)

在 Fedora 29 上进行了测试,它也适用于非 root 用户。

这些值可用于通过如下函数将 TSC 计数转换为纳秒:

static uint64_t mul_u64_u32_shr(uint64_t cyc, uint32_t mult, uint32_t shift)
{
    __uint128_t x = cyc;
    x *= mult;
    x >>= shift;
    return x;
}
Run Code Online (Sandbox Code Playgroud)

CPUID/MSR

另一种获得 TSC 率的方法是跟随 DPDK 的领先优势

x86_64 上的 DPDK 基本上使用以下策略:

  1. 通过 cpuid 内在函数(不需要特殊权限)读取“时间戳计数器和标称核心晶体时钟信息叶”(如果可用)
  2. 如果可能,从 MSR 读取它(需要rawio 功能和读取权限/dev/cpu/*/msr
  3. 通过其他方式在用户空间校准它,否则

FWIW,快速测试表明cpuid 叶似乎没有那么广泛可用,例如i7 Skylake 和goldmont atom 没有它。否则,从 DPDK 代码可以看出,使用 MSR 需要一堆复杂的大小写区别。

但是,如果程序已经使用 DPDK,获取 TSC 速率、获取 TSC 值或转换 TSC 值只是使用正确的 DPDK API 的问题。


Cra*_*tey 2

TSC 速率与 中的“cpu MHz”直接相关/proc/cpuinfo。实际上,更好的数字是“bogomips”。原因是,虽然 TSC 的频率是最大 CPU 频率,但当前的“cpu Mhz”可能会在调用时发生变化。

bogomips 值是在启动时计算的。您需要根据核心数量和处理器数量(即超线程数量)调整该值,这将为您提供[小数] MHz。这就是我用来做你想做的事情的方法。

要获取处理器计数,请查找最后一个“processor:”行。处理器数量为<value> + 1。称之为“cpu_count”。

要获取核心数量,任何“cpu cores:”都可以。核心数为<value>。称之为“core_count”。

所以,公式是:

smt_count = cpu_count;
if (core_count)
    smt_count /= core_count;
cpu_freq_in_khz = (bogomips * scale_factor) / smt_count;
Run Code Online (Sandbox Code Playgroud)

这是从我的实际代码中提取的,如下所示。


这是我使用的实际代码。您将无法直接使用它,因为它依赖于我拥有的样板,但它应该给您一些想法,特别是如何计算

// syslgx/tvtsc -- system time routines (RDTSC)

#include <tgb.h>
#include <zprt.h>

tgb_t systvinit_tgb[] = {
    { .tgb_val = 1, .tgb_tag = "cpu_mhz" },
    { .tgb_val = 2, .tgb_tag = "bogomips" },
    { .tgb_val = 3, .tgb_tag = "processor" },
    { .tgb_val = 4, .tgb_tag = "cpu_cores" },
    { .tgb_val = 5, .tgb_tag = "clflush_size" },
    { .tgb_val = 6, .tgb_tag = "cache_alignment" },
    TGBEOT
};

// _systvinit -- get CPU speed
static void
_systvinit(void)
{
    const char *file;
    const char *dlm;
    XFIL *xfsrc;
    int matchflg;
    char *cp;
    char *cur;
    char *rhs;
    char lhs[1000];
    tgb_pc tgb;
    syskhz_t khzcpu;
    syskhz_t khzbogo;
    syskhz_t khzcur;
    sysmpi_p mpi;

    file = "/proc/cpuinfo";

    xfsrc = fopen(file,"r");
    if (xfsrc == NULL)
        sysfault("systvinit: unable to open '%s' -- %s\n",file,xstrerror());

    dlm = " \t";

    khzcpu = 0;
    khzbogo = 0;

    mpi = &SYS->sys_cpucnt;
    SYSZAPME(mpi);

    // (1) look for "cpu MHz : 3192.515" (preferred)
    // (2) look for "bogomips : 3192.51" (alternate)
    // FIXME/CAE -- on machines with speed-step, bogomips may be preferred (or
    // disable it)
    while (1) {
        cp = fgets(lhs,sizeof(lhs),xfsrc);
        if (cp == NULL)
            break;

        // strip newline
        cp = strchr(lhs,'\n');
        if (cp != NULL)
            *cp = 0;

        // look for symbol value divider
        cp = strchr(lhs,':');
        if (cp == NULL)
            continue;

        // split symbol and value
        *cp = 0;
        rhs = cp + 1;

        // strip trailing whitespace from symbol
        for (cp -= 1;  cp >= lhs;  --cp) {
            if (! XCTWHITE(*cp))
                break;
            *cp = 0;
        }

        // convert "foo bar" into "foo_bar"
        for (cp = lhs;  *cp != 0;  ++cp) {
            if (XCTWHITE(*cp))
                *cp = '_';
        }

        // match on interesting data
        matchflg = 0;
        for (tgb = systvinit_tgb;  TGBMORE(tgb);  ++tgb) {
            if (strcasecmp(lhs,tgb->tgb_tag) == 0) {
                matchflg = tgb->tgb_val;
                break;
            }
        }
        if (! matchflg)
            continue;

        // look for the value
        cp = strtok_r(rhs,dlm,&cur);
        if (cp == NULL)
            continue;

        zprt(ZPXHOWSETUP,"_systvinit: GRAB/%d lhs='%s' cp='%s'\n",
            matchflg,lhs,cp);

        // process the value
        // NOTE: because of Intel's speed step, take the highest cpu speed
        switch (matchflg) {
        case 1:  // genuine CPU speed
            khzcur = _systvinitkhz(cp);
            if (khzcur > khzcpu)
                khzcpu = khzcur;
            break;

        case 2:  // the consolation prize
            khzcur = _systvinitkhz(cp);

            // we've seen some "wild" values
            if (khzcur > 10000000)
                break;

            if (khzcur > khzbogo)
                khzbogo = khzcur;
            break;

        case 3:  // remember # of cpu's so we can adjust bogomips
            mpi->mpi_cpucnt = atoi(cp);
            mpi->mpi_cpucnt += 1;
            break;

        case 4:  // remember # of cpu cores so we can adjust bogomips
            mpi->mpi_corecnt = atoi(cp);
            break;

        case 5:  // cache flush size
            mpi->mpi_cshflush = atoi(cp);
            break;

        case 6:  // cache alignment
            mpi->mpi_cshalign = atoi(cp);
            break;
        }
    }

    fclose(xfsrc);

    // we want to know the number of hyperthreads
    mpi->mpi_smtcnt = mpi->mpi_cpucnt;
    if (mpi->mpi_corecnt)
        mpi->mpi_smtcnt /= mpi->mpi_corecnt;

    zprt(ZPXHOWSETUP,"_systvinit: FINAL khzcpu=%d khzbogo=%d mpi_cpucnt=%d mpi_corecnt=%d mpi_smtcnt=%d mpi_cshalign=%d mpi_cshflush=%d\n",
        khzcpu,khzbogo,mpi->mpi_cpucnt,mpi->mpi_corecnt,mpi->mpi_smtcnt,
        mpi->mpi_cshalign,mpi->mpi_cshflush);

    if ((mpi->mpi_cshalign == 0) || (mpi->mpi_cshflush == 0))
        sysfault("_systvinit: cache parameter fault\n");

    do {
        // use the best reference
        // FIXME/CAE -- with speed step, bogomips is better
#if 0
        if (khzcpu != 0)
            break;
#endif

        khzcpu = khzbogo;
        if (mpi->mpi_smtcnt)
            khzcpu /= mpi->mpi_smtcnt;
        if (khzcpu != 0)
            break;

        sysfault("_systvinit: unable to obtain cpu speed\n");
    } while (0);

    systvkhz(khzcpu);

    zprt(ZPXHOWSETUP,"_systvinit: EXIT\n");
}

// _systvinitkhz -- decode value
// RETURNS: CPU freq in khz
static syskhz_t
_systvinitkhz(char *str)
{
    char *src;
    char *dst;
    int rhscnt;
    char bf[100];
    syskhz_t khz;

    zprt(ZPXHOWSETUP,"_systvinitkhz: ENTER str='%s'\n",str);

    dst = bf;
    src = str;

    // get lhs of lhs.rhs
    for (;  *src != 0;  ++src, ++dst) {
        if (*src == '.')
            break;
        *dst = *src;
    }

    // skip over the dot
    ++src;

    // get rhs of lhs.rhs and determine how many rhs digits we have
    rhscnt = 0;
    for (;  *src != 0;  ++src, ++dst, ++rhscnt)
        *dst = *src;

    *dst = 0;

    khz = atol(bf);
    zprt(ZPXHOWSETUP,"_systvinitkhz: PRESCALE bf='%s' khz=%d rhscnt=%d\n",
        bf,khz,rhscnt);

    // scale down (e.g. we got xxxx.yyyy)
    for (;  rhscnt > 3;  --rhscnt)
        khz /= 10;

    // scale up (e.g. we got xxxx.yy--bogomips does this)
    for (;  rhscnt < 3;  ++rhscnt)
        khz *= 10;

    zprt(ZPXHOWSETUP,"_systvinitkhz: EXIT khz=%d\n",khz);

    return khz;
}
Run Code Online (Sandbox Code Playgroud)

更新:

叹。是的。

我使用的是“cpu MHz”/proc/cpuinfo在引入具有“速度步进”技术的处理器之前,当我导出它时,我只能访问超线程机器。然而,我发现一个旧的不是,并且 SMT 东西无效。

然而,bogomips 似乎始终是[最大] CPU 速度的 2 倍。请参阅http://www.clifton.nl/bogo-faq.html多年来,这并不总是我在所有内核版本上的经验 [IIRC,我从 0.99.x 开始],但现在这可能是一个可靠的假设。

constant_tsc对于“恒定 TSC”(所有较新的处理器都具有)(在flags:中的字段中表示)/proc/cpuinfo,TSC 速率是最大 CPU 频率。

最初,获取频率信息的唯一方法是从/proc/cpuinfo. 然而,现在,在更现代的内核中,有另一种方法可能更容易、更明确[我在我的其他软件中对此进行了代码覆盖,但已经忘记了]:

/sys/devices/system/cpu/cpu0/cpufreq/cpuinfo_max_freq
Run Code Online (Sandbox Code Playgroud)

该文件的内容是最大 CPU 频率(以 kHz 为单位)。其他 CPU 内核也有类似的文件。对于大多数正常的主板来说,这些文件应该是相同的(例如,由相同型号芯片组成的主板,并且不要尝试混合[比如说] i7s 和atoms)。否则,您必须跟踪每个核心的信息,这会很快变得混乱。

给定的目录还有其他有趣的文件。例如,如果您的处理器具有“速度步进”[并且其他一些文件可以告诉您这一点],您可以通过写入performancescaling_governor文件来强制实现最大性能。这将禁用速度步的使用。

如果处理器没有constant_tsc,您必须禁用速度步进[并以最大速率运行内核]才能获得准确的测量结果