BPF ve eBPF'ye Kısa Bir Giriş

Merhaba Habr! Yayınlanmak üzere bir kitap hazırladığımızı bilmenizi isteriz."BPF ile Linux Gözlemlenebilirliği".

BPF ve eBPF'ye Kısa Bir Giriş
BPF sanal makinesi gelişmeye devam ettiği ve pratikte aktif olarak kullanıldığı için, sizin için ana yeteneklerini ve mevcut durumunu anlatan bir makale tercüme ettik.

Son yıllarda, yüksek performanslı paket işlemenin gerekli olduğu durumlarda Linux çekirdeğinin sınırlamalarını telafi etmek için programlama araçları ve teknikleri giderek daha popüler hale geldi. Bu türün en popüler tekniklerinden birine denir çekirdek atlaması (çekirdek bypass) ve çekirdek ağ katmanını atlayarak tüm paket işlemlerinin kullanıcı alanından gerçekleştirilmesine olanak tanır. Çekirdeği atlamak aynı zamanda ağ kartını kontrol etmeyi de içerir. Kullanıcı alanı. Başka bir deyişle, bir ağ kartıyla çalışırken sürücüye güveniyoruz Kullanıcı alanı.

Ağ kartının tam kontrolünü bir kullanıcı alanı programına aktararak, 10 Gb/s veya daha yüksek hızlarda çalışırken oldukça önemli olan çekirdek yükünü (bağlam değiştirme, ağ katmanı işleme, kesintiler vb.) azaltıyoruz. Çekirdek bypass artı diğer özelliklerin bir kombinasyonu (toplu işleme) ve dikkatli performans ayarı (NUMA muhasebesi, CPU izolasyonuvb.) kullanıcı alanında yüksek performanslı ağ işlemenin temellerine karşılık gelir. Paket işlemeye yönelik bu yeni yaklaşımın örnek niteliğindeki bir örneği belki de DPDK Intel'den (Veri Düzlemi Geliştirme Kiti), ancak Cisco'nun VPP'si (Vektör Paket İşleme), Netmap ve elbette, dahil olmak üzere iyi bilinen başka araçlar ve teknikler de vardır. snabb.

Kullanıcı alanında ağ etkileşimlerini organize etmenin bir takım dezavantajları vardır:

  • İşletim sistemi çekirdeği, donanım kaynakları için bir soyutlama katmanıdır. Kullanıcı alanı programlarının kaynaklarını doğrudan yönetmeleri gerektiğinden, aynı zamanda kendi donanımlarını da yönetmeleri gerekir. Bu genellikle kendi sürücülerinizi programlamanız gerektiği anlamına gelir.
  • Çekirdek alanından tamamen vazgeçtiğimiz için, aynı zamanda çekirdeğin sağladığı tüm ağ oluşturma işlevlerinden de vazgeçmiş oluyoruz. Kullanıcı alanı programları, çekirdek veya işletim sistemi tarafından halihazırda sağlanmış olabilecek özellikleri yeniden uygulamalıdır.
  • Programlar, etkileşimlerini ciddi şekilde sınırlayan ve işletim sisteminin diğer bölümleriyle entegrasyonlarını önleyen sanal alan modunda çalışır.

Temel olarak, kullanıcı alanında ağ oluştururken, paket işlemeyi çekirdekten kullanıcı alanına taşıyarak performans kazanımları elde edilir. XDP tam tersini yapar: ağ oluşturma programlarını kullanıcı alanından (filtreler, çözümleyiciler, yönlendirme vb.) çekirdek alanına taşır. XDP, bir paket bir ağ arayüzüne çarptığı anda ve çekirdek ağ alt sistemine doğru ilerlemeye başlamadan önce bir ağ işlevini gerçekleştirmemize olanak tanır. Sonuç olarak paket işleme hızı önemli ölçüde artar. Ancak çekirdek, kullanıcının programlarını çekirdek alanında yürütmesine nasıl izin veriyor? Bu soruyu cevaplamadan önce BPF'nin ne olduğuna bakalım.

BPF ve eBPF

Kafa karıştırıcı ismine rağmen BPF (Berkeley Paket Filtreleme) aslında bir sanal makine modelidir. Bu sanal makine başlangıçta paket filtrelemeyi gerçekleştirmek için tasarlandı, dolayısıyla adı da buradan geliyor.

BPF'yi kullanan en ünlü araçlardan biri tcpdump. Kullanarak paketleri yakalarken tcpdump kullanıcı paketleri filtrelemek için bir ifade belirtebilir. Yalnızca bu ifadeyle eşleşen paketler yakalanacaktır. Örneğin “ifadesitcp dst port 80”, 80 numaralı bağlantı noktasına gelen tüm TCP paketlerini ifade eder. Derleyici bu ifadeyi BPF bayt koduna dönüştürerek kısaltabilir.

