Breve introdução ao BPF e eBPF

Olá Habr! Informamos que estamos nos preparando para lançar o livro " Observabilidade do Linux com BPF ".


À medida que a máquina virtual BPF continua a evoluir e é ativamente aplicada na prática, traduzimos um artigo para você que descreve seus principais recursos e status atual.

Nos últimos anos, as ferramentas e técnicas de programação começaram a ganhar popularidade, projetadas para compensar as limitações do kernel Linux nos casos em que o processamento de pacotes de alto desempenho é necessário. Um dos métodos mais populares desse tipo é chamado de desvio do kernel (desvio do kernel) e permite que os núcleos da camada de rede passantes executem todo o processamento de pacotes no espaço do usuário. Ignorar o kernel também envolve gerenciar a placa de rede a partir do espaço do usuário . Em outras palavras, ao trabalhar com uma placa de rede, contamos com o driver de espaço do usuário .

Ao transferir o controle total da placa de rede para o programa a partir do espaço do usuário, reduzimos os custos associados ao kernel (alternando o contexto, processando o nível da rede, interrompendo etc.), o que é bastante importante quando se trabalha a velocidades de 10 Gb / s ou mais. O desvio do kernel, além de uma combinação de outros recursos ( processamento em lote ) e ajuste preciso do desempenho ( contabilidade NUMA , isolamento da CPU , etc.) correspondem aos princípios básicos do processamento de rede de alto desempenho no espaço do usuário. Talvez um exemplo de modelo dessa nova abordagem para o processamento de pacotes seja o DPDK da Intel ( Data Plane Development Kit)), embora existam outras ferramentas e técnicas conhecidas, incluindo o VPP da Cisco (Vector Packet Processing), Netmap e, é claro, Snabb .

A organização das interações de rede no espaço do usuário tem várias desvantagens:

  • O kernel do sistema operacional é o nível de abstração dos recursos de hardware. Como os programas de espaço do usuário precisam gerenciar seus recursos diretamente, eles também precisam gerenciar seu próprio hardware. Isso geralmente significa que você precisa programar seus próprios drivers.
  • , , . , , , .
  • , .

Em essência, ao organizar interações de rede no espaço do usuário, é possível aumentar a produtividade transferindo o processamento de pacotes do kernel para o espaço do usuário. O XDP faz exatamente o oposto: move os programas de rede do espaço do usuário (filtros, conversores, roteamento etc.) para a área do kernel. O XDP nos permite executar uma função de rede assim que o pacote chega à interface de rede e antes de começar a subir para o subsistema de rede do kernel. Como resultado, a velocidade de processamento de pacotes aumenta significativamente. No entanto, como o kernel permite ao usuário executar seus programas no espaço do kernel? Antes de responder a essa pergunta, vejamos o que é BPF.

BPF e eBPF

Apesar do nome não tão claro BPF (Packet Filtering, Berkeley), é, de fato, um modelo de máquina virtual. Esta máquina virtual foi projetada originalmente para lidar com a filtragem de pacotes, daí o nome.

Uma das ferramentas mais conhecidas usando BPF é tcpdump. Ao capturar pacotes com, o tcpdumpusuário pode especificar uma expressão para filtrar pacotes. Somente pacotes correspondentes a esta expressão serão capturados. Por exemplo, a expressão “ tcp dst port 80” se aplica a todos os pacotes TCP que chegam à porta 80. O compilador pode encurtar essa expressão convertendo-a no bytecode BPF. Aqui está o que o programa acima basicamente faz:

$ 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




  • Instrução (000): baixa um pacote no deslocamento 12 como uma palavra de 16 bits na bateria. Um deslocamento de 12 corresponde a um pacote ethertype.
  • (001): 0x86dd, , ethertype- IPv6. true, (002), – (006).
  • (006): 0x800 (ethertype- IPv4). true, (007), – (015).

E assim por diante, até que o programa de filtragem de pacotes retorne um resultado. Isso geralmente é um bulean. Retornar um valor diferente de zero (instrução (014)) significa que o pacote se aproximou e retornar zero (instrução (015)) significa que o pacote não chegou.

A máquina virtual BPF e seu bytecode foram propostos por Steve McCann e Van Jacobson no final de 1992, quando o artigo BSD Packet Filter: Uma Nova Arquitetura para Captura de Pacotes no Nível do Usuário foi introduzido pela primeira vez em uma conferência da Usenix no inverno de 1993.

Como o BPF é uma máquina virtual, ele define o ambiente em que os programas são executados. Além do bytecode, ele também define um modelo de memória em lote (instruções de inicialização são implicitamente aplicadas ao pacote), registradores (A e X; registros de bateria e índice), armazenamento de memória temporária e um contador implícito de programa. Curiosamente, o bytecode BPF foi modelado após o Motorola 6502 ISA. Como Steve McCann lembrou em sua palestra plenária no Sharkfest '11, ele estava familiarizado com a versão 6502 desde o colegial, quando programava no Apple II, e esse conhecimento influenciou seu trabalho na criação do bytecode da BPF.

