Сигналы и зависшие процессы: kill, и что делать с D-state

Рейтинг: 61% · 6 голосов
Подробный курс по диагностике и производительности Linux для админов и DevOps: с чего начать, методология USE, логи (journalctl, dmesg), процессы, CPU, память и OOM, диск и IO (iostat), сеть (ss, tcpdump), системные вызовы и strace, ltrace, perf, флеймграфы, ftrace, eBPF и bpftrace, lsof, мониторинг. Разбор каждого инструмента с чтением вывода.
Ответить
Аватара пользователя
Dmitry_SRE
Сообщения: 47
Зарегистрирован: 11 май 2026, 05:31

Сигналы и зависшие процессы: kill, и что делать с D-state

Сообщение Dmitry_SRE »

Оглавление курса (47)
  1. С чего начать диагностику Linux: методология вместо паники
  2. Первые 60 секунд: экспресс-диагностика нагруженного сервера
  3. Методологии диагностики: USE, RED и здравый смысл
  4. Логи systemd через journalctl: где искать причину
  5. Где лежат логи Linux: /var/log, dmesg и rsyslog
  6. Источники правды: load average, /proc и /sys
  7. Процессы Linux: ps, pstree и состояния процессов
  8. Интерактивный мониторинг: top и htop
  9. Метрики процессов во времени: pidstat
  10. Приоритеты и ограничения: nice, ionice, cgroups
  11. Сигналы и зависшие процессы: kill, и что делать с D-state (вы здесь)
  12. Загрузка CPU: user, system, iowait, steal и контекст-свитчи
  13. Диагностика CPU по ядрам: vmstat и mpstat
  14. Профилирование CPU: perf top и поиск пожирателя
  15. Частоты, троттлинг и NUMA: turbostat и numastat
  16. Память Linux: RSS, VSZ, page cache и миф о нехватке памяти
  17. Сколько памяти занято: free, /proc/meminfo и vmstat
  18. Поиск утечек и пожирателей памяти: smem, pmap, smaps
  19. Swap и подкачка: swapon, swappiness, когда своп - это боль
  20. OOM killer: кто и за что убил процесс
  21. Память ядра и slab: slabtop и куда уходит RAM
  22. Подсистема ввода-вывода: путь запроса от приложения до диска
  23. Диагностика диска: iostat и чтение await, %util
  24. Кто грузит диск: iotop и атрибуция io процессам
  25. Файловые системы: df, du, иноды и куда делось место
  26. Глубокая диагностика блочного слоя: blktrace и biolatency
  27. Кеш страниц и грязные данные: dirty pages, fsync, drop_caches
  28. Сетевой стек Linux: путь пакета и где возникают задержки
  29. Сокеты и соединения: ss и netstat
  30. Захват трафика: tcpdump для диагностики сети
  31. Задержки и потери в сети: ping, mtr, диагностика латентности
  32. Сеть как источник проблем: nftables, conntrack, дропы
  33. Что такое системный вызов и зачем его трассировать
  34. strace: трассировка системных вызовов на практике
  35. strace в бою: почему программа висит, падает или тормозит
  36. ltrace: трассировка вызовов библиотек
  37. Накладные расходы трассировки и безопасные альтернативы
  38. perf: универсальный профайлер Linux
  39. Флеймграфы: визуализация профиля производительности
  40. Трассировка ядра: ftrace и trace-cmd
  41. eBPF: революция в наблюдаемости Linux
  42. Инструменты eBPF на практике: BCC и bpftrace
  43. lsof: открытые файлы, дескрипторы, кто держит файл и порт
  44. Разбор кейса: приложение тормозит - пошаговая диагностика
  45. Непрерывный мониторинг: sar, node_exporter, Grafana, Zabbix
  46. Диагностика в контейнерах: cgroups, лимиты, Docker и Kubernetes
  47. Карта инструментов и сквозной разбор инцидента производительности
Рано или поздно у каждого админа случается этот момент: процесс сожрал память, висит мёртвым грузом или не хочет закрываться. Первый рефлекс новичка - "ну сейчас я его kill -9, и всё". А он не умирает. Ты бьёшь по нему девяткой ещё раз, ещё раз, а зависший процесс в Linux стоит как вкопанный. В этом уроке разберёмся, как на самом деле работают сигналы linux, почему kill это не "выстрел в голову", а вежливая просьба, и главное - что делать, когда процесс реально не убивается. Спойлер: бывают состояния, в которых kill 9 linux бессилен по дизайну, и это не баг, а защита целостности системы.

