BPF және eBPF-ке қысқаша кіріспе

Эй Хабр! Кітап шығаруға дайындалып жатқанымызды хабарлаймыз»BPF көмегімен Linux бақылау мүмкіндігі".

BPF және eBPF-ке қысқаша кіріспе
BPF виртуалды машинасы дамуын жалғастыруда және тәжірибеде белсенді түрде қолданылуда, біз оның негізгі мүмкіндіктері мен ағымдағы күйін сипаттайтын мақаланы сіз үшін аудардық.

Соңғы жылдары өнімділігі жоғары пакеттерді өңдеу қажет болған жағдайда Linux ядросының шектеулерін өтеу үшін бағдарламалау құралдары мен әдістері танымал болды. Осы түрдегі ең танымал әдістердің бірі деп аталады негізгі айналып өту (ядроны айналып өту) және ядроның желілік деңгейін өткізіп жіберіп, пайдаланушы кеңістігінен барлық пакеттік өңдеуді орындауға мүмкіндік береді. Ядроны айналып өту желілік картаны басқаруды да қамтиды пайдаланушы кеңістігі. Басқаша айтқанда, желілік картамен жұмыс істегенде, біз драйверге сүйенеміз пайдаланушы кеңістігі.

Желілік картаны толық басқаруды пайдаланушы-кеңістік бағдарламасына беру арқылы біз ядроның үстеме шығынын азайтамыз (контекстік қосқыштар, желілік деңгейді өңдеу, үзілістер және т.б.), бұл 10 Гб/с жылдамдықпен жұмыс істегенде өте маңызды немесе жоғарырақ. Ядроны және басқа мүмкіндіктердің комбинациясын айналып өту (пакеттік өңдеу) және өнімділікті мұқият реттеу (NUMA есебі, CPU оқшаулаужәне т.б.) өнімділігі жоғары пайдаланушы-кеңістік желісінің негіздеріне сәйкес келеді. Пакеттерді өңдеуге осы жаңа тәсілдің үлгілі мысалы болуы мүмкін DPDK Intel компаниясынан (Деректер жазықтығын әзірлеу жинағы), басқа да танымал құралдар мен әдістер бар, соның ішінде Cisco-дан VPP (Vector Packet Processing), Netmap және, әрине, Снабб.

Пайдаланушы кеңістігінде желілік өзара әрекеттесуді ұйымдастырудың бірқатар кемшіліктері бар:

  • ОЖ ядросы аппараттық ресурстарға арналған абстракциялық деңгей болып табылады. Пайдаланушы-кеңістік бағдарламалары өз ресурстарын тікелей басқаруы керек болғандықтан, олар өздерінің аппараттық құралдарын да басқаруы керек. Бұл көбінесе өзіңіздің драйверлеріңізді бағдарламалауды білдіреді.
  • Біз ядро ​​кеңістігінен толығымен бас тартқандықтан, ядромен қамтамасыз етілген барлық желілік функциялардан бас тартамыз. Пайдаланушы-кеңістік бағдарламалары ядро ​​немесе операциялық жүйе қамтамасыз етуі мүмкін мүмкіндіктерді қайта іске асыруы керек.
  • Бағдарламалар құмсалғыш режимінде жұмыс істейді, бұл олардың өзара әрекеттесуін айтарлықтай шектейді және операциялық жүйенің басқа бөліктерімен біріктіруіне жол бермейді.

Негізінде, пайдаланушы кеңістігінде желіні құру кезінде өнімділік арттыру пакеттерді өңдеуді ядродан пайдаланушы кеңістігіне жылжыту арқылы қол жеткізіледі. XDP мүлдем керісінше жасайды: ол желілік бағдарламаларды пайдаланушы кеңістігінен (сүзгілер, түрлендіргіштер, маршруттау және т.б.) ядро ​​аймағына жылжытады. XDP желілік функцияны пакет желілік интерфейске тиген кезде және ядроның желілік ішкі жүйесіне дейін қозғала бастағанға дейін орындауға мүмкіндік береді. Нәтижесінде пакеттерді өңдеу жылдамдығы айтарлықтай артады. Дегенмен, ядро ​​пайдаланушыға өз бағдарламаларын ядро ​​кеңістігінде іске қосуға қалай мүмкіндік береді? Бұл сұраққа жауап бермес бұрын, BPF дегеніміз не екенін қарастырайық.

