Перейти к содержимому
← Все заметки

// заметки

pkill -f убивает сам себя: шаблон совпадает с командой

pkill -f и pgrep -f сопоставляют шаблон со всей командной строкой, включая обёртку bash -c. Разбираю, почему команда убивает себя, и какие фильтры помогают

Опубликовано: 2026-10-175 мин чтения
Linuxbashpkillpgrepdebugging

Скрипт обрывается на полуслове с [killed by signal: SIGTERM], хотя kill по собственному PID в нём нет. pgrep -f сопоставляет шаблон не с именем программы, а со всей командной строкой процесса, и в неё попадает текст обёртки bash -c, внутри которой этот скрипт запущен

Ниже — диагностика, разбор причины и три замены для -f

Симптом

Команда завершается без трейсбека: вывод обрывается на середине, в логе остаётся только маркер сигнала

$ ./restart-agent.sh
останавливаю старый процесс
[killed by signal: SIGTERM]
$ echo $?
143

Код 143 — это 128 + 15, то есть SIGTERM. OOM killer сюда не подходит: он присылает SIGKILL и код 137. Значит, сигнал отправил кто-то из своих же команд в том же скрипте

Характерная деталь: руками в интерактивной оболочке скрипт отрабатывает, а из обёртки bash -c падает. Так команду запускают раннеры CI, инструментальные shell и системы сборки: весь скрипт приходит одним аргументом

Диагностика

Начните с того, кого реально выбирает шаблон. Флаг -a заставляет pgrep печатать PID и полную командную строку каждого совпадения

$ pgrep -a -f 'authentication-agent'
4242 bash -c PAT='authentication-agen[t]'; old=$(pgrep -f "$PAT"); ...
4301 /usr/lib/polkit-kde-authentication-agent-1

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

$ echo $$
4242
$ tr '\0' ' ' < /proc/$$/cmdline; echo
bash -c PAT='authentication-agen[t]'; old=$(pgrep -f "$PAT"); ...

Файл /proc/<pid>/cmdline хранит аргументы через нулевой байт, pgrep склеивает их пробелами и применяет шаблон как расширенное регулярное выражение. Отдельной проверки, что совпадение пришлось на имя программы, при -f нет

Причина

pgrep -f обходит все процессы, читает /proc/<pid>/cmdline и проверяет шаблон против всей строки аргументов. Для процесса, запущенного как bash -c '<скрипт>', эта строка содержит скрипт целиком: и присваивание PAT='authentication-agen[t]', и путь /usr/lib/polkit-kde-authentication-agent-1

Приём с квадратными скобками рассчитан на один случай. Шаблон authentication-agen[t] — это регулярное выражение, где [t] обозначает класс из одного символа: он совпадает с текстом authentication-agent, но не совпадает с текстом authentication-agen[t]

Пока шаблон встречается в командной строке только сам по себе, самоматча нет. Здесь authentication-agent есть второй раз, внутри пути к бинарю, и от этого вхождения скобки не защищают

$ printf '%s\n' "PAT='authentication-agen[t]'" "/usr/lib/polkit-kde-authentication-agent-1" \
    | grep -E 'authentication-agen[t]'
/usr/lib/polkit-kde-authentication-agent-1

Следующая ловушка — попытка исключить себя сравнением с $$. В bash $$ — это PID оболочки, которая стартовала, и в подоболочке он не меняется: в ( ... ), в конвейере cmd | while read и в подстановке $( ) переменная остаётся прежней. Реальный PID текущего процесса отдаёт $BASHPID

$ echo "оболочка: $$, текущий: $BASHPID"
оболочка: 4242, текущий: 4242
$ ( echo "оболочка: $$, текущий: $BASHPID" )
оболочка: 4242, текущий: 4388

Если проверка [[ "$p" == "$$" ]] && continue выполнялась в подоболочке, она пропускает родителя, но не сам текущий процесс, и kill бьёт по нему

Решение

Надёжнее опираться не на текст аргументов, а на путь к реально исполняемому файлу. readlink /proc/<pid>/exe возвращает его, причём без обрезки

Цикл отбрасывает свои PID и убивает только процесс, чей exe указывает на агента. Сначала сухой прогон: вместо kill стоит echo

AGENT=/usr/lib/polkit-kde-authentication-agent-1
for p in $(pgrep -f 'authentication-agent' 2>/dev/null); do
  [[ "$p" == "$$" || "$p" == "$BASHPID" ]] && continue
  [[ "$(readlink /proc/$p/exe 2>/dev/null)" == "$AGENT" ]] && echo kill "$p"
