Perf linux как пользоваться

Обновлено: 06.07.2024

The captured trace file can be reported in a number of ways, summarized by the help message:

perf sched latency will summarize scheduler latencies by task, including average and maximum delay:

To shed some light as to how this is instrumented and calculated, I'll show the events that led to the top event's "Maximum delay at" of 29.702 ms. Here are the raw events from perf sched script:

The time from the wakeup (991962.918368, which is in seconds) to the context switch (991962.948070) is 29.702 ms. This process is listed as "sh" (shell) in the raw events, but execs "cat" soon after, so is shown as "cat" in the perf sched latency output.

perf sched map shows all CPUs and context-switch events, with columns representing what each CPU was doing and when. It's the kind of data you see visualized in scheduler analysis GUIs (including perf timechart, with the layout rotated 90 degrees). Example output:

This is an 8 CPU system, and you can see the 8 columns for each CPU starting from the left. Some CPU columns begin blank, as we've yet to trace an event on that CPU at the start of the profile. They quickly become populated.

The two character codes you see ("A0", "C0") are identifiers for tasks, which are mapped on the right ("=>"). This is more compact than using process (task) IDs. The "*" shows which CPU had the context switch event, and the new event that was running. For example, the very last line shows that at 991962.886917 (seconds) CPU 4 context-switched to K0 (a "cc1" process, PID 16945).

That example was from a busy system. Here's an idle system:

Idle CPUs are shown as ".".

Remember to examine the timestamp column to make sense of this visualization (GUIs use that as a dimension, which is easier to comprehend, but here the numbers are just listed). It's also only showing context switch events, and not scheduler latency. The newer timehist command has a visualization (-V) that can include wakeup events.

perf sched timehist was added in Linux 4.10, and shows the scheduler latency by event, including the time the task was waiting to be woken up (wait time) and the scheduler latency after wakeup to running (sch delay). It's the scheduler latency that we're more interested in tuning. Example output:

This output includes the sleep command run to set the duration of perf itself to one second. Note that sleep's wait time is 1000.104 milliseconds because I had run "sleep 1": that's the time it was asleep waiting its timer wakeup event. Its scheduler latency was only 0.006 milliseconds, and its time on-CPU was 0.269 milliseconds.

There are a number of options to timehist, including -V to add a CPU visualization column, -M to add migration events, and -w for wakeup events. For example:

The CPU visualization column ("012345678") has "s" for context-switch events, and "m" for migration events, showing the CPU of the event.

The last events in that output include those related to the "sleep 1" command used to time perf. The wakeup happened at 991963.885734, and at 991963.885740 (6 microseconds later) CPU 1 begins to context-switch to the sleep process. The column for that event still shows ":17008[17008]" for what was on-CPU, but the target of the context switch (sleep) is not shown. It is in the raw events:

The 991963.886005 event shows that the perf command received a wakeup while sleep was running (almost certainly sleep waking up its parent process because it terminated), and then we have the context switch on 991963.886009 where sleep stops running, and a summary is printed out: 1000.104 ms waiting (the "sleep 1"), with 0.006 ms scheduler latency, and 0.269 ms of CPU runtime.

Here I've decorated the timehist output with the details of the context switch destination in red:

When sleep finished, a waiting "cc1" process then executed. perf ran on the following context switch, and is the last event in the profile (perf terminated). I've submitted a patch to add this info when a -n option is used.

perf sched script dumps all events (similar to perf script):

You can comment here, but I can't guarantee your comment will remain here forever: I might switch comment systems at some point (e.g., if disqus add advertisements).


Site Navigation

Systems Performance 2nd Ed.

Иногда возникает необходимость заглянуть внутрь программы, что-бы посмотреть почему она "так тормозит". Естественно начинается все с высокоуровневых средств htop/atop/nettop/iotop/sar. Но если они показывают, что производительность упирается в процессор, то необходимо знать какие функции требуют больше всего вычислительных мощностей. Это может помочь как переписать свою программу для провышения производительности, так и подобрать оптимальные настройки для чужой.

Собственно функция профилировщика - найти узкие места в программе и выдать нам как можно больше информации о том куда уходят процессорные такты. Можно выделить три основных модели профилирования:

Гарантированное профилирование (determine - не нашел как перевести это лучше). Целевая программа модифицируется (чаще всего перекомпилируется с определенными флагами - -pg для gcc). - в код внедряются вызовы библиотеки профилирования, собирающие информацию о выполняемой программе и передающие ее профилировщику. Наиболее часто замеряется время выполнения каждой функции и после тестового прогона программы можно увидеть вывод, подобный следующему:

