Это очередная статья из цикла BPF для самых маленьких (0, 1, 2) и первая из серии практических статей про трассировку Linux современными средствами.
Из нее вы узнаете о программе и языке bpftrace
самом простом способе погрузиться в мир BPF с практической точки
зрения, даже если вы не знаете про BPF ровным счетом ничего.
Утилита bpftrace
позволяет при помощи простого языка
прямо из командной строки создавать программы-обработчики и
подсоединять их к огромному количеству событий ядра и пространства
пользователя. Посмотрите на КПДВ ниже поздравляю, вы уже умеете
трейсить системные вызовы при помощи bpftrace
!
В отличие от предыдущих статей серии, эта относительно короткая
и ее основная часть написана в формате туториала, так что уже после
пяти минут чтения вы сможете создавать обработчики и собирать
статистику о любом событии в ядре Linux прямо из командной строки.
В конце статьи рассказывается об альтернативах ply
и
BCC. Минуты во фразе после пяти минут чтения подразумеваются
меркурианские. Появление уникальных навыков после пяти минут чтения
не гарантируется.
Содержание
- Установка bpftrace
- Какие события мы можем трейсить?
- Bpftrace: tutorial
- BCC: утилиты и фреймворк
- Ply: bpftrace для бедных
Установка bpftrace
Короткая версия. Попробуйте выполнить команду sudo
apt install bpftrace
(скорректированную под ваш
дистрибутив). Если bpftrace
установился, то переходите
к следующему разделу.
Длинная версия. Хотя bpftrace
и доступен в
качестве пакета в популярных дистрибутивах, но не во всех, а кроме
этого он может быть собран криво, например, без поддержки BTF.
Поэтому давайте посмотрим на то, как добыть bpftrace
альтернативными средствами.
При каждом обновлении master-ветки репозитория bpftrace собирается
и публикуется новый docker image с упакованным внутри
bpftrace
. Таким образом, мы можем просто скачать и
скопировать бинарник:
$ docker pull quay.io/iovisor/bpftrace:latest$ cd /tmp$ docker run -v $(pwd):/o quay.io/iovisor/bpftrace:latest /bin/bash -c "cp /usr/bin/bpftrace /o"$ sudo ./bpftrace -Vbpftrace v0.11.4
Если bpftrace ругается на слишком старую версию
glibc
, то вам нужен другой docker image со старой
glibc
.
Проверим, что программа работает. Для этого запустим пример из
КПДВ, который трейсит системный вызов execve(2)
и в
реальном времени показывает какие программы запускаются в системе,
и кем:
$ sudo ./bpftrace -e 'tracepoint:syscalls:sys_enter_execve { printf("%s -> %s\n", comm, str(uptr(args->filename))); }'Attaching 1 probe...bash -> /bin/echobash -> /usr/bin/lsgnome-shell -> /bin/shsh -> /home/aspsk/bin/geeqiesh -> /usr/local/sbin/geeqie...
Наконец, когда мы убедились, что все работает, давайте положим бинарник в правильное место:
$ sudo mv /tmp/bpftrace /usr/local/bin
Если вам этого мало, то можете скачать исходники, запустить чуть
больше проверок, собрать свое ядро с поддержкой BTF, а также свою
версию bpftrace
, при помощи docker или локально.
Инструкции по самостоятельной сборке bpftrace
см.
в официальной
документации.
Важная деталь. Если bpftrace
и/или ядро
собрано без поддержки BTF, то для полноценной работы нужно
установить kernel headers. Если вы не знаете как это сделать, то в
документации bpftrace
есть универсальный
дистрибутивонезависимый рецепт.
Мы не будем в этой статье подробно рассказывать про BTF, скажем только, что BTF убирает зависимость от kernel headers, позволяет писать более простые программы, а также расширяет набор событий, к которым мы можем подключиться. Если внешние обстоятельства требуют от вас изучения BTF прямо сейчас, то начните с этой статьи и продолжите этой и этой.
Какие события мы можем трейсить?
Вы сидите в лесу с фотоаппаратом на удобном раскладном стуле. Идет мелкий дождь, но он вам не страшен, ведь шалаш из еловых веток надежно укрывает вас как от дождя, так и от случайных взглядов. Рядом стоит термос с пуэром, йогурт и поднос с копчеными селедками. Но тут вы вдруг задумываетесь: а зачем я сюда пришел и что я буду снимать?! Когда вы просыпаетесь, на лице краснеет слепок клавиатуры, а в терминале написано
$ sudo apt install bpftraceThe following NEW packages will be installed: bpftracePreparing to unpack .../bpftrace_0.11.0-1_amd64.deb ...Unpacking bpftrace (0.11.0-1) ...Setting up bpftrace (0.11.0-1) ...Processing triggers for man-db (2.9.3-2) ...$
Итак, bpftrace
у нас уже установлен. Какие события
мы можем инструментировать? Давайте посмотрим на них по очереди, а
заодно познакомимся с синтаксисом языка bpftrace
. Вот
спойлер-оглавление данной секции:
- Специальные события:
BEGIN
,END
- События, основанные на kprobes:
kprobe
,kretprobe
,uprobe
,uretprobe
- События, основанные на BPF trampolines:
kfunc
,kretfunc
- События, основанные на tracepoints:
tracepoint
- Статическая отладка в пространстве пользователя:
usdt
- События, основанные на подсистеме perf:
software
,hardware
,profile
,interval
,watchpoint
В зависимости от настроения, вы можете прочитать весь этот раздел, можете один-два пункта на ваш выбор, а можете просто перейти к следующему разделу и возвращаться сюда за справкой.
Bpftrace: hello world
Язык bpftrace
создавался по аналогии с языком
awk и поэтому в нем есть два
специальных события, BEGIN
и END
, которые
случаются в момент запуска и выхода из bpftrace
,
соответственно. Вот первая простая программа:
# bpftrace -e 'BEGIN { printf("Hello world!\n"); }'Attaching 1 probe...Hello world!^C
Программа сразу после старта напечатала "Hello
world!"
. Заметьте, что нам пришлось нажимать
Ctrl-C
, чтобы завершить работу bpftrace
это его поведение по умолчанию. Мы можем завершить работу
bpftrace
из любого события при помощи функции
exit
. Продемонстрируем это, а заодно добавим и
обработку END
:
# bpftrace -e ' BEGIN { printf("Hello world!\n"); exit(); } END { printf("So long\n"); } 'Attaching 2 probes...Hello world!So long
Kprobes динамическая инструментация ядра
Ядро это большая программа, функции этой программы, как водится,
состоят из инструкций, а механизм ядра под названием kprobes
(Kernel Probe ядерный зонд) позволяет нам поставить
точку останова практически на любой инструкции, а точнее, по началу
конкретной функции или коду внутри нее. В контексте данной статьи
нам, вообще говоря, не важно как именно создаются обработчики
kprobes
, но вы можете узнать об этом из предыдущих
статей этой серии, ссылки на которые есть в конце, а также из
будущих статей, в которых мы разберем все технические
подробности трассировки Linux с BPF.
В качестве примера давайте посмотрим на то, как часто и кем
вызывается функция schedule
:
$ sudo bpftrace -e ' k:schedule { @[comm] = count(); } i:s:2 { exit();} END { print(@, 10); clear(@); }'Attaching 3 probes...@[Timer]: 147@[kworker/u65:0]: 147@[kworker/7:1]: 153@[kworker/13:1]: 158@[IPC I/O Child]: 170@[IPC I/O Parent]: 172@[kworker/12:1]: 185@[Web Content]: 229@[Xorg]: 269@[SCTP timer]: 1566
Мы также сказали программе выйти через две секунды и в конце
напечатать только десять верхних элементов словаря
@
.
Много ли функций можно потрейсить при помощи кей-проб? Это легко проверить:
$ sudo bpftrace -l 'k:*' | wc -l61106
Это почти все функции загруженного в данный момент ядра. Исключения составляют функции, которые компилятор решил встроить в код и немногочисленные функции, которые запрещено трейсить при помощи kprobe, например, функции, которые реализуют сам механизм kprobes.
kretprobes
Для каждой kprobe
мы можем создать обработчик
kretprobe
. Если kprobe
запускается в
момент входа в функцию, то kretporobe
запускается в
момент выхода из функции. При этом код возврата функции содержится
в специальной встроенной переменной
retval
.
Например, вот что на отрезке в две секунды возвращала функция
vfs_write
на моей системе (в виде логарифмической
гистограммы):
$ sudo bpftrace -e 'kr:vfs_write { @ = hist(retval); } i:s:2 { exit(); }'Attaching 2 probes...@:[1] 606 |@@@@@@@@@@@@@@@@@@@@@@@@@ |[2, 4) 0 | |[4, 8) 0 | |[8, 16) 1223 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@|[16, 32) 0 | |[32, 64) 25 |@ |
uprobes и uretprobes
Кроме инструментации функций ядра, мы можем инструментировать
каждую программу (и библиотеку), работающую в пространстве
пользователя. Реализуется это при помощи тех же
kprobes
. Для этого в bpftrace
определены
события uprobes
и uretprobes
вызов и
возврат из функции.
Вот как мы можем подглядывать за тем, что печатают пользователи баша (в квадратных скобках печатается UID пользователя):
$ sudo bpftrace -e 'uretprobe:/bin/bash:readline { printf("readline: [%d]: \"%s\"\n", uid, str(uptr(retval))); }'Attaching 1 probe...readline: [1000]: "echo "hello habr""readline: [0]: "echo "hello from root""^C
Динамическая инструментация ядра, версия 2
Для счастливых обладателей CONFIG_DEBUG_INFO_BTF=y
в конфиге ядра
существует более дешевый, по сравнению с kprobes, способ
динамической инструментации ядра, основанный на bpf trampolines. Так как BTF в
дистрибутивных ядрах обычно выключен, я про эти события дальше не
рассказываю. Если интересно, то смотрите сюда и/или задавайте вопросы
в комментариях.
Tracepoints статическая инструментация ядра
Если что-то можно сделать динамически, то, с большой вероятностью, это же можно сделать статически чуть быстрее и удобнее. Механизм ядра под названием tracepoints предоставляет набор статических точек останова ядра для наиболее важных функций. Вы можете посмотреть на свой набор так:
$ sudo bpftrace -l 't:*'
Их сильно меньше, чем kprobes
:
$ sudo bpftrace -l 't:*' | wc -l1782
но самой важной особенностью tracepoints является то, что они
предоставляют стабильный API: вы можете быть уверены, что
tracepoint, на основе которой вы написали свой код для отладки или
сбора информации, не пропадет или не поменяет семантику в самый
неудобный момент. Еще одним удобным отличием является то, что
bpftrace
может нам рассказать о том, какие аргументы
передаются в конкретный tracepoint, например:
$ sudo bpftrace -lv tracepoint:thermal:thermal_temperaturetracepoint:thermal:thermal_temperature __data_loc char[] thermal_zone; int id; int temp_prev; int temp;
В случае kprobe
, если у вас не включен BTF, вам
придется читать исходники ядра, причем той версии, которую вы
используете. А с BTF вы можете смотреть и на строение
kprobes
и kfuncs
.
usdt статическая инструментация в пространстве пользователя
Статическая инструментация программ пользователя позволяет напихать в программу статических точек останова в момент компиляции. Давайте сразу посмотрим на пример (который я стянул почти без изменений отсюда):
#include <sys/sdt.h>#include <sys/time.h>#include <unistd.h>int main(int argc, char **argv){ struct timeval tv; for (;;) { gettimeofday(&tv, NULL); DTRACE_PROBE1(test, probe, tv.tv_sec); sleep(1); }}
Мы добавили одну статическую точку останова под названием
test:probe
, в которую передаем один аргумент
tv.tv_sec
текущее время в секундах. Чтобы
скомпилировать эту программу, нужно поставить пакет
systemtap-sdt-dev
(или аналогичный для вашего
дистрибутива). Дальше мы можем посмотреть на то, что
получилось:
$ cc /tmp/test.c -o /tmp/test$ sudo bpftrace -l 'usdt:/tmp/test'usdt:/tmp/test:test:probe
Запустим /tmp/test
в одном терминале, а в другом
скажем
$ sudo bpftrace -e 'usdt:/tmp/test:test:probe { printf("московское время %u\n", arg0); }'Attaching 1 probe...московское время 1612903991московское время 1612903992московское время 1612903993московское время 1612903994московское время 1612903995...
Здесь arg0
это значение tv.tv_sec
,
которое мы передаем в breakpoint.
События Perf
Программа bpftrace
поддерживает множество событий,
предоставляемых подсистемой ядра Perf. Мы сейчас коротко посмотрим
на следующие типы событий, поддерживаемые
bpftrace
:
software
: статически-сгенерированные софтверные событияhardware
: железные PMCsinterval
: интервальное событиеprofile
: интервальное событие для профилирования
События типа `software`
В ядре определяется несколько статических событий perf, посмотреть их список можно так:
# bpftrace -l 's:*'software:alignment-faults:software:bpf-output:software:context-switches:software:cpu-clock:software:cpu-migrations:software:dummy:software:emulation-faults:software:major-faults:software:minor-faults:software:page-faults:software:task-clock:
События такого типа могут происходить очень часто, поэтому на практике указывается количество сэмплов, например, команда
# bpftrace -e 'software:cpu-migrations:10 { @[comm] = count(); }'Attaching 2 probes...^C@[kworker/u65:1]: 1@[bpftrace]: 1@[SCTP timer]: 2@[Xorg]: 2
Подсчитает каждое десятое событие миграции процесса с одного CPU
а другой. Значение событий из списка выше объясняется в
perf_event_open(2)
, например,
cpu-migrations
, которую мы использовали выше можно
найти в этой man-странице под именем
PERF_COUNT_SW_CPU_MIGRATIONS
.
События типа `hardware`
Ядро дает нам доступ к некоторым hardware counters, а
bpftrace
может вешать на них программы BPF. Точный
список событий зависит от архитектуры и ядра, например:
bpftrace -l 'h*'hardware:backend-stalls:hardware:branch-instructions:hardware:branch-misses:hardware:bus-cycles:hardware:cache-misses:hardware:cache-references:hardware:cpu-cycles:hardware:frontend-stalls:hardware:instructions:hardware:ref-cycles:
Посмотрим на то как мой процессор предсказывает инструкции
перехода (считаем каждое стотысячное событие, см.
PERF_COUNT_HW_BRANCH_MISSES
):
bpftrace -e 'hardware:branch-misses:100000 { @[tid] = count(); }'Attaching 3 probes...@[1055]: 4@[3078]: 4@[1947]: 5@[1066]: 6@[2551]: 6@[0]: 29
События типа `interval` и `profile`
События типов interval
и profile
позволяют пользователю запускать обработчики через заданные
интервалы времени. Событие первого типа запустится один раз на
одном CPU, а событие второго на каждом из CPU.
Мы уже использовали интервал раньше, чтобы выйти из программы. Давайте посмотрим на этот пример еще раз, но чуть пропатчим его:
$ sudo bpftrace -e ' kr:vfs_write { @ = hist(retval); } interval:s:2 { print(@); clear(@); }'
Первой строчкой мы подключаемся к выходу из функции ядра
vfs_write
и считаем в ней гистограмму всех вызовов, а
на второй строчке используем interval
, который будет
запускаться каждые две секунды, печатать и очищать словарь
@
.
Аналогично можно использовать и profile
:
# bpftrace -e ' profile:hz:99 { @[kstack] = count(); } i:s:10 { exit(); } END { print(@,1); clear(@); }'Attaching 3 probes...@[ cpuidle_enter_state+202 cpuidle_enter+46 call_cpuidle+35 do_idle+487 cpu_startup_entry+32 start_secondary+345 secondary_startup_64+182]: 14455
Здесь мы запускаем profile
на каждом ядре 99 раз в
секунду, через десять секунд выстрелит интервал и вызовет
exit()
, а секция END
напечатает только
верхний элемент словаря @
самый часто встречающийся
ядерный стек (по которому мы видим, что моя система, в основном,
бездействует).
Bpftrace: tutorial
Базовые навыки
Начнем с простого, запустим bpftrace
без
аргументов:
# bpftrace
Программа напечатает короткую справку о том, как ее
использовать. В частности, посмотрите какие переменные окружения
использует bpftrace
. Заметьте, что мы запускаем
bpftrace
от рута. Для вывода справки это не нужно, но
для любых других действий да.
Посмотрим внимательнее на аргумент -l
. Он позволяет
найти события по шаблону. (Если что-то дальше не ясно, то читайте
про события в предыдущем разделе, который вы пропустили.) Вот как
можно искать события среди всех возможных:
# bpftrace -l '*kill_all*'kprobe:rfkill_allockprobe:kill_allkprobe:btrfs_kill_all_delayed_nodes
А здесь мы ищем события только среди
tracepoints
:
# bpftrace -l 't:*kill*'tracepoint:cfg80211:rdev_rfkill_polltracepoint:syscalls:sys_enter_killtracepoint:syscalls:sys_exit_killtracepoint:syscalls:sys_enter_tgkilltracepoint:syscalls:sys_exit_tgkilltracepoint:syscalls:sys_enter_tkilltracepoint:syscalls:sys_exit_tkill
Подмножество tracepoint:syscalls
, на которое мы
только что наткнулись, можно использовать для самостоятельных
экспериментов по изучению bpftrace
. Для каждого
системного вызова X
определены две точки останова:
tracepoint:syscalls:sys_enter_Xtracepoint:syscalls:sys_exit_X
Поиграемся с каким-нибудь системным вызовом, например, execve(2)
. Для
того, чтобы посмотреть на то, как использовать какой-либо
tracepoint
можно использовать дополнительный аргумент
-v
, например:
# bpftrace -lv 't:s*:sys_*_execve'tracepoint:syscalls:sys_enter_execve int __syscall_nr; const char * filename; const char *const * argv; const char *const * envp;tracepoint:syscalls:sys_exit_execve int __syscall_nr; long ret;
(заметьте как ловко мы дважды использовали *
, чтобы
не писать syscalls
полностью и чтобы не перечислять
sys_enter_execve
и sys_exit_execve
по
отдельности). Параметры, перечисленные в выводе -lv
доступны через встроенную переменную args
:
# bpftrace -e ' t:s*:sys_enter_execve { printf("ENTER: %s\n", str(uptr(args->filename))); } t:s*:sys_exit_execve { printf("EXIT: %s: %d\n", comm, args->ret); }'Attaching 2 probes...ENTER: /bin/lsEXIT: ls: 0ENTER: /bin/lssEXIT: bash: -2
Этот короткий листинг позволяет разглядеть несколько интересных вещей.
В первом обработчике мы печатаем аргумент
args->filename
. Так как передается он нам как
указатель, нам нужно вычитать строку при помощи встроенной функции
str
, но просто так ее использовать нельзя: указатель
этот указывает в пространство пользователя, а значит мы должны об
этом специально сказать при помощи функции uptr
. Сам
bpftrace
постарается угадать принадлежность указателя,
но он не гарантирует результат. Также, к сожалению, вызов
bpftrace -lv
не расскажет нам о семантике указателя,
для этого придется изучать исходники, в данном случае, мы посмотрим
на определение системного вызова
execve (обратите внимание на квалификатор типа
__user
).
Во втором обработчике мы используем встроенную переменную
comm
, которая возвращает текущее имя потока. Код
возврата системного вызова доступен через переменную
args->ret
. Как известно, этот системный вызов "не
возвращается" в текущую программу, так как его работа заключается
собственно в замене кода программы новым. Однако, в случае ошибки
он-таки вернется, как мы и можем видеть в выводе выше: в первом
случае я запустил /bin/ls
из баша и exec
запустился нормально и вернул 0 (внутри процесса ls
прямо перед запуском кода /bin/ls
), а во втором случае
я запустил несуществующую программу /bin/lss
и
exec
вернул -2
внутри процесса
bash
.
Упражнение. Возьмите ваш любимый системный вызов и
напишите несколько программ, аналогичных приведенной выше.
Попробуйте напечатать все аргументы системного вызова и значение,
которое он возвращает. Не забудьте использовать uptr
, если
нужно.
Структура программ `bpftrace`
Язык bpftrace
создавался по аналогии с языком awk
(см. также главу 6 в книжке The AWK Programming
Language
) и имеет очень похожую структуру. Программы
состоят из списка блоков вида
<probe> <filter> { <action> }
Например,
# bpftrace -e 'BEGIN { printf("Hello\n") } END { printf("World\n") }'
Здесь <probe>
это BEGIN
и
END
, а <action>
это
printf
. Поле
<filter>
является опциональным и используется
для фильтрации событий,
например, программа
# bpftrace -e 'p:s:1 /cpu == 0/ { printf("Привет с CPU%d\n", cpu); }'Attaching 1 probe...Привет с CPU0Привет с CPU0^C
будет передавать привет, только если запускается на CPU 0.
Упражнение. Что выведет эта команда на вашей машине, если
убрать фильтр /cpu == 0/
?
На практике <filter>
удобно использовать для
синхронизации между двумя событиями. Например, вы хотите подсчитать
время выполнения системного вызова write
на вашей
системе. Для этого мы можем использовать пару трейспоинтов
sys_enter_write
и sys_exit_write
и
считать время выполнения по тредам:
# cat /tmp/write-times.btt:syscalls:sys_enter_write { @[tid] = nsecs}t:syscalls:sys_exit_write /@[tid]/ { @write[comm] = sum((nsecs - @[tid]) / 1000); delete(@[tid]);}END { print(@write, 10); clear(@write); clear(@);}
Эта программа уже довольно длинная, поэтому мы записали ее в отдельный файл. Запустить ее можно так:
# bpftrace /tmp/write-times.bt
В первом событии, sys_enter_write
, мы запоминаем
время запуска системного вызова write
в наносекундах в
словаре @
, ключом к которому служит tid
.
Во втором событии, sys_exit_write
, мы при помощи
фильтра /@[tid]/
проверяем, запускался ли обработчик
первого события для данного потока. Нам нужно это делать, ведь мы
могли запустить программу в тот момент, когда какой-то поток был
внутри системного вызова write
. Дальше мы записываем
потраченное время (в микросекундах) в отдельный словарь
@write
и удаляем элемент @[tid]
.
Наконец, после того как мы нажимаем ^C
, запускается
секция END
, в которой мы печатаем десять самых
прожорливых процессов и чистим словари @write
и
@
, чтобы bpftrace
не выводил их
содержимое.
Упражнение. Так что же именно может пойти не так, если
убрать фильтр /@[tid]/
?
Храним состояние: переменные и мапы
Внутри программ bpftrace
вы можете использовать
обычные для языка C конструкции, например, :?
,
++
, --
. Вы можете использовать блоки
if {} else {}
. Можно составлять циклы при помощи
while
и экзотического unroll
(который
появился в то время, когда в архитектуре BPF циклы были запрещены).
Содержание же во все эти конструкции добавляют переменные и
структуры ядра, доступные из контекста.
Переменные бывают двух типов: локальные и глобальные. Локальные
переменные начинаются со знака доллара $
и доступны в
пределах заданного события, оба следующих варианта сработают:
# bpftrace -e 'BEGIN { $x = 1; printf("%d\n", ++$x); exit(); }'# bpftrace -e 'BEGIN { if (1) { $x = 1; } printf("%d\n", ++$x); exit(); }'
а следующее нет:
# bpftrace -e 'BEGIN { $x = 1; exit(); } END { printf("%d\n", $x); }'
Глобальные переменные, с которыми мы уже встречались выше,
начинаются со знака @
и доступны между событиями. Вы
можете использовать "безымянную" глобальную переменную
@
, как мы делали выше для хранения начала вызова
write
(@[tid]
). (Глобальные переменные в
bpftrace
хранятся в мапах специальных размеченных
областях памяти. Они, между прочим, глобальные в более общем
смысле: любая программа с рутовым доступом на системе может их
читать и писать. Но для данной статьи это не так важно, смотрите
предыдущие серии, если вам интересны подробности.)
И теперь, мы переходим к самому главному: зачем нам нужны
переменные и что мы в них будем записывать? Каждое событие
bpftrace
запускается с определенным контекстом. Для
kprobes
нам доступны аргументы вызываемой функции, для
tracepoints
аргументы, передаваемые в tracepoint, а
для событий Perf, как и для других программ, глобальные переменные.
Мы уже видели как мы можем работать с tracepoints
, в
этой и следующих секциях мы посмотрим на kprobes
, а в
секции Веселые Картинки мы посмотрим
на события Perf.
Аргументы kprobes
доступны внутри программы как
arg0
, arg1
, и т.д. Аргументы передаются
без типа, так что вам придется к нужному типу их приводить вручную.
Пример:
#include <linux/skbuff.h>#include <linux/ip.h>k:icmp_echo { $skb = (struct sk_buff *) arg0; $iphdr = (struct iphdr *) ($skb->head + $skb->network_header); @pingstats[ntop($iphdr->saddr), ntop($iphdr->daddr)]++;}
Эта программа строит статистику о том, кто пингует данный хост.
Мы цепляемся к kprobe
на входе в функцию icmp_echo
,
которая вызывается на приход ICMPv4 пакета типа echo request. Ее
первый аргумент, arg0
в нашей программе, это указатель
на структуру типа sk_buff
, описывающую пакет. Из этой
структуры мы достаем IP адреса и увеличиваем соответствующий
счетчик в глобальной переменной @pingstats
. Все,
теперь у нас есть полная статистика о том, кто и как часто пинговал
наши IP адреса! Раньше для написания такой программы вам пришлось
бы писать модуль ядра, регистрировать в нем обработчик kprobe, а
также придумывать механизм взаимодействия с user space, чтобы
хранить и читать статистику.
Посмотрим на нее еще раз. Вначале мы включили два хедера ядра,
для этого нужно установить пакет с kernel headers. Эти хедеры нужны
нам для определения структур sk_buff
и
iphdr
, которые мы собираемся разыменовывать. (Если бы
у нас был собран BTF, то нам не нужно было бы это делать ни
устанавливать пакет, ни включать хедеры.) В первой строчке
программы мы приводим единственный аргумент функции
icmp_echo
к указателю на sk_buff
и
сохраняем его в локальной переменной $skb
. На второй
строчке мы разыменовываем $skb
и находим место в
памяти, где хранится сетевой заголовок, который мы, в свою очередь,
приводим к указателю на iphdr
. На третьей строчке мы
используем сетевой заголовок и встроенную функцию ntop
языка bpftrace
, которая преобразует бинарный IP адрес
в строку.
Упражнение. Возьмите любую интересующую вас функцию ядра
и попробуйте разыменовать ее аргументы. Не забывайте про
uptr
и kptr
. Например: возьмите функцию
vfs_write
ядра, ее первый аргумент это указатель на
структуру struct file
, определенную в заголовке
<linux/fs.h>
. Попробуйте напечатать интересующие
вас флаги файла до и после вызова vfs_write
. (Hint:
как вы можете передать указатель на struct file
внутрь
kretprobe
?)
Упражнение. Напишите программу bpftrace
,
которая будет печатать имя и пароль пользователя, всякий раз, как
он запускает sudo
.
Считаем и агрегируем события
В предыдущей программе про ping
мы сделали ошибку
не защитились от того, что программа может быть запущена на разных
CPU. Для более точного подсчета мы можем использовать функцию
count
. Следующий пример иллюстрирует проблему:
# bpftrace -e 'p:hz:5000 { @x++; @y = count(); } i:s:10 { exit(); }'Attaching 2 probes...@x: 760528@y: 799002
В течение 10 секунд по 5000 раз в секунду на каждом из 16 ядер
моей системы мы увеличиваем значения двух счетчиков @x
и @y
. Операция ++
выполняется безо всяких
блокировок и поэтому значение счетчика не совсем точное. Операция
count()
на самом деле выполняется тоже безо всяких
блокировок, но использует CPU-локальные переменные: для каждого из
CPU хранится свой счетчик, значения которых при печати
суммируются.
Кроме подсчета событий в bpftrace
есть несколько
полезных функций, которые могут быстро показать какие-то баги в
работающей системе. Главный инструмент тут это гистограммы.
Посмотрим на простой пример.
# bpftrace -e 'kr:vfs_write { @ = hist(retval); } i:s:10 { exit() }'Attaching 2 probes...@:[1] 14966407 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@|[2, 4) 0 | |[4, 8) 0 | |[8, 16) 1670 | |[16, 32) 0 | |[32, 64) 123 | |[64, 128) 0 | |[128, 256) 0 | |[256, 512) 0 | |[512, 1K) 0 | |[1K, 2K) 0 | |[2K, 4K) 0 | |[4K, 8K) 0 | |[8K, 16K) 7531982 |@@@@@@@@@@@@@@@@@@@@@@@@@@ |
В течении десяти секунд мы строим гистограмму возвращаемых
значений функции vfs_write
, и мы можем заметить, что
кто-то уверенно пытается писать по одному байту! Давайте чуть
усовершенствуем программу (то заняло у меня около 20 секунд):
# bpftrace -e ' kr:vfs_write /retval == 1/ { @[pid, comm] = hist(retval); } i:s:10 { exit() } END { print(@, 1); clear(@); }'Attaching 3 probes...@[133835, dd]:[1] 14254511 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@|
Для начала, мы смотрим только на записи, в которых
retval
равен единице. Для того, чтобы различить
процессы мы добавляем ключи идентификатор и имя процесса. Наконец,
в END
мы печатаем только процесс, который сделал
больше всего записей. Что же он делает? Думаю вы уже
догадались:
# tr '\000' ' ' < /proc/133835/cmdlinedd if=/dev/zero of=/dev/null bs=1
Упражнение. Найдите ссылку на официальный туториал по
bpftrace
в конце статьи и выполните все шаги, в
которых встречаются гистограммы.
Веселые Картинки: flame graphs
События типа profile
помогают нам смотреть на
систему. Так мы можем строить гистограммы событий и собирать сэмплы
с работающей системы. Напомню, что событие типа
profile
описывается в одной из следующих форм:
profile:hz:rateprofile:s:rateprofile:ms:rateprofile:us:rate
В первом случае мы задаем частоту события в HZ, а в следующих частоту события в секундах, миллисекундах и микросекундах, соответственно.
Одной из интересных глобальных переменных, которые мы можем сэмплировать, является стек ядра, выглядит он так:
bpftrace -e 'profile:hz:99 { print(kstack); exit() }'Attaching 1 probe... cpuidle_enter_state+202 cpuidle_enter+46 call_cpuidle+35 do_idle+487 cpu_startup_entry+32 rest_init+174 arch_call_rest_init+14 start_kernel+1724 x86_64_start_reservations+36 x86_64_start_kernel+139 secondary_startup_64+182
здесь в момент сэмплирования мы попали на ядро-бездельника.
Для практического профилирования, однако, смотреть на миллионы стеков ядра не очень удобно. Поэтому БГ разработал механизм агрегации под названием flame graphs, превращающий текст в удобную кликабельную картинку.
Для начала, нам нужно запустить bpftrace
следующим
образом:
# bpftrace -e 'profile:hz:333 { @[kstack] = count(); } i:s:10 { exit() }' > /tmp/raw# wc -l /tmp/raw3374 /tmp/raw
Здесь мы по 333 раза в секунду сэмплируем стек ядра и считаем
сколько раз мы увидели разные стеки (мы используем
kstack
как ключ в словаре @
, ведь
kstack
это просто строка).
Далее нам нужно склонировать репозиторий FlameGraph и запустить пару скриптов:
$ git clone https://github.com/brendangregg/FlameGraph.git$ cd FlameGraph$ ./stackcollapse-bpftrace.pl /tmp/raw > /tmp/ready$ ./flamegraph.pl /tmp/ready > /tmp/kstack.svg
Первый скрипт приводит вывод bpftrace
к
каноническому виду, а второй строит по нему картинку (кликните на
нее, чтобы открылся gist с SVG):
Здесь моя система наполовину бездействует, а на половине CPU
крутится все тот же dd
, копирующий
/dev/zero
в /dev/null
по одному байту.
Кликайте на картинку, чтобы посмотреть подробности.
Упражнение. Снимки стека можно делать не только при
помощи bpftrace
. Загляните в в репозиторий
FlameGraph
и сделайте снимок своей системы другим
способом.
Пора закругляться
Если вы дочитали до этого момента и запустили хотя бы половину
примеров, то вы уже можете считать себя профессионалом в нелегком
деле отладки при помощи bpftrace
. Смотрите на ссылки в
конце статьи для того, чтобы узнать куда двигаться дальше.
BCC: утилиты и фреймворк
Благодаря проекту BCC люди, роботы и не определившиеся могут использовать возможности BPF без необходимости утруждать себя программированием проект содержит почти 100 готовых утилит. Эти утилиты не случайные примеры, а рабочие инструменты, используемые повседневно в недрах Netflix, Facebook и других компаний добра. См. ссылки на книжки БГ в конце статьи, в которых подробно описано большинство утилит и подробно обсуждается почему и зачем они нужны.
Утилиты написаны на основе libbcc
и
libbpf
и представляют из себя код на питоне, в который
встроены куски псевдо-C кода, так что их легко редактировать и
расширять даже на боевой машине. Также вы можете писать новые
утилиты по аналогии с существующими, см. следующий подраздел.
Утилиты BCC должны быть более-менее опакечены в популярных
дистрибутивах. Например, на убунте достаточно поставить пакет
bpfcc-tools
. После этого мы можем сразу ими
пользоваться. Например, команда
# funccount-bpfcc 'vfs_*' -d 5Tracing 67 functions for "b'vfs_*'"... Hit Ctrl-C to end.FUNC COUNTb'vfs_statfs' 1b'vfs_unlink' 1b'vfs_lock_file' 2b'vfs_fallocate' 31b'vfs_statx_fd' 32b'vfs_getattr' 80b'vfs_getattr_nosec' 88b'vfs_open' 108b'vfs_statx' 174b'vfs_writev' 2789b'vfs_write' 6554b'vfs_read' 7363Detaching...
посчитает сколько раз были вызваны функции ядра с префиксом
vfs_
на интервале в пять секунд. Чуть интереснее
подсунуть программе параметр -p
, в котором передается
PID процесса. Например, вот что делает мой mplayer
,
пока я это пишу:
# funccount-bpfcc 'vfs_*' -d 5 -p 29380Tracing 67 functions for "b'vfs_*'"... Hit Ctrl-C to end.FUNC COUNTb'vfs_write' 208b'vfs_read' 629Detaching...
Пишем новую утилиту BCC
Давайте напишем простую утилиту BCC. Эта утилита будет считать
сколько раз в секунду были вызваны функции ядра
mutex_lock
и mutex_unlock
. Ее полный код
приведен далее, также вы можете прочитать его здесь.
#! /usr/bin/python3from bcc import BPFfrom ctypes import c_intfrom time import sleep, strftimefrom sys import argvb = BPF(text="""BPF_PERCPU_ARRAY(mutex_stats, u64, 2);static inline void inc(int key){ u64 *x = mutex_stats.lookup(&key); if (x) *x += 1;}void do_lock(struct pt_regs *ctx) { inc(0); }void do_unlock(struct pt_regs *ctx) { inc(1); }""")b.attach_kprobe(event="mutex_lock", fn_name="do_lock")b.attach_kprobe(event="mutex_unlock", fn_name="do_unlock")print("%-8s%10s%10s" % ("TIME", "LOCKED", "UNLOCKED"))while 2 * 2 == 4: try: sleep(1) except KeyboardInterrupt: exit() print("%-8s%10d%10d" % ( strftime("%H:%M:%S"), b["mutex_stats"].sum(0).value, b["mutex_stats"].sum(1).value)) b["mutex_stats"].clear()
Вначале мы подключаем нужные библиотеки. Понятно, что самая
интересная часть тут это импорт класса BPF
:
from bcc import BPF
Этот класс позволяет нам определить программы BPF, которые мы
будем подключать к событиям. В качестве параметра класс
BPF
принимает текст программы на псевдо-C. В нашем
случае это
BPF_PERCPU_ARRAY(mutex_stats, u64, 2);static inline void inc(int key){ u64 *x = mutex_stats.lookup(&key); if (x) *x += 1;}void do_lock(struct pt_regs *ctx) { inc(0); }void do_unlock(struct pt_regs *ctx) { inc(1); }
Этот код написан на магическом C, вы не сможете скомпилировать
его в таком виде, но при инициализации класса BPF
некоторые части будут заменены реальным кодом на C.
Так или иначе, вначале мы определяем массив
mutex_stats
из двух элементов типа u64
,
наших счетчиков. Заметьте, что мы использовали PERCPU массив, это
означает, что для каждого логического CPU будет создан свой массив.
Далее мы определяем функцию inc
, принимающую в
качестве аргумента индекс в массиве mutex_stats
. Эта
функция увеличивает значение соответствующего счетчика.
Наконец, тривиальные функции do_lock
и
do_unlock
увеличивают каждая свой счетчик.
На этом с ядерной частью почти покончено во время инициализации
класс BPF
обратится к библиотеке libllvm
,
чтобы скомпилировать код, и потом зальет его в ядро. Осталось
только подключить программы к интересующим нас
kprobes
:
b.attach_kprobe(event="mutex_lock", fn_name="do_lock")b.attach_kprobe(event="mutex_unlock", fn_name="do_unlock")
Пользовательская часть кода занимается исключительно сбором информации:
print("%-8s%10s%10s" % ("TIME", "LOCKED", "UNLOCKED"))while 2 * 2 == 4: try: sleep(1) except KeyboardInterrupt: exit() print("%-8s%10d%10d" % ( strftime("%H:%M:%S"), b["mutex_stats"].sum(0).value, b["mutex_stats"].sum(1).value)) b["mutex_stats"].clear()
После печати заголовка бесконечный цикл раз в секунду печатает
значение счетчиков и обнуляет массив mutex_stats
.
Значение счетчиков мы получаем при помощи метода sum
массива mutex_stats
, который суммирует значения
счетчиков для каждого CPU:
sum(index) { result = 0 для каждого CPU { result += cpu->mutex_stats[index] } return result}
Вот и все. Программа должна работать примерно так:
$ sudo ./bcc-tool-exampleTIME LOCKED UNLOCKED18:06:33 11382 1299318:06:34 11200 1278318:06:35 18597 2255318:06:36 20776 2551618:06:37 59453 6820118:06:38 49282 5806418:06:39 25541 2742818:06:40 22094 2528018:06:41 5539 725018:06:42 5662 735118:06:43 5205 690518:06:44 6733 8438
Где-то в 18:06:35 я переключился из терминала на вкладку с youtube в Firefox, поставил youtube на паузу и затем в 18:06:40 переключился назад в терминал. Итого, программа показала, что при просмотре youtube вы заставляете ядро локать примерно сорок тысяч мьютексов в секунду.
Напоследок хочется сказать, что если вы предпочитаете писать на
C, то смотрите в сторону libbpf
и CO-RE. Использование
libbpf
напрямую позволяет избавиться от тяжелых
зависимостей времени запуска, типа libllvm
, ускоряет
время работы, а также экономит дисковое пространство. В частности,
некоторые утилиты BCC уже переписаны на libbpf
+ CO-RE
прямо внутри проекта BCC, см. libbpf-tools. За
подробностями обращайтесь к статье BCC to libbpf conversion
guide (или ждите следующую статью из этой серии).
Ply: bpftrace для бедных
Утилита ply
, написанная
шведом Tobias Waldekranz в доисторическом 2015 году, является в
определенном смысле прямым предком bpftrace
. Она
поддерживает awk-подобный язык для создания и инструментации ядра
программами BPF, например,
ply 'tracepoint:tcp/tcp_receive_reset { printf("saddr:%v port:%v->%v\n", data->saddr, data->sport, data->dport);}'
Отличительной особенностью ply
является минимизация
зависимостей: ей нужна только libc
(любая). Это
удобно, если вы хотите с минимальными усилиями поиграться в BPF на
встроенных системах. Для того, чтобы отрезать все зависимости, в
ply
встроен компилятор ply script language ->
BPF
.
Однако, не умаляя достоинств ply
, стоит отметить,
что разработка проекта к настоящему времени заглохла
ply
работает, поддерживается, но новые фичи не
появляются. Вы все еще можете использовать ply
,
например, для того, чтобы потестировать сборку ядра на встроенной
системе или для тестирования прототипов, но я бы советовал сразу
писать программы на C с использованием libbpf
для
эмбедщиков это не составит труда, см., например, статью Building BPF applications with
libbpf-bootstrap.
Ссылки
Предыдущие серии:
- BPF для самых маленьких, часть 0: classic BPF
- BPF для самых маленьких, часть 1: extended BPF
- BPF для самых маленьких, часть 2: разнообразие типов программ BPF
Ссылки на ресурсы по bpftrace
, BCC и вообще отладке
Linux:
-
The bpftrace One-Liner Tutorial. Это туториал по
bpftrace
, в котором перечисляются основные возможности. Представляет из себя список из двенадцати однострочных, или около того, программ. -
bpftrace Reference Guide. Все, что вы хотели знать про использование
bpftrace
, но боялись спросить. Если вам этого документа мало, то идите читать про внутренностиbpftrace
. -
BCC Tutorial. Если вы освоились с
bpftrace
и хотите копнуть глубже (но еще не готовы к освоениюlibbpf
и настоящим приключениям), то смотрите на этот туториал по BCC, на BCC Reference Guide, а также на книжки, перечисленные ниже. -
Brendan Gregg, BPF Performance Tools. БГ распознал потенциал BPF в деле трассировки Linux сразу после его появления и в данной книжке описывает результаты работы последних пяти лет сотню или больше отладочных утилит из проекта BCC. Книжка является отличным справочным дополнением по BPF к следующей.
-
Brendan Gregg, Systems Performance: Enterprise and the Cloud, 2nd Edition (2020). Это второе издание знаменитой Systems Performance. Главные изменения: добавлен материал по BPF, выкинут материал по Solaris, а сам БГ стал на пять лет опытнее. Если книжка BPF Performance Tools отвечает на вопрос как?, то эта книжка отвечает на вопрос почему?, а также рассказывает о других техниках (не BPF единым жив человек).