Estender o kernel com eBPF

O Filtro de Pacote Berkeley Estendido (eBPF) é 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 sondas 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 tem suporte para compilar programas em C para eBPF usando a sintaxe simples de arquivo de build descrita neste documento.

Para mais informações sobre os aspectos internos e a arquitetura do eBPF, acesse a página do eBPF de Brendan Gregg (em inglês).

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 em 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 passam a fazer parte da imagem do sistema.

Formato de um programa C eBPF do Android

Um programa em 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 de 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 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 ele. 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 Hooks PROGFUNC em 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 para mais informações sobre kprobes.
tracepoint Conecta PROGFUNC a um ponto de rastreamento. 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 programador seria SEC("tracepoint/sched/sched_switch"), em que sched é o nome do subsistema de rastreamento e sched_switch é o nome do evento de rastreamento. Consulte a documentação do kernel de eventos de rastreamento para mais informações sobre pontos de rastreamento.
skfilter O programa funciona como um filtro de soquete de rede.
schedcls O programa funciona 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 rastreamento sched:sched_switch. Para mais informações, consulte Como anexar programas a pontos de rastreamento.

#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 ele usa funções auxiliares do BPF fornecidas pelo kernel. Especifique o nome da licença do programa como uma 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 .c eBPF, é necessário criar uma entrada no arquivo Android.bp do projeto. Por exemplo, para criar um programa eBPF C 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 em 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 em sysfs

Durante a inicialização, o sistema Android carrega automaticamente todos os objetos eBPF de /system/etc/bpf/, cria os mapas necessários para o programa 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 leitura de mapas. Esta seção descreve as convenções usadas para nomear esses arquivos e os locais deles no sysfs.

Os seguintes arquivos 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 ponto de rastreamento 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 ponto de rastreamento 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 de arquivo pode ser usado para outras operações, como leitura de mapas ou anexação de um programa a um ponto de rastreamento.

Biblioteca BPF do Android

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

Anexar programas a tracepoints

Os programas de ponto de rastreamento são carregados automaticamente na inicialização. Depois do carregamento, o programa de tracepoint precisa ser ativado seguindo estas etapas:

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

O exemplo de código a seguir mostra como anexar o ponto de rastreamento 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 são compatíveis com 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 compatíveis com tipos, resultando em um código mais limpo.

Para mais informações sobre BpfMap, consulte as fontes 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. Filtrar os 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 oferecem mais exemplos de uso do eBPF:

  • O programa em C eBPF do netd é usado pelo daemon de rede (netd) no Android para várias finalidades, como filtragem de soquetes e coleta de estatísticas. Para saber como esse programa é usado, confira as fontes do monitor de tráfego eBPF.

  • O time_in_state programa eBPF C calcula o tempo que um app Android passa em diferentes frequências de CPU, que é usado para calcular a energia.

  • No Android 12, o gpu_mem programa eBPF C rastreia o uso total da memória da GPU para cada processo e para todo o sistema. Esse programa é usado para criação de perfil de memória da GPU.