Очевидным минусом гарантированного профилирования является необходимость перекомпиляции программы, что неудобно, а иногда и невозможно (если это не ваша программа, например). Также гарантированное профилирование значительно влияет на скорость исполнения программы, делая получение коректного времени исполнения затруднительным. Типичный представитель - gprof,

Статистическое профилирование (statistic profiling) - исполнение программы периодически (

100-1000 раз в секунду) останавливается, управление передается профилировщику, который анализирует текущий констекст исполнения. Затем, используя отладочную информацию или таблици символов из исполняемого файла программы и загруженных библиотек, определяется исполняемая в текущий момент функция и, если возможно, последовательность вызовов в стеке. Профилировщик увеличивает счетчик попаданий для активной функции, а после окончания профилирования можно увидеть сколько раз программа "была поймана" на исполнении определенных функций. Идея очень простая - если в некоторой функции A программа проводит в два раза больше времени, чем в функции B, то в среднем ( при достаточно длительном исполнении) мы будем в два раза чаще останавливаться внутри A, чем внутри B. Статистическое профилирование не требует перекомпиляции программы (хотя некоторые флаги компилятора могут помочь восстанавливать стек), значительно меньше гарантированного профилирования влияет на время и тайминги исполнения, но требует длительного или многократного прогона программы что бы получить надежные результаты. Типичный представитель - oprofile.

Профилирование на эмуляторе . Идея состоит в написании эмулятора, очень точно воспроизводящего целевой процессор, включая кеши со всеми механизмами замещения, декодировщики инструкций, конвееров и прочего. В общем случае задача похожа на не решаемую, учитываю сложность современных процессоров и огромное количество факторов, влияющих на производительность. Кроме этого профилирования на эмуляторе может занимать ооочень много времени из за накладных расходов на эмуляцию. Едиственный известный мне представитель/попытка - valgrind, хотя VTune от intel частично использует подобную технику для анализа кода.

По сумме характеристик статистическое профилирование является самым интересным (IMHO), посколько достаточно просто реализуется, практически не влияет на исполнение программы, способно профилировать даже те компоненты, для которых нет исходных текстов (например внешние библиотеки, линкуемые бинарно или с помощую .so файлов). Если же значительная нагрузка ложится на ядро или внешние сервисы (например комбинация из postgresql + python) то оно становится вообще единственным средством, помогающим понять "кто все съел".

Все серьезные современные процессоры имеют встроенную поддержку стат профилирования через аппаратные счетчики событий производительности. Если кратко то в каждом ядре есть несколько регистров (для core 2 - 7), которые можно настроить на подсчет определенного типа событий, например тактов процессора, исполненных инструкций, промахов в кеши, TLB и т.п.

Обычно счетчики настраиваются на вызов прерывания при достижении определенного значения (например 1M тактов CPU), по входу в прерывание управление передается модулю профилировщика. В линукс до 2.6.31 поддержка аппаратных счетчиков была в основном в intel vtune и oprofile.

Большим плюсом счетчиков является возможность понять почему код работает с наблюдаемой скоростью, посколько они позволяют разложить время исполнения на более мелкие составляющие.

Начнем с динозавра стат профилирования - oprofile:

По итогу получаем следующий вывод:

Итого: основное время системы проводит освобождая/выделяя память, шифруя передаваемую информацию и разбирая sql запросы.

Несмотря на то, что oprofile достаточно удобен его возможностей не хватало и длительное время Stephane Eranian разрабатывал perfmon3 - поддержку счетчиков проиводительности в ядре linux, которая однако никак не попадала в основное ядро. А потом неожиданно появился Thomas Gleixner и вездесущий Ingo Molnar, которые предложили perf патч для ядра и одновременно закидали конфетами perfmon. Чуть позже Торвальдс закидал теми же конфетами разработчиков oprofile и одобрил включение утилит пользовательского режима для поддержки perf подсистемы в состав ядра.

$ cd tools/perf $ make $ sudo cp perf /usr/bin/perf_`uname -r`

По сравнению с oprofile модуль ядра perf получил:

более чистый API

(сравните "opcontrol --list-events" и "perf list | grep '[Hardware'")

поддержку в структурах ядра (теперь при переключении процессов ядро сохраняет счетчики для старого процесса в его структуре и загружает в регистры процессора созраненные счетчики нового процесса - это улочшает результаты профилирования в многопоточной среде)

програмные счетчики - события из ядра: переключения контекстов, minor/major page fault, счетчики системных вызовов и интеграцию c systemtap.

