Pengantar Singkat BPF dan eBPF

Hei Habr! Kami informasikan bahwa kami sedang mempersiapkan untuk merilis buku "Observabilitas Linux dengan BPF".

Pengantar Singkat BPF dan eBPF
Karena mesin virtual BPF terus berkembang dan digunakan secara aktif dalam praktiknya, kami telah menerjemahkan artikel untuk Anda yang menjelaskan fitur utamanya dan keadaan saat ini.

Dalam beberapa tahun terakhir, alat dan teknik pemrograman telah mendapatkan popularitas untuk mengkompensasi keterbatasan kernel Linux dalam kasus di mana pemrosesan paket berkinerja tinggi diperlukan. Salah satu metode paling populer dari jenis ini disebut bypass inti (bypass kernel) dan memungkinkan, melewati lapisan jaringan kernel, untuk melakukan semua pemrosesan paket dari ruang pengguna. Melewati kernel juga melibatkan pengelolaan kartu jaringan dari ruang pengguna. Dengan kata lain, saat bekerja dengan kartu jaringan, kami mengandalkan driver ruang pengguna.

Dengan mentransfer kendali penuh kartu jaringan ke program ruang pengguna, kami mengurangi overhead kernel (sakelar konteks, pemrosesan lapisan jaringan, interupsi, dll.), yang cukup penting saat berjalan dengan kecepatan 10 Gb / dtk atau lebih tinggi. Melewati kernel plus kombinasi fitur lain (pemrosesan batch) dan penyetelan kinerja yang hati-hati (akuntansi NUMA, isolasi CPU, dll.) sesuai dengan dasar-dasar jaringan ruang pengguna berkinerja tinggi. Mungkin contoh teladan dari pendekatan baru untuk pemrosesan paket ini adalah DPDK dari Intel (Kit Pengembangan Bidang Data), meskipun ada alat dan teknik terkenal lainnya, termasuk VPP dari Cisco (Vector Packet Processing), Netmap dan, tentu saja, snabb.

Organisasi interaksi jaringan di ruang pengguna memiliki sejumlah kelemahan:

  • Kernel OS adalah lapisan abstraksi untuk sumber daya perangkat keras. Karena program ruang pengguna harus mengelola sumber dayanya secara langsung, mereka juga harus mengelola perangkat kerasnya sendiri. Ini sering berarti memprogram driver Anda sendiri.
  • Karena kami benar-benar melepaskan ruang kernel, kami juga melepaskan semua fungsionalitas jaringan yang disediakan oleh kernel. Program ruang pengguna harus mengimplementasikan ulang fitur yang mungkin sudah disediakan oleh kernel atau sistem operasi.
  • Program bekerja dalam mode kotak pasir, yang secara serius membatasi interaksinya dan mencegahnya berintegrasi dengan bagian lain dari sistem operasi.

Pada intinya, saat membuat jaringan di ruang pengguna, peningkatan kinerja dicapai dengan memindahkan pemrosesan paket dari kernel ke ruang pengguna. XDP melakukan sebaliknya: ia memindahkan program jaringan dari ruang pengguna (filter, konverter, perutean, dll.) ke area kernel. XDP memungkinkan kita untuk menjalankan fungsi jaringan segera setelah paket menyentuh antarmuka jaringan dan sebelum mulai berjalan ke subsistem jaringan dari kernel. Akibatnya, kecepatan pemrosesan paket meningkat secara signifikan. Namun, bagaimana kernel mengizinkan pengguna untuk menjalankan program mereka di ruang kernel? Sebelum menjawab pertanyaan ini, mari kita lihat apa itu BPF.

BPF dan eBPF

Meskipun namanya tidak sepenuhnya jelas, BPF (Packet Filtering, Berkeley) sebenarnya adalah model mesin virtual. Mesin virtual ini awalnya dirancang untuk menangani pemfilteran paket, oleh karena itu namanya.

Salah satu alat yang lebih terkenal menggunakan BPF adalah tcpdump. Saat menangkap paket dengan tcpdump pengguna dapat menentukan ekspresi untuk pemfilteran paket. Hanya paket yang cocok dengan ekspresi ini yang akan ditangkap. Misalnya ungkapan β€œtcp dst port 80” mengacu pada semua paket TCP yang tiba di port 80. Kompiler dapat mempersingkat ekspresi ini dengan mengonversinya menjadi 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

