Μια σύντομη εισαγωγή στο BPF και το eBPF

Γεια σου Χαμπρ! Σας ενημερώνουμε ότι ετοιμαζόμαστε να κυκλοφορήσουμε ένα βιβλίο»Παρατηρησιμότητα Linux με BPF".

Μια σύντομη εισαγωγή στο BPF και το eBPF
Καθώς η εικονική μηχανή BPF συνεχίζει να εξελίσσεται και χρησιμοποιείται ενεργά στην πράξη, έχουμε μεταφράσει ένα άρθρο για εσάς που περιγράφει τα κύρια χαρακτηριστικά και την τρέχουσα κατάστασή της.

Τα τελευταία χρόνια, τα εργαλεία και οι τεχνικές προγραμματισμού έχουν αποκτήσει δημοτικότητα για να αντισταθμίσουν τους περιορισμούς του πυρήνα του Linux σε περιπτώσεις όπου απαιτείται επεξεργασία πακέτων υψηλής απόδοσης. Μία από τις πιο δημοφιλείς μεθόδους αυτού του είδους ονομάζεται παράκαμψη πυρήνα (παράκαμψη πυρήνα) και επιτρέπει, παρακάμπτοντας το επίπεδο δικτύου του πυρήνα, την εκτέλεση όλης της επεξεργασίας πακέτων από το χώρο χρήστη. Η παράκαμψη του πυρήνα περιλαμβάνει επίσης τη διαχείριση της κάρτας δικτύου από χώρο χρήστη. Με άλλα λόγια, όταν εργαζόμαστε με μια κάρτα δικτύου, βασιζόμαστε στον οδηγό χώρο χρήστη.

Μεταφέροντας τον πλήρη έλεγχο της κάρτας δικτύου σε ένα πρόγραμμα χώρου χρήστη, μειώνουμε την επιβάρυνση που προκαλείται από τον πυρήνα (διακόπτες περιβάλλοντος, επεξεργασία επιπέδου δικτύου, διακοπές κ.λπ.), κάτι που είναι πολύ σημαντικό όταν εκτελείται σε ταχύτητες 10 Gb/s ή πιο ψηλά. Παράκαμψη του πυρήνα συν έναν συνδυασμό άλλων χαρακτηριστικών (επεξεργασία παρτίδων) και προσεκτική ρύθμιση απόδοσης (Λογιστική NUMA, Απομόνωση CPU, κ.λπ.) ταιριάζει στα βασικά της υψηλής απόδοσης δικτύωσης χώρου χρήστη. Ίσως ένα υποδειγματικό παράδειγμα αυτής της νέας προσέγγισης στην επεξεργασία πακέτων είναι DPDK από την Intel (Κιτ ανάπτυξης δεδομένων επιπέδου), αν και υπάρχουν άλλα γνωστά εργαλεία και τεχνικές, όπως το VPP από τη Cisco (Vector Packet Processing), το Netmap και, φυσικά, Σναμπ.

Η οργάνωση των αλληλεπιδράσεων δικτύου στο χώρο των χρηστών έχει μια σειρά από μειονεκτήματα:

  • Ένας πυρήνας λειτουργικού συστήματος είναι ένα στρώμα αφαίρεσης για πόρους υλικού. Επειδή τα προγράμματα χώρου χρήστη πρέπει να διαχειρίζονται άμεσα τους πόρους τους, πρέπει επίσης να διαχειρίζονται το δικό τους υλικό. Αυτό συχνά σημαίνει προγραμματισμό των δικών σας προγραμμάτων οδήγησης.
  • Δεδομένου ότι εγκαταλείπουμε εντελώς τον χώρο του πυρήνα, εγκαταλείπουμε επίσης όλες τις λειτουργίες δικτύωσης που παρέχει ο πυρήνας. Τα προγράμματα χώρου χρήστη πρέπει να εφαρμόσουν εκ νέου λειτουργίες που μπορεί να παρέχονται ήδη από τον πυρήνα ή το λειτουργικό σύστημα.
  • Τα προγράμματα λειτουργούν σε λειτουργία sandbox, γεγονός που περιορίζει σοβαρά την αλληλεπίδρασή τους και τα εμποδίζει να ενσωματωθούν με άλλα μέρη του λειτουργικού συστήματος.