Но конечных пользователей больше интересуют утилиты пользовательского режима, и тут много всего полезного ( perf невозможно использовать при загруженном модуле oprofile, так как они используют одни и те же апаратные регистры. Перед использованием perf необходимо выгрузить oprofile - "oprofile --deinit" ):

perf top - показывает в реальном времени какие функции/процессы в системе генерируют больше всего нагрузки. Например "perf top" покажеть распределение нагрузки по функциям. "perf top -e L1-dcache-loads" покажет кто хуже всех относится к кешу процессора "perf top -e dTLB-load-misses,iTLB-load-misses" покажет промахи в TLB и позволит оценить имеет ли смысл переключаться на огромные страници.

perf stat cmd генерирует отчет по отдельной команде - аналог утилиты time.

perf record cmd исполняет программу и сохраняет итог профилирования в perf.dat, "perf report" позволит его просмотреть, а "perf annotate" покажет распределение событий по строкам исходного текста или по ассемблерному листингу.

Также perf "знает" о потоках, позволяя просматривать события для каждого потока в отдельности и умеет использовать встроенные в процессор средства тонкой настройки работы счетчиков.

В отличии от oprofile я не нашел как заставить perf записывать события только для некоторого набора бинарных файлов, но есть возможность поместить все необходимые процессы в отдельную группу(cgroup) и мониторить только ее.

Возможности perf на этом не исчерпываются, "perf help" покажет много полезных инструментов.

Пример использования perf с тем же тестом, что и oprofile:

2к раз в секунду) . $ perf report

О накладных расходах: perf замедлял тест примерно на

0.5%, а oprofile на 2-5%. Увеличивая порог срабатывания события можно уменьшить влияние профилировшика, но прийдется соответвенно удлиннить тесты для сохранения качества результатов.

В итоге perf дейсвительно Замечательный Инструмент, который позволяет не только профилировать свои программы но и практически незаметно мониторить что происходит в живой системе.