Ini pada dasarnya adalah apa yang dilakukan program di atas:

  • Instruksi (000): Memuat paket pada offset 12, sebagai kata 16-bit, ke dalam akumulator. Offset 12 sesuai dengan ethertype dari paket.
  • Instruksi (001): membandingkan nilai dalam akumulator dengan 0x86dd, yaitu dengan nilai ethertype untuk IPv6. Jika hasilnya benar, maka penghitung program menuju ke instruksi (002), dan jika tidak, maka ke (006).
  • Instruksi (006): membandingkan nilai dengan 0x800 (nilai ethertype untuk IPv4). Jika jawabannya benar, maka program menuju (007), jika tidak, maka ke (015).

Begitu seterusnya, hingga program pemfilteran paket mengembalikan hasilnya. Biasanya boolean. Mengembalikan nilai bukan nol (instruksi (014)) berarti paket cocok, dan mengembalikan nol (instruksi (015)) berarti paket tidak cocok.

Mesin virtual BPF dan bytecode-nya diusulkan oleh Steve McCann dan Van Jacobson pada akhir 1992 ketika makalah mereka diterbitkan. Filter Paket BSD: Arsitektur baru untuk pengambilan paket tingkat pengguna, untuk pertama kalinya teknologi ini dipresentasikan pada konferensi Usenix pada musim dingin tahun 1993.

Karena BPF adalah mesin virtual, ini menentukan lingkungan tempat program dijalankan. Selain bytecode, ia juga mendefinisikan model memori paket (instruksi pemuatan diterapkan secara implisit ke paket), register (A dan X; register akumulator dan indeks), penyimpanan memori awal, dan penghitung program implisit. Menariknya, bytecode BPF dimodelkan setelah Motorola 6502 ISA. Seperti yang diingat Steve McCann dalam bukunya laporan paripurna di Sharkfest '11, dia akrab dengan build 6502 dari sekolah menengah saat memprogram di Apple II, dan pengetahuan ini memengaruhi pekerjaannya merancang bytecode BPF.

Dukungan BPF diimplementasikan di kernel Linux pada versi v2.5 dan yang lebih baru, terutama ditambahkan oleh Jay Schullist. Kode BPF tetap tidak berubah hingga 2011, ketika Eric Dumaset mendesain ulang juru bahasa BPF untuk bekerja dalam mode JIT (Sumber: JIT untuk Filter Paket). Setelah itu, alih-alih menginterpretasikan bytecode BPF, kernel dapat langsung mengonversi program BPF ke arsitektur target: x86, ARM, MIPS, dll.

Kemudian, pada tahun 2014, Alexei Starovoitov mengusulkan mekanisme JIT baru untuk BPF. Bahkan, JIT baru ini menjadi arsitektur baru berdasarkan BPF dan disebut eBPF. Saya pikir kedua VM hidup berdampingan untuk beberapa waktu, tetapi pemfilteran paket saat ini diterapkan di atas eBPF. Faktanya, dalam banyak contoh dokumentasi modern, BPF disebut sebagai eBPF, dan BPF klasik sekarang dikenal sebagai cBPF.

eBPF memperluas mesin virtual BPF klasik dalam beberapa cara:

  • Bergantung pada arsitektur 64-bit modern. eBPF menggunakan register 64-bit dan meningkatkan jumlah register yang tersedia dari 2 (akumulator dan X) menjadi 10. eBPF juga menyediakan opcode tambahan (BPF_MOV, BPF_JNE, BPF_CALL…).
  • Terpisah dari subsistem lapisan jaringan. BPF terikat dengan model data batch. Karena digunakan untuk memfilter paket, kodenya ada di subsistem yang menyediakan interaksi jaringan. Namun, mesin virtual eBPF tidak lagi terikat pada model data dan dapat digunakan untuk tujuan apa pun. Nah, sekarang program eBPF sudah bisa dikoneksikan ke tracepoint atau ke kprobe. Ini membuka pintu untuk instrumentasi eBPF, analisis kinerja, dan banyak kasus penggunaan lainnya dalam konteks subsistem kernel lainnya. Sekarang kode eBPF terletak di jalurnya sendiri: kernel/bpf.
  • Penyimpanan data global yang disebut Maps. Peta adalah penyimpanan nilai kunci yang menyediakan pertukaran data antara ruang pengguna dan ruang kernel. eBPF menyediakan beberapa jenis kartu.
  • Fungsi sekunder. Secara khusus, untuk menimpa sebuah paket, menghitung checksum, atau mengkloning sebuah paket. Fungsi-fungsi ini berjalan di dalam kernel dan bukan milik program ruang pengguna. Selain itu, panggilan sistem dapat dilakukan dari program eBPF.
  • Akhiri panggilan. Ukuran program di eBPF dibatasi hingga 4096 byte. Fitur panggilan akhir memungkinkan program eBPF untuk mentransfer kontrol ke program eBPF baru dan dengan demikian melewati batasan ini (hingga 32 program dapat dirangkai dengan cara ini).

