Przejdź do treści
Lekcja 23 z 117

Kurs › Moduł 2 › Lekcja 23

Sygnały i zabijanie procesów

⏱ ok. 12 min czytania· moduł 2: Procesy i jak działa system

Po tej lekcji: kursant wie, czym jest sygnał, zna najważniejsze (SIGTERM, SIGKILL, SIGHUP, SIGINT, SIGSTOP, SIGCONT), wysyła je przez kill, killall i pkill, rozumie, czemu kill -9 to ostateczność, i czyta kody wyjścia 130, 137 i 143.

Wstęp

Aplikacja się zawiesiła. Kolega na dyżurze robi to, co zawsze: kill -9. Aplikacja znika, startuje od nowa, wszyscy idą spać.

Rano okazuje się, że plik z danymi jest ucięty w połowie, w folderze leży plik blokady, przez który kolejne uruchomienie odmawia startu, a połowa zamówień z nocy nie zapisała się nigdzie.

A wystarczyło najpierw grzecznie poprosić. Dziś poznasz różnicę między prośbą a wyciągnięciem wtyczki z gniazdka i dowiesz się, czemu kill wcale nie znaczy „zabij”.

Co to jest sygnał

Sygnał to krótka wiadomość od jądra do procesu. Bardzo krótka: sam numer. Bez treści, bez załączników. Jak dzwonek w kuchni. Jeden dźwięk znaczy „zamówienie gotowe”, dwa „zamykamy”, trzy „pożar”.

Zobacz wszystkie:

kill -l
 1) SIGHUP       2) SIGINT       3) SIGQUIT      4) SIGILL       5) SIGTRAP
 6) SIGABRT      7) SIGBUS       8) SIGFPE       9) SIGKILL     10) SIGUSR1
11) SIGSEGV     12) SIGUSR2     13) SIGPIPE     14) SIGALRM     15) SIGTERM
16) SIGSTKFLT   17) SIGCHLD     18) SIGCONT     19) SIGSTOP     20) SIGTSTP
...

Sześćdziesiąt kilka sygnałów. Spokojnie, na co dzień admin używa może ośmiu. Każdy ma numer i nazwę. kill -l 15 przetłumaczy numer na nazwę, kill -l TERM nazwę na numer.

Co proces robi z sygnałem

Każdy sygnał ma domyślną reakcję. Większość kończy proces. Niektóre zatrzymują, niektóre są ignorowane. Ale i tu jest haczyk: proces może powiedzieć jądru „ten sygnał obsłużę sam”. Wtedy zamiast domyślnej reakcji uruchamia się jego własny kod: zapisuje dane, sprząta, zamyka połączenia, albo w ogóle ignoruje.

I dlatego kill to myląca nazwa. kill niczego nie zabija. kill wysyła sygnał. Co się potem stanie, zależy od sygnału i od procesu.

Zobaczmy to na własnym procesie. Pamiętasz, że skrypt to przepis kulinarny? Oto przepis na lokal, który ma swoje zasady:

#!/bin/bash
# Lokal, który reaguje na sygnały
trap 'echo "[$$] SIGTERM: zmywam naczynia, gaszę światło, zamykam."; exit 0' TERM
trap 'echo "[$$] SIGHUP: czytam nowe menu (przeładowanie konfiguracji)."' HUP
trap 'echo "[$$] SIGINT: ktoś wcisnął Ctrl+C, ale ja jeszcze obsługuję gości."' INT
echo "[$$] Lokal otwarty."
while true; do
  sleep 1
done

trap to sprzątaczka na koniec imprezy. Mówi: „kiedy przyjdzie taki sygnał, zrób to”. Szczegółowo poznasz ją w module o skryptach. Dziś tylko oglądamy, jak działa.