Я давно искал какой-нибудь инструмент для низкоуровневой отладки сети Linux. Linux позволяет создавать сложные сети, запускаемые прямо на хосте, используя комбинацию из виртуальных интерфейсов и сетевого пространства имен. Когда что-то идет не так решение возникших проблем утомительно. Если это проблема маршрутизации L3, mtr (Matt's traceroute) имеет неплохие шансы принести пользу. Однако, если проблема на более низком уровне, обычно все заканчивается тем, что я вручную проверяю каждый интерфейс / мост / пространство имен сети / iptables и пару раз запускаю tcpdump в попытках понять что происходит. Если вы не знакомы с настройками сети, то при решении проблем в ней, вас ждет запутанный лабиринт.

Что мне нужно, так это инструмент который скажет мне: "Эй, я видел там твой пакет: он прошел этим путем, через этот интерфейс в этом пространстве имен сети".

В общем-то, мне нужен mtr для L2.

Не существует? Сделаем свой!

В конце этой статьи мы получим простой и легкий в использовании трассировщик пакетов низкого уровня. Если вы пропингуете локальный контейнер Docker, он выдаст что-то вроде:

Трассировка к спасению

Один из способов выйти из лабиринта — исследование. Это то, что вы делаете, когда поиск выхода это часть игры. Другой же способ — это поменять точку зрения и посмотреть на пройденный путь со стороны знающего где выход.

В терминах Linux это значит сместиться на точку зрения ядра, где пространство имен сети это лишь ярлыки, а не "контейнеры". Пакеты и интерфейсы внутри ядра являются простыми наблюдаемыми объектами.

В этой статье я сконцентрируюсь на двух инструментах трассировки. perf и eBPF.

Введение в perf и eBPF

perf это основной инструмент для любого анализа производительности в Linux. Он разработан в той же ветке исходников что и ядро Linux и должен быть специально собран для ядра, которое вы планируете использовать для отслеживания. Имеется возможность трассировки как ядра, так и пользовательских программ. Есть возможность работы путем выборки или с использованием точек трассировки. Просто думайте о perf как о надмножестве strace с меньшими заморочками. Мы будем использовать его только немного, если же вы хотите узнать больше о perf, я настоятельно рекомендую вам посетить блог Брэндана Грегга (Brendan Gregg).

eBPF это относительно недавнее дополнение к ядру Linux. Как и говорится в названии, это расширенная (extended) версия BPF bytecode, известный как "Berkeley Packet Filter", используемый для… фильтрации пакетов в семействе BSD. Ну вы поняли. В Linux это так же может использоваться для безопасного запуска платформенно-независимого кода внутри работающего ядра, обеспечивая этим некоторую степень безопасности. Например, доступ к памяти выдается ДО того, как программа может запуститься и тогда должно быть возможно подтвердить, что программа завершится за ограниченный промежуток времени. Если ядро не может подтвердить это, то, даже если все безопасно и всегда завершается, запуск будет отклонен.

Такие программы могут быть использованы как сетевой классификатор для QOS, очень низкого уровня сети и фильтрации части eXpress Data Plane (XDP), как агент трассировки и во многих других областях. Зонды трассировки можно применить к любой функции, чьи символы экспортируются в /proc/kallsyms или любую точку трассировки. В этой статье я сфокусируюсь на агентах трассировки прикрепленных к точкам трассировки.

В качестве примера подобных зондов, применяемых к функции ядра, или более мягкого введения, я приглашаю вас прочитать мой предыдущий пост по поводу eBPF.

Настройка Lab

Для этой статьи нам нужен perf и несколько других инструментов по работе с eBPF. Я не большой фанат ручной сборки, так что использую здесь bcc. Это мощный и гибкий инструмент для написания зондов ядра на ограниченном Си и использования их в пользовательской среде Python. Тяжеловесно в продакшне, но прекрасно для разработки!

Я повторю здесь инструкцию по установке для Ubuntu 17.04 (Zesty), на котором и работает ОС на моем ноутбуке. Инструкция для "perf" не должна сильно отличаться для других дистрибутивов, а специальную инструкцию по установке bcc можно найти на GitHub.

Примечание: для применения eBPF к точкам трассировки необходимо иметь версию ядра Linux не ниже 4.7.

Установка perf:

Установка bcc:

Поиск хороших точек трассировки или "ручная трассировка пути пакета с perf”

Есть несколько способов найти хорошие точки трассировки. В предыдущей версии этой статьи, я начал с кода veth драйверов, и пошел далее по цепочке, чтобы найти функцию для трассировки. Несмотря на приемлемые результаты, я не смог отследить все пакеты. Общие пути, по которым проходят все пакеты, находятся в неэкспортируемых (встроенных или статических) методах. Тогда же я понял, что у Linux есть точки трассировки, и решил переписать старую статью и связанный с ней код, используя точки трассировки. Это было довольно раздражающе, но и гораздо интереснее (для меня).

Хватит разговоров обо мне, возвращаемся к работе.

Цель — проследить путь, пройденный пакетом. В зависимости от пересекаемых интерфейсов пересекаемые точки трассировки могут отличаться (спойлер: так и есть).

Чтобы найти подходящие точки трассировки, я использовал пинг двух внутренних и двух внешних целей от трассировки perf.

  1. localhost с IP 127.0.0.1
  2. Невинный контейнер Docker с IP 172.17.0.2
  3. Мой телефон через USB привязку с IP 192.168.42.129
  4. Мой телефон через WiFi с IP 192.168.43.1

perf trace это подкоманда perf, которая создает вывод схожий с strace (с ГОРАЗДО меньшей головной болью). Мы можем легко настроить его, чтобы скрыть сами системные вызовы и вместо этого печатать события категории «net». Например, отслеживание ping до контейнера Docker с IP 172.17.0.2 будет выглядеть так:

Если сохранять только имена событий и skbaddr, то будет выглядеть читабельнее.

Есть несколько вещей, которые надо сказать сейчас. Самое очевидное — skbaddr изменяется в середине, но в остальном остается неизменным. Это происходит, когда пакет эхо-ответа генерируется как ответ на эхо-запрос (пинг). Оставшееся время сетевой пакет перемещается между интерфейсами, к счастью без копирования. Копирование так дорого.

Другой интересный момент — мы ясно видим, что пакет проходит через мост docker0, затем через veth на стороне хоста, в моем случае это veth79215ff, и наконец, через контейнерную сторону veth, притворяющуюся eth0. Вы еще не видим пространство имен сети, но уже имеем неплохой обзор.

Наконец, после просмотра пакета на eth0, мы попадаем в точки трассировки в обратном порядке. Это не отклик, а завершение передачи.

Повторив подобный процесс на 4 целевых сценариях, мы можем выбрать подходящие точки трассировки для отслеживания пути пакета. Я выбрал четыре следующих:

  • net_dev_queue
  • netif_receive_skb_entry
  • netif_rx
  • napi_gro_receive_entry

Выбор этих точек даст мне возможность упорядочить события без дублирования, тем самым сэкономив немного времени на дедупликации.

Мы можем легко дважды проверить этот выбор, например:

Если вы хотите пойти дальше и изучить список доступных сетевых точек трассировки, вы можете использовать perf list:

Это должно вернуть список точек трассировки, называющийся net:netif_rx. Часть перед двоеточием это категория события (‘net’). После двоеточия это имя события в этой категории.

Пишем заказной трассировщик с помощью eBPF / bcc

Этого будет более чем достаточно для большинства ситуаций. Если вы читаете эту статью, чтобы узнать как отслеживать путь пакета в системе Linux, вы уже получили всё что необходимо. Но, если вы хотите копнуть глубже, запустить свой фильтр, отслеживать больше данных, вроде пространств имен сети пересекаемых пакетами, или начальный и конечный IP, прошу потерпите меня ещё.

Начиная с Linux Kernel 4.7, приложения eBPF могут быть привязаны к точкам трассировки ядра. До этого единственной альтернативой для создания этого трассировщика было бы прикрепление зондов к экспортируемым символам ядра. Несмотря на то, что это могло сработать, было и несколько недостатков.

  1. Внутренний API ядра нестабилен, в отличие от точек трассировки.
  2. В соображениях производительности, большинство внутренних сетевых функций встроены или статичны. Ничто из этого нельзя исследовать.
  3. Это просто утомительно, искать все возможные вызовы от сайтов для этих функций, а некоторые из необходимых данных могут и вовсе быть недоступны на этом этапе.

В более ранней версии этой статьи я пытался использовать kprobes, что легче, но результаты были неполноценными.

Будем честны, доступ к данным через точки трассировки намного утомительнее, чем с аналогом kprobe. И хотя я старался сделать эту статью как можно более простой для чтения, возможно вам придется заглянуть в более старые посты. [“Как переделать любой системный вызов в событий: Вступление в зонды ядра eBPF”] (/2016/03/30/turn-any-syscall-into-event-introducing-ebpf-kernel-probes/).

Этот фрагмент прикрепляется к четырем точкам трассировки типа “net”, загружает поле skbaddr и отправляет их в общую секцию, где пока что загрузит только имя программы. Если вы задумались, откуда приходят эти args->skbaddr, то знайте, их структура генерируется с помощью bcc, вне зависимости от выбранных точек трассировки в TRACEPOINT_PROBE. Поскольку эта структура генерируется на лету, нет простого способа увидеть ее определение, НО можно сделать и поумнее. Мы посмотрим прямо в источник данных, в ядро. К счастью, существует вход /sys/kernel/debug/tracing/events для каждой точки трассировки. Например, для net:netif_rx просто пишем “cat” /sys/kernel/debug/tracing/events/net/netif_rx/format и получаем подобный вывод:

Вы можете заметить строку “print fmt” в конце записи. Это в точности то, что использовал трассировщик perf для генерации вывода.

С использованием plumbing и хорошим пониманием сути, мы можем переделать это в скрипт Python для отображения каждого события отосланного со стороны зонда eBPF:

Можете протестировать сейчас. Вам нужны root права.

Примечание: Сортировки на этом этапе не будет. Даже с низким фоновым использованием сети ваша консоль будет быстро захламляться.

В данном случае, можно заметить, что я использовал ping и ping6, а драйвер WiFi только что получил несколько пакетов. Значит, это был эхо-ответ.

Давайте добавим немного полезной информации / фильтров.

В этой статье я не концентрируюсь на производительности. Так лучше покажут себя сильные и слабые стороны eBPF. Чтобы ускорить процесс, можно использовать размер пакета для эвристики, предполагая отсутствие странных вариантов IP. Использование программ из примера без изменений может замедлить ваш сетевой трафик.

Примечание: чтобы излишне не удлинять статью, я сфокусируюсь на C/eBPF. Я оставлю ссылку на полный исходный код в конце.

Добавление информации о сетевом интерфейсе.

Для начала, вы можете спокойно удалять поля “comm”, loading и заголовок sched.h. В нашем случае от этого не будет никакой пользы.

Теперь можно включить в проект net/inet_sock.h, так как все необходимые объявления сделаны, и добавить char ifname[IFNAMSIZ]; в структуру событий.

Теперь загрузим имя устройства из его структуры. Это интересный момент, ведь это действительно полезная информация, и она демонстрирует возможности загрузки любых данных в нужном масштабе:

Можете проверить — должно работать. И не забудьте добавить соответствующую часть на сторону Python :)