Всё, что ниже, актуально на 2026 год: современный Linux с systemd, ядро 6.x, cgroup v2 по умолчанию и зрелый eBPF в арсенале.

Что такое сигнал и почему kill это про сигналы

Сигнал - это короткое асинхронное уведомление, которое ядро доставляет процессу. По сути это "стук в дверь": эй, тебе тут сообщение. Процесс может на этот стук отреагировать своим обработчиком (закрыть файлы, дописать лог, попрощаться), может проигнорировать, а может ничего не делать - и тогда сработает действие по умолчанию (чаще всего завершение). Всё зависит от того, какой сигнал и что для него настроено.

Команда kill не "убивает" сама по себе. Она просто отправляет сигнал процессу по его PID. Название историческое и сбивает с толку новичков - на самом деле это "послать сигнал". Полный список сигналов смотрим так:
Сигналов десятки, но в реальной работе тебе важны буквально несколько. Номера 1, 2, 3, 9, 15 стабильны на всех архитектурах - их можно слать по номеру не задумываясь. А вот USR1/USR2 и некоторые другие на разных архитектурах имеют разные номера, поэтому их всегда шли по имени.
  • TERM (15) - "пожалуйста, завершись". Сигнал по умолчанию, если номер не указан. Процесс получает шанс прибраться: сбросить буферы на диск, закрыть соединения, удалить временные файлы. Так и надо просить в первую очередь.
  • KILL (9) - "умри немедленно". Этот сигнал процесс НЕ может перехватить, заблокировать или проигнорировать - его обрабатывает само ядро, минуя код процесса. Но и тут есть подвох, о нём дальше.
  • HUP (1) - "перечитай конфиг". Исторически "hang up" (повесили трубку модема), сейчас демоны вроде nginx, rsyslog, haproxy по нему перечитывают конфигурацию без рестарта. Это не убийство, это "обновись". Будь осторожен: у некоторых программ HUP по умолчанию означает именно завершение, так что сверяйся с докой конкретного демона.
  • INT (2) - то, что шлёт Ctrl+C в терминале. Вежливое прерывание, его можно перехватить.
  • QUIT (3) - Ctrl+\ в терминале. Как INT, но действие по умолчанию - завершение С дампом памяти (core dump). Удобно, когда надо снять состояние зависшего процесса для разбора.
  • STOP / CONT - заморозить процесс и разморозить. STOP, как и KILL, нельзя перехватить или проигнорировать. Парный ему TSTP (Ctrl+Z) - перехватываемый вариант заморозки.
  • USR1 / USR2 - "пользовательские" сигналы без предопределённого смысла. Программы вешают на них что угодно: nginx по USR1 переоткрывает лог-файлы (нужно после ротации логов), по USR2 запускает обновлённый бинарник на лету.
Отправить конкретный сигнал по имени или номеру:

Код: Выделить всё

kill -TERM 4821
kill -15 4821
kill -9 4821
kill -HUP $(pidof nginx)
kill -s SIGUSR1 $(pidof nginx)
Золотое правило: сначала TERM, и только если не помогло - KILL. Если сразу шарашить девяткой, процесс не успеет прибраться - можно получить битый файл базы, потерянный буфер записи, оставшийся lock-файл. KILL это аварийный молоток, а не основной инструмент. На практике дай процессу несколько секунд на TERM (именно так делает systemd: шлёт TERM, ждёт TimeoutStopSec, и только потом добивает KILL).

Изображение

kill, killall и pkill: как убить зависший процесс по имени

Гоняться за PID руками неудобно. Есть инструменты, которые бьют по имени или по пользователю.

killall - шлёт сигнал всем процессам с точным совпадением имени:

Код: Выделить всё

killall firefox
sudo killall -9 nginx
Важно: killall ищет точное имя процесса. firefox убьёт firefox, но не firefox-bin. И ещё нюанс: имя процесса в Linux урезано до 15 символов (TASK_COMM_LEN), так что для длинных имён killall может промахнуться - тогда переходи на pkill -f.

pkill - то же, но матчит по части имени (regex по умолчанию) и умеет фильтровать по куче критериев:

Код: Выделить всё

pkill -f "python.*worker"
pkill -u www-data
pkill -HUP nginx
Флаг -f важный: без него pkill смотрит только на имя бинарника (те самые 15 символов), а с -f - на всю командную строку целиком. Это спасает, когда десяток процессов python, а прибить надо только воркер с конкретным аргументом. Парный pgrep делает то же самое, но не убивает, а только показывает - всегда проверяй им ПЕРЕД pkill, чтобы не снести лишнего:

Код: Выделить всё

pgrep -af "python.*worker"
Сначала pgrep -af посмотрел, что попадает под шаблон, убедился - потом pkill той же строкой. Иначе одним движением можно положить пол-системы. Особо коварен pkill -f с коротким шаблоном вроде "java" - под него попадёт и твой grep, и вообще всё, где в командной строке мелькает это слово.

Отдельно про systemd (актуально на 2026, cgroup v2 по умолчанию). Если процесс - часть сервиса, правильнее работать не с PID, а с юнитом. Тогда ядро гарантированно снимет ВСЁ дерево процессов сервиса через cgroup, и ни один форк-потомок не сбежит:

Код: Выделить всё

systemctl kill mysql.service
systemctl kill -s SIGKILL mysql.service
systemd-cgls            # дерево процессов по cgroup
systemctl status mysql  # видно главный PID и потомков
Это надёжнее ручного pkill: cgroup v2 умеет атомарно прибить всю группу (механизм cgroup.kill, появился в ядре 5.14), не оставляя осиротевших детей.

Почему kill -9 не помогает: зависший процесс в состоянии D

А теперь главное, ради чего урок. Ты шлёшь kill -9, а процесс жив. Смотришь в ps, а у него в колонке состояния стоит буква D. Это не баг и не зомби - это uninterruptible sleep, непрерываемый сон.

Сначала научимся читать состояния. Запусти:

Код: Выделить всё

ps -eo pid,ppid,user,stat,wchan:24,comm
Колонка STAT (или STATE) - это и есть состояние процесса. Что означают буквы:
  • R - бежит на CPU или готов бежать (стоит в очереди runqueue). Норма. Если таких много и постоянно - смотри на загрузку CPU.
  • S - спит, ждёт события (сети, ввода, таймера), прерываемый сон. Абсолютное большинство процессов в системе именно такие, это норма. Такой сон сигналом будится мгновенно.
  • D - непрерываемый сон. Процесс застрял внутри ядра, ждёт завершения операции (почти всегда дискового или сетевого ввода-вывода). Вот он сигналы НЕ принимает прямо сейчас.
  • Z - зомби (defunct). Процесс уже мёртв, но родитель ещё не забрал его код возврата.
  • T - остановлен (получил STOP/TSTP). Заглавная T - стоп; строчная t означает остановку трассировщиком (под gdb/strace).
  • I - idle, простаивающий поток ядра (kernel thread). Современные ядра выделяют такие в отдельное состояние, чтобы они не пугали в выводе как D. Это норма.
Рядом может стоять дополнительный символ: < (высокий приоритет), N (низкий, nice), s (лидер сессии), l (многопоточный), + (в foreground-группе терминала). То есть Ssl или D+ - это нормальная запись, не пугайся.

Почему D не убивается? Когда процесс просит ядро прочитать блок с диска или с NFS, он засыпает ВНУТРИ ядра и помечается как непрерываемый (TASK_UNINTERRUPTIBLE). В этом состоянии он физически не проверяет очередь сигналов - ему сейчас не до них, он ждёт железо, и прерывать его в этот момент небезопасно (можно поймать порчу данных или утечку ресурсов ядра при DMA). SIGKILL встанет в очередь, но доставится только когда процесс вернётся из ядра, то есть когда ввод-вывод завершится или отвалится по таймауту. Пока диск тупит или сетевая ФС не отвечает - процесс будет в D, и kill 9 linux его не возьмёт. Ядро не игнорирует твой сигнал, оно его откладывает.

Это означает важную вещь: массовый D-state - это не проблема процессов, это симптом проблемы с диском или сетью. Смотреть надо не в kill, а в железо. Куда копать:

Код: Выделить всё

# кто именно висит в D
ps -eo pid,stat,wchan:32,comm | awk '$2 ~ /^D/'

# на чём конкретно застрял процесс (имя функции ядра)
cat /proc/4821/wchan; echo

# полный стек в ядре - самое полезное
sudo cat /proc/4821/stack
Читаем вывод. В /proc/PID/stack ты увидишь цепочку функций ядра. Если там мелькают nfs_, cifs_, rpc_ - значит процесс висит на сетевой файловой системе (отвалился сервер или сеть). Если jbd2, blk_, io_schedule, wait_on_page, folio_wait - это локальный диск тормозит или сыпется. Параллельно глянь нагрузку на диск:

Код: Выделить всё