done

Сначала посмотрите список, потом уберите echo. В рабочем варианте добавляется проверка владельца: сигнал уйдёт только своему процессу

AGENT=/usr/lib/polkit-kde-authentication-agent-1
for p in $(pgrep -f 'authentication-agent' 2>/dev/null); do
  [[ "$p" == "$$" || "$p" == "$BASHPID" ]] && continue
  [[ "$(readlink /proc/$p/exe 2>/dev/null)" == "$AGENT" ]] || continue
  [[ "$(stat -c %u /proc/$p)" == "$(id -u)" ]] && kill "$p"
done

Для чужого пользователя readlink вернёт пустую строку, а stat покажет другой UID, так что такой PID не пройдёт условие. Это безопасный отказ: лучше не убить, чем убить не то

Если имя программы уникально, хватит pkill -x. Без -f он сопоставляет шаблон с comm (имя исполняемого файла, как его видит ядро), а не с аргументами, и собственная обёртка в выборку не попадает

pkill -u "$USER" -x swaync 2>/dev/null || true
pkill -u "$USER" -SIGUSR2 waybar 2>/dev/null || true

swaync и waybar — панели Wayland, а SIGUSR2 для waybar означает «перечитать конфиг»

comm обрезается до 15 символов. Для /usr/lib/polkit-kde-authentication-agent-1 в /proc/<pid>/comm лежит polkit-kde-auth, поэтому pkill -x polkit-kde-authentication-agent-1 не найдёт ничего

Третий случай — проверить, жив ли процесс, который запустил сам скрипт. Поиск по имени для этого не нужен: PID последнего фонового задания лежит в $!

cmd & AP=$!
sleep 1
kill -0 "$AP" 2>/dev/null && echo жив || echo умер

kill -0 не отправляет сигнал, а только проверяет, что процесс существует и доступен. На чужом PID он тоже вернёт ошибку, так что как проверка «жив ли» этот тест годится только для своих процессов

Проверка

Проверки идут от общего к частному: кто совпадает с шаблоном, есть ли среди них обёртка и что вернул pgrep

# 1) кто совпадает с шаблоном
pgrep -a -f 'authentication-agent'

# 2) попала ли в выборку обёртка
pgrep -a -f 'authentication-agent' | grep -q 'bash -c' \
  && echo 'обёртка в выборке' || echo 'обёртки нет'

# 3) код возврата pgrep
pgrep -f 'authentication-agent'; echo "rc=$?"

Код возврата pgrep: 0 — совпадения есть, 1 — совпадений нет; единица не ошибка, а нормальный ответ. Проверьте также сам фильтр по exe на живом процессе

$ readlink /proc/4301/exe
/usr/lib/polkit-kde-authentication-agent-1

Воспроизводите сценарий в той же обёртке, в которой он падал, через bash -c '...'. В интерактивной оболочке командная строка короче, и матч может не появиться

Подводные камни

  • comm обрезается до 15 символов, поэтому pkill -x по длинному имени молча не находит ничего: сверяйтесь с /proc/<pid>/comm перед тем, как писать шаблон
  • pkill -f задевает посторонние процессы с похожим текстом в аргументах: редактор с открытым скриптом, grep по нему, tail -f лога
  • Пустой результат pgrep и «процесса нет» неразличимы в цепочке pgrep ... && kill: код 1 молча пропускает всю логику, и ошибка вылезает позже
  • Между pgrep и kill PID может достаться другому процессу. Для короткой паузы риск мал, для длинной берите $! или pid-файл
  • Сравнение с $$ в подоболочке не исключает текущий процесс: добавляйте $BASHPID
  • Если список PID пуст, kill $old без проверки на пустоту печатает usage и возвращает 2, а под set -e это обрывает скрипт
  • Если матч явно чужой и непонятно, откуда взялся процесс, помогает ход по родительскому PID: Кто вызывает unshare

Итог

-f в pgrep и pkill означает «искать по всей командной строке», а не «по имени программы». Команда, запущенная через bash -c, содержит свой текст в аргументах и попадает в собственный шаблон, а квадратные скобки помогают только при единственном вхождении. Для остановки по имени берите pkill -x с обрезанным comm, для точной цели — readlink /proc/<pid>/exe, для своего процесса — $! и kill -0

Нужно решить такую задачу на вашем проекте?