$ 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

Yukarıdaki programın temel olarak yaptığı şey budur:

  • Talimat (000): 12 ofsetindeki paketi 16 bitlik bir kelime olarak akümülatöre yükler. Ofset 12, paketin eter tipine karşılık gelir.
  • Talimat (001): akümülatördeki değeri 0x86dd ile yani IPv6 için ethertype değeriyle karşılaştırır. Sonuç doğruysa, program sayacı talimat (002)'ye, değilse (006)'ya gider.
  • Talimat (006): değeri 0x800 (IPv4 için ethertype değeri) ile karşılaştırır. Cevap doğruysa program (007)'ye, değilse (015)'e gider.

Ve bu, paket filtreleme programı bir sonuç döndürene kadar devam eder. Bu genellikle bir Boolean'dır. Sıfır olmayan bir değer döndürmek (talimat (014)) paketin kabul edildiği anlamına gelir ve sıfır değer döndürmek (talimat (015)) paketin kabul edilmediği anlamına gelir.

BPF sanal makinesi ve bayt kodu, 1992 sonlarında makaleleri yayınlandığında Steve McCann ve Van Jacobson tarafından önerildi. BSD Paket Filtresi: Kullanıcı Düzeyinde Paket Yakalama için Yeni MimariBu teknoloji ilk kez 1993 kışında Usenix konferansında sunuldu.

BPF bir sanal makine olduğundan programların çalıştığı ortamı tanımlar. Bayt koduna ek olarak, aynı zamanda toplu bellek modelini (yükleme talimatları gruba dolaylı olarak uygulanır), kayıtları (A ve X; akümülatör ve indeks kayıtları), karalama belleği depolamayı ve örtülü bir program sayacını da tanımlar. İlginçtir ki, BPF bayt kodu Motorola 6502 ISA'dan sonra modellenmiştir. Steve McCann'in hatırladığı gibi genel kurul raporu Sharkfest '11'de, lise günlerindeki Apple II programcılığından 6502 yapımına aşinaydı ve bu bilgi onun BPF bayt kodunu tasarlama çalışmasını etkiledi.

BPF desteği, Linux çekirdeğinde v2.5 ve üzeri sürümlerde uygulanmakta olup, esas olarak Jay Schullist'in çabalarıyla eklenmiştir. BPF kodu, Eric Dumaset'in BPF yorumlayıcısını JIT modunda çalışacak şekilde yeniden tasarladığı 2011 yılına kadar değişmeden kaldı (Kaynak: Paket filtreleri için JIT). Bundan sonra çekirdek, BPF bayt kodunu yorumlamak yerine BPF programlarını doğrudan hedef mimariye dönüştürebilir: x86, ARM, MIPS, vb.

Daha sonra 2014 yılında Alexey Starovoitov, BPF için yeni bir JIT mekanizması önerdi. Aslında bu yeni JIT, yeni bir BPF tabanlı mimari haline geldi ve eBPF olarak adlandırıldı. Her iki VM'nin de bir süre bir arada var olduğunu düşünüyorum, ancak şu anda paket filtreleme eBPF'ye dayalı olarak uygulanıyor. Aslında birçok modern belgeleme örneğinde BPF, eBPF olarak anlaşılmaktadır ve klasik BPF, günümüzde cBPF olarak bilinmektedir.

eBPF, klasik BPF sanal makinesini çeşitli şekillerde genişletir:

  • Modern 64 bit mimarilere dayanmaktadır. eBPF, 64 bitlik kayıtları kullanır ve mevcut kayıtların sayısını 2'den (akümülatör ve X) 10'a çıkarır. eBPF ayrıca ek işlem kodları da sağlar (BPF_MOV, BPF_JNE, BPF_CALL...).
  • Ağ katmanı alt sisteminden ayrılmıştır. BPF toplu veri modeline bağlıydı. Paket filtreleme amacıyla kullanıldığından kodu ağ iletişimini sağlayan alt sistemde bulunuyordu. Ancak eBPF sanal makinesi artık veri modeline bağlı değildir ve herhangi bir amaç için kullanılabilir. Böylece eBPF programı artık tracepoint veya kprobe'a bağlanabilir. Bu, eBPF araçlarının, performans analizinin ve diğer çekirdek alt sistemleri bağlamındaki diğer birçok kullanım durumunun yolunu açar. Artık eBPF kodu kendi yolunda bulunuyor: kernel/bpf.
  • Haritalar adı verilen küresel veri depoları. Haritalar, kullanıcı alanı ile çekirdek alanı arasında veri alışverişini sağlayan anahtar-değer depolarıdır. eBPF çeşitli harita türleri sağlar.
  • İkincil işlevler. Özellikle bir paketi yeniden yazmak, sağlama toplamı hesaplamak veya bir paketi klonlamak için. Bu işlevler çekirdeğin içinde çalışır ve kullanıcı alanı programları değildir. eBPF programlarından da sistem çağrıları yapabilirsiniz.
  • Aramaları sonlandırın. eBPF'deki program boyutu 4096 bayt ile sınırlıdır. Kuyruk çağrısı özelliği, bir eBPF programının kontrolü yeni bir eBPF programına aktarmasına ve böylece bu sınırlamayı atlamasına olanak tanır (bu şekilde en fazla 32 program bağlanabilir).