Ουσιαστικά, κατά τη δικτύωση στο χώρο χρήστη, τα κέρδη απόδοσης επιτυγχάνονται με τη μετακίνηση της επεξεργασίας πακέτων από τον πυρήνα στο χώρο του χρήστη. Το XDP κάνει ακριβώς το αντίθετο: μετακινεί προγράμματα δικτύου από τον χώρο χρήστη (φίλτρα, μετατροπείς, δρομολόγηση κ.λπ.) στην περιοχή του πυρήνα. Το XDP μας επιτρέπει να εκτελέσουμε τη λειτουργία δικτύου μόλις το πακέτο φτάσει στη διεπαφή δικτύου και προτού αρχίσει να ταξιδεύει μέχρι το υποσύστημα δικτύου του πυρήνα. Ως αποτέλεσμα, η ταχύτητα επεξεργασίας πακέτων αυξάνεται σημαντικά. Ωστόσο, πώς ο πυρήνας επιτρέπει στο χρήστη να τρέξει τα προγράμματά του στο χώρο του πυρήνα; Πριν απαντήσουμε σε αυτήν την ερώτηση, ας δούμε τι είναι το BPF.

BPF και eBPF

Παρά το όχι εντελώς σαφές όνομα, το BPF (Packet Filtering, Berkeley) είναι, στην πραγματικότητα, ένα μοντέλο εικονικής μηχανής. Αυτή η εικονική μηχανή σχεδιάστηκε αρχικά για να χειρίζεται το φιλτράρισμα πακέτων, εξ ου και το όνομα.

Ένα από τα πιο γνωστά εργαλεία που χρησιμοποιούν το BPF είναι tcpdump. Κατά τη λήψη πακέτων με tcpdump ο χρήστης μπορεί να καθορίσει μια έκφραση για φιλτράρισμα πακέτων. Μόνο τα πακέτα που ταιριάζουν με αυτήν την έκφραση θα καταγράφονται. Για παράδειγμα, η έκφραση "tcp dst port 80” αναφέρεται σε όλα τα πακέτα TCP που φτάνουν στη θύρα 80. Ο μεταγλωττιστής μπορεί να συντομεύσει αυτήν την έκφραση μετατρέποντάς την σε bytecode BPF.

$ 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

Αυτό είναι βασικά αυτό που κάνει το παραπάνω πρόγραμμα:

  • Οδηγία (000): Φορτώνει το πακέτο στη μετατόπιση 12, ως λέξη 16-bit, στον συσσωρευτή. Η μετατόπιση 12 αντιστοιχεί στον αιθέρα τύπο του πακέτου.
  • Οδηγία (001): συγκρίνει την τιμή στον συσσωρευτή με το 0x86dd, δηλαδή με την τιμή αιθέριου για το IPv6. Εάν το αποτέλεσμα είναι αληθές, τότε ο μετρητής προγράμματος πηγαίνει στην εντολή (002) και αν όχι, τότε στο (006).
  • Οδηγία (006): συγκρίνει την τιμή με το 0x800 (τιμή αιθέρα για IPv4). Εάν η απάντηση είναι αληθής, τότε το πρόγραμμα πηγαίνει στο (007), αν όχι, τότε στο (015).

Και ούτω καθεξής, μέχρι το πρόγραμμα φιλτραρίσματος πακέτων να επιστρέψει ένα αποτέλεσμα. Συνήθως είναι boolean. Η επιστροφή μιας μη μηδενικής τιμής (εντολή (014)) σημαίνει ότι το πακέτο ταιριάζει και η επιστροφή μηδέν (εντολή (015)) σημαίνει ότι το πακέτο δεν ταιριάζει.

