Lyhyt esittely BPF:stä ja eBPF:stä

Hei Habr! Ilmoitamme, että valmistaudumme julkaisemaan kirjan "Linuxin havainnointi BPF:llä".

Lyhyt esittely BPF:stä ja eBPF:stä
Koska BPF-virtuaalikone kehittyy jatkuvasti ja sitä käytetään aktiivisesti käytännössä, olemme kääntäneet sinulle artikkelin, jossa kuvataan sen tärkeimmät ominaisuudet ja nykytila.

Viime vuosina ohjelmointityökalut ja -tekniikat ovat saavuttaneet suosiota Linux-ytimen rajoitusten kompensoimiseksi tapauksissa, joissa tarvitaan korkean suorituskyvyn pakettien käsittelyä. Yksi tämän tyyppisistä suosituimmista menetelmistä on ns ytimen ohitus (ytimen ohitus) ja sallii, ohittaen ytimen verkkokerroksen, suorittaa kaiken paketinkäsittelyn käyttäjätilasta. Ytimen ohittaminen sisältää myös verkkokortin hallinnan käyttäjätilaa. Toisin sanoen, kun työskentelemme verkkokortin kanssa, luotamme ohjaimeen käyttäjätilaa.

Siirtämällä verkkokortin täyden hallinnan käyttäjätilaohjelmalle vähennämme ytimen ylikuormitusta (kontekstikytkimet, verkkokerroksen käsittely, keskeytykset jne.), mikä on varsin tärkeää käytettäessä 10 Gb/s tai XNUMX Gb/s nopeudella tai korkeampi. Ytimen ohittaminen ja yhdistelmä muita ominaisuuksia (eräkäsittely) ja huolellinen suorituskyvyn viritys (NUMA-kirjanpito, CPU-eristysjne.) sopivat tehokkaan käyttäjätilan verkottumisen perusteisiin. Ehkä esimerkkinä tästä uudesta lähestymistavasta pakettien käsittelyyn on DPDK Inteliltä (Data Plane Development Kit), vaikka on olemassa muitakin tunnettuja työkaluja ja tekniikoita, kuten Ciscon VPP (Vector Packet Processing), Netmap ja tietysti nappaa.

Verkkovuorovaikutusten järjestämisellä käyttäjätilassa on useita haittoja:

  • OS-ydin on laitteistoresurssien abstraktiokerros. Koska käyttäjäavaruusohjelmien on hallittava resurssejaan suoraan, niiden on myös hallittava omaa laitteistoaan. Tämä tarkoittaa usein omien ajurien ohjelmointia.
  • Koska luovumme kokonaan ydintilasta, luovumme myös kaikista ytimen tarjoamista verkkotoiminnallisuuksista. Käyttäjäavaruusohjelmien on otettava uudelleen käyttöön ominaisuuksia, jotka voivat jo olla ytimen tai käyttöjärjestelmän tarjoamia.
  • Ohjelmat toimivat hiekkalaatikkotilassa, mikä rajoittaa vakavasti niiden vuorovaikutusta ja estää niitä integroimasta käyttöjärjestelmän muihin osiin.

Pohjimmiltaan, kun verkotetaan käyttäjätilassa, suorituskykyä parannetaan siirtämällä pakettien käsittely ytimestä käyttäjätilaan. XDP tekee juuri päinvastoin: se siirtää verkko-ohjelmat käyttäjätilasta (suodattimet, muuntimet, reititys jne.) ydinalueelle. XDP:n avulla voimme suorittaa verkkotoiminnon heti, kun paketti osuu verkkoliitäntään ja ennen kuin se alkaa kulkea ytimen verkkoalijärjestelmään. Tämän seurauksena pakettien käsittelynopeus kasvaa merkittävästi. Miten ydin kuitenkin sallii käyttäjän ajaa ohjelmiaan ydintilassa? Ennen kuin vastaat tähän kysymykseen, katsotaanpa mitä BPF on.

BPF ja eBPF

Epäselvästä nimestä huolimatta BPF (Packet Filtering, Berkeley) on itse asiassa virtuaalikoneen malli. Tämä virtuaalikone suunniteltiin alun perin käsittelemään pakettisuodatusta, mistä johtuu nimi.

Yksi tunnetuimmista BPF:ää käyttävistä työkaluista on tcpdump. Kun kaapataan paketteja tcpdump käyttäjä voi määrittää lausekkeen pakettisuodatusta varten. Vain tätä lauseketta vastaavat paketit siepataan. Esimerkiksi ilmaisu "tcp dst port 80” viittaa kaikkiin TCP-paketteihin, jotka saapuvat porttiin 80. Kääntäjä voi lyhentää tätä lauseketta muuttamalla sen BPF-tavukoodiksi.

$ 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

