Расширьте ядро ​​с помощью eBPF

Расширенный фильтр пакетов Беркли (eBPF) — это виртуальная машина ядра, которая запускает предоставленные пользователем программы eBPF для расширения функциональности ядра. Эти программы могут быть подключены к зондам или событиям в ядре и использоваться для сбора полезной статистики ядра, мониторинга и отладки. Программа загружается в ядро ​​с помощью системного вызова bpf(2) и предоставляется пользователем в виде бинарного блока машинных инструкций eBPF. Система сборки Android поддерживает компиляцию программ на языке C в eBPF с использованием простого синтаксиса файла сборки, описанного в этом документе.

Более подробную информацию о внутренней структуре и архитектуре eBPF можно найти на странице Брендана Грегга, посвященной eBPF .

В состав Android входит загрузчик и библиотека eBPF, которые загружают программы eBPF при загрузке системы.

Android BPF загрузчик

При загрузке Android загружаются все программы eBPF, расположенные в /system/etc/bpf/ . Эти программы представляют собой бинарные объекты, созданные системой сборки Android из программ на языке C, и сопровождаются файлами Android.bp в исходном коде Android. Система сборки сохраняет сгенерированные объекты в /system/etc/bpf , и эти объекты становятся частью образа системы.

Формат программы Android eBPF на языке C

Программа eBPF на языке C должна иметь следующий формат:

#include <bpf_helpers.h>

/* Define one or more maps in the maps section, for example
 * define a map of type array int -> uint32_t, with 10 entries
 */
DEFINE_BPF_MAP(name_of_my_map, ARRAY, int, uint32_t, 10);

/* this also defines type-safe accessors:
 *   value * bpf_name_of_my_map_lookup_elem(&key);
 *   int bpf_name_of_my_map_update_elem(&key, &value, flags);
 *   int bpf_name_of_my_map_delete_elem(&key);
 * as such it is heavily suggested to use lowercase *_map names.
 * Also note that due to compiler deficiencies you cannot use a type
 * of 'struct foo' but must instead use just 'foo'.  As such structs
 * must not be defined as 'struct foo {}' and must instead be
 * 'typedef struct {} foo'.
 */

DEFINE_BPF_PROG("PROGTYPE/PROGNAME", AID_*, AID_*, PROGFUNC)(..args..) {
   <body-of-code
    ... read or write to MY_MAPNAME
    ... do other things
   >
}

LICENSE("GPL"); // or other license

Где:

  • name_of_my_map — это имя вашей переменной карты. Это имя сообщает загрузчику BPF о типе создаваемой карты и о параметрах, которые она должна содержать. Определение этой структуры предоставляется входящим в комплект заголовочным файлом bpf_helpers.h .
  • PROGTYPE/PROGNAME обозначает тип программы и её имя. Тип программы может быть любым из перечисленных в следующей таблице. Если тип программы не указан, строгих правил именования программы нет; имя должно быть известно процессу, который её подключает.

  • PROGFUNC — это функция, которая после компиляции помещается в раздел результирующего файла.

kprobe Привязывает PROGFUNC к инструкции ядра, используя инфраструктуру kprobe. PROGNAME должно быть именем функции ядра, для которой выполняется kprobe. Для получения дополнительной информации о kprobe обратитесь к документации ядра kprobe .
точка трассировки Привязывает PROGFUNC к точке трассировки. Имя PROGNAME должно иметь формат SUBSYSTEM/EVENT . Например, раздел точки трассировки для привязки функций к событиям переключения контекста планировщика будет выглядеть так: SEC("tracepoint/sched/sched_switch") , где sched — имя подсистемы трассировки, а sched_switch — имя события трассировки. Для получения дополнительной информации о точках трассировки см. документацию ядра по событиям трассировки .
skfilter Программа функционирует как фильтр сетевых сокетов.
расписания Программа выполняет функцию классификатора сетевого трафика.
cgroupskk, cgroupsock Программа запускается всякий раз, когда процессы в группе CGroup создают сокет AF_INET или AF_INET6.

Дополнительные типы данных можно найти в исходном коде загрузчика .

Например, следующая программа myschedtp.c добавляет информацию о последнем PID задачи, которая выполнялась на конкретном ЦП. Эта программа достигает своей цели, создавая карту и определяя функцию tp_sched_switch , которую можно прикрепить к событию трассировки sched:sched_switch . Для получения дополнительной информации см. раздел «Прикрепление программ к точкам трассировки» .

#include <linux/bpf.h>
#include <stdbool.h>
#include <stdint.h>
#include <bpf_helpers.h>

DEFINE_BPF_MAP(cpu_pid_map, ARRAY, int, uint32_t, 1024);

struct switch_args {
    unsigned long long ignore;
    char prev_comm[16];
    int prev_pid;
    int prev_prio;
    long long prev_state;
    char next_comm[16];
    int next_pid;
    int next_prio;
};

DEFINE_BPF_PROG("tracepoint/sched/sched_switch", AID_ROOT, AID_SYSTEM, tp_sched_switch)
(struct switch_args *args) {
    int key;
    uint32_t val;

    key = bpf_get_smp_processor_id();
    val = args->next_pid;

    bpf_cpu_pid_map_update_elem(&key, &val, BPF_ANY);
    return 1; // return 1 to avoid blocking simpleperf from receiving events
}

LICENSE("GPL");

