Кароткае ўвядзенне ў BPF і eBPF

Прывітанне, Хабр! Паведамляем, што ў нас рыхтуецца да выхаду кнігаLinux Observability with BPF".

Кароткае ўвядзенне ў BPF і eBPF
Паколькі віртуальная машына BPF працягвае эвалюцыянаваць і актыўна прымяняецца на практыцы, мы перавялі для вас артыкул, які апісвае яе асноўныя магчымасці і стан на цяперашні час.

У апошнія гады сталі набіраць папулярнасць інструментарыі для праграмавання і прыёмы, закліканыя кампенсаваць абмежаванні ядра Linux у выпадках, калі патрабуецца высокапрадукцыйная апрацоўка пакетаў. Адзін з найбольш папулярных прыёмаў такога роду называецца абыход ядра (kernel bypass) і дазваляе, прапускаючы сеткавы ўзровень ядра, выконваць усю апрацоўку пакетаў з карыстацкай прасторы. Абыход ядра таксама мяркуе кіраванне сеткавай картай з карыстацкай прасторы. Іншымі словамі, пры працы з сеткавай картай мы належым на драйвер карыстацкай прасторы.

Перадаючы поўны кантроль над сеткавай картай праграме з карыстацкай прасторы, мы скарачаем выдаткі, абумоўленыя працай ядра (пераключэнне кантэксту, апрацоўка сеткавага ўзроўня, перапыненні, т.д.), што досыць важна пры працы на хуткасцях 10Гб/з або вышэй. Абыход ядра плюс камбінацыя іншых магчымасцяў (пакетная апрацоўка) і акуратная настройка прадукцыйнасці (улік NUMA, ізаляцыя CPU, г.д.) адпавядаюць асновам высокапрадукцыйнай сеткавай апрацоўкі ў карыстацкай прасторы. Магчыма, узорны прыклад такога новага падыходу да апрацоўкі пакетаў - гэта ДПДК ад Intel (Data Plane Development Kit), хоць, існуюць і іншыя шырока вядомыя інструментары і прыёмы, сярод якіх VPP ад Cisco (Vector Packet Processing), Netmap і, вядома ж, Снабб.

У арганізацыі сеткавых узаемадзеянняў у карыстацкай прасторы ёсць шэраг недахопаў:

  • Ядро АС - гэта ўзровень абстрагавання для апаратных рэсурсаў. Паколькі праграмам карыстацкай прасторы даводзіцца кіраваць сваімі рэсурсамі напроста, ім таксама даводзіцца кіраваць і ўласным апаратным забеспячэннем. Часцяком гэта азначае неабходнасць праграмавання ўласных драйвераў.
  • Паколькі мы цалкам адмаўляемся ад прасторы ядра, мы таксама адмаўляемся і ад усяго сеткавага функцыяналу, які прадстаўляецца ядром. Праграмам карыстацкай прасторы даводзіцца нанова рэалізаваць тыя функцыі, якія, магчыма, ужо падаюцца ядром ці аперацыйнай сістэмай.
  • Праграмы працуюць у рэжыме пясочніцы, што сур'ёзна абмяжоўвае магчымасці іх узаемадзеяння і перашкаджае ім інтэгравацца з іншымі часткамі аперацыйнай сістэмы.

У сутнасці, пры арганізацыі сеткавых узаемадзеянняў у карыстацкай прасторы павышэнне прадукцыйнасці дасягаецца шляхам пераносу апрацоўкі пакетаў з ядра ў карыстацкую прастору. XDP робіць роўна наадварот: перамяшчае сеткавыя праграмы з карыстацкай прасторы (фільтры, пераўтваральнікі, маршрутызацыя, г.д.) у вобласць ядра. XDP дазваляе нам выканаць сеткавую функцыю, як толькі пакет пападае на сеткавы інтэрфейс і да таго, як ён пачынае рух уверх у сеткавую падсістэму ядра. У выніку хуткасць апрацоўкі пакетаў істотна павялічваецца. Аднак, як ядро ​​дазваляе карыстачу выконваць свае праграмы ў прасторы ядра? Перш чым адказаць на гэтае пытанне, давайце разгледзім, што такое BPF.

BPF і eBPF

Нягледзячы на ​​не зусім зразумелую назву BPF (Фільтраванне пакетаў, Берклі) - гэта, фактычна, мадэль віртуальнай машыны. Дадзеная віртуальная машына зыходна праектавалася для апрацоўкі фільтрацыі пакетаў, адгэтуль і назоў.