BPF және eBPF

Аты анық емес болса да, BPF (Packet Filtering, Berkeley) шын мәнінде виртуалды машина үлгісі болып табылады. Бұл виртуалды машина бастапқыда пакеттік сүзуді өңдеуге арналған, сондықтан атауы.

BPF қолданатын ең танымал құралдардың бірі tcpdump. көмегімен пакеттерді түсіру кезінде tcpdump пайдаланушы пакетті сүзу үшін өрнекті көрсете алады. Осы өрнекке сәйкес келетін пакеттер ғана түсіріледі. Мысалы, «tcp dst port 80” 80 портына келетін барлық TCP пакеттерін білдіреді. Компилятор бұл өрнекті 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 офсеттік пакетті жүктейді. Офсет 12 пакеттің этертипіне сәйкес келеді.
  • Нұсқау (001): аккумулятордағы мәнді 0x86dd мәнімен, яғни IPv6 үшін ethertype мәнімен салыстырады. Нәтиже ақиқат болса, онда бағдарлама санағышы (002) нұсқаулығына, ал болмаса (006) ауысады.
  • Нұсқау (006): мәнді 0x800 (IPv4 үшін ethertype мәні) салыстырады. Егер жауап дұрыс болса, онда бағдарлама (007), егер жоқ болса, (015) нөміріне өтеді.

Пакетті сүзу бағдарламасы нәтижені қайтарғанша және т.б. Әдетте бұл логикалық. Нөлдік емес мәнді қайтару (нұсқау (014)) пакеттің сәйкестігін білдіреді, ал нөлді қайтару (нұсқау (015)) пакеттің сәйкес келмейтінін білдіреді.

BPF виртуалды машинасын және оның байт кодын Стив МакКенн мен Ван Джейкобсон 1992 жылдың соңында қағазы шыққан кезде ұсынған. BSD пакеттік сүзгісі: пайдаланушы деңгейіндегі пакеттерді түсіруге арналған жаңа архитектура, бұл технология алғаш рет 1993 жылдың қысында Usenix конференциясында ұсынылды.

BPF виртуалды машина болғандықтан, ол бағдарламалар жұмыс істейтін ортаны анықтайды. Байтекодтан басқа, ол дестелік жады моделін (жүктеу нұсқаулары пакетке жанама түрде қолданылады), регистрлерді (А және Х; аккумулятор және индекстік регистрлер), скретч жадысын және жасырын бағдарлама есептегішін анықтайды. Бір қызығы, BPF байт коды Motorola 6502 ISA үлгісінен кейін үлгіленген. Стив МакКэнн өз кітабында еске түсіргендей пленарлық баяндама Sharkfest '11-де ол Apple II-де бағдарламалау кезінде орта мектептен 6502 құрастырумен таныс болды және бұл білім оның BPF байт кодын жобалау жұмысына әсер етті.

BPF қолдауы Linux ядросында v2.5 және одан кейінгі нұсқаларда жүзеге асырылады, негізінен Джей Шуллист қосқан. BPF коды 2011 жылға дейін өзгеріссіз қалды, Эрик Дюмасет BPF интерпретаторын JIT режимінде жұмыс істеу үшін қайта құрастырған (Дереккөз: Пакет сүзгілеріне арналған JIT). Осыдан кейін ядро ​​​​BPF байт кодын интерпретациялаудың орнына BPF бағдарламаларын мақсатты архитектураға тікелей түрлендіре алады: x86, ARM, MIPS және т.б.