iostat -x 1
# %util близко к 100 - устройство загружено под завязку
# await и r_await/w_await растут (десятки-сотни мс) - запросы ждут
# aqu-sz большой - длинная очередь к диску
Маленькая оговорка на 2026: %util на современных NVMe с внутренним параллелизмом уже не означает "перегружен" даже при 100% - один NVMe обслуживает кучу запросов разом. Поэтому опирайся в первую очередь на латентность (await), а не на %util.

И обязательно загляни в dmesg - там ядро прямым текстом расскажет про проблемы:

Код: Выделить всё

sudo dmesg -T | tail -40
Ищи строки про "task ... blocked for more than 120 seconds" (это hung task detector - ядро само замечает зависшие D-задачи), про I/O error, про NFS "server not responding". Это и есть корень. Ещё мощный приём - попросить ядро вывалить стеки всех заблокированных задач в системный лог через магический SysRq:

Код: Выделить всё

echo w | sudo tee /proc/sysrq-trigger
sudo dmesg -T | tail -60
После этого в dmesg появятся стеки всех D-задач разом. Это безопасно (мы только читаем состояние), в отличие от echo b, который мгновенно ребутнет машину - его НЕ трогай.

eBPF: современный способ поймать причину (актуально на 2026)

В 2026 году для разбора "почему процесс не на CPU" уже не нужно гадать по wchan вручную - есть зрелый eBPF (нужно ядро 4.9+, реально это любой современный дистрибутив). Инструменты идут в пакетах bpfcc-tools (bcc) и bpftrace.

Код: Выделить всё

# сколько времени и на каких стеках процессы спят вне CPU
sudo offcputime-bpfcc -p $(pidof myapp) 10

# гистограмма латентности блочного I/O (диск)
sudo biolatency-bpfcc 5 1

# латентность I/O с привязкой к стеку инициации запроса
sudo biostacks.bt
offcputime ловит ровно то, что нас мучает в D-state: блокировки на диске, сети, локах, page fault - и показывает стек, где процесс реально завис. biolatency рисует распределение задержек диска: если хвост уехал в десятки-сотни миллисекунд, диск виноват. Для разовых вопросов хватает однострочников bpftrace, для постоянного мониторинга - bcc-инструменты. eBPF тут заменил то, что раньше делали тяжёлым perf/strace, и делает это с почти нулевым оверхедом.

Итог по D: убить процесс в D нельзя в принципе. Варианты ровно три - дождаться, пока ввод-вывод завершится и процесс сам умрёт от уже поставленного в очередь сигнала; починить источник (поднять NFS-сервер, перемонтировать зависшую ФС, заменить умирающий диск); в безвыходной ситуации - перезагрузка узла. Никакого "форсированного kill для D" в Linux нет, и это сознательное решение разработчиков ядра.

Зомби и типичные грабли

Второй процесс, который "не убивается" - зомби (STAT = Z, в выводе ps помечен как <defunct>). Тут засада в логике: зомби уже мёртв. Его код отработал, память освобождена, осталась одна строчка в таблице процессов - чтобы родитель успел прочитать код возврата вызовом wait(). Слать kill в зомби бессмысленно, убивать там нечего, он не потребляет ни CPU, ни память.

Зомби исчезает, когда родитель его "пожинает" (reap). Если родитель забыл это сделать (баг в программе, не вызывает wait) - зомби копится. Лечится так: найти родителя и разобраться с ним.

Код: Выделить всё

ps -eo pid,ppid,stat,comm | awk '$3 ~ /Z/'
# взять PPID зомби и посмотреть, что за родитель
ps -p <PPID> -o pid,comm
# мягко пнуть родителя - часто он дочищает детей
kill -HUP <PPID>
Если родитель уже умер, зомби усыновляет init/systemd (PID 1), и он их подбирает сам - такие долго не живут. Кстати, частая причина зомби в контейнерах - когда главным процессом стоит приложение, не умеющее жать детей: лечится запуском с init-обёрткой (tini, docker run --init). Пара зомби в системе - норма, это не утечка. Тревога - если их сотни и число растёт: это уже исчерпание PID и заметный признак бага в родителе.

