Sygnały i zabijanie procesów
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
donetrap 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.shTrzy 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:
- Najpierw
kill PID, czyliSIGTERM. - Odczekaj kilka, kilkanaście sekund. Duża baza danych może potrzebować więcej.
- Sprawdź, czy zniknął:
kill -0 PIDalbops -p PID. - 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ładsshdpoSIGHUPwczytuje od nowa swój plik ustawień, tak opisuje toman 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, iSIGTSTP, 20. Zatrzymanie.SIGTSTPwysyła Ctrl+Z i proces może go obsłużyć.SIGSTOPdziała zawsze.SIGCONT, 18. Wznów. Odmraża zatrzymany proces.SIGUSR1iSIGUSR2. Dla programistów, każdy program używa ich po swojemu. Przykład: działającyddpoSIGUSR1wypisuje, 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 %1Pięć 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 killkill is a shell builtin
kill is /usr/bin/kill
kill is /bin/killBash 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 -0niczego 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 17206Twó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-vmówi, co zabił.-ipyta przy każdym procesie.-utylko procesy danego użytkownika.-wczeka, aż naprawdę umrą. Nic nie znalazł — kod 1.killalljest w pakieciepsmisc. 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)-emówi, co zabił.-xdokładna nazwa,-P $$tylko dzieci mojego Basha. Precyzyjny strzał.-u stazystawszystkie procesy użytkownika. Tak wyrzuca się kogoś z serwera.-HUP -x sshdprzykład sygnału innego niż domyślny.-fszuka 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
| Komenda | Flaga | Co robi |
|---|---|---|
kill | -l | lista sygnałów, -l 15 tłumaczy numer na nazwę |
kill | (bez flagi) | wysyła SIGTERM |
kill | -9 / -KILL | wysyła SIGKILL, ostateczność |
kill | -HUP | przeładuj konfigurację (u demonów) |
kill | -STOP / -CONT | zatrzymaj / wznów |
kill | -0 | tylko sprawdź, czy proces istnieje |
kill | -s NAZWA | sygnał po nazwie |
killall | -v / -i | pokaż, co zabija / pytaj przy każdym |
killall | -u użytkownik | tylko procesy użytkownika |
killall | -w | czekaj, aż procesy umrą |
pkill | -x | dokładna nazwa |
pkill | -f | wzorzec w całej linii komend |
pkill | -u / -P PID | po użytkowniku / tylko dzieci procesu |
pkill | -e | wypisz, co zabił |
pkill | -SYGNAŁ | inny sygnał, np. -HUP |
pgrep | -a | pró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: 143Napis „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
-xz nazwą programu zamiast-f, jeśli tylko się da. - Zawężaj:
-uużytkownik,-Prodzic. - 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, tylkosystemctl stopalbosystemctl reload. Systemd wie dokładnie, które procesy należą do usługi.
✎ Zadanie
- Przepisz skrypt
lokal.shz tej lekcji i sprawdź go ShellCheckiem. - Uruchom go w tle i po kolei wyślij
SIGHUP,SIGINTiSIGTERM. Każdy raz po kilku sekundach. - Uruchom go jeszcze raz i zakończ przez
kill -9. Przezwait $!iecho $?sprawdź kod wyjścia w obu przypadkach i wyjaśnij, skąd się wziął. - Uruchom trzy procesy
sleep 3333 &. Najpierwpgrep -apokaż dokładnie te trzy i nic więcej, a potem jedną komendąpkillzakoń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: 137Po 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.