Tämä on periaatteessa mitä yllä oleva ohjelma tekee:

  • Ohje (000): Lataa paketin offsetissa 12 16-bittisenä sanana akkuun. Offset 12 vastaa paketin eetterityyppiä.
  • Ohje (001): vertaa akun arvoa arvoon 0x86dd, eli IPv6:n ethertype-arvoon. Jos tulos on tosi, niin ohjelmalaskuri siirtyy käskyyn (002), ja jos ei, niin (006).
  • Ohje (006): vertaa arvoa arvoon 0x800 (ethertype-arvo IPv4:lle). Jos vastaus on tosi, niin ohjelma siirtyy kohtaan (007), jos ei, niin kohtaan (015).

Ja niin edelleen, kunnes pakettisuodatusohjelma palauttaa tuloksen. Yleensä se on Boolean. Nollasta poikkeavan arvon palauttaminen (käsky (014)) tarkoittaa, että paketti täsmäsi, ja nollan palauttaminen (käsky (015)) tarkoittaa, että paketti ei täsmää.

BPF-virtuaalikonetta ja sen tavukoodia ehdottivat Steve McCann ja Van Jacobson vuoden 1992 lopulla, kun heidän paperinsa julkaistiin. BSD-pakettisuodatin: Uusi arkkitehtuuri käyttäjätason pakettien sieppaamiseen, tämä tekniikka esiteltiin ensimmäistä kertaa Usenix-konferenssissa talvella 1993.

Koska BPF on virtuaalikone, se määrittää ympäristön, jossa ohjelmat toimivat. Tavukoodin lisäksi se määrittelee myös pakettimuistimallin (kuormituskäskyjä sovelletaan implisiittisesti pakettiin), rekisterit (A ja X; akku- ja indeksirekisterit), scratch-muistin tallennustilan ja implisiittisen ohjelmalaskurin. Mielenkiintoista on, että BPF-tavukoodi mallinnettiin Motorola 6502 ISA:n mukaan. Kuten Steve McCann muisteli omassaan täysistunnon mietintö Sharkfest '11:ssä hän tunsi build 6502:n lukiosta ohjelmoidessaan Apple II:lla, ja tämä tieto vaikutti hänen työhönsä BPF-tavukoodin suunnittelussa.

BPF-tuki on toteutettu Linux-ytimessä versiossa v2.5 ja uudemmissa, ja sen on lisännyt pääasiassa Jay Schullis. BPF-koodi pysyi muuttumattomana vuoteen 2011 asti, jolloin Eric Dumaset suunnitteli BPF-tulkin uudelleen toimimaan JIT-tilassa (Lähde: JIT pakettisuodattimille). Sen jälkeen BPF-tavukoodin tulkinnan sijasta ydin voisi muuntaa BPF-ohjelmat suoraan kohdearkkitehtuuriin: x86, ARM, MIPS jne.

Myöhemmin, vuonna 2014, Aleksei Starovoitov ehdotti uutta JIT-mekanismia BPF:lle. Itse asiassa tästä uudesta JIT:stä tuli uusi BPF-pohjainen arkkitehtuuri ja sitä kutsuttiin eBPF:ksi. Luulen, että molemmat VM:t olivat rinnakkain jonkin aikaa, mutta pakettisuodatus on tällä hetkellä toteutettu eBPF:n päällä. Itse asiassa monissa nykyaikaisissa dokumentaatioesimerkeissä BPF:ää kutsutaan nimellä eBPF, ja klassinen BPF tunnetaan nykyään nimellä cBPF.

eBPF laajentaa klassista BPF-virtuaalikonetta useilla tavoilla:

  • Luottaa moderneihin 64-bittisiin arkkitehtuureihin. eBPF käyttää 64-bittisiä rekistereitä ja lisää käytettävissä olevien rekisterien määrää 2:sta (akku ja X) 10:een. eBPF tarjoaa myös lisätoimintokoodeja (BPF_MOV, BPF_JNE, BPF_CALL…).
  • Irrotettu verkkokerroksen alijärjestelmästä. BPF sidottiin erätietomalliin. Koska sitä käytettiin pakettien suodattamiseen, sen koodi oli alijärjestelmässä, joka tarjosi verkkovuorovaikutuksia. eBPF-virtuaalikonetta ei kuitenkaan ole enää sidottu tietomalliin, ja sitä voidaan käyttää mihin tahansa tarkoitukseen. Joten nyt eBPF-ohjelma voidaan yhdistää tracepointiin tai kprobeen. Tämä avaa oven eBPF-instrumentointiin, suorituskykyanalyysiin ja moniin muihin käyttötapauksiin muiden ytimen alijärjestelmien yhteydessä. Nyt eBPF-koodi sijaitsee omalla polullaan: kernel/bpf.
  • Maailmanlaajuiset tietovarastot nimeltä Maps. Kartat ovat avainarvovarastoja, jotka tarjoavat tiedonvaihtoa käyttäjätilan ja ydintilan välillä. eBPF tarjoaa useita erilaisia ​​kortteja.
  • Toissijaiset toiminnot. Erityisesti paketin päällekirjoittaminen, tarkistussumman laskeminen tai paketin kloonaaminen. Nämä funktiot toimivat ytimen sisällä eivätkä kuulu käyttäjäavaruusohjelmiin. Lisäksi järjestelmäkutsuja voidaan soittaa eBPF-ohjelmista.
  • Lopeta puhelut. Ohjelman koko eBPF:ssä on rajoitettu 4096 tavuun. Puhelun lopetusominaisuuden avulla eBPF-ohjelma voi siirtää ohjauksen uudelle eBPF-ohjelmalle ja siten ohittaa tämän rajoituksen (jopa 32 ohjelmaa voidaan ketjuttaa tällä tavalla).

