Служба на сервере зависла: процесс числится в списке задач, но не отвечает, лог молчит, жалобы копятся. Команда ps покажет идентификатор и состояние, top добавит цифры нагрузки, но главный вопрос останется открытым: что процесс делает в эту секунду. Утилита strace отвечает построчно: она перехватывает каждый системный вызов, через который программа обращается к ядру, от открытия файла и чтения сокета до ожидания блокировки и обработки сигнала. По рисунку вызовов зависание превращается в задачу с именем вызова и дескриптором. Ниже собраны рабочие приёмы: подключение к процессу, сводный режим, фильтры по группам и именам, метки времени, типовые картины и ограничители наблюдения.

Подключение strace к работающему процессу и слежение за потомками

У наблюдения два старта: программу запускают под трассой с первой инструкции либо утилиту подключают к уже работающему процессу. Для зависаний основной второй вариант: процесс живёт в памяти со всем состоянием, а перезапуск стирает картину раньше, чем её успевают рассмотреть.

Подключение выполняется ключом -p с идентификатором:

sudo strace -p 1234

Утилита печатает строку о подключении и открывает поток вызовов. Отключение по Ctrl+C не меняет состояние процесса: он продолжает работу, наблюдатель именно отключается, а не завершает службу.

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

strace -f ./app --config /etc/app/app.conf

Ключ -f включает слежение за потомками: fork, vfork и clone создают новые процессы, и каждый попадает в общий поток. Для службы с рабочими процессами режим обязателен, иначе картина ограничится родителем, который спокойно ждёт в wait4, пока вызовы выполняют его дети. В паре с -o ключ -ff раскладывает трассы потомков по отдельным файлам:

strace -f -ff -o /tmp/worker.trace ./app

Для подключения нужны права. Утилита работает через механизм ядра ptrace: root подключается к любому процессу, обычный пользователь ограничен собственной областью и настройками подсистемы yama. Порог задаётся в файле /proc/sys/kernel/yama/ptrace_scope: во многих дистрибутивах по умолчанию разрешена трассировка только собственных потомков, поэтому подключение к чужой службе без sudo заканчивается сообщением Operation not permitted.

Сводный режим со счётчиками вызовов и ошибок для первичной картины

Активная служба выдаёт тысячи вызовов в секунду, чтение потока без подготовки утомительно. Первый шаг разумнее делать в сводном режиме: ключ -c собирает таблицу со счётчиками вместо потока. Сеанс запускают, дают процессу поработать несколько секунд и отключают:

sudo strace -c -p 1234

После отключения утилита печатает примерно такую таблицу:

% time     seconds  usecs/call     calls    errors syscall
------ ----------- ----------- --------- --------- ----------------
 76.90    8.104000        2019      4012         0 futex
 11.52    1.214000         792      1533      1533 read
  5.80    0.611000         858       712       712 openat
  3.81    0.402000         630       638         0 write
  1.97    0.208000          39      5210         0 clock_gettime
------ ----------- ----------- --------- --------- ----------------
100.00   10.539000                 12105      2245    total

Колонка errors сразу называет проблемные вызовы, счётчик calls показывает интенсивность, seconds распределяет время. В примере читается типичный сюжет: futex с подавляющей долей времени говорит об ожидании блокировки, read с ошибкой на каждом вызове указывает на цикл опроса неблокирующего дескриптора, openat со стопроцентными ошибками ведёт к отсутствующему файлу. Диагноз становится вопросом выбора фильтра.

Для анализа ожиданий полезен ключ -w: он суммирует время по часам, а не по процессорным тратам, и долгие блокировки не прячутся за короткими вычислениями.

Порядок диагностики строится от общего к частному:
1. несколько секунд в режиме -c дают распределение вызовов, времени и ошибок;
2. лидер таблицы подсказывает группу для фильтра, например %network;
3. точечный фильтр по именам сокращает поток до нескольких строк;
4. метки времени и длительность вызовов превращают поток в хронологию;
5. дескриптор из подозрительной строки сверяется с каталогом /proc/PID/fd.

Фильтры по группам системных вызовов и по конкретным именам

Смотреть всё подряд расточительно: поток разбухает до нечитаемости, а класс операций для проверки известен заранее. Ключ -e trace= принимает имена и группы:

sudo strace -e trace=%file -p 1234
sudo strace -e trace=%network -p 1234
sudo strace -e trace=%process -p 1234
sudo strace -e trace=%signal -p 1234
sudo strace -e trace=%memory -p 1234
sudo strace -e trace=%desc -p 1234

Группа %file ловит работу с именами файлов: openat, stat, statx, unlink, rename. Группа %network покрывает сокеты: socket, bind, connect, sendto, recvfrom, accept4. Группа %process описывает жизненный цикл задач: fork, clone, execve, wait4, exit_group. Группа %signal собирает отправку и получение сигналов, %memory отвечает за вызовы отображения памяти вроде mmap и mprotect, %desc покрывает операции над дескрипторами: read, write, close, poll, select.

Точечный фильтр оставляет в трассе только имена из списка:

sudo strace -e trace=openat,connect,futex -p 1234

Обратный ход исключает шум: ожидания futex у многопоточной службы генерируют сотни строк, их вырезают из трассы:

sudo strace -e trace=!futex -p 1234

Аргументы-структуры по умолчанию печатаются сокращённо, ключ -v разворачивает их полностью.

Метки времени, длительность вызовов и вывод в файл для читаемого журнала

Трасса без времени не говорит, когда началось ожидание и как долго оно тянется. Ключи выстраивают хронологию: -t печатает время суток, -tt добавляет микросекунды, -ttt переводит метку в секунды эпохи Unix, -r показывает интервал от предыдущего вызова. Главная метрика для зависаний - ключ -T: он выводит длительность каждого вызова в угловых скобках после результата.

sudo strace -tt -T -p 1234
12:41:03.221435 openat(AT_FDCWD, "/etc/app/app.conf", O_RDONLY|O_CLOEXEC) = 4 <0.000021>
12:41:03.221601 futex(0x7f5b6c0020c8, FUTEX_WAIT_PRIVATE, 0, NULL) = 0 <2.008133>

Строка futex с длительностью две секунды сообщает: поток провёл это время в ожидании блокировки, а не крутился впустую. Так различают ожидание и цикл.

Длинные наблюдения направляют в файл: ключ -o сохраняет трассу целиком, а вместе с -ff каждый процесс получит отдельный журнал:

sudo strace -f -tt -T -o /tmp/app.trace -p 1234

Строки по умолчанию обрезаны на 32 символах. Ключ -s 256 поднимает лимит, имена файлов печатаются целиком всегда, урезание касается только содержимого буферов:

sudo strace -s 256 -p 1234

Дескрипторы по умолчанию отображаются числами, и строка read(4, ...) молчит об источнике. Ключ -y приписывает к числу путь, -yy добавляет сведения о сокетах и устройствах:

sudo strace -y -p 1234

Рабочий рецепт собирает всё вместе: короткий сеанс с выводом в файл и ограничением по времени:

sudo timeout -s INT 15 strace -f -tt -T -s 256 -y -o /tmp/app.trace -p 1234

За пятнадцать секунд файл накопит хронологию с длительностями и путями, сеанс завершится сам.

Типовые картины зависания в трассе от цикла коротких вызовов до блокирующего чтения

Картины зависания повторяются, каждая читается однозначно. Первая - бесконечный цикл коротких вызовов. Трасса забита парами nanosleep и clock_gettime либо чередованием futex с интервалами в микросекунды, ключ -r подтверждает крошечные расстояния между вызовами. Процессор занят полностью, полезной работы нет. Причина обычно в коде или конфигурации: цикл ждёт события, которое никогда не наступит.

Вторая - блокирующее чтение с нулевым результатом. Последняя строка трассы остаётся без возврата: read, recvfrom или readv начался и не завершился. Метка времени показывает, когда ожидание началось, вычитание даёт его длительность. Аргумент называет дескриптор, а каталог процессов источник: файл на диске, сокет или pipe.

Третья - ожидание на незакрывающемся вызове. futex без возврата означает ожидание блокировки, занятой другим потоком: если держатель завис, очередь не рассосётся. poll или epoll_wait без возврата говорит об ожидании события на дескрипторах, wait4 информирует о потомке, который не завершился, accept4 остаётся в ожидании входящих соединений, которых нет. Аргументы вызова почти прямым текстом называют причину.