eBPF: örnek

Linux çekirdek kaynaklarında eBPF için birkaç örnek vardır. Bunlar sample/bpf/ adresinde mevcuttur. Bu örnekleri derlemek için şunu girmeniz yeterlidir:

$ sudo make samples/bpf/

eBPF için kendim yeni bir örnek yazmayacağım, ancak sample/bpf/'de bulunan örneklerden birini kullanacağım. Kodun bazı bölümlerine bakacağım ve nasıl çalıştığını açıklayacağım. Örnek olarak programı seçtim tracex4.

Genel olarak sample/bpf/ dosyasındaki örneklerin her biri iki dosyadan oluşur. Bu durumda:

  • tracex4_kern.c, çekirdekte eBPF bayt kodu olarak yürütülecek kaynak kodunu içerir.
  • tracex4_user.c, kullanıcı alanından bir program içerir.

Bu durumda derlememiz gerekiyor. tracex4_kern.c eBPF bayt koduna. Şu anda gcc eBPF için arka uç yoktur. Neyse ki, clang eBPF bayt kodunun çıktısını alabilir. Makefile использует clang derleme için tracex4_kern.c nesne dosyasına.

Yukarıda eBPF'nin en ilginç özelliklerinden birinin haritalar olduğundan bahsetmiştim. tracex4_kern bir haritayı tanımlar:

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 tarafından sunulan birçok kart türünden biridir. Bu durumda bu sadece bir karmadır. Ayrıca bir reklam da fark etmiş olabilirsiniz SEC("maps"). SEC, ikili dosyanın yeni bir bölümünü oluşturmak için kullanılan bir makrodur. Aslında örnekte tracex4_kern iki bölüm daha tanımlanmıştır:

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

Bu iki işlev, haritadan bir girişi silmenizi sağlar (kprobe/kmem_cache_free) ve haritaya yeni bir giriş ekleyin (kretprobe/kmem_cache_alloc_node). Büyük harflerle yazılan tüm fonksiyon adları, burada tanımlanan makrolara karşılık gelir. bpf_helpers.h.

Nesne dosyasının bölümlerini dökersem, bu yeni bölümlerin zaten tanımlanmış olduğunu görmeliyim:

$ 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

Hala var tracex4_user.c, ana program. Temel olarak bu program olayları dinler kmem_cache_alloc_node. Böyle bir olay meydana geldiğinde ilgili eBPF kodu yürütülür. Kod, nesnenin IP niteliğini bir haritaya kaydeder ve daha sonra nesne ana program aracılığıyla döngüye alınır. Örnek:

$ 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

Bir kullanıcı alanı programı ile eBPF programı arasında nasıl bir ilişki vardır? Başlatma sırasında tracex4_user.c bir nesne dosyası yükler tracex4_kern.o işlevi kullanma 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;
}

Yaparken load_bpf_file eBPF dosyasında tanımlanan problar şuraya eklenir: /sys/kernel/debug/tracing/kprobe_events. Artık bu olayları dinliyoruz ve programımız bu olaylar gerçekleştiğinde bir şeyler yapabiliyor.

$ 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/ dosyasındaki diğer tüm programlar benzer şekilde yapılandırılmıştır. Her zaman iki dosya içerirler:

  • XXX_kern.c: eBPF programı.
  • XXX_user.c: ana program.

eBPF programı, bir bölümle ilişkili haritaları ve işlevleri tanımlar. Çekirdek belirli türde bir olay yayınladığında (örneğin, tracepoint), bağlı işlevler yürütülür. Kartlar, çekirdek programı ile kullanıcı alanı programı arasındaki iletişimi sağlar.

Sonuç

Bu makale genel hatlarıyla BPF ve eBPF'yi tartıştı. Bugün eBPF hakkında pek çok bilgi ve kaynak olduğunu biliyorum, bu nedenle daha ileri çalışmalar için birkaç kaynak daha önereceğim

Okumanı tavsiye ederim:

Kaynak: habr.com

Yorum ekle