eBPF esimerkki

Linux-ytimen lähteissä on useita esimerkkejä eBPF:stä. Ne ovat saatavilla osoitteesta samples/bpf/. Voit koota nämä esimerkit kirjoittamalla:

$ sudo make samples/bpf/

En kirjoita itse uutta esimerkkiä eBPF:lle, vaan käytän jotakin samples/bpf/-tiedostossa olevista näytteistä. Tarkastelen joitain koodin osia ja selitän, kuinka se toimii. Esimerkkinä valitsin ohjelman tracex4.

Yleisesti ottaen kukin mallin samples/bpf/ esimerkeistä koostuu kahdesta tiedostosta. Tässä tapauksessa:

  • tracex4_kern.c, sisältää lähdekoodin, joka suoritetaan ytimessä eBPF-tavukoodina.
  • tracex4_user.c, sisältää ohjelman käyttäjätilasta.

Tässä tapauksessa meidän on koottava tracex4_kern.c eBPF-tavukoodiin. Tällä hetkellä sisään gcc eBPF:lle ei ole palvelinosaa. Onneksi, clang voi tuottaa eBPF-tavukoodia. Makefile käyttää clang kääntää tracex4_kern.c objektitiedostoon.

Mainitsin yllä, että yksi eBPF:n mielenkiintoisimmista ominaisuuksista on kartat. tracex4_kern määrittelee yhden kartan:

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 on yksi monista eBPF:n tarjoamista korttityypeistä. Tässä tapauksessa se on vain hash. Olet ehkä myös huomannut mainoksen SEC("maps"). SEC on makro, jota käytetään binääritiedoston uuden osan luomiseen. Itse asiassa esimerkissä tracex4_kern on määritelty kaksi muuta osaa:

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

Näiden kahden toiminnon avulla voit poistaa merkinnän kartalta (kprobe/kmem_cache_free) ja lisää uusi merkintä karttaan (kretprobe/kmem_cache_alloc_node). Kaikki isoilla kirjaimilla kirjoitetut funktioiden nimet vastaavat kohdassa määriteltyjä makroja bpf_helpers.h.

Jos vedän objektitiedoston osat, minun pitäisi nähdä, että nämä uudet osat on jo määritetty:

$ 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

Vielä on tracex4_user.c, pääohjelma. Periaatteessa tämä ohjelma kuuntelee tapahtumia kmem_cache_alloc_node. Kun tällainen tapahtuma tapahtuu, vastaava eBPF-koodi suoritetaan. Koodi tallentaa kohteen IP-attribuutin karttaan, jonka jälkeen objekti silmukataan pääohjelman läpi. Esimerkki:

$ 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

Miten käyttäjäavaruusohjelma ja eBPF-ohjelma liittyvät toisiinsa? Alustusvaiheessa tracex4_user.c lataa objektitiedoston tracex4_kern.o funktiota käyttämällä 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;
}

Tekemisen aikana load_bpf_file eBPF-tiedostossa määritellyt anturit lisätään /sys/kernel/debug/tracing/kprobe_events. Nyt kuuntelemme näitä tapahtumia ja ohjelmamme voi tehdä jotain, kun niitä tapahtuu.

$ 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

Kaikki muut ohjelmat mallissa/bpf/ ovat rakenteeltaan samanlaisia. Ne sisältävät aina kaksi tiedostoa:

  • XXX_kern.c: eBPF-ohjelma.
  • XXX_user.c: pääohjelma.

eBPF-ohjelma määrittää osuuteen liittyvät kartat ja funktiot. Kun ydin lähettää tietyn tyyppisen tapahtuman (esim. tracepoint), sidotut funktiot suoritetaan. Kartat tarjoavat tiedonsiirron ydinohjelman ja käyttäjätilaohjelman välillä.

Johtopäätös

Tässä artikkelissa BPF:stä ja eBPF:stä keskusteltiin yleisesti. Tiedän, että eBPF:stä on nykyään paljon tietoa ja resursseja, joten suosittelen muutamaa materiaalia lisätutkimuksia varten.

Suosittelen lukemaan:

Lähde: will.com

Lisää kommentti