Окей, так как это работает? Чтобы загрузить имя интерфейса, нам необходимо структура интерфейса устройства. Я начну с последнего, как более простого для понимания, хотя предыдущее на деле просто сложнее. Используем bpf_probe_read для чтения длины IFNAMSIZ из dev->name и скопируем его в evt.ifname. Первая строка следует точно той же логике. Загружаем значение указателя skb-> dev в dev. К несчастью, я не смог найти другого способа загрузить поле адреса без этого маленького offsetof / typeof трюка.

Напоминаю, что цель eBPF обеспечить безопасное написание скриптов ядра. То есть запрещены случайные доступы к памяти. Любые обращения к памяти должны быть подтверждены. Кроме случаев, когда требуемая память находится в стеке, тогда вам нужно средство доступа для чтения bpf_probe_read. Такой способ делает код громоздким для чтения \ написания, но улучшает безопасность. bpf_probe_read это вроде безопасной версии memcpy, он определен в bpf_trace.c внутри ядра. А вот и занимательные моменты:

  1. Это похоже на memcpy. Будьте внимательны к возможному падению производительности из-за множества копий.
  2. В случае ошибки, программа сначала вернет буфер инициализированный в 0, а затем выдаст ошибку. Однако это не прервет и не остановит программу.

В оставшейся статье я буду использовать следующий макрос, чтобы поддерживать всё удобочитаемым:

