XDP/BPF: существует ли альтернатива `bpf_ktime_get_ns` в пользовательском пространстве?

Я хочу вставлять отметку времени в пакеты, которые я получаю в своей программе XDP. Единственный способ, которым я знаю, как получить метку времени, — это вызвать bpf_ktime_get_ns.

Но какой должна быть функция, эквивалентная пользовательскому пространству, которая создает сопоставимые временные метки? Насколько я знаю, ktime_get_ns возвращает время с момента запуска системы (в наносекундах). Есть

$ uptime
 11:45:35 up 2 days,  3:15,  3 users,  load average: 0.19, 0.29, 0.27

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

Редактировать: Это была исключительно моя вина. @Qeole и @tuilagio совершенно правы. Я сделал ошибку в арифметике указателя в коде пользовательского пространства, где я получал указатель временной метки.


person binaryBigInt    schedule 01.04.2020    source источник
comment
@Qeole Какой из них? CLOCK_REALTIME, CLOCK_MONOTONIC, CLOCK_PROCESS_CPUTIME_ID, CLOCK_THREAD_CPUTIME_ID. Согласно этому описанию: linux.die.net/man/3/clock_gettime они не то же самое, что sourceware.org/systemtap/tapsets/API-ktime- get-ns.html   -  person binaryBigInt    schedule 01.04.2020
comment
Если вы не найдете ничего лучше, я полагаю, у вас всегда есть возможность протестировать (BPF_PROG_TEST_RUN) программу BPF, чтобы получить метку времени из ядра :).   -  person Qeole    schedule 02.04.2020


Ответы (2)


Вы можете использовать clock_gettime() :

static unsigned long get_nsecs(void)
{
    struct timespec ts;

    clock_gettime(CLOCK_MONOTONIC, &ts);
    return ts.tv_sec * 1000000000UL + ts.tv_nsec;
}

и назовите его с помощью unsigned long now = get_nsecs(); или uint64_t now = get_nsecs();

Эта временная метка возврата в разрешении нсек.

Источник: https://github.com/torvalds/linux/blob/master/samples/bpf/xdpsock_user.c#L114

person tuilagio    schedule 01.04.2020
comment
Вы читали мой комментарий? bpf_ktime_get_ns и clock_gettime не основаны на одном и том же источнике времени PS: Я знаю, это звучит странно, например, почему они предоставляют вспомогательную функцию bpf с отметкой времени, которая не сопоставима с отметками времени, которые мы можем получить в пользовательском пространстве? Просто вообще не имеет смысла - person binaryBigInt; 02.04.2020
comment
@binaryBigInt Имеет смысл. Часы монотонны, и их основной вариант использования — вычисление длительности (как описано в зафиксировать журналы). Поскольку это происходит на стороне ядра, оно повторно использует систему часов, которая уже была там. - person Qeole; 02.04.2020
comment
@Qeole Так вы говорите, что bpf_ktime_get_ns и clock_gettime сравнимы? Причина, по которой я спрашиваю, заключается в том, что я получаю совершенно нереалистичные результаты, такие как 65500 (что близко к UINT16_MAX (тип данных, который я использую для µs-Latency)). Я не думаю, что пакету требуется 65 мс, чтобы пройти от XDP-Kernel до пользовательского пространства... - person binaryBigInt; 02.04.2020
comment
@binaryBigInt Нет, я говорю, что запуск bpf_ktime_get_ns() из программы зацепил, например. Вход TC и bpf_ktime_get_ns() снова от другого подключенного, например. Выход ТС сопоставим. Или для трассировки запуск bpf_ktime_get_ns() как на входе (kprobe), так и на выходе (kretprobe), или в разных функциях на заданном пути кода сравним. Я никогда не пытался сравнивать с временными метками пользователя, поэтому не знаю, что вам нужно. Использование программы BPF для получения метки времени (просто вызовите вспомогательную функцию и верните значение) является хаком, но может сработать, хотя это не должно стоить намного больше, чем системный вызов. - person Qeole; 02.04.2020
comment
@binaryBigInt Это должно быть как-то связано с вашим кодом. Я также измеряю время с момента прибытия пакетов до момента их входа в пространство пользователя и получаю значение ‹ 10 мкс (микросекунд). - person tuilagio; 03.04.2020
comment
Измерение было выполнено с использованием bpf_ktime_get_ns в коде пространства ядра и clock_gettime в коде пространства пользователя. Насчет источников времени я не уверен, однако результаты кажутся разумными. Извините, я проглядел ваш пост. - person tuilagio; 03.04.2020
comment
@tuilagio Спасибо за ответ, вы использовали CLOCK_MONOTONIC? Как вы вставили временную метку в пакет в своей XDP-программе? - person binaryBigInt; 03.04.2020
comment
Самое смешное, что даже если я сплю 1000 мкс, чтобы собрать пакеты и затем обработать их в пакетном режиме, я получаю задержки в 100 мкс. - person binaryBigInt; 03.04.2020
comment
@binaryBigInt Отметка времени, возвращаемая bpf_ktime_get_ns, записывается в пакет, когда пакет поступает в ядро, а в пользовательском пространстве код пользователя вызывает get_nsecs(), когда пакет поступает. Просто используйте код, который я предоставил, и посмотрите. - person tuilagio; 03.04.2020

Это, вероятно, не канонический способ сделать это, но, по крайней мере, это весело: мы можем получить временную метку ядра... из самого BPF!