chmod +x lokal.sh
./lokal.sh &
P=$!
kill -HUP $P
kill -INT $P
kill $P
[20022] Lokal otwarty.
[20022] SIGHUP: czytam nowe menu (przeładowanie konfiguracji).
[20022] SIGINT: ktoś wcisnął Ctrl+C, ale ja jeszcze obsługuję gości.
[20022] SIGTERM: zmywam naczynia, gaszę światło, zamykam.
[1]+  Done                    ./lokal.sh

Trzy sygnały, trzy różne reakcje, i za każdym razem proces sam zdecydował, co zrobić. Po SIGTERM posprzątał i wyszedł z kodem 0. Bash pisze Done, czyli „skończył normalnie”.

A teraz wyłączamy prąd:

./lokal.sh &
kill -9 $!
[20272] Lokal otwarty.
[1]+  Killed                  ./lokal.sh

Żadnego zmywania. Żadnego gaszenia światła. Killed. Proces nawet nie wiedział, że umiera.

SIGTERM kontra SIGKILL

To jest najważniejsza para w całej lekcji.

SIGTERM, numer 15. Grzeczna prośba o zamknięcie lokalu. Kierownik podchodzi i mówi: „Zamykamy, proszę kończyć”. Kucharze dokńczają danie, zmywają, zapisują utarg, zamykają drzwi. To jest domyślny sygnał komendy kill. Program może go przechwycić i posprzątać, a może też, w teorii, zignorować.

SIGKILL, numer 9. Wyłączenie prądu w całym budynku. Nikt niczego nie dokńcza. Garnki zostają na ogniu, kasa otwarta, rachunki niezapisane. Tego sygnału nie da się przechwycić, zablokować ani zignorować. Jądro w ogóle nie pyta procesu o zdanie, tylko go usuwa. To samo dotyczy SIGSTOP. Obie zasady stoją czarno na białym w man 7 signal.

Czemu -9 jest groźne? Bo program nie zdąży:

  • dopisać danych do pliku, więc plik zostaje ucięty,
  • usunąć pliku blokady albo pliku z PID-em, więc następny start myśli, że program wciąż działa,
  • skasować plików tymczasowych,
  • grzecznie zamknąć połączeń z bazą albo z klientami,
  • zakończyć swoich dzieci, które zostają sierotami.

Zasada admina:

  1. Najpierw kill PID, czyli SIGTERM.
  2. Odczekaj kilka, kilkanaście sekund. Duża baza danych może potrzebować więcej.
  3. Sprawdź, czy zniknął: kill -0 PID albo ps -p PID.
  4. Dopiero jeśli nadal żyje i nie reaguje, kill -9 PID.

Dokładnie tak robi systemd, kiedy zatrzymujesz usługę: najpierw wysyła SIGTERM, czeka, a dopiero po upływie czasu oczekiwania SIGKILL. Opisuje to man systemd.kill. Jeśli kierownik zmiany tak robi, to Ty też możesz.

I jeszcze jedno. Pamiętasz stan D z lekcji 20? Proces czekający na dysk nie odbierze nawet SIGKILL, dopóki się nie obudzi. Więc jeśli kill -9 „nie działa”, sprawdź stan procesu. Pewnie to D albo Z, a wtedy problem jest gdzie indziej.

Pozostałe sygnały, które warto znać

  • SIGHUP, 1. „Hang up”, odłożenie słuchawki. Kiedyś terminal łączył się z komputerem przez modem i ten sygnał znaczył „użytkownik się rozłączył”. Dziś ma dwa życia. Pierwsze: dostają go procesy, kiedy zamykasz terminal albo zrywa się SSH. O tym cały następna lekcja. Drugie: demony umownie traktują go jako „przeczytaj ponownie konfigurację”. Na przykład sshd po SIGHUP wczytuje od nowa swój plik ustawień, tak opisuje to man sshd. Nowe menu bez zamykania restauracji.
  • SIGINT, 2. „Interrupt”. To właśnie wysyła Ctrl+C.
  • SIGQUIT, 3. Ctrl+. Kończy proces i może zostawić zrzut pamięci dla programisty.
  • SIGSTOP, 19, i SIGTSTP, 20. Zatrzymanie. SIGTSTP wysyła Ctrl+Z i proces może go obsłużyć. SIGSTOP działa zawsze.
  • SIGCONT, 18. Wznów. Odmraża zatrzymany proces.
  • SIGUSR1 i SIGUSR2. Dla programistów, każdy program używa ich po swojemu. Przykład: działający dd po SIGUSR1 wypisuje, ile już skopiował, i kopiuje dalej.
  • SIGCHLD, 17. „Twoje dziecko skończyło”. Znasz go z lekcji o zombie.

kill: listonosz z jedną kartką

kill 1234
kill -15 1234
kill -TERM 1234
kill -SIGTERM 1234
kill -s TERM 1234
kill -9 1234
kill -HUP 1234
kill 1234 5678 9012
kill %1

Pięć pierwszych linijek robi dokładnie to samo: wysyła SIGTERM. Można podać kilka PID-ów naraz. A %1 to numer zadania w tle z Twojego Basha, o tym w następnej lekcji.

Ciekawostka: kill istnieje dwa razy.

type -a kill
kill is a shell builtin
kill is /usr/bin/kill
kill is /bin/kill

Bash ma własnego kill, wbudowanego. Dzięki temu zadziała nawet wtedy, gdy system jest tak zapchany, że nie da się uruchomić żadnego nowego procesu. A /bin/kill to drogowskaz do /usr/bin/kill, czyli tego samego programu, ten sam numer, dwie tabliczki. Plan biurowca z lekcji 8 się kłania.

Trzy przydatne sztuczki:

kill 999999
kill -0 1234; echo "żyje? kod: $?"
su ania -c "kill 20603"
bash: kill: (999999) - No such process
żyje? kod: 0
bash: line 1: kill: (20603) - Operation not permitted
  • Nie ma takiego procesu: komunikat No such process.
  • kill -0 niczego nie wysyła. Tylko sprawdza, czy proces istnieje i czy wolno Ci do niego pisać. Kod 0 to kciuk w górę. Idealne do skryptów.
  • Zwykły użytkownik nie wyśle sygnału do cudzego procesu: Operation not permitted. Klucze tylko do swoich pokojów. Root może wszystko, z wyjątkiem procesu numer 1, który jest chroniony, pamiętasz?

I jeszcze jedna rzecz, która zaskakuje:

kill $$; echo "Nadal żyję, PID $$"
Nadal żyję, PID 17206

Twój interaktywny Bash ignoruje SIGTERM. Specjalnie, żebyś przez przypadek nie zastrzelił urzędnika, z którym rozmawiasz. Tak pisze man bash w części o sygnałach.

killall: po nazwie

kill potrzebuje numeru. killall bierze nazwę i wysyła sygnał do wszystkich procesów o tej nazwie.

killall -v nginx
killall -i sleep
killall -u ania bash
killall -w myapp
killall nicniema; echo "kod: $?"
nicniema: no process found
kod: 1
  • -v mówi, co zabił. -i pyta przy każdym procesie. -u tylko procesy danego użytkownika. -w czeka, aż naprawdę umrą. Nic nie znalazł — kod 1.
  • killall jest w pakiecie psmisc. Na Ubuntu zwykle już jest, na minimalnym RHEL czasem trzeba go doinstalować: sudo dnf install psmisc.

Uwaga, pułapka nazwy: „wszystkie procesy o tej nazwie” to naprawdę wszystkie. Jeśli Ty, kolega i trzy skrypty systemowe używacie sleep, killall sleep ubije każdy. Na serwerze, na którym pracuje więcej osób, to jak ogłoszenie przez megafon: „wszyscy Kowalscy opuszczają budynek”. A podręcznik killall dodatkowo ostrzega, że na innych systemach niż Linux killall potrafi zrobić coś zupełnie innego, szczególnie jako root. Na starym Uniksie w banku ta komenda może dosłownie zabić wszystko. Zanim wpiszesz killall na obcej maszynie, sprawdź, gdzie jesteś.

pkill: po wzorcu

pkill to brat pgrep, którego znasz z lekcji 19. Te same opcje, tylko zamiast wypisywać numery, wysyła sygnał.

pkill -e -x sleep -P $$
pkill -u stazysta
pkill -HUP -x sshd
pkill -f "python3 raport.py"
sleep killed (pid 21763)
sleep killed (pid 21764)
  • -e mówi, co zabił.
  • -x dokładna nazwa, -P $$ tylko dzieci mojego Basha. Precyzyjny strzał.
  • -u stazysta wszystkie procesy użytkownika. Tak wyrzuca się kogoś z serwera.
  • -HUP -x sshd przykład sygnału innego niż domyślny.
  • -f szuka w całej linii komend. Potężne i niebezpieczne. Zaraz zobaczysz czemu.

Złota zasada: najpierw pgrep z tymi samymi opcjami, potem pkill. pgrep -a to próba generalna: pokazuje dokładnie te procesy, które za chwilę dostaną sygnał.

Kody wyjścia po sygnale

Kiedy proces ginie od sygnału, Bash ustawia kod wyjścia na 128 plus numer sygnału.

sleep 100 & kill -9 $!; wait $!; echo "kod: $?"
sleep 100 & kill $!; wait $!; echo "kod: $?"
[1]+  Killed                  sleep 100
kod: 137
[1]+  Terminated              sleep 100
kod: 143
  • 130 = 128 + 2, czyli przerwany Ctrl+C.
  • 137 = 128 + 9, czyli SIGKILL. Bardzo często to robota mechanizmu, który zabija procesy przy braku pamięci. Wrócimy do niego przy lekcji o pamięci.
  • 143 = 128 + 15, czyli SIGTERM.

Zobaczysz te liczby w logach usług, w CI/CD i w kontenerach. Teraz będziesz wiedział, że to nie przypadkowe numery, tylko podpis sygnału.

Jak to działa pod spodem

kill, killall, pkill i F9 w htop robią jedno: wywołują funkcję jądra o nazwie kill, z numerem procesu i numerem sygnału. Jądro sprawdza uprawnienia i zaznacza w kartotece procesu „czeka na ciebie sygnał numer 15”. Kiedy proces następnym razem dostaje procesor, jądro doręcza sygnał: uruchamia jego obsługę, jeśli proces ją zgłosił, albo domyślną reakcję.

SIGKILL i SIGSTOP jądro załatwia samo, bez pytania procesu. Dlatego nie da się ich przechwycić.

Ściąga flag

KomendaFlagaCo robi
kill-llista sygnałów, -l 15 tłumaczy numer na nazwę
kill(bez flagi)wysyła SIGTERM
kill-9 / -KILLwysyła SIGKILL, ostateczność
kill-HUPprzeładuj konfigurację (u demonów)
kill-STOP / -CONTzatrzymaj / wznów
kill-0tylko sprawdź, czy proces istnieje
kill-s NAZWAsygnał po nazwie
killall-v / -ipokaż, co zabija / pytaj przy każdym
killall-u użytkowniktylko procesy użytkownika
killall-wczekaj, aż procesy umrą
pkill-xdokładna nazwa
pkill-fwzorzec w całej linii komend
pkill-u / -P PIDpo użytkowniku / tylko dzieci procesu
pkill-ewypisz, co zabił
pkill-SYGNAŁinny sygnał, np. -HUP
pgrep-apróba generalna przed pkill

⚠ Typowy błąd

pkill -f w skrypcie albo w komendzie przez SSH. Przykład na żywo: w tle działa sleep 4444, a ja chcę go zabić z małego skryptu i potem coś jeszcze zrobić.

sleep 4444 &
bash -c 'pkill -f "sleep 4444"; echo "Przeżyłem!"'; echo "kod: $?"
[1]+  Terminated              sleep 4444
Terminated
kod: 143

Napis „Przeżyłem!” się nie pojawił. Kod 143, czyli SIGTERM. Kto zabił nasz skrypt? On sam.

pkill -f szuka wzorca w całej linii komend wszystkich procesów. A linia komend naszego bash -c zawierała… tekst sleep 4444, bo to był argument dla pkill. pkill uprzejmie pomija sam siebie, ale swojego rodzica już nie. Strzelił do celu i do własnego szefa jednocześnie. Tak samo kończy się ssh serwer 'pkill -f moja_aplikacja': zabijasz powłokę SSH, która wykonuje Twoją komendę.

Jak tego uniknąć:

  • Najpierw pgrep -a -f "wzorzec" i popatrz, kto jest na liście.
  • Użyj -x z nazwą programu zamiast -f, jeśli tylko się da.
  • Zawężaj: -u użytkownik, -P rodzic.
  • Albo trik z lekcji 19: pkill -f "[s]leep 4444". Wzorzec nie pasuje do własnego zapisu.
  • A dla usług najlepiej w ogóle nie używać pkill, tylko systemctl stop albo systemctl reload. Systemd wie dokładnie, które procesy należą do usługi.

✎ Zadanie

  • Przepisz skrypt lokal.sh z tej lekcji i sprawdź go ShellCheckiem.
  • Uruchom go w tle i po kolei wyślij SIGHUP, SIGINT i SIGTERM. Każdy raz po kilku sekundach.
  • Uruchom go jeszcze raz i zakończ przez kill -9. Przez wait $! i echo $? sprawdź kod wyjścia w obu przypadkach i wyjaśnij, skąd się wziął.
  • Uruchom trzy procesy sleep 3333 &. Najpierw pgrep -a pokaż dokładnie te trzy i nic więcej, a potem jedną komendą pkill zakończ je, nie ruszając niczego innego.
Pokaż rozwiązanie
./lokal.sh & sleep 1; kill -HUP $!; sleep 2; kill -INT $!; sleep 2; kill $!; wait $!; echo "kod: $?"
[32616] Lokal otwarty.
[32616] SIGHUP: czytam nowe menu (przeładowanie konfiguracji).
[32616] SIGINT: ktoś wcisnął Ctrl+C, ale ja jeszcze obsługuję gości.
[32616] SIGTERM: zmywam naczynia, gaszę światło, zamykam.
[1]+  Done                    ./lokal.sh
kod: 0
./lokal.sh & sleep 1; kill -9 $!; wait $!; echo "kod: $?"
[1]+  Killed                  ./lokal.sh
kod: 137

Po SIGTERM kod 0, bo skrypt sam posprzątał i wyszedł przez exit 0. Po SIGKILL kod 137, czyli 128 plus 9. Skrypt nie miał nic do powiedzenia.

A trzy sleep:

sleep 3333 & sleep 3333 & sleep 3333 &
pgrep -a -f "^sleep 3333$"
pkill -e -f "^sleep 3333$"
32720 sleep 3333
32722 sleep 3333
32723 sleep 3333
sleep killed (pid 32720)
sleep killed (pid 32722)
sleep killed (pid 32723)

Daszek i dolar mówią: „cała linia komend ma być dokładnie taka, od początku do końca”. Więc nie złapie ani innych sleep, ani własnej powłoki, w której ten tekst stoi gdzieś w środku. Jeśli Ci przy tym zadaniu zniknął terminal, to znaczy, że pkill -f znowu strzelił do szeryfa. Wracaj do poprzedniej lekcji, jest o tym cały akapit.

Co dalej

Ctrl+C, Ctrl+Z, %1 i ten tajemniczy SIGHUP przy zamykaniu terminala. W następnej lekcji składamy to w całość: programy w tle, jobs, fg, bg, nohup i disown. Czyli jak odpalić coś długiego na serwerze i spokojnie zamknąć laptopa.