Кейінірек, 2014 жылы Алексей Старовойтов BPF үшін жаңа JIT механизмін ұсынды. Іс жүзінде бұл жаңа JIT BPF негізіндегі жаңа архитектура болды және eBPF деп аталды. Менің ойымша, екі VM да біраз уақыт бірге өмір сүрді, бірақ пакетті сүзгілеу қазіргі уақытта eBPF үстінде жүзеге асырылады. Шын мәнінде, көптеген заманауи құжаттама мысалдарында BPF eBPF деп аталады, ал классикалық BPF бүгін cBPF ретінде белгілі.

eBPF классикалық BPF виртуалды машинасын бірнеше жолмен кеңейтеді:

  • Заманауи 64 биттік архитектураға сүйенеді. eBPF 64-биттік регистрлерді пайдаланады және қолжетімді регистрлердің санын 2-ден (аккумулятор және X) 10-ға дейін арттырады. eBPF сонымен қатар қосымша операция кодтарын (BPF_MOV, BPF_JNE, BPF_CALL…) қамтамасыз етеді.
  • Желілік деңгейдің ішкі жүйесінен ажыратылған. BPF пакеттік деректер үлгісіне байланысты болды. Ол пакеттерді сүзу үшін пайдаланылғандықтан, оның коды желілік өзара әрекеттесуді қамтамасыз ететін ішкі жүйеде болды. Дегенмен, eBPF виртуалды машинасы енді деректер үлгісіне байланысты емес және оны кез келген мақсат үшін пайдалануға болады. Сонымен, енді eBPF бағдарламасын tracepoint немесе kprobe-ге қосуға болады. Бұл eBPF аспаптарына, өнімділікті талдауға және басқа ядролық ішкі жүйелер контекстінде көптеген басқа пайдалану жағдайларына есік ашады. Енді eBPF коды өз жолында орналасқан: kernel/bpf.
  • Карталар деп аталатын ғаламдық деректер қоймалары. Карталар – пайдаланушы кеңістігі мен ядро ​​кеңістігі арасында деректер алмасуды қамтамасыз ететін кілттер қоймасы. eBPF карталардың бірнеше түрін ұсынады.
  • Қосымша функциялар. Атап айтқанда, буманы қайта жазу, бақылау сомасын есептеу немесе буманы клондау. Бұл функциялар ядроның ішінде жұмыс істейді және пайдаланушы-кеңістік бағдарламаларына жатпайды. Сонымен қатар, жүйелік қоңырауларды eBPF бағдарламаларынан жасауға болады.
  • Қоңырауларды аяқтау. eBPF ішіндегі бағдарлама өлшемі 4096 байтпен шектелген. Қоңырауды аяқтау мүмкіндігі eBPF бағдарламасына басқаруды жаңа eBPF бағдарламасына тасымалдауға және осылайша бұл шектеуді айналып өтуге мүмкіндік береді (осы жолмен 32 бағдарламаға дейін тізбектелуге болады).

eBPF мысалы

Linux ядросының көздерінде eBPF үшін бірнеше мысалдар бар. Олар үлгілер/bpf/ мекенжайында қолжетімді. Бұл мысалдарды құрастыру үшін жай теріңіз:

$ sudo make samples/bpf/

Мен өзім eBPF үшін жаңа мысал жазбаймын, бірақ үлгілерде/bpf/ қол жетімді үлгілердің бірін қолданамын. Мен кодтың кейбір бөліктерін қарастырамын және оның қалай жұмыс істейтінін түсіндіремін. Мысал ретінде мен бағдарламаны таңдадым tracex4.

Жалпы үлгілердегі/bpf/ мысалдарының әрқайсысы екі файлдан тұрады. Бұл жағдайда:

  • tracex4_kern.c, ядрода eBPF байт коды ретінде орындалатын бастапқы кодты қамтиды.
  • tracex4_user.c, пайдаланушы кеңістігіндегі бағдарламаны қамтиды.

Бұл жағдайда біз компиляция жасауымыз керек tracex4_kern.c eBPF байт кодына. Қазіргі уақытта в gcc eBPF үшін сервер бөлігі жоқ. Бақытқа орай, clang 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

пікір қалдыру