Подсистема BPF имеет функцию «тестового прогона», которая позволяет тестировать некоторые типы программ с данными, предоставленными пользователем, причем прогон запускается системным вызовом bpf(). Вот пример приложения, делающего именно так:

  1. Он загружает программу BPF (XDP, но тип не имеет большого значения) и получает FD.
  2. Он повторно использует FD для запуска «тестового запуска» этой программы BPF.
  3. При запуске программа вызывает bpf_ktime_get_ns(), копирует значение в буфер вывода данных (data_out), и нам просто нужно прочитать его, чтобы получить метку времени.
#define _GNU_SOURCE
#include <errno.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/syscall.h>
#include <unistd.h>

#include <linux/bpf.h>

int main(__attribute__((unused))int argc,
         __attribute__((unused))char **argv)
{
    union bpf_attr load_attr = { }, run_attr = { };
    const struct bpf_insn insns[] = {
        /* w0 = 1                | r0 = XDP_DROP */
        { .code = 0xb4, .src_reg = 0, .dst_reg = 0, .off = 0, .imm = 1, },
        /* r2 = *(u32 *)(r1 + 4) | r2 = ctx->data_end */
        { .code = 0x61, .src_reg = 1, .dst_reg = 2, .off = 4, .imm = 0, },
        /* r6 = *(u32 *)(r1 + 0) | r6 = ctx->data */
        { .code = 0x61, .src_reg = 1, .dst_reg = 6, .off = 0, .imm = 0, },
        /* r1 = r6               | r1 = ctx->data */
        { .code = 0xbf, .src_reg = 6, .dst_reg = 1, .off = 0, .imm = 0, },
        /* r1 += 8               | r1 += sizeof(uint64_t) */
        { .code = 0x07, .src_reg = 0, .dst_reg = 1, .off = 0, .imm = 8, },
        /* if r1 > r2 goto +3    | if (data + 8 > data_end) return */
        { .code = 0x2d, .src_reg = 2, .dst_reg = 1, .off = 3, .imm = 0, },
        /* call bpf_ktime_get_ns() */
        { .code = 0x85, .src_reg = 0, .dst_reg = 0, .off = 0, .imm = BPF_FUNC_ktime_get_ns, },
        /* *(u64 *)(r6 + 0) = r0 | *(ctx->data) = bpf_ktime_get_ns() */
        { .code = 0x7b, .src_reg = 0, .dst_reg = 6, .off = 0, .imm = 0, },
        /* w0 = 2                | r0 = XDP_PASS */
        { .code = 0xb4, .src_reg = 0, .dst_reg = 0, .off = 0, .imm = 2, },
        /* exit                  | return r0 */
        { .code = 0x95, .src_reg = 0, .dst_reg = 0, .off = 0, .imm = 0, },
    };
    const char license[] = "GPL";   /* required for bpf_ktime_get_ns() */
    /*
     * Data buffers data_in/data_out must be at least the minimal size for
     * an Ethernet frame: 14 header bytes
     */
    const uint8_t data_out[14];
    const uint8_t data_in[14];
    int fd, res;

    /* Load program */

    load_attr.prog_type = BPF_PROG_TYPE_XDP;
    load_attr.insn_cnt = sizeof(insns) / sizeof(insns[0]);
    load_attr.insns = (uint64_t)insns;
    load_attr.license = (uint64_t)license;

    fd = syscall(__NR_bpf, BPF_PROG_LOAD, &load_attr, sizeof(load_attr));
    if (fd < 0) {
        fprintf(stderr, "failed to load BPF program: %s\n",
                strerror(errno));
        return EXIT_FAILURE;
    }

    /* Run program */

    run_attr.test.prog_fd = fd;
    run_attr.test.data_size_in = sizeof(data_in);
    run_attr.test.data_size_out = sizeof(data_out);
    run_attr.test.data_in = (uint64_t)data_in;
    run_attr.test.data_out = (uint64_t)data_out;

    res = syscall(__NR_bpf, BPF_PROG_TEST_RUN, &run_attr, sizeof(run_attr));
    if (res) {
        fprintf(stderr, "failed to run BPF program: %s\n",
                strerror(errno));
        close(fd);
        return EXIT_FAILURE;
    }

    /* Extract result */

    fprintf(stdout, "%lu\n", (uint64_t)run_attr.test.data_out);

    close(fd);
    return EXIT_SUCCESS;
}

Обратите внимание, что мы также можем извлечь данные из возвращаемого программой значения (run_attr.test.retval), но это 32-битное целое число, поэтому вы не получите полную метку времени. Это может быть использовано для извлечения, например. только количество секунд для этой метки времени со сдвигом вправо r0 >>= 32, чтобы избежать проверки длины data/data_end и копирования в data_out. Не то, чтобы это должно сильно изменить производительность.

Запуск всего приложения (загрузка + запуск), очевидно, займет больше времени, чем последующие запуски, из-за шага проверки, выполняемого в ядре при загрузке программы.

Дополнение: Программа BPF была сгенерирована из следующего кода:

#include <linux/bpf.h>

static unsigned long long (*bpf_ktime_get_ns)(void) =
    (void *)BPF_FUNC_ktime_get_ns;

int xdp(struct xdp_md *ctx)
{
    void *data_end = (void *) (long) ctx->data_end;
    void *data = (void *) (long) ctx->data;

    if (data + sizeof(unsigned long long) > data_end)
        return XDP_DROP;

    *(unsigned long long *)data = bpf_ktime_get_ns();
    return XDP_PASS;
}
person Qeole    schedule 03.04.2020
comment
Извините, я допустил ошибку в своей программе. Ты прав. Благодарю вас! Я отредактировал исходное сообщение - person binaryBigInt; 03.04.2020
comment
Ничего страшного, баги случаются :). Рад, что у вас все работает! - person Qeole; 03.04.2020