O suporte ao BPF é implementado no kernel do Linux na versão v2.5 e superior, adicionado principalmente pelos esforços de Jay Schullist. O código BPF permaneceu inalterado até 2011, quando Eric Dumazett redesenhou o intérprete BPF para operar no modo JIT (Fonte: JIT para filtros de pacotes ). Depois disso, o kernel, em vez de interpretar o código de bytes BPF, pode converter diretamente os programas BPF na arquitetura de destino: x86, ARM, MIPS, etc.

Mais tarde, em 2014, Alexey Starovoitov propôs um novo mecanismo JIT para o BPF. De fato, esse novo JIT se tornou a nova arquitetura baseada em BPF e é chamado eBPF. Eu acho que por algum tempo as duas máquinas virtuais coexistiram, mas atualmente a filtragem de pacotes é implementada com base no eBPF. De fato, em muitos exemplos de documentação moderna, BPF é entendido como eBPF, e o BPF clássico agora é conhecido como cBPF.

O eBPF estende a máquina virtual clássica da BPF de várias maneiras:

  • Baseado em arquiteturas modernas de 64 bits. O eBPF usa registros de 64 bits e aumenta o número de registros disponíveis de 2 (bateria e X) para 10. O eBPF também fornece códigos de operação adicionais (BPF_MOV, BPF_JNE, BPF_CALL ...).
  • . BPF . , , . , eBPF . , eBPF tracepoint kprobe. eBPF, . eBPF : kernel/bpf.
  • . – «-», . eBPF .
  • . , , . . , eBPF .
  • . eBPF 4096 . eBPF eBPF- ( 32 ).

eBPF: um exemplo

Existem vários exemplos de eBPF nas fontes do kernel do Linux. Eles estão disponíveis em samples / bpf /. Para compilar esses exemplos, basta digitar:

$ sudo make samples/bpf/

Não escreverei um novo exemplo para o eBPF, mas usarei uma das amostras disponíveis em samples / bpf /. Vou examinar algumas seções do código e explicar como ele funciona. Como exemplo, eu escolhi um programa tracex4.

Em geral, cada um dos exemplos em samples / bpf / consiste em dois arquivos. Nesse caso:

  • tracex4_kern.c, contém o código-fonte que deve ser executado no kernel como código de bytes do eBPF.
  • tracex4_user.c, contém um programa do espaço do usuário.

Nesse caso, precisamos compilar o tracex4_kern.ceBPF no bytecode. No momento, gccnão há parte do servidor para o eBPF. Felizmente, ele clangpode emitir o bytecode do eBPF. Makefileusa clangpara compilar tracex4_kern.cum arquivo de objeto.

Mencionei acima que um dos recursos mais interessantes dos eBPFs são os cartões. tracex4_kern define um mapa:

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- Um dos muitos tipos de cartões oferecidos pelo eBPF. Nesse caso, é apenas um hash. Você também pode ter notado um anúncio SEC("maps"). SEC é uma macro usada para criar uma nova seção de um arquivo binário. Na verdade, o exemplo tracex4_kerndefine mais duas seções:

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

Essas duas funções permitem excluir uma entrada do cartão ( kprobe/kmem_cache_free) e adicionar um novo registro ( kretprobe/kmem_cache_alloc_node) ao cartão . Todos os nomes de funções escritos em letras maiúsculas correspondem às macros definidas em bpf_helpers.h.

Se eu emitir um despejo de seções do arquivo de objeto, devo ver que essas novas seções já estão definidas: Ainda está lá , o programa principal. Basicamente, este programa escuta eventos . Quando esse evento ocorre, o código eBPF correspondente é executado. O código armazena o atributo IP do objeto no mapa e, em seguida, esse objeto é exibido ciclicamente no programa principal. Exemplo: como o programa espacial do usuário e o programa eBPF estão relacionados? Na inicialização, ele carrega um arquivo de objeto usando uma função .

$ 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.ckmem_cache_alloc_node

$ 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


tracex4_user.ctracex4_kern.oload_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;
}

Quando executadas, as load_bpf_fileanálises definidas no arquivo eBPF são adicionadas /sys/kernel/debug/tracing/kprobe_events. Agora estamos ouvindo esses eventos, e nosso programa pode fazer algo quando eles acontecem. Todos os outros programas em sample / bpf / são estruturados de maneira semelhante. Eles sempre têm dois arquivos:

$ 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




  • XXX_kern.c: programa eBPF.
  • XXX_user.c: programa principal.

O programa eBPF define os cartões e funções associados à seção. Quando o kernel lança um evento de um determinado tipo (por exemplo tracepoint), as funções associadas são executadas. Os mapas fornecem troca de dados entre o programa do kernel e o programa de espaço do usuário.

Conclusão

Este artigo descreveu o BPF e o eBPF. Sei que hoje há uma grande quantidade de informações e recursos sobre EBPF, então eu recomendo mais alguns materiais para um estudo mais aprofundado I.

Recomendo a leitura:


All Articles