Что позволяет нам писать:

Добавим ID в пространство имен сети.

Это, вероятно, наиболее ценная информация. Само по себе это уже веская причина для всех этих усилий, но к сожалению и самая трудная для загрузки.

Идентификатор пространства имен может быть загружен из двух мест:

  1. Структура сокета ‘sk’
  2. Структура устройства ‘dev’

Изначально я использовал сокетную структуру, так раньше работал с ней при написании solisten.py. К несчастью, я не вполне уверен почему, но пространство имен идентификатора становится недоступно для чтения, как только пересекает границу пространства имен. В значении поля все нули, что ясно свидетельствует об отсутствии доступа к памяти (вспомните как ведет себя bpf_probe_read в случае ошибок) и полностью ломает процесс.

К счастью, вариант через устройство все еще работает. Думайте об этом, как о вопросе к пакету, на каком он интерфейсе, и к интерфейсу, в чьему пространству имен он принадлежит.

Как дополнительный эффект, это дает возможность упростить макрос member_read. Оставлю это как самостоятельное задание для читателя.

Соединяем это вместе и… вуаля!

Это то, что вы должны увидеть при отправке запроса в контейнер Docker. Пакет проходит через локальный мост docker0, а затем перемещается к паре veth, пересекая границу пространства имен сети, и возвращает ответ по тому же пути в обратную сторону.

Это было неприятно!

Идем дальше: трассировать только запрашиваемые ответы и эхо-ответы пакетов.

В дополнение, мы также загрузим IP из пакетов, хотя заголовок IP необходимо прочитать в любом случае. Я буду придерживаться IPv4, но эта же логика подходит и для IPv6.

Плохая новость в том, что не будет ничего простого. Помните, мы работаем с ядром в плане сетевого взаимодействия. Некоторые пакеты еще не открыты. Это означает, что часть смещения заголовков еще не инициализированы. Необходимо вычислить всё, начиная с заголовка MAC адреса, и заголовка IP, и заканчивая заголовком ICMP.

Начнем понемногу, с загрузки адреса заголовка MAC и вывода адреса заголовка IP. Сам по себе заголовок MAC загружать нет необходимости, вместо этого предположим, что он в длину 14 байтов.

В основном, это означает что заголовок IP начинается с skb->head + skb->mac_header + MAC_HEADER_SIZE;.

Теперь возможно расшифровать версию IP по первым четырем битам заголовка, то есть по первой половине первого байта, и убедится, что это IPv4.

Теперь: загрузим весь заголовок IP, возьмем сам IP, чтобы сделать информацию Python полезнее. Убедимся, что следующий заголовок это ICMP, и вычислим смещение заголовка ICMP.

Наконец можно загрузить заголовок ICMP и удостовериться, что это эхо-запрос ответа, и загрузить из него id и seq:

Вот и всё, ребята!

Если хотите сортировать ICMP по определенному экземпляру ping, можно предположить, что evt.icmpid это PID ping, по крайней мере используя ping Linux.

Время для шоу!

Имея обычный Python для обработки события, мы можем протестировать его в нескольких сценариях. Запустите программу с правами root, создайте некоторый “ping” в другом терминале и наблюдайте:

Эхо-запрос ICMP отправляется процессом 20212 (ICMP id в ping Linux) с интерфейса loopback, доставляется на тот же самый интерфейс, где эхо-ответ генерируется и отправляется назад. Интерфейс loopback является как передающим, так и принимающим интерфейсом.

А что насчет моего WiFi шлюза?

В этом случае и эхо-запрос, и эхо-ответ проходят через интерфейс WiFi. Легкотня.

Немного не относящееся к делу: помните, когда мы печатали только «comm» процесса, владеющего пакетом? В этом случае эхо-запрос будет принадлежать процессу ping, в то время как ответ будет принадлежать драйверу WiFi, так как драйвер его и генерирует, если мы говорим о Linux.

И напоследок, мое любимое — пинг контейнера Docker. Это моё НЕлюбимое из-за Docker, и любимое, потому что лучше прочих показывает силу eBPF. Это позволило создать почти «рентгеновский» инструмент для пинга.

Теперь с некоторыми рисунками это выглядит так:

Заключение

eBPF / bcc позволяет нам написать новый набор инструментов для глубокой диагностики, трассировки и отслеживания проблем в местах, ранее недоступных, без исправления ядра. Точки трассировки также весьма удобны, так как они дают хорошую подсказку о точках интереса, устраняя необходимость утомительного чтения кода ядра и могут быть помещены в части кода, которые в противном случае были бы недоступны для kprobes, например встроенные или статические функции.

Можно пойти дальше и добавить поддержку IPv6. Это довольно легко сделать, и я оставлю это в качестве упражнения для читателя. В идеале я хотел бы также измерить влияние на производительность, но этот пост уже и так очень и очень длинный. Было бы интересно улучшить этот инструмент, с помощью трассировки маршрутизации, iptables и ARP-пакеты. Все это превратило бы этот инструмент в идеальный «рентгеновский» трассировщик пакетов для таких людей, как я, кто когда-то боролся с нетривиальными сетевыми настройками Linux.

Как и обещал, полный код (с поддержкой IPv6) вы можете посмотреть на Github.

Наконец, я хотел бы поблагодарить @fcabestre за помощь, за помощь в спасении рабочего черновика этой статьи из неисправного жесткого диска, @bluxte за его терпеливую корректуру и людей из bcc, которые сделали этот пост технически возможным.

Дедовский метод

double total_time_ms = time_end - time_begin ;

Примечание: См также кроссплатформенную реализацию процедуры getCurrentTimeMs из заметки Продолжаем изучение OpenGL: простой вывод текста.

Пропарсить полученный лог можно, к примеру, с использованием Perl:

Несмотря на тупость метода, он очень удобен, например, когда в приложении что-то тормозит иногда, а не постоянно. При использовании метода нужно помнить, что системный вызов gettimeofday сам по себе сравнительно дорогой. Если использовать его, скажем, между захватом и освобождения спинлока, бутылочное горлышко, которое вы ищите, может уехать вообще в другое место. Также вы можете получать неправильные цифры, если между двумя замерами времени шедулер решит переключиться на выполнение другого процесса.

Утилиты strace и ltrace

Программа strace показывает системные вызовы и их возвраты, что бывает довольно удобно при отладке. С флагом -c она показывает топ системных вызовов, их количество, и сколько времени было проведено в вызове:

$ strace -c pwd
/home/ubuntu
% time seconds usecs/call calls errors syscall
------ ----------- ----------- --------- --------- ----------------
31.73 0.000132 15 9 mmap
19.95 0.000083 28 3 3 access
11.30 0.000047 16 3 open
10.58 0.000044 22 2 munmap
8.17 0.000034 9 4 mprotect
4.57 0.000019 5 4 fstat
3.37 0.000014 3 5 close
2.88 0.000012 12 1 write
2.64 0.000011 4 3 brk
1.68 0.000007 7 1 execve
1.44 0.000006 6 1 read
0.96 0.000004 4 1 getcwd
0.72 0.000003 3 1 arch_prctl
------ ----------- ----------- --------- --------- ----------------
100.00 0.000416 38 3 total

Программа ltrace делает то же самое и для библиотечных вызовов:

$ ltrace -c pwd
/home/ubuntu
% time seconds usecs/call calls function
------ ----------- ----------- --------- --------------------
17.31 0.000636 636 1 setlocale
12.93 0.000475 118 4 __freading
9.71 0.000357 178 2 fclose
8.11 0.000298 298 1 puts
6.56 0.000241 120 2 fileno
5.69 0.000209 209 1 getenv
5.61 0.000206 103 2 fflush
5.52 0.000203 203 1 strrchr
5.36 0.000197 98 2 __fpending
5.25 0.000193 193 1 __cxa_atexit
4.24 0.000156 156 1 bindtextdomain
4.16 0.000153 153 1 textdomain
3.86 0.000142 142 1 getcwd
2.94 0.000108 108 1 getopt_long
2.75 0.000101 101 1 free
------ ----------- ----------- --------- --------------------
100.00 0.003675 22 total