Η εικονική μηχανή BPF και ο bytecode της προτάθηκαν από τον Steve McCann και τον Van Jacobson στα τέλη του 1992 όταν κυκλοφόρησε η εργασία τους. Φίλτρο πακέτων BSD: Νέα αρχιτεκτονική για τη λήψη πακέτων σε επίπεδο χρήστη, για πρώτη φορά αυτή η τεχνολογία παρουσιάστηκε στο συνέδριο Usenix τον χειμώνα του 1993.

Επειδή το BPF είναι μια εικονική μηχανή, ορίζει το περιβάλλον στο οποίο εκτελούνται τα προγράμματα. Εκτός από τον bytecode, ορίζει επίσης ένα μοντέλο μνήμης πακέτων (οι εντολές φόρτωσης εφαρμόζονται σιωπηρά σε ένα πακέτο), καταχωρητές (Α και Χ, καταχωρητές συσσωρευτή και ευρετηρίου), αποθήκευση μνήμης ξυσίματος και έναν σιωπηρό μετρητή προγράμματος. Είναι ενδιαφέρον ότι ο bytecode BPF διαμορφώθηκε σύμφωνα με το Motorola 6502 ISA. Όπως θυμόταν ο Steve McCann στο δικό του έκθεση της ολομέλειας στο Sharkfest '11, ήταν εξοικειωμένος με το build 6502 από το γυμνάσιο όταν προγραμματιζόταν στο Apple II, και αυτή η γνώση επηρέασε τη δουλειά του σχεδίασης του bytecode BPF.

Η υποστήριξη BPF υλοποιείται στον πυρήνα Linux στην έκδοση 2.5 και μεταγενέστερη, που προστέθηκε κυρίως από τον Jay Schullist. Ο κώδικας BPF παρέμεινε αμετάβλητος μέχρι το 2011, όταν ο Eric Dumaset επανασχεδίασε τον διερμηνέα BPF για να λειτουργεί σε λειτουργία JIT (Πηγή: JIT για φίλτρα πακέτων). Μετά από αυτό, αντί να ερμηνεύει τον bytecode BPF, ο πυρήνας θα μπορούσε να μετατρέψει απευθείας τα προγράμματα BPF στην αρχιτεκτονική στόχο: x86, ARM, MIPS, κ.λπ.

Αργότερα, το 2014, ο Alexei Starovoitov πρότεινε έναν νέο μηχανισμό JIT για την BPF. Στην πραγματικότητα, αυτό το νέο JIT έγινε μια νέα αρχιτεκτονική βασισμένη στο BPF και ονομάστηκε eBPF. Νομίζω ότι και τα δύο VM συνυπήρχαν για κάποιο χρονικό διάστημα, αλλά το φιλτράρισμα πακέτων εφαρμόζεται επί του παρόντος πάνω από το eBPF. Στην πραγματικότητα, σε πολλά σύγχρονα παραδείγματα τεκμηρίωσης, το BPF αναφέρεται ως eBPF και το κλασικό BPF είναι γνωστό σήμερα ως cBPF.

