Получение скорости TSC в ядре x86

У меня есть встроенная система Linux, работающая на Atom, который является достаточно новым процессором, чтобы иметь инвариантный TSC (счетчик меток времени), частоту которого ядро ​​измеряет при запуске. Я использую TSC в своем собственном коде, чтобы сохранить время (избегая вызовов ядра), и мой код запуска измеряет скорость TSC, но я бы просто использовал измерения ядра. Есть ли способ получить это из ядра? Его нет ни в /proc/cpuinfo.

2 ответа

BPFtrace

Как root вы можете получить скорость TSC ядра с помощью bpftrace:

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

(тестировал на CentOS 7 и Fedora 29)

То есть значение, которое определенно, экспортируется и поддерживается / калибровано в арке / x86 / ядро / tsc.c.

GDB

В качестве альтернативы вы также можете прочитать его как 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

(тестировал на CentOS 7 и Fedora 29)

SystemTap

Если в системе нет доступных 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

Конечно, вы также можете написать небольшой модуль ядра, который обеспечивает доступ к tsc_khz через /sysпсевдо файловая система. Более того, кто-то уже это сделал, и модуль tsc_freq_khz доступен на GitHub. При этом должно работать следующее:

# modprobe tsc_freq_khz
$ cat /sys/devices/system/cpu/cpu0/tsc_freq_khz

(проверено на 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

Но на Fedora 29 Intel Atom просто:

kernel: tsc: Detected 2200.000 MHz processor

В системе 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

Ценности Perf

Ядро Linux пока не предоставляет API для чтения скорости TSC. Но он дает возможность получитьmult а также shiftзначения, которые можно использовать для преобразования отсчетов TSC в наносекунды. Эти значения получены из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);
}

Фактический код:

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);
}

Протестировано в 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;
}

Скорость TSC напрямую связана с "CPU MHz" в /proc/cpuinfo, На самом деле, лучше использовать число "bogomips". Причина в том, что, хотя частота для TSC - это максимальная частота процессора, текущая скорость процессора может меняться во время вашего вызова.

Значение bogomips вычисляется при загрузке. Вам нужно будет отрегулировать это значение по количеству ядер и количеству процессоров (т. Е. Количеству гиперпотоков), которые дают вам [дробные] МГц. Это то, что я использую, чтобы делать то, что вы хотите сделать.

Чтобы узнать количество процессоров, ищите последнюю строку "процессор:". Количество процессоров <value> + 1, Назовите это "cpu_count".

Чтобы получить количество ядер, работает любое ядро "процессор:". количество ядер <value>, Назовите это "core_count".

Итак, формула имеет вид:

smt_count = cpu_count;
if (core_count)
    smt_count /= core_count;
cpu_freq_in_khz = (bogomips * scale_factor) / smt_count;

Это извлечено из моего фактического кода, который ниже.


Вот фактический код, который я использую. Вы не сможете использовать его напрямую, потому что он опирается на шаблон, который у меня есть, но он должен дать вам некоторые идеи, особенно о том, как вычислить

// 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;
}

ОБНОВИТЬ:

Вздох. Да.

Я использовал "процессор МГц" из /proc/cpuinfo до введения процессоров с технологией "speed step", поэтому я переключился на "bogomips", и алгоритм был выведен эмпирически на основе этого. Когда я получил его, у меня был доступ только к многопоточным машинам. Тем не менее, я нашел старый, который не является, и вещь SMT не действительна.

Однако, похоже, что bogomips всегда в 2 раза больше [максимальной] скорости процессора. См. http://www.clifton.nl/bogo-faq.html Это не всегда был мой опыт работы со всеми версиями ядра на протяжении многих лет [IIRC, я начал с 0.99.x], но в наши дни это, вероятно, надежное предположение.,

С "постоянным TSC" [который есть у всех новых процессоров], обозначенным constant_tsc в flags: поле в /proc/cpuinfoчастота TSC - максимальная частота процессора.

Первоначально единственный способ получить информацию о частоте был от /proc/cpuinfo, Теперь, однако, в более современных ядрах есть другой способ, который может быть более простым и определенным [у меня было покрытие кода для этого в моем другом программном обеспечении, но я забыл об этом]:

/sys/devices/system/cpu/cpu0/cpufreq/cpuinfo_max_freq

Содержимое этого файла - максимальная частота процессора в кГц. Есть аналогичные файлы для других ядер процессора. Файлы должны быть идентичны для большинства нормальных материнских плат (например, те, которые состоят из одного и того же модельного чипа и не пытаются смешивать, скажем, i7 и атомы). В противном случае вам придется отслеживать информацию на уровне ядра, и это быстро станет беспорядочным.

В данном каталоге также есть другие интересные файлы. Например, если ваш процессор имеет "шаг скорости" [и некоторые другие файлы могут вам это сказать], вы можете добиться максимальной производительности, написав performance к scaling_governor файл. Это отключит использование шага скорости.

Если процессора не было constant_tscвам нужно будет отключить шаг скорости [и запустить ядра с максимальной скоростью], чтобы получить точные измерения

Я кратко посмотрел, и, кажется, нет встроенного способа напрямую получить эту информацию из ядра.

Тем не менее, символtsc_khz (я думаю, это то, что вы хотите) экспортируется ядром. Вы можете написать небольшой модуль ядра, который предоставляет интерфейс sysfs, и использовать его для считывания значения tsc_khz из пространства пользователя.

Если запись модуля ядра невозможна, возможно, можно использовать Dark Magic ™ для считывания значения непосредственно из пространства памяти ядра. Разобрать двоичный файл ядра или System.map файл, чтобы найти местоположение tsc_khz и прочитайте его из / dev / {k} mem. Это, конечно, возможно только при условии, что ядро ​​настроено с соответствующими параметрами.

Наконец, после прочтения комментариев к исходным текстам ядра, похоже, что на некоторых платформах TSC может быть нестабильным. Я не знаю много о внутренней работе арки x86, но это может быть то, что вы хотите принять во внимание.

Другие вопросы по тегам