Brève introduction à BPF et eBPF

Bonjour, Habr! Nous vous informons que nous nous apprêtons à sortir le livre " Linux Observability with BPF ".


Alors que la machine virtuelle BPF continue d'évoluer et est activement appliquée dans la pratique, nous avons traduit pour vous un article qui décrit ses principales fonctionnalités et son état actuel.

Ces dernières années, les outils et techniques de programmation ont commencé à gagner en popularité, conçus pour compenser les limitations du noyau Linux dans les cas où un traitement de package hautes performances est requis. L'une des méthodes les plus populaires de ce type est appelée contournement du noyau (contournement du noyau) et permet aux cœurs de couche réseau qui passent d'effectuer tout le traitement des paquets à partir de l'espace utilisateur. Le contournement du noyau implique également la gestion de la carte réseau à partir de l'espace utilisateur . En d'autres termes, lorsque vous travaillez avec une carte réseau, nous nous appuyons sur le pilote de l' espace utilisateur .

En transférant le contrôle total sur la carte réseau au programme depuis l'espace utilisateur, nous réduisons les coûts associés au noyau (contexte de commutation, traitement du niveau réseau, interruptions, etc.), ce qui est assez important lorsque vous travaillez à des vitesses de 10 Gb / s ou plus. Le contournement du noyau et une combinaison d'autres fonctionnalités ( traitement par lots ) et un réglage précis des performances ( comptabilité NUMA , isolation du processeur , etc.) correspondent aux bases du traitement réseau hautes performances dans l'espace utilisateur. Le DPDK ( Data Plane Development Kit) d' Intel est peut-être un exemple modèle de cette nouvelle approche du traitement des packages .), bien qu'il existe d'autres outils et techniques bien connus, notamment VPP de Cisco (Vector Packet Processing), Netmap et, bien sûr, Snabb .

L'organisation des interactions réseau dans l'espace utilisateur présente un certain nombre d'inconvénients:

  • Le noyau du système d'exploitation est le niveau d'abstraction des ressources matérielles. Étant donné que les programmes de l'espace utilisateur doivent gérer leurs ressources directement, ils doivent également gérer leur propre matériel. Cela signifie souvent que vous devez programmer vos propres pilotes.
  • , , . , , , .
  • , .

En substance, lors de l'organisation des interactions réseau dans l'espace utilisateur, une productivité accrue est obtenue en transférant le traitement des paquets du noyau vers l'espace utilisateur. XDP fait exactement le contraire: déplace les programmes réseau de l'espace utilisateur (filtres, convertisseurs, routage, etc.) vers la zone du noyau. XDP nous permet d'effectuer une fonction réseau dès que le paquet arrive à l'interface réseau et avant qu'il ne commence à remonter dans le sous-système réseau du noyau. Par conséquent, la vitesse de traitement des paquets augmente considérablement. Cependant, comment le noyau permet-il à l'utilisateur d'exécuter ses programmes dans l'espace noyau? Avant de répondre à cette question, regardons ce qu'est BPF.

BPF et eBPF

Malgré le nom peu clair BPF (Packet Filtering, Berkeley), il s'agit en fait d'un modèle de machine virtuelle. Cette machine virtuelle a été initialement conçue pour gérer le filtrage de paquets, d'où son nom.

L'un des outils les plus connus utilisant BPF est tcpdump. Lors de la capture de paquets avec, l' tcpdumputilisateur peut spécifier une expression pour filtrer les paquets. Seuls les paquets correspondant à cette expression seront capturés. Par exemple, l'expression « tcp dst port 80» s'applique à tous les paquets TCP arrivant au port 80. Le compilateur peut raccourcir cette expression en la convertissant en bytecode BPF. Voici essentiellement ce que fait le programme ci-dessus:

$ 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




  • Instruction (000): télécharge un paquet à décalage 12 en tant que mot de 16 bits dans la batterie. Un décalage de 12 correspond à un paquet de type éther.
  • (001): 0x86dd, , ethertype- IPv6. true, (002), – (006).
  • (006): 0x800 (ethertype- IPv4). true, (007), – (015).

Et ainsi de suite, jusqu'à ce que le programme de filtrage de paquets renvoie un résultat. Il s'agit généralement d'un Bulean. Le retour d'une valeur non nulle (instruction (014)) signifie que le paquet s'est approché, et le retour de zéro (instruction (015)) signifie que le paquet n'est pas arrivé.

La machine virtuelle BPF et son bytecode ont été proposés par Steve McCann et Van Jacobson à la fin de 1992 lorsque leur article BSD Packet Filter: A New Architecture for Packet Capture at the User Level , a été présenté pour la première fois lors d'une conférence Usenix à l'hiver 1993.

