Краткое введение в BPF и eBPF

Автор оригинала: Diego Pino García
  • Перевод
Привет, Хабр! Сообщаем, что у нас готовится к выходу книга "Linux Observability with BPF".


Поскольку виртуальная машина BPF продолжает эволюционировать и активно применяется на практике, мы перевели для вас статью, описывающую ее основные возможности и состояние на настоящее время.

В последние годы стали набирать популярность инструментарии для программирования и приемы, призванные компенсировать ограничения ядра Linux в случаях, когда требуется высокопроизводительная обработка пакетов. Один из наиболее популярных приемов такого рода называется обход ядра (kernel bypass) и позволяет, пропуская сетевой уровень ядра, выполнять всю обработку пакетов из пользовательского пространства. Обход ядра также предполагает управление сетевой картой из пользовательского пространства. Иными словами, при работе с сетевой картой мы полагаемся на драйвер пользовательского пространства.

Передавая полный контроль над сетевой картой программе из пользовательского пространства, мы сокращаем издержки, обусловленные работой ядра (переключение контекста, обработка сетевого уровня, прерывания, т.д.), что достаточно важно при работе на скоростях 10Гб/с или выше. Обход ядра плюс комбинация других возможностей (пакетная обработка) и аккуратная настройка производительности (учет NUMA, изоляция CPU, т.д.) соответствуют основам высокопроизводительной сетевой обработки в пользовательском пространстве. Возможно, образцовый пример такого нового подхода к обработке пакетов – это DPDK от Intel (Data Plane Development Kit), хотя, существуют и другие широко известные инструментарии и приемы, среди которых VPP от Cisco (Vector Packet Processing), Netmap и, конечно же, Snabb.

У организации сетевых взаимодействий в пользовательском пространстве есть ряд недостатков:

  • Ядро ОС – это уровень абстрагирования для аппаратных ресурсов. Поскольку программам пользовательского пространства приходится управлять своими ресурсами напрямую, им также приходится управлять и собственным аппаратным обеспечением. Зачастую это означает необходимость программирования собственных драйверов.
  • Поскольку мы полностью отказываемся от пространства ядра, мы также отказываемся и от всего сетевого функционала, предоставляемого ядром. Программам пользовательского пространства приходится заново реализовать те функции, которые, возможно, уже предоставляются ядром или операционной системой.
  • Программы работают в режиме песочницы, что серьезно ограничивает возможности их взаимодействия и мешает им интегрироваться с другими частями операционной системы.

В сущности, при организации сетевых взаимодействий в пользовательском пространстве повышение производительности достигается путем переноса обработки пакетов из ядра в пользовательское пространство. XDP делает ровно наоборот: перемещает сетевые программы из пользовательского пространства (фильтры, преобразователи, маршрутизация, т.д.) в область ядра. XDP позволяет нам выполнить сетевую функцию, как только пакет попадает на сетевой интерфейс и до того, как он начинает движение вверх в сетевую подсистему ядра. В результате скорость обработки пакетов существенно увеличивается. Однако, как ядро позволяет пользователю выполнять свои программы в пространстве ядра? Прежде чем ответить на этот вопрос, давайте рассмотрим, что такое BPF.

BPF и eBPF

Несмотря на не вполне понятное название BPF (Фильтрация пакетов, Беркли) – это, фактически, модель виртуальной машины. Данная виртуальная машина исходно проектировалась для обработки фильтрации пакетов, отсюда и название.

Одним из наиболее известных инструментов, использующих BPF, является tcpdump. При захвате пакетов с помощью tcpdump пользователь может задать выражение для фильтрации пакетов. Захватываться будут лишь пакеты, соответствующие этому выражению. Например, выражение “tcp dst port 80” касается всех TCP-пакетов, поступающих на порт 80. Компилятор может сократить это выражение, преобразовав его в байт-код BPF.

