Estender o kernel com eBPF

O Filtro de Pacote Berkeley Estendido (eBPF, na sigla em inglês) é uma máquina virtual no kernel que executa programas eBPF fornecidos pelo usuário para estender a funcionalidade do kernel. Esses programas podem ser conectados a probes ou eventos no kernel e usados para coletar estatísticas úteis do kernel, monitorar e depurar. Um programa é carregado no kernel usando a syscall bpf(2) e é fornecido pelo usuário como um blob binário de instruções de máquina eBPF. O sistema de build do Android oferece suporte à compilação de programas C para eBPF usando a sintaxe simples de arquivo de build descrita neste documento.

Mais informações sobre os elementos internos e a arquitetura do eBPF podem ser encontradas na página eBPF de Brendan Gregg.

O Android inclui um carregador e uma biblioteca eBPF que carregam programas eBPF na inicialização.

Carregador BPF do Android

Durante a inicialização do Android, todos os programas eBPF localizados em /system/etc/bpf/ são carregados. Esses programas são objetos binários criados pelo sistema de build do Android a partir de programas C e são acompanhados por arquivos Android.bp na árvore de origem do Android. O sistema de build armazena os objetos gerados em /system/etc/bpf, e esses objetos se tornam parte da imagem do sistema.

Formato de um programa C eBPF do Android

Um programa C eBPF precisa ter o seguinte formato:

#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

Em que:

  • name_of_my_map é o nome da variável de mapa. Esse nome informa ao carregador BPF o tipo de mapa a ser criado e com quais parâmetros. Essa definição de struct é fornecida pelo cabeçalho bpf_helpers.h incluído.
  • PROGTYPE/PROGNAME representa o tipo do programa e o nome do programa. O tipo de programa pode ser qualquer um dos listados na tabela a seguir. Quando um tipo de programa não está listado, não há uma convenção de nomenclatura estrita para o programa. O nome só precisa ser conhecido pelo processo que anexa o programa.

  • PROGFUNC é uma função que, quando compilada, é colocada em uma seção do arquivo resultante.

kprobe Conecta PROGFUNC a uma instrução do kernel usando a infraestrutura kprobe. PROGNAME precisa ser o nome da função do kernel que está sendo kprobed. Consulte a documentação do kernel kprobe (link em inglês) para mais informações sobre kprobes.
tracepoint Conecta PROGFUNC a um tracepoint. PROGNAME precisa estar no formato SUBSYSTEM/EVENT. Por exemplo, uma seção de tracepoint para anexar funções a eventos de troca de contexto do agendador seria SEC("tracepoint/sched/sched_switch"), em que sched é o nome do subsistema de trace e sched_switch é o nome do evento de trace. Consulte a documentação do kernel de eventos de trace (link em inglês) para mais informações sobre tracepoints.
skfilter Funções de programa como um filtro de soquete de rede.
schedcls Funções de programa como um classificador de tráfego de rede.
cgroupskb, cgroupsock O programa é executado sempre que processos em um CGroup criam um soquete AF_INET ou AF_INET6.

Outros tipos podem ser encontrados no código-fonte do carregador.

Por exemplo, o programa myschedtp.c a seguir adiciona informações sobre o PID da tarefa mais recente que foi executada em uma CPU específica. Esse programa atinge a meta criando um mapa e definindo uma função tp_sched_switch que pode ser anexada ao evento de trace sched:sched_switch. Para mais informações, consulte Anexar programas a tracepoints.

#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");

A macro LICENSE é usada para verificar se o programa é compatível com a licença do kernel quando o programa usa funções auxiliares BPF fornecidas pelo kernel. Especifique o nome da licença do programa no formato de string, como LICENSE("GPL") ou LICENSE("Apache 2.0").

Formato do arquivo Android.bp

Para que o sistema de build do Android crie um programa eBPF .c, é necessário criar uma entrada no arquivo Android.bp do projeto. Por exemplo, para criar um programa C eBPF chamado bpf_test.c, faça a seguinte entrada no arquivo Android.bp do projeto:

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

Essa entrada compila o programa C, resultando no objeto /system/etc/bpf/bpf_test.o. Na inicialização, o sistema Android carrega automaticamente o programa bpf_test.o no kernel.