Грабли, на которые наступают почти все:
  • Сразу kill -9 вместо TERM. Отучайся. Девятка не даёт прибраться - привет битым данным и осиротевшим lock-файлам.
  • Думать, что -9 убьёт что угодно. Нет: D-state и зомби ему не по зубам, и это правильное поведение ядра, а не глюк.
  • Бить по PID, который уже переиспользован. Пока ты копировал старый PID, процесс умер, а номер занял другой. Поэтому pkill/killall по имени или systemctl kill по юниту надёжнее для короткоживущих.
  • pkill без pgrep. Шаблон шире, чем кажется (это regex) - проверяй охват заранее, особенно с -f.
  • Грузить систему попытками убить D-процесс. Бесполезно. Иди диагностировать ввод-вывод: stack, iostat, dmesg, offcputime.
  • Слать KILL сервису через PID. Потомки могут сбежать из-под наблюдения. Через systemctl kill уйдёт всё дерево по cgroup.
На Astra Linux и RED OS всё это работает один в один - утилиты те же (procps), интерфейс /proc стандартный, systemd с cgroup v2 на месте. Разница может быть лишь в мелочах вывода ps на старых ядрах.

Мини-лаба: повтори руками прямо сейчас
  • Запусти sleep 600 & в фоне, найди его через pgrep -a sleep, отправь kill -STOP <PID>, посмотри ps - состояние станет T. Разбуди через kill -CONT, потом сними kill -TERM. Прочувствуй разницу сигналов на живом примере.
  • Открой top или htop, найди процессы в состоянии S - убедись, что таких большинство и это норма.
  • Запусти ps -eo pid,stat,wchan:32,comm и посмотри, кто в каком сне и на какой функции ядра ждёт.
  • Сделай sudo dmesg -T | tail -40 и просто почитай, о чём ядро говорит прямо сейчас. Привыкай туда заглядывать первым делом.
  • Если есть права - запусти sudo offcputime-bpfcc 5 и посмотри, на каких стеках процессы реально спят вне CPU. Это твой будущий главный инструмент.
Контрольные вопросы
  • Чем TERM отличается от KILL и почему по умолчанию правильно слать именно TERM?
  • Процесс в состоянии D не реагирует на kill -9. Почему так происходит и куда смотреть, чтобы найти причину?
  • Можно ли убить зомби сигналом KILL? Если нет - что с ним вообще делать?
  • В чём разница между killall и pkill, зачем перед pkill запускать pgrep, и почему для сервиса лучше systemctl kill?
Что запомнить

kill не убивает, а шлёт сигнал. Проси по-хорошему через TERM, девятку держи как аварийный молоток. kill 9 linux всесилен только над живыми процессами в R и S - но бессилен над D (непрерываемый ввод-вывод) и над зомби (там убивать уже нечего). Если зависший процесс linux стоит в D - это симптом проблемы с диском или сетью: читай /proc/PID/stack, iostat и dmesg, в 2026 добавь offcputime/biolatency, а не долби сигналами. Зомби лечатся через родителя. Сервисы прибивай через systemctl kill, чтобы не упустить потомков. И всегда pgrep перед pkill, чтобы не снести лишнего.
👍 ❤️4 🔥1 😄 🤔
Аватара пользователя
zfs91
Сообщения: 1
Зарегистрирован: 22 май 2026, 07:46

Re: Сигналы и зависшие процессы: kill, и что делать с D-state

Сообщение zfs91 »

Спасибо, наконец дошло почему мой rsync висел и kill -9 не брал - оказалось NFS отвалилась, в /proc/PID/stack как раз были nfs функции. До этого тупо долбил девяткой полчаса.
👍 ❤️1 🔥 😄 🤔
Аватара пользователя
malmo123
Сообщения: 1
Зарегистрирован: 21 май 2026, 06:11

Re: Сигналы и зависшие процессы: kill, и что делать с D-state

Сообщение malmo123 »

Попробовал offcputime-bpfcc на проде вместо ковыряния в wchan - сразу видно стек где приложение спит на диске. Раньше бы strace вешал, а тут оверхеда почти ноль. Топ совет.
👍1 ❤️ 🔥 😄 🤔
Ответить
← Предыдущая глава
Приоритеты и ограничения: nice, ionice, cgroups
Следующая глава →
Загрузка CPU: user, system, iowait, steal и контекст-свитчи

Все главы курса «Диагностика и производительность Linux: логи, strace, perf и eBPF»

Поделиться темой: ✈ Telegram VK
Похожие запросы: perf top как найти что грузит процессорчто такое системный вызов и зачем трассироватьЛоги и диагностика macOSкак посмотреть процессы в linux и убить зависшийчто такое load average в linux и какое значение нормальноеПроизводительность Mac

Вернуться в «Диагностика и производительность Linux: логи, strace, perf и eBPF»

Кто сейчас на конференции

Сейчас этот форум просматривают: нет зарегистрированных пользователей и 1 гость