$ sudo tcpdump -d "tcp dst port 80"
(000) ldh [12]
(001) jeq #0x86dd jt 2 jf 6
(002) ldb [20]
(003) jeq #0x6 jt 4 jf 15
(004) ldh [56]
(005) jeq #0x50 jt 14 jf 15
(006) jeq #0x800 jt 7 jf 15
(007) ldb [23]
(008) jeq #0x6 jt 9 jf 15
(009) ldh [20]
(010) jset #0x1fff jt 15 jf 11
(011) ldxb 4*([14]&0xf)
(012) ldh [x + 16]
(013) jeq #0x50 jt 14 jf 15
(014) ret #262144
(015) ret #0


Вот что, в принципе, делает вышеприведенная программа:

  • Инструкция (000): загружает пакет со смещением 12, в виде 16-разрядного слова в аккумулятор. Смещение 12 соответствует ethertype пакета.
  • Инструкция (001): сравнивает значение в аккумуляторе с 0x86dd, то есть, с ethertype-значением для IPv6. Если результат равен true, то счетчик программы переходит к инструкции (002), а если нет – то к (006).
  • Инструкция (006): сравнивает значение с 0x800 (ethertype-значение для IPv4). Если ответ true, то программа переходит к (007), если нет – то к (015).

И так далее, пока программа фильтрации пакетов не вернет результат. Обычно это булеан. Возврат ненулевого значения (инструкция (014)) означает, что пакет подошел, а возврат нулевого (инструкция (015)) означает, что пакет не подошел.

Виртуальная машина BPF и ее байт-код были предложены Стивом Мак-Канном и Ваном Джейкобсоном в конце 1992, когда вышла их статья Фильтр пакетов BSD: Новая архитектура для захвата пакетов на пользовательском уровне, впервые данная технология была представлена на конференции Usenix зимой 1993 года.

Поскольку BPF – это виртуальная машина, она определяет среду, в которой выполняются программы. Кроме байт-кода она также определяет пакетную модель памяти (инструкции загрузки неявно применяются к пакету), регистры (A и X; регистры аккумулятора и индекса), хранилище скретч-памяти и неявный счетчик программ. Интересно, что байт-код BPF был смоделирован по образцу Motorola 6502 ISA. Как вспоминал Стив Мак-Канн в своем пленарном докладе на Sharkfest ‘11, он был знаком со сборкой 6502 еще со старших классов, когда программировал на Apple II, и эти знания повлияли на его работу по проектированию байт-кода BPF.

Поддержка BPF реализована в ядре Linux в версии v2.5 и выше, добавлена в основном усилиями Джея Шуллиста. Код BPF оставался без серьезных изменений вплоть до 2011 года, когда Эрик Думазет переделал интерпретатор BPF для работы в режиме JIT (Источник: JIT для пакетных фильтров). После этого ядро вместо интерпретации байт-кода BPF могло напрямую преобразовывать программы BPF под целевую архитектуру: x86, ARM, MIPS, т.д.

Позже, в 2014 году, Алексей Старовойтов предложил новый JIT-механизм для BPF. Фактически этот новый JIT стал новой архитектурой на основе BPF и получил название eBPF. Думаю, в течение некоторого времени обе виртуальные машины сосуществовали, но в настоящее время фильтрация пакетов реализуется на основе eBPF. Фактически, во многих образцах современной документации под BPF понимается eBPF, а классическая BPF сегодня известна как cBPF.

eBPF в нескольких отношениях расширяет классическую виртуальную машину BPF:

  • Опирается на современные 64-разрядные архитектуры. eBPF использует 64-разрядные регистры и увеличивает количество доступных регистров с 2 (аккумулятор и X) до 10. В eBPF также предоставляются дополнительные коды операций (BPF_MOV, BPF_JNE, BPF_CALL…).
  • Откреплена от подсистемы сетевого уровня. BPF была завязана на пакетную модель данных. Поскольку она использовалась для фильтрации пакетов, код ее находился в подсистеме, обеспечивающей сетевые взаимодействия. Однако, виртуальная машина eBPF больше не привязана к модели данных и может использоваться в любых целях. Так, теперь программу eBPF можно подключить к tracepoint или к kprobe. Это открывает путь к инструментированию eBPF, анализу производительности и многим другим вариантам использования в контексте других подсистем ядра. Теперь код eBPF располагается по собственному пути: kernel/bpf.
  • Глобальные хранилища данных под названием Карты. Карты – это хранилища типа «ключ-значение», обеспечивающие обмен данными между пользовательским пространством и пространством ядра. В eBPF предоставляются карты нескольких типов.
  • Вспомогательные функции. В частности, для перезаписи пакета, вычисления контрольной суммы или клонирования пакета. Эти функции выполняются внутри ядра и не относятся к программам пользовательского пространства. Кроме того, из программ eBPF можно выполнять системные вызовы.
  • Концевые вызовы. Размер программы в eBPF ограничен 4096 байт. Возможность концевого вызова позволяет программе eBPF передать управление новой eBPF-программе и таким образом обойти данное ограничение (таким образом можно связать до 32 программ).