Четвёртая - повторяющиеся ошибки с одним кодом. Строки вида = -1 ENOENT (нет такого файла или каталога) или = -1 ECONNREFUSED (отказ в соединении) идут подряд десятками:

openat(AT_FDCWD, "/var/lib/app/queue.lock", O_RDWR|O_CREAT) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/var/lib/app/queue.lock", O_RDWR|O_CREAT) = -1 ENOENT (No such file or directory)

Для ядра это рутина, для приложения нередко бесконечные попытки без паузы, которые снаружи выглядят как зависание. Код ошибки задаёт направление поиска: ENOENT ведёт к отсутствующему пути, ECONNREFUSED - к порту, где никто не принимает соединения, EAGAIN - к неблокирующему дескриптору, который читают в цикле без обработки пустого результата.

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

Анализ цепочек вызовов и поиск дескриптора через каталог процессов

Отдельный вызов говорит немного, смысл собирается из последовательностей. Классическая цепочка работы с файлом: openat возвращает дескриптор, read читает содержимое, close закрывает. Обрыв цепочки информативен не меньше: openat с ошибкой и следующий openat по другому пути выдают порядок поиска конфигурации и файл, который приложение так и не нашло.

Цепочка сетевого клиента: socket готовит дескриптор, setsockopt настраивает параметры, connect устанавливает соединение, sendto отправляет запрос, recvfrom ждёт ответ. Зависание на connect указывает на сеть или удалённую сторону, зависание на recvfrom после успешного соединения - на молчание партнёра.

Дескриптор - связь между строкой трассы и объектом системы. Ключ -y показывает путь прямо в строке, без него помогает каталог процессов:

ls -l /proc/1234/fd
readlink /proc/1234/fd/6

Вывод раскрывает, куда ведёт дескриптор: на файл журнала, на сокет с пометкой socket, на pipe или на стёртый файл с пометкой deleted.

Сопоставление трассы и каталога отвечает на прикладные вопросы: какой файл читается в цикле, какой сокет молчит, какой канал остался без пишущей стороны. read на пустом pipe блокируется, а после закрытия пишущего конца возвращает ноль, и приложение, не готовое к нулевому чтению, превращает это в бесконечный цикл.

Ограничители наблюдения и команды с таймаутом для контроля сеанса

strace - тяжёлый инструмент. Каждый системный вызов наблюдаемого процесса ядро останавливает дважды, на входе и на выходе. Для приложений с десятками тысяч вызовов в секунду замедление заметно, поэтому наблюдение держат коротким: для картины хватает нескольких секунд.

Права - второй ограничитель. Обычному пользователю разрешено подключение к собственным процессам, чужие службы требуют прав root либо привилегии CAP_SYS_PTRACE, а подсистема yama способна ограничить подключение даже к своим процессам. Сообщение Operation not permitted означает не поломку утилиты, а отказ по правилам доступа.

Третий ограничитель - дисциплина отключения: Ctrl+C и сигнал от timeout снимают трассировку, процесс работает дальше, состояние службы не меняется.

Для неостановимого наблюдения сеанс ограничивают утилитой timeout, она завершает strace по истечении заданного времени:

timeout 10 strace -c -p 1234
timeout -s INT 30 strace -f -tt -T -o /tmp/app.trace -p 1234

Первый вариант собирает сводку за десять секунд. Второй имитирует Ctrl+C сигналом INT: тридцать секунд полной трассы уходят в файл, strace отключается сам, процесс продолжает работу. Если ptrace недоступен, подсказку дают файлы ядра: /proc/PID/wchan называет функцию ожидания, /proc/PID/stack показывает стек на стороне ядра.

strace не исправляет зависание и не заменяет отладчик. Его сила в честности: утилита показывает ровно то, что процесс просит у ядра, без домыслов. Несколько секунд сводки, точечный фильтр по имени вызова, метка времени у последней строки - и вместо слова "зависло" в отчёте появляется строка с именем вызова и дескриптором. За строкой стоят конкретные действия: проверить файл, проверить сокет, освободить блокировку или исправить конфигурацию.