Το eBPF επεκτείνει την κλασική εικονική μηχανή BPF με διάφορους τρόπους:

  • Βασίζεται σε σύγχρονες αρχιτεκτονικές 64-bit. Το eBPF χρησιμοποιεί καταχωρητές 64-bit και αυξάνει τον αριθμό των διαθέσιμων καταχωρητών από 2 (συσσωρευτής και X) σε 10. Το eBPF παρέχει επίσης πρόσθετους κωδικούς λειτουργίας (BPF_MOV, BPF_JNE, BPF_CALL…).
  • Αποσπάστηκε από το υποσύστημα του επιπέδου δικτύου. Το BPF συνδέθηκε με το μοντέλο δεδομένων παρτίδας. Δεδομένου ότι χρησιμοποιήθηκε για το φιλτράρισμα πακέτων, ο κώδικάς του βρισκόταν στο υποσύστημα που παρείχε αλληλεπιδράσεις δικτύου. Ωστόσο, η εικονική μηχανή eBPF δεν είναι πλέον συνδεδεμένη με ένα μοντέλο δεδομένων και μπορεί να χρησιμοποιηθεί για οποιονδήποτε σκοπό. Έτσι, τώρα το πρόγραμμα eBPF μπορεί να συνδεθεί σε σημείο εντοπισμού ή kprobe. Αυτό ανοίγει την πόρτα σε όργανα eBPF, ανάλυση απόδοσης και πολλές άλλες περιπτώσεις χρήσης στο πλαίσιο άλλων υποσυστημάτων πυρήνα. Τώρα ο κώδικας eBPF βρίσκεται στη δική του διαδρομή: kernel/bpf.
  • Παγκόσμια καταστήματα δεδομένων που ονομάζονται Χάρτες. Οι χάρτες είναι καταστήματα κλειδιών-τιμών που παρέχουν ανταλλαγή δεδομένων μεταξύ του χώρου χρήστη και του χώρου του πυρήνα. Το eBPF παρέχει διάφορους τύπους καρτών.
  • Δευτερεύουσες λειτουργίες. Συγκεκριμένα, για να αντικαταστήσετε ένα πακέτο, να υπολογίσετε ένα άθροισμα ελέγχου ή να κλωνοποιήσετε ένα πακέτο. Αυτές οι συναρτήσεις εκτελούνται μέσα στον πυρήνα και δεν ανήκουν σε προγράμματα χώρου χρήστη. Επιπλέον, μπορούν να πραγματοποιηθούν κλήσεις συστήματος από προγράμματα eBPF.
  • Τερματισμός κλήσεων. Το μέγεθος του προγράμματος σε eBPF περιορίζεται στα 4096 byte. Η δυνατότητα τερματισμού κλήσης επιτρέπει σε ένα πρόγραμμα eBPF να μεταφέρει τον έλεγχο σε ένα νέο πρόγραμμα eBPF και έτσι να παρακάμψει αυτόν τον περιορισμό (μέχρι 32 προγράμματα μπορούν να συνδεθούν με αυτόν τον τρόπο).

Παράδειγμα eBPF

Υπάρχουν πολλά παραδείγματα για το eBPF στις πηγές του πυρήνα του Linux. Διατίθενται στο samples/bpf/. Για να συγκεντρώσετε αυτά τα παραδείγματα, απλώς πληκτρολογήστε:

$ sudo make samples/bpf/

Δεν θα γράψω ο ίδιος νέο παράδειγμα για το eBPF, αλλά θα χρησιμοποιήσω ένα από τα δείγματα που είναι διαθέσιμα στο samples/bpf/. Θα κοιτάξω μερικά μέρη του κώδικα και θα εξηγήσω πώς λειτουργεί. Για παράδειγμα, επέλεξα το πρόγραμμα tracex4.

Γενικά, κάθε ένα από τα παραδείγματα στα δείγματα/bpf/ αποτελείται από δύο αρχεία. Σε αυτήν την περίπτωση:

  • tracex4_kern.c, περιέχει τον πηγαίο κώδικα που θα εκτελεστεί στον πυρήνα ως bytecode eBPF.
  • tracex4_user.c, περιέχει ένα πρόγραμμα από το χώρο χρήστη.

Σε αυτήν την περίπτωση, πρέπει να μεταγλωττίσουμε tracex4_kern.c σε bytecode eBPF. Αυτή τη στιγμή στο gcc δεν υπάρχει τμήμα διακομιστή για το eBPF. Ευτυχώς, clang μπορεί να παράγει bytecode eBPF. Makefile χρήσεις clang να μεταγλωττίσει tracex4_kern.c στο αρχείο αντικειμένου.