eBPF: пример

В исходниках ядра Linux есть несколько примеров для eBPF. Они доступны по адресу samples/bpf/. Чтобы скомпилировать эти примеры, просто введите:

$ sudo make samples/bpf/

Я не буду сам писать новый пример для eBPF, а воспользуюсь одним из образцов, доступных в samples/bpf/. Рассмотрю некоторые участки кода и объясню, как он работает. В качестве примера я выбрал программу tracex4.

Вообще, каждый из примеров в samples/bpf/ состоит из двух файлов. В данном случае:

  • tracex4_kern.c, содержит исходный код, который должен выполняться в ядре как байт-код eBPF.
  • tracex4_user.c, содержит программу из пользовательского пространства.

В таком случае, нам нужно скомпилировать tracex4_kern.c в байт-код eBPF. В настоящий момент в gcc отсутствует серверная часть для eBPF. К счастью, clang может выдавать байт-код eBPF. Makefile использует clang для компиляции tracex4_kern.c в файл объекта.

Выше я упоминал, что одной из наиболее интересных фич eBPF являются карты. tracex4_kern определяет одну карту:

struct pair {
    u64 val;
    u64 ip;
};  

struct bpf_map_def SEC("maps") my_map = {
    .type = BPF_MAP_TYPE_HASH,
    .key_size = sizeof(long),
    .value_size = sizeof(struct pair),
    .max_entries = 1000000,
};

BPF_MAP_TYPE_HASH – один из многих типов карт, предлагаемых eBPF. В данном случае, это просто хеш. Также вы могли заметить объявление SEC("maps"). SEC – это макрос, используемый для создания новой секции двоичного файла. Собственно, в примере tracex4_kern определяется еще две секции:

SEC("kprobe/kmem_cache_free")
int bpf_prog1(struct pt_regs *ctx)
{   
    long ptr = PT_REGS_PARM2(ctx);

    bpf_map_delete_elem(&my_map, &ptr); 
    return 0;
}
    
SEC("kretprobe/kmem_cache_alloc_node") 
int bpf_prog2(struct pt_regs *ctx)
{
    long ptr = PT_REGS_RC(ctx);
    long ip = 0;

    // получаем ip-адрес вызывающей стороны kmem_cache_alloc_node() 
    BPF_KRETPROBE_READ_RET_IP(ip, ctx);

    struct pair v = {
        .val = bpf_ktime_get_ns(),
        .ip = ip,
    };
    
    bpf_map_update_elem(&my_map, &ptr, &v, BPF_ANY);
    return 0;
}   

Две эти функции позволяют удалить запись из карты (kprobe/kmem_cache_free) и добавить на карту новую запись (kretprobe/kmem_cache_alloc_node). Все имена функций, записанные заглавными буквами, соответствуют макросам, определенным в bpf_helpers.h.

Если я выведу дамп секций объектного файла, то должен увидеть, что эти новые секции уже определены:

$ objdump -h tracex4_kern.o

tracex4_kern.o: file format elf64-little

Sections:
Idx Name Size VMA LMA File off Algn
0 .text 00000000 0000000000000000 0000000000000000 00000040 2**2
CONTENTS, ALLOC, LOAD, READONLY, CODE
1 kprobe/kmem_cache_free 00000048 0000000000000000 0000000000000000 00000040 2**3
CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
2 kretprobe/kmem_cache_alloc_node 000000c0 0000000000000000 0000000000000000 00000088 2**3
CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
3 maps 0000001c 0000000000000000 0000000000000000 00000148 2**2
CONTENTS, ALLOC, LOAD, DATA
4 license 00000004 0000000000000000 0000000000000000 00000164 2**0
CONTENTS, ALLOC, LOAD, DATA
5 version 00000004 0000000000000000 0000000000000000 00000168 2**2
CONTENTS, ALLOC, LOAD, DATA
6 .eh_frame 00000050 0000000000000000 0000000000000000 00000170 2**3
CONTENTS, ALLOC, LOAD, RELOC, READONLY, DATA


Еще есть tracex4_user.c, основная программа. В принципе, эта программа слушает события kmem_cache_alloc_node. Когда происходит такое событие, выполняется соответствующий код eBPF. Код сохраняет IP-атрибут объекта в карту, и затем этот объект циклически выводится в основной программе. Пример:

$ sudo ./tracex4
obj 0xffff8d6430f60a00 is 2sec old was allocated at ip ffffffff9891ad90
obj 0xffff8d6062ca5e00 is 23sec old was allocated at ip ffffffff98090e8f
obj 0xffff8d5f80161780 is 6sec old was allocated at ip ffffffff98090e8f


Как связаны программа пользовательского пространства и программа eBPF? При инициализации tracex4_user.c загружает файл объекта tracex4_kern.o при помощи функции load_bpf_file.

int main(int ac, char **argv)
{
    struct rlimit r = {RLIM_INFINITY, RLIM_INFINITY};
    char filename[256];
    int i;

    snprintf(filename, sizeof(filename), "%s_kern.o", argv[0]);

    if (setrlimit(RLIMIT_MEMLOCK, &r)) {
        perror("setrlimit(RLIMIT_MEMLOCK, RLIM_INFINITY)");
        return 1;
    }

    if (load_bpf_file(filename)) {
        printf("%s", bpf_log_buf);
        return 1;
    }

    for (i = 0; ; i++) {
        print_old_objects(map_fd[1]);
        sleep(1);
    }

    return 0;
}

При выполнении load_bpf_file зонды, определенные в файле eBPF, добавляются в /sys/kernel/debug/tracing/kprobe_events. Теперь мы слушаем эти события, и наша программа может что-либо делать, когда они происходят.

$ sudo cat /sys/kernel/debug/tracing/kprobe_events
p:kprobes/kmem_cache_free kmem_cache_free
r:kprobes/kmem_cache_alloc_node kmem_cache_alloc_node


Все остальные программы в sample/bpf/ структурированы сходим образом. В них всегда по два файла:

  • XXX_kern.c: программа eBPF.
  • XXX_user.c: основная программа.

Программа eBPF определяет карты и функции, привязанные к секции. Когда ядро выдает событие определенного типа (например, tracepoint), привязанные функции выполняются. Карты обеспечивают обмен данными между программой ядра и программой пользовательского пространства.

Заключение

В этой статье в общих чертах были рассмотрены BPF и eBPF. Я знаю, что сегодня очень много информации и ресурсов об eBPF, поэтому порекомендую еще несколько материалов для дальнейшего изучения

Рекомендую почитать:

  • BPF: the universal in-kernel virtual machine Джонатана Корбетта. Введение в BPF и рассказ о том, как она эволюционировала в eBPF.
  • A thorough introduction to eBPF Брендана Грегга. Статья с сайта LWN.net. Брендан часто пишет твиты об eBPF и ведет список ресурсов на эту тему у себя в блоге.
  • Notes on BPF & eBPF Джулии Эванс. Комментарии к презентации Сучакры Шармы “The BSD Packet Filter: A New Architecture for User-level Packet Capture”. Комментарии хорошие и действительно помогают разобраться в слайдах.
  • eBPF, part1: Past, Present and Future Ферриса Эллиса. Лонгрид с продолжением, но читать стоит. Одна из лучших статей об eBPF, какие мне попадались.
Издательский дом «Питер»
Компания

Комментарии 3

    0
    ph_piter когда?
      0
      Ориентировочно через 2-3 месяца. Однако сами понимаете, сейчас работа затруднена и точных дат ожидать не приходится.
      0
      Ссылка на Snabb приведена не на оф. реп, а на форк.
      Вот тут оф. репозиторий github.com/snabbco/snabb

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

      Самое читаемое