contoh BPF

Ada beberapa contoh untuk eBPF di sumber kernel Linux. Mereka tersedia di sampel/bpf/. Untuk mengkompilasi contoh-contoh ini, cukup ketik:

$ sudo make samples/bpf/

Saya sendiri tidak akan menulis contoh baru untuk eBPF, tetapi akan menggunakan salah satu sampel yang tersedia di sampel/bpf/. Saya akan melihat beberapa bagian kode dan menjelaskan cara kerjanya. Sebagai contoh, saya memilih program tracex4.

Secara umum, setiap contoh pada sample/bpf/ terdiri dari dua file. Pada kasus ini:

  • tracex4_kern.c, berisi kode sumber yang akan dieksekusi di kernel sebagai bytecode eBPF.
  • tracex4_user.c, berisi program dari ruang pengguna.

Dalam hal ini, kita perlu mengkompilasi tracex4_kern.c ke kode byte eBPF. Saat ini di gcc tidak ada bagian server untuk eBPF. Untung, clang dapat menghasilkan bytecode eBPF. Makefile menggunakan clang untuk mengkompilasi tracex4_kern.c ke file objek.

Saya sebutkan di atas bahwa salah satu fitur eBPF yang paling menarik adalah peta. tracex4_kern mendefinisikan satu peta:

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 adalah salah satu dari banyak jenis kartu yang ditawarkan oleh eBPF. Dalam hal ini, itu hanya sebuah hash. Anda mungkin juga telah memperhatikan iklan tersebut SEC("maps"). SEC adalah makro yang digunakan untuk membuat bagian baru dari file biner. Sebenarnya, dalam contoh tracex4_kern dua bagian lagi didefinisikan:

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

Kedua fungsi ini memungkinkan Anda menghapus entri dari peta (kprobe/kmem_cache_free) dan tambahkan entri baru ke peta (kretprobe/kmem_cache_alloc_node). Semua nama fungsi yang ditulis dengan huruf kapital sesuai dengan makro yang didefinisikan di bpf_helpers.h.

Jika saya membuang bagian dari file objek, saya akan melihat bahwa bagian baru ini sudah ditentukan:

$ 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

Masih ada tracex4_user.c, program utama. Pada dasarnya, program ini mendengarkan acara kmem_cache_alloc_node. Saat kejadian seperti itu terjadi, kode eBPF yang sesuai dijalankan. Kode menyimpan atribut IP objek ke peta, dan kemudian objek tersebut dilingkarkan melalui program utama. Contoh:

$ 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

Bagaimana program ruang pengguna dan program eBPF terkait? Pada inisialisasi tracex4_user.c memuat file objek tracex4_kern.o menggunakan fungsi 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;
}

Saat melakukan load_bpf_file probe yang ditentukan dalam file eBPF ditambahkan ke /sys/kernel/debug/tracing/kprobe_events. Sekarang kami mendengarkan peristiwa ini dan program kami dapat melakukan sesuatu ketika itu terjadi.

$ 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

Semua program lain dalam sample/bpf/ disusun dengan cara yang sama. Mereka selalu berisi dua file:

  • XXX_kern.c: program eBPF.
  • XXX_user.c: program utama.

Program eBPF mendefinisikan peta dan fungsi yang terkait dengan suatu bagian. Ketika kernel mengeluarkan event dengan tipe tertentu (misalnya, tracepoint), fungsi terikat dijalankan. Peta menyediakan komunikasi antara program kernel dan program ruang pengguna.

Kesimpulan

Pada artikel ini, BPF dan eBPF dibahas secara umum. Saya tahu ada banyak informasi dan sumber daya tentang eBPF hari ini, jadi saya akan merekomendasikan beberapa materi lagi untuk dipelajari lebih lanjut.

Saya sarankan membaca:

Sumber: www.habr.com

Tambah komentar