Здравей, Хабр! Уведомяваме ви, че подготвяме книга "".

Тъй като виртуалната машина 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 г., когато беше публикувана тяхната статия. , за първи път тази технология беше представена на конференцията Usenix през зимата на 1993 година.
Тъй като BPF е виртуална машина, тя определя средата, в която се изпълняват програмите. Освен байт-кода, тя също така определя модела за пакетиране на памет (инструкциите за зареждане неявно се прилагат към пакета), регистрите (A и X; регистри на акумулатора и индекса), хранилището на временната памет и неявния брояч на програмата. Интересно е, че байт-кодът BPF е модел по образец на Motorola 6502 ISA. Както си спомня Стив Мак-Канн в своята на Sharkfest ‘11, той беше запознат със сборката 6502 още от гимназията, когато програмираше на Apple II, и тези знания повлияха на работата му по проектирането на байт-кода BPF.
Подкрепата за BPF е реализирана в ядрото на Linux в версия v2.5 и по-високи, добавена основно благодарение на усилията на Джей Шулист. Кодът на BPF остава без сериозни промени до 2011 г., когато Ерик Думазет преосмисля интерпретатора на BPF, за да работи в режим 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. използва 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). Всички имена на функции, написани с големи букви, съответстват на макроси, дефинирани в .
Ако направя дамп на секциите на обектния файл, трябва да видя, че тези нови секции вече са определени:
$ objdump -h tracex4_kern.o
tracex4_kern.o: файл формат elf64-little
Секции:
Idx Име Размер VMA LMA Off Файл Algn
0 .text 00000000 0000000000000000 0000000000000000 00000040 2**2
СОДЪРЖАНИЕ, ЗА РАЗПОЛОЖЕНИЕ, ЗА ЗАРЕЖДАНЕ, ЧЕТЕМО, КОД
1 kprobe\/kmem_cache_free 00000048 0000000000000000 0000000000000000 00000040 2**3
СОДЪРЖАНИЕ, ЗА РАЗПОЛОЖЕНИЕ, ЗА ЗАРЕЖДАНЕ, РЕЛОК, ЧЕТЕМО, КОД
2 kretprobe\/kmem_cache_alloc_node 000000c0 0000000000000000 0000000000000000 00000088 2**3
СОДЪРЖАНИЕ, ЗА РАЗПОЛОЖЕНИЕ, ЗА ЗАРЕЖДАНЕ, РЕЛОК, ЧЕТЕМО, КОД
3 maps 0000001c 0000000000000000 0000000000000000 00000148 2**2
СОДЪРЖАНИЕ, ЗА РАЗПОЛОЖЕНИЕ, ЗА ЗАРЕЖДАНЕ, ДАННИ
4 license 00000004 0000000000000000 0000000000000000 00000164 2**0
СОДЪРЖАНИЕ, ЗА РАЗПОЛОЖЕНИЕ, ЗА ЗАРЕЖДАНЕ, ДАННИ
5 version 00000004 0000000000000000 0000000000000000 00000168 2**2
СОДЪРЖАНИЕ, ЗА РАЗПОЛОЖЕНИЕ, ЗА ЗАРЕЖДАНЕ, ДАННИ
6 .eh_frame 00000050 0000000000000000 0000000000000000 00000170 2**3
СОДЪРЖАНИЕ, ЗА РАЗПОЛОЖЕНИЕ, ЗА ЗАРЕЖДАНЕ, РЕЛОК, ЧЕТЕМО, ДАННИ
Има също , основната програма. По принцип, тази програма слуша събития kmem_cache_alloc_node. Когато настъпи такова събитие, се изпълнява съответния код на eBPF. Кодът запазва IP атрибута на обекта в картата, а след това този обект циклично се извежда в основната програма. Пример:
$ sudo .\/tracex4
обект 0xffff8d6430f60a00 е на 2 секунди, алокиран е на ip ffffffff9891ad90
обект 0xffff8d6062ca5e00 е на 23 секунди, алокиран е на ip ffffffff98090e8f
обект 0xffff8d5f80161780 е на 6 секунди, алокиран е на ip ffffffff98090e8f
Как са свързани програмата в потребителското пространство и програмата eBPF? При инициализация tracex4_user.c зарежда обектния файл tracex4_kern.o с помощта на функция зареди_bpf_файл.
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;
} При изпълнение на сонди, определени във файла 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 и разказ за това как тя е еволюирала в eBPF.
- Брендан Грегг. Статия от сайта LWN.net. Брендан често пише туитове за eBPF и поддържа списък с ресурси на тази тема в своя .
- Джулия Евънс. Коментари към презентацията на Сучакра Шарма “The BSD Packet Filter: A New Architecture for User-level Packet Capture”. Коментарите са добри и наистина помагат за разбирането на слайдовете.
- Ферис Еллис. Дълго четиво с , но си струва да се чете. Една от най-добрите статии за eBPF, на които съм попадал.
Източник: habr.com