Ανέφερα παραπάνω ότι ένα από τα πιο ενδιαφέροντα χαρακτηριστικά του eBPF είναι οι χάρτες. Το tracex4_kern ορίζει έναν χάρτη:

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 είναι ένας από τους πολλούς τύπους καρτών που προσφέρει το eBPF. Σε αυτή την περίπτωση, είναι απλώς ένα χασίς. Μπορεί επίσης να έχετε προσέξει τη διαφήμιση SEC("maps"). Το SEC είναι μια μακροεντολή που χρησιμοποιείται για τη δημιουργία μιας νέας ενότητας ενός δυαδικού αρχείου. Στην πραγματικότητα, στο παράδειγμα tracex4_kern ορίζονται δύο ακόμη ενότητες:

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

Αυτές οι δύο λειτουργίες σάς επιτρέπουν να αφαιρέσετε μια καταχώρηση από τον χάρτη (kprobe/kmem_cache_free) και προσθέστε μια νέα καταχώρηση στον χάρτη (kretprobe/kmem_cache_alloc_node). Όλα τα ονόματα συναρτήσεων γραμμένα με κεφαλαία γράμματα αντιστοιχούν σε μακροεντολές που ορίζονται στο bpf_helpers.h.

Εάν αποθέσω τις ενότητες του αρχείου αντικειμένου, θα πρέπει να δω ότι αυτές οι νέες ενότητες έχουν ήδη οριστεί:

$ 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.c, κύριο πρόγραμμα. Βασικά, αυτό το πρόγραμμα ακούει εκδηλώσεις kmem_cache_alloc_node. Όταν συμβεί ένα τέτοιο συμβάν, εκτελείται ο αντίστοιχος κώδικας eBPF. Ο κώδικας αποθηκεύει το χαρακτηριστικό IP του αντικειμένου σε έναν χάρτη και, στη συνέχεια, το αντικείμενο επαναλαμβάνεται μέσω του κύριου προγράμματος. Παράδειγμα:

$ 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

Πώς σχετίζονται το πρόγραμμα χώρου χρήστη και το πρόγραμμα eBPF; Κατά την προετοιμασία tracex4_user.c φορτώνει το αρχείο αντικειμένου tracex4_kern.o χρησιμοποιώντας τη συνάρτηση load_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;
}

Κάνοντας load_bpf_file Προστίθενται ανιχνευτές που ορίζονται στο αρχείο eBPF /sys/kernel/debug/tracing/kprobe_events. Τώρα ακούμε αυτά τα γεγονότα και το πρόγραμμά μας μπορεί να κάνει κάτι όταν συμβαίνουν.

$ 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

Όλα τα άλλα προγράμματα στο sample/bpf/ έχουν παρόμοια δομή. Περιέχουν πάντα δύο αρχεία:

  • XXX_kern.c: Πρόγραμμα eBPF.
  • XXX_user.c: κύριο πρόγραμμα.

Το πρόγραμμα eBPF ορίζει τους χάρτες και τις λειτουργίες που σχετίζονται με μια ενότητα. Όταν ο πυρήνας εκπέμπει ένα συμβάν συγκεκριμένου τύπου (για παράδειγμα, tracepoint), εκτελούνται οι δεσμευμένες συναρτήσεις. Οι χάρτες παρέχουν επικοινωνία μεταξύ ενός προγράμματος πυρήνα και ενός προγράμματος χώρου χρήστη.

Συμπέρασμα

Σε αυτό το άρθρο, το BPF και το eBPF συζητήθηκαν με γενικούς όρους. Γνωρίζω ότι υπάρχουν πολλές πληροφορίες και πόροι για το eBPF σήμερα, γι' αυτό θα προτείνω μερικά ακόμη υλικά για περαιτέρω μελέτη.

Συνιστώ να διαβάσετε:

Πηγή: www.habr.com

Προσθέστε ένα σχόλιο