Макрос LICENSE используется для проверки совместимости программы с лицензией ядра, если программа использует вспомогательные функции BPF, предоставляемые ядром. Укажите имя лицензии вашей программы в строковом формате, например, LICENSE("GPL") или LICENSE("Apache 2.0") .

Формат файла Android.bp

Для того чтобы система сборки Android могла собрать программу eBPF .c , необходимо создать соответствующую запись в файле Android.bp проекта. Например, чтобы собрать программу eBPF на языке C с именем bpf_test.c , добавьте следующую запись в файл Android.bp вашего проекта:

bpf {
    name: "bpf_test.o",
    srcs: ["bpf_test.c"],
    cflags: [
        "-Wall",
        "-Werror",
    ],
}

Эта запись компилирует программу на языке C, в результате чего создается объект /system/etc/bpf/bpf_test.o . При загрузке система Android автоматически загружает программу bpf_test.o в ядро.

Файлы доступны в sysfs

При загрузке система Android автоматически загружает все объекты eBPF из /system/etc/bpf/ , создает необходимые программе карты и привязывает загруженную программу с картами к файловой системе BPF. Затем эти файлы можно использовать для дальнейшего взаимодействия с программой eBPF или чтения карт. В этом разделе описываются соглашения, используемые для именования этих файлов и их расположения в sysfs.

Создаются и закрепляются следующие файлы:

  • Для любой загруженной программы, если PROGNAME — это имя программы, а FILENAME — имя файла eBPF на языке C, загрузчик Android создаст и закрепит каждую программу по адресу /sys/fs/bpf/prog_FILENAME_PROGTYPE_PROGNAME .

    Например, для предыдущего примера трассировки sched_switch в myschedtp.c создается файл программы, который закрепляется в каталоге /sys/fs/bpf/prog_myschedtp_tracepoint_sched_sched_switch .

  • Для всех созданных карт, если предположить, что MAPNAME — это имя карты, а FILENAME — имя файла eBPF на языке C, загрузчик Android создаст и закрепит каждую карту в каталоге /sys/fs/bpf/map_FILENAME_MAPNAME .

    Например, для предыдущего примера трассировки sched_switch в myschedtp.c создается файл карты и закрепляется в каталоге /sys/fs/bpf/map_myschedtp_cpu_pid_map .

  • bpf_obj_get() из библиотеки Android BPF возвращает дескриптор файла из закрепленного файла /sys/fs/bpf . Этот дескриптор файла можно использовать для дальнейших операций, таких как чтение карт или привязка программы к точке трассировки.

Библиотека Android BPF

Библиотека Android BPF называется libbpf_android.so и является частью образа системы. Эта библиотека предоставляет пользователю низкоуровневые возможности eBPF, необходимые для создания и чтения карт, создания зондов, точек трассировки и буферов производительности.

Прикрепляйте программы к точкам трассировки

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

  1. Вызовите функцию bpf_obj_get() , чтобы получить fd программы из места расположения закрепленного файла. Для получения дополнительной информации см. раздел «Файлы, доступные в sysfs» .
  2. Вызовите функцию bpf_attach_tracepoint() из библиотеки BPF, передав ей fd программы и имя точки трассировки.

Приведённый ниже пример кода показывает, как подключить точку трассировки sched_switch определённую в предыдущем исходном файле myschedtp.c (проверка ошибок не показана):

  char *tp_prog_path = "/sys/fs/bpf/prog_myschedtp_tracepoint_sched_sched_switch";
  char *tp_map_path = "/sys/fs/bpf/map_myschedtp_cpu_pid";

  // Attach tracepoint and wait for 4 seconds
  int mProgFd = bpf_obj_get(tp_prog_path);
  int mMapFd = bpf_obj_get(tp_map_path);
  int ret = bpf_attach_tracepoint(mProgFd, "sched", "sched_switch");
  sleep(4);

  // Read the map to find the last PID that ran on CPU 0
  android::bpf::BpfMap<int, int> myMap(mMapFd);
  printf("last PID running on CPU %d is %d\n", 0, myMap.readValue(0));

Читайте по картам

BPF-карты поддерживают произвольно сложные структуры или типы ключей и значений. Библиотека Android BPF включает класс android::BpfMap , который использует шаблоны C++ для создания экземпляров BpfMap на основе типа ключа и значения для рассматриваемой карты. В предыдущем примере кода показано использование BpfMap с ключом и значением в виде целых чисел. Целые числа также могут быть произвольными структурами.

Таким образом, шаблонизированный класс BpfMap позволяет определить пользовательский объект BpfMap подходящий для конкретной карты. Затем к карте можно получить доступ с помощью сгенерированных пользователем функций, которые учитывают типы данных, что приводит к более чистому коду.

Для получения более подробной информации о BpfMap обратитесь к исходному коду Android .

Проблемы отладки

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

Примеры использования eBPF в Android

Следующие программы в AOSP содержат дополнительные примеры использования eBPF:

  • Программа netd eBPF на языке C используется сетевым демоном (netd) в Android для различных целей, таких как фильтрация сокетов и сбор статистики. Чтобы узнать, как используется эта программа, ознакомьтесь с исходным кодом монитора трафика eBPF .

  • Программа eBPF на языке C time_in_state вычисляет время, которое приложение Android проводит на разных частотах процессора, и это время используется для расчета энергопотребления.

  • В Android 12 программа gpu_mem eBPF на языке C отслеживает общее использование памяти графического процессора для каждого процесса и для всей системы. Эта программа используется для профилирования памяти графического процессора.