Étant donné que BPF est une machine virtuelle, elle définit l'environnement dans lequel les programmes s'exécutent. En plus du bytecode, il définit également un modèle de mémoire par lots (les instructions de démarrage sont implicitement appliquées au package), les registres (A et X; batterie et registres d'index), le stockage de la mémoire de travail et un compteur de programme implicite. Fait intéressant, le bytecode BPF a été calqué sur le Motorola 6502 ISA. Comme Steve McCann l'a rappelé lors de sa conférence plénière au Sharkfest '11, il connaissait la version 6502 depuis le lycée lorsqu'il a programmé dans Apple II, et cette connaissance a influencé son travail sur la conception du bytecode BPF.

La prise en charge de BPF est implémentée dans le noyau Linux dans la version v2.5 et supérieure, ajoutée principalement par les efforts de Jay Schullist. Le code BPF est resté inchangé jusqu'en 2011, date à laquelle Eric Dumazett a repensé l'interpréteur BPF pour fonctionner en mode JIT (Source: JIT pour les filtres de paquets ). Après cela, le noyau, au lieu d'interpréter le code d'octet BPF, pourrait convertir directement les programmes BPF en architecture cible: x86, ARM, MIPS, etc.

Plus tard, en 2014, Alexey Starovoitov a proposé un nouveau mécanisme JIT pour BPF. En fait, ce nouveau JIT est devenu la nouvelle architecture basée sur BPF et s'appelle eBPF. Je pense que pendant un certain temps, les deux machines virtuelles ont coexisté, mais actuellement le filtrage de paquets est mis en œuvre sur la base d'eBPF. En fait, dans de nombreux exemples de documentation moderne, BPF est censé signifier eBPF, et BPF classique est maintenant connu sous le nom de cBPF.

eBPF étend la machine virtuelle BPF classique de plusieurs manières:

  • Basé sur des architectures 64 bits modernes. eBPF utilise des registres 64 bits et augmente le nombre de registres disponibles de 2 (batterie et X) à 10. eBPF fournit également des codes de fonctionnement supplémentaires (BPF_MOV, BPF_JNE, BPF_CALL ...).
  • . BPF . , , . , eBPF . , eBPF tracepoint kprobe. eBPF, . eBPF : kernel/bpf.
  • . – «-», . eBPF .
  • . , , . . , eBPF .
  • . eBPF 4096 . eBPF eBPF- ( 32 ).

eBPF: un exemple

Il existe plusieurs exemples pour eBPF dans les sources du noyau Linux. Ils sont disponibles sur samples / bpf /. Pour compiler ces exemples, entrez simplement:

$ sudo make samples/bpf/

Je n'écrirai pas moi-même un nouvel exemple pour eBPF, mais j'utiliserai l'un des exemples disponibles dans samples / bpf /. Je vais regarder certaines sections du code et expliquer comment cela fonctionne. À titre d'exemple, j'ai choisi un programme tracex4.

En général, chacun des exemples dans samples / bpf / se compose de deux fichiers. Dans ce cas:

  • tracex4_kern.c, contient le code source qui doit être exécuté dans le noyau en tant que bytecode eBPF.
  • tracex4_user.c, contient un programme de l'espace utilisateur.

Dans ce cas, nous devons compiler l' tracex4_kern.ceBPF en bytecode. Il n'y a actuellement gccaucune partie serveur pour eBPF. Heureusement, il clangpeut émettre un bytecode eBPF. Makefileutilise clangpour compiler tracex4_kern.cun fichier objet.

J'ai mentionné ci-dessus que l'une des caractéristiques les plus intéressantes des eBPF est les cartes. tracex4_kern définit une carte:

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- L'un des nombreux types de cartes proposés par eBPF. Dans ce cas, c'est juste un hachage. Vous avez peut-être également remarqué une annonce SEC("maps"). SEC est une macro utilisée pour créer une nouvelle section d'un fichier binaire. En fait, l'exemple tracex4_kerndéfinit deux autres sections:

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

Ces deux fonctions vous permettent de supprimer une entrée de la carte ( kprobe/kmem_cache_free) et d'ajouter un nouvel enregistrement ( kretprobe/kmem_cache_alloc_node) à la carte . Tous les noms de fonction écrits en majuscules correspondent aux macros définies dans bpf_helpers.h.

Si je produis un vidage de sections du fichier objet, je dois voir que ces nouvelles sections sont déjà définies: Toujours là , le programme principal. Fondamentalement, ce programme écoute les événements . Lorsqu'un tel événement se produit, le code eBPF correspondant est exécuté. Le code stocke l'attribut IP de l'objet dans la carte, puis cet objet est affiché cycliquement dans le programme principal. Exemple: Comment le programme d'espace utilisateur et le programme eBPF sont-ils liés? À l'initialisation, il charge un fichier objet à l' aide d'une fonction .

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

Une fois exécutées, les load_bpf_filesondes définies dans le fichier eBPF sont ajoutées à /sys/kernel/debug/tracing/kprobe_events. Maintenant, nous écoutons ces événements et notre programme peut faire quelque chose quand ils se produisent. Tous les autres programmes de sample / bpf / sont structurés de manière similaire. Ils ont toujours deux fichiers:

$ 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: programme eBPF.
  • XXX_user.c: programme principal.

Le programme eBPF définit les cartes et les fonctions associées à la section. Lorsque le noyau lance un événement d'un certain type (par exemple, tracepoint), les fonctions liées sont exécutées. Les cartes permettent l'échange de données entre le programme du noyau et le programme de l'espace utilisateur.

Conclusion

Cet article décrit BPF et eBPF. Je sais qu'aujourd'hui il y a beaucoup d'informations et de ressources sur EBPF, donc je recommande quelques autres matériaux pour une étude plus approfondie je.

Recommande la lecture:


All Articles