Адным з найболей вядомых прылад, выкарыстоўвалых BPF, з'яўляецца tcpdump. Пры захопе пакетаў з дапамогай tcpdump карыстач можа задаць выраз для фільтрацыі пакетаў. Захоплівацца будуць толькі пакеты, якія адпавядаюць гэтаму выразу. Напрыклад, выраз “tcp dst port 80” дакранаецца ўсіх TCP-пакетаў, якія паступаюць на порт 80. Кампілятар можа скараціць гэты выраз, пераўтварыўшы яго ў байт-код 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 адпавядае ethertype пакета.
  • Інструкцыя (001): параўноўвае значэнне ў акумулятары з 0x86dd, гэта значыць, з ethertype-значэннем для IPv6. Калі вынік роўны true, то лічыльнік праграмы пераходзіць да інструкцыі (002), а калі не - то да (006).
  • Інструкцыя (006): параўноўвае значэнне з 0x800 (ethertype-значэнне для IPv4). Калі адказ true, то праграма пераходзіць да (007), калі не - то да (015).

І гэтак далей, пакуль праграма фільтрацыі пакетаў не верне вынік. Звычайна гэта булеан. Зварот ненулявога значэння (інструкцыя (014)) азначае, што пакет падышоў, а зварот нулявога (інструкцыя (015)) азначае, што пакет не падышоў.

Віртуальная машына BPF і яе байт-код былі прапанаваныя Стывам Мак-Канам і Ванам Джэйкабсанам у канцы 1992, калі выйшаў іх артыкул Фільтр пакетаў BSD: Новая архітэктура для захопу пакетаў на карыстацкім узроўні, упершыню дадзеная тэхналогія была прадстаўлена на канферэнцыі Usenix зімой 1993 года.

Паколькі BPF - гэта віртуальная машына, яна вызначае асяроддзе, у якой выконваюцца праграмы. Акрамя байт-кода яна таксама вызначае пакетную мадэль памяці (інструкцыі загрузкі няяўна прымяняюцца да пакета), рэгістры (A і X; рэгістры акумулятара і азначніка), сховішча скрэтч-памяці і няяўны лічыльнік праграм. Цікава, што байт-код BPF быў змадэляваны па ўзоры Motorola 6502 ISA. Як успамінаў Стыў Мак-Кан у сваім пленарным дакладзе на Sharkfest '11, ён быў знаёмы са зборкай 6502 яшчэ са старэйшых класаў, калі праграмаваў на Apple II, і гэтыя веды паўплывалі на яго працу па праектаванні байт-кода BPF.

Падтрымка BPF рэалізаваная ў ядры Linux у версіі v2.5 і вышэй, дададзеная ў асноўным намаганнямі Джэя Шуліста. Код BPF заставаўся без сур'ёзных зменаў аж да 2011 года, калі Эрык Думазет перарабіў інтэрпрэтатар BPF для працы ў рэжыме JIT (Крыніца: JIT для пакетных фільтраў). Пасля гэтага ядро ​​замест інтэрпрэтацыі байт-кода BPF магло напроста пераўтвараць праграмы BPF пад мэтавую архітэктуру: x86, ARM, MIPS, т.д.

Пазней, у 2014 годзе, Аляксей Старавойтаў прапанаваў новы JIT-механізм для BPF. Фактычна гэты новы JIT стаў новай архітэктурай на аснове BPF і атрымаў назву eBPF. Думаю, на працягу некаторага часу абедзве віртуальныя машыны суіснавалі, але ў наш час фільтраванне пакетаў рэалізуецца на аснове 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. Яны даступныя па адрасе samples/bpf/. Каб скампіляваць гэтыя прыклады, проста ўвядзіце:

$ sudo make samples/bpf/

Я не буду сам пісаць новы прыклад для eBPF, а скарыстаюся адным з узораў, даступных у samples/bpf/. Разгледжу некаторыя ўчасткі кода і растлумачу, як ён працуе. У якасці прыкладу я абраў праграму tracex4.

Наогул, кожны з прыкладаў у samples/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, таму параю яшчэ некалькі матэрыялаў для далейшага вывучэння

Рэкамендую пачытаць:

  • BPF: Universal in-kernel virtual machine Джонатана Корбетта. Уводзіны ў BPF і аповяд аб тым, як яна эвалюцыянавала ў eBPF.
  • A thorough introduction to eBPF Брэндана Грэга. Артыкул з сайта LWN.net. Брэндан часта піша твіты аб eBPF і вядзе спіс рэсурсаў на гэтую тэму ў сябе ў блогу.
  • Notes on BPF & eBPF Джуліі Эванс. Каментары да прэзентацыі Сучакры Шармы "The BSD Packet Filter: A New Architecture for User-level Packet Capture". Каментары добрыя і сапраўды дапамагаюць разабрацца ў слайдах.
  • eBPF, part1: Past, Present and Future Фэрыса Эліса. Лонгрыд з працягам, Але чытаць варта. Адзін з лепшых артыкулаў аб eBPF, якія мне трапляліся.

Крыніца: habr.com

Дадаць каментар