Arquivos disponíveis no sysfs

Durante a inicialização, o sistema Android carrega automaticamente todos os objetos eBPF de /system/etc/bpf/, cria os mapas de que o programa precisa e fixa o programa carregado com os mapas no sistema de arquivos BPF. Esses arquivos podem ser usados para mais interações com o programa eBPF ou para leitura de mapas. Esta seção descreve as convenções usadas para nomear esses arquivos e os locais deles no sysfs.

Os arquivos a seguir são criados e fixados:

  • Para todos os programas carregados, supondo que PROGNAME seja o nome do programa e FILENAME seja o nome do arquivo C eBPF, o carregador do Android cria e fixa cada programa em /sys/fs/bpf/prog_FILENAME_PROGTYPE_PROGNAME.

    Por exemplo, para o exemplo anterior de tracepoint sched_switch em myschedtp.c, um arquivo de programa é criado e fixado em /sys/fs/bpf/prog_myschedtp_tracepoint_sched_sched_switch.

  • Para todos os mapas criados, supondo que MAPNAME seja o nome do mapa e FILENAME seja o nome do arquivo C eBPF, o carregador do Android cria e fixa cada mapa em /sys/fs/bpf/map_FILENAME_MAPNAME.

    Por exemplo, para o exemplo anterior de tracepoint sched_switch em myschedtp.c, um arquivo de mapa é criado e fixado em /sys/fs/bpf/map_myschedtp_cpu_pid_map.

  • bpf_obj_get() na biblioteca BPF do Android retorna um descritor de arquivo do arquivo /sys/fs/bpf fixado. Esse descritor do arquivo pode ser usado para outras operações, como leitura de mapas ou anexação de um programa a um tracepoint.

Biblioteca BPF do Android

A biblioteca BPF do Android é chamada de libbpf_android.so e faz parte da imagem do sistema. Essa biblioteca fornece ao usuário recursos eBPF de baixo nível necessários para criar e ler mapas, criar probes, tracepoints e buffers de desempenho.

Anexar programas a tracepoints

Os programas de tracepoint são carregados automaticamente na inicialização. Após o carregamento, o programa de tracepoint precisa ser ativado seguindo estas etapas:

  1. Chame bpf_obj_get() para receber o programa fd do local do arquivo fixado. Para mais informações, consulte Arquivos disponíveis no sysfs.
  2. Chame bpf_attach_tracepoint() na biblioteca BPF, transmitindo o programa fd e o nome do tracepoint.

O exemplo de código a seguir mostra como anexar o tracepoint sched_switch definido no arquivo de origem myschedtp.c anterior (a verificação de erros não é mostrada):

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

Ler dos mapas

Os mapas BPF oferecem suporte a estruturas ou tipos de chave e valor complexos arbitrários. A biblioteca BPF do Android inclui uma classe android::BpfMap que usa modelos C++ para instanciar BpfMap com base no tipo de chave e valor do mapa em questão. O exemplo de código anterior demonstra o uso de um BpfMap com chave e valor como números inteiros. Os números inteiros também podem ser estruturas arbitrárias.

Assim, a classe BpfMap com modelo permite definir um objeto BpfMap personalizado adequado para o mapa específico. O mapa pode ser acessado usando as funções geradas personalizadas, que são conscientes do tipo, resultando em um código mais limpo.

Para mais informações sobre BpfMap, consulte as origens do Android.

Depurar problemas

Durante a inicialização, várias mensagens relacionadas ao carregamento do BPF são registradas. Se o processo de carregamento falhar por qualquer motivo, uma mensagem de registro detalhada será fornecida no logcat. A filtragem dos registros do logcat por bpf imprime todas as mensagens e erros detalhados durante o tempo de carregamento, como erros do verificador eBPF.

Exemplos de eBPF no Android

Os programas a seguir no AOSP fornecem outros exemplos de uso do eBPF:

Para mais informações sobre como usar o eBPF para coletar o gerenciamento de memória do kernel de baixa sobrecarga e histogramas de latência de energia para statsd, consulte Coletar telemetria do kernel com eBPF.