Чтобы подключиться при помощи strace или ltrace к уже работающему процессу, используйте флаг -p .

Во FreeBSD утилита, аналогичная strace, называется truss. Кроме того, strace и ltrace доступны в портах.

Использование gprof

Утилита gprof доступна как в Linux, так и во FreeBSD. Для примера попробуем прогнать ее на программе из заметки Реализация хэш-таблиц, почти как в Perl.

Если программа уже была собрана, делаем make clean . Затем пересобираем ее с флагом -pg :

Запускаем программу без gprof:

Будет создан двоичный файл gmon.out. Этот шаг обязателен, иначе при попытке профайлинга увидим ошибку:

Теперь запускаем программу под gprof:

Each sample counts as 0.01 seconds.
% cumulative self self total
time seconds seconds calls ns/call ns/call name
30.37 0.10 0.10 475254 210.86 295.21 htable_del
18.22 0.16 0.06 1425762 42.17 42.17 _htable_hash
18.22 0.22 0.06 475254 126.52 210.86 htable_set
12.15 0.26 0.04 950508 42.17 42.17 _htable_resize
12.15 0.30 0.04 475254 84.34 126.52 htable_get
3.04 0.31 0.01 main
0.00 0.31 0.00 1 0.00 0.00 htable_free
0.00 0.31 0.00 1 0.00 0.00 htable_new

Также можно построить красивый граф вызовов:

sudo apt-get install graphviz
sudo pip install gprof2dot

gprof2dot . / profile | dot -Tsvg -o output.svg

Пример полученной картинки (кликабельно, SVG,

gprof2dot, пример результата

Подробности про утилиту dot и Graphviz см в заметке Рисуем красивые графы при помощи Graphviz.

Получение стэктрейсов при помощи Gdb

Как ни странно, gdb можно использовать и для профайлинга. Просто говорим:

Понятно, что таким образом можно собирать не только стэктрейсы, но и другую информацию, например, значения переменных, переданных аргументов, и так далее. Этот прием показал себя весьма полезным, например, при поиске и устранении lock contention. Если программа часто висит в ожидании лока, мы будем видеть это ожидание по стэктрейсам.

Профайлинг при помощи perf top

Установка perf в Ubuntu / Debian:

При первом запуске увидим что-то вроде:

$ perf top -u postgres
WARNING: perf not found for kernel 3.13.0-71

You may need to install the following packages for this
specific kernel:
linux-tools-3.13.0-71-generic
linux-cloud-tools-3.13.0-71-generic

You may also want to install one of the following packages to keep
up to date:
linux-tools-generic
linux-cloud-tools-generic

Ставим и эти пакеты тоже:

sudo apt-get install linux-tools-generic linux-cloud-tools-generic

Можно посмотреть top по всей системе:

Вот как это примерно выглядит:

Пример вывода perf top

Картинка обновляется в реальном времени. При помощи стрелочек и клавиши Enter можно «проваливаться внутрь» процессов и функций, вплоть до подсвечивания строчек кода и ассемблерных инструкций, которые тормозят. Очень удобно!

Во FreeBSD, насколько я понимаю, аналогичный функционал предоставляется программой pmcstat. Однако на практике я ею пока не пользовался.

Строим флемграфы

Помимо отображения топа самых часто вызываемых процедур программа perf умеет много чего еще.

Запуск конкретной программы под perf производится так:

На выходе получаем файл perf.data.

Но читать его в таком виде не очень-то удобно. Намного удобнее построить флеймграф:

Получаем красивую картинку вроде такой:

Пример флеймграфа

В полученном SVG-файле можно кликать на названия процедур для увеличения соответствующего участка флеймграфа, а также искать процедуры по имени. Здесь и здесь можно найти больше информации и примеров. Еще этот набор скриптов умеет работать с dtrace и строить разные другие виды флеймграфов, но это уже тема для отдельной заметки.

Заключение

Как видите, недостатка в инструментах профайлинга в мире C/C++ не наблюдается. Не один из описанных приемов не является универсальным, но при правильном их комбинировании можно разобраться практически в любой проблеме. А как вы профилируете свой код?

Читайте также: