151. Os.Kill + Signal + Psutil Process Control
Killing processes safely with signals
151. Os.Kill + Signal + Psutil Process Control
“The AI told me to close Chrome. I want Python to do it for me.”
🤔 Socratic question: When you Force Quit an app on macOS, or End Task on Windows, what actually happens at the OS level? How does the OS force a program to stop without the program’s cooperation? This is signal theory — and it’s beautiful in a brutal way.
🏛️ History: Signals were designed as part of the original Unix specification in 1969. The entire signal system — SIGTERM, SIGKILL, SIGHUP, SIGINT — was designed by Ken Thompson and Dennis Ritchie. When you press Ctrl+C to stop a Python script, you’re triggering SIGINT, a mechanism designed in 1969. Some code never dies.
Before writing the code, Aryan needs to understand signals.
Process Signals: How Programs Talk to Each Other 📡
A signal is a message the OS sends directly to a process. The process can handle it, ignore it, or be killed by it.
kill -l # list all signals
# 1) SIGHUP 2) SIGINT 3) SIGQUIT
# 15) SIGTERM 9) SIGKILLYou’ve used signals before:
Ctrl+Csends SIGINT — interrupt (usually terminates)Ctrl+Zsends SIGTSTP — suspend (sends to background)kill PIDsends SIGTERM by default
💡 Every time you Ctrl+C a Python script, you’re sending SIGINT. The KeyboardInterrupt exception Python raises? That’s just Python’s way of handling SIGINT.
SIGTERM vs SIGKILL ⚔️
| SIGTERM (15) | SIGKILL (9) | |
|---|---|---|
| Can be caught by process? | Yes | No |
| Process can clean up? | Yes | No |
| Databases flush writes? | Yes | No |
| Always works? | No (can be ignored) | Yes |
SIGTERM = “please exit” — the polite way ☕ SIGKILL = “you will die now” — the nuclear option ☢️
kill 812 # SIGTERM (default)
kill -TERM 812 # same thing
kill -9 812 # SIGKILL — last resort💀 Culture:
kill -9has such a following in dev culture that you’ll findkill -9stickers at hackathons, people withkill -9tattooed on them, and entire Reddit threads debating when it’s acceptable. Spoiler: almost never in production code.
The Graceful Shutdown Pattern 🕊️
Always try SIGTERM first. Escalate to SIGKILL only after a timeout.
import psutil
def kill_process(pid: int) -> bool:
try:
proc = psutil.Process(pid)
proc.terminate() # SIGTERM — polite
try:
proc.wait(timeout=3) # wait up to 3 seconds
except psutil.TimeoutExpired:
proc.kill() # SIGKILL — force quit
return True
except psutil.NoSuchProcess:
return False # already gone
except psutil.AccessDenied:
return False # no permission💡 Real-world: Kubernetes (the system that runs most of the internet’s containers) uses exactly this pattern. When it needs to shut down a container, it sends SIGTERM first, waits 30 seconds, then sends SIGKILL. This is called the “graceful shutdown period” and it’s in every production system.
What Happens to Child Processes? 👶
When a parent process receives SIGKILL, it dies instantly. Its children become orphans — they keep running, reparented to PID 1 (init/launchd).
This is why proc.terminate() (SIGTERM) is better:
- A well-behaved parent sends SIGTERM to its children before exiting
- Children clean up their own resources
- No orphan processes left behind
For Chrome: killing the main process may leave tab renderer processes running. Use pkill chrome (by name) to kill the whole family.
🤯 Mind-blown: Chrome runs EACH TAB as a separate process. That’s why when a tab crashes it doesn’t crash the whole browser. And it’s why Chrome uses so much RAM — each process needs its own memory space. Google made this architectural decision for stability. You’re now equipped to understand it.
Permissions: Who Can Kill Whom? 🔐
You can only kill processes you own (same user ID).
$ kill 1 # can't kill launchd/init
# bash: kill: (1) - Operation not permitted
$ kill 812 # can kill your own Chrome
# (success)In Python: psutil.AccessDenied tells you when you lack permission. Always catch it — never crash on a permission error.
💡 Fun fact: The graceful shutdown timeout in Kubernetes is 30 seconds by default, but teams configure it based on how long their app needs to drain connections and finish in-flight requests. Some databases need minutes to flush WAL (Write-Ahead Log) buffers safely. This is why kill -9 on a database can corrupt data — it skips the flush entirely.
⚠️ Watch out: os.kill(pid, signal.SIGKILL) has no try/except and raises ProcessLookupError if the PID doesn’t exist. psutil.Process(pid).kill() still raises psutil.NoSuchProcess. Always wrap process operations in try/except for both exceptions. A process can exit between the time you list it and the time you try to signal it.
🤔 Think about it: Why does os.kill(pid, 0) not actually kill the process? What does signal 0 mean? If psutil.Process(pid).is_running() is the safer alternative — why is it safer? What race condition does checking pid then calling kill expose you to?
⚡ Aryan wants the RAM manager to actually kill offending processes when the AI recommends it. He uses signal.SIGKILL (force kill) when he should start with SIGTERM (polite ask), doesn’t check if the process exists first, and uses os.kill when psutil.Process gives a cleaner cross-platform API.
Learning objectives
- Use SIGTERM before SIGKILL (polite before force)
- Use psutil.Process.terminate() and .kill() for cross-platform
- Catch NoSuchProcess and AccessDenied — processes can die at any time
- Use os.kill(pid, 0) or psutil to check if a process is running
- [object Object]
Key concepts
- SIGTERM — polite shutdown request (signal 15)
- SIGKILL — instant force kill (signal 9, uncatchable)
- proc.terminate() — send SIGTERM via psutil
- proc.kill() — send SIGKILL via psutil
- proc.wait(timeout=N) — wait for exit, TimeoutExpired if slow
- NoSuchProcess — process already gone
Try it
Concept detail
Killing Processes Safely
Shell commands (the quick way)
kill 812 # SIGTERM to PID 812 (polite)
kill -9 812 # SIGKILL to PID 812 (force)
kill -SIGTERM 812 # explicit name
pkill chrome # SIGTERM to all processes named chrome
pkill -9 chrome # SIGKILL
killall chrome # same as pkill on most systems
pgrep chrome # list PIDs named chrome
pgrep -a chrome # list PIDs + full command lineSignals explained
| Signal | Number | Meaning |
|---|---|---|
| SIGTERM | 15 | Please stop (can be caught/ignored) |
| SIGKILL | 9 | Stop NOW (cannot be caught) |
| SIGHUP | 1 | Terminal closed / reload config |
| SIGINT | 2 | Ctrl+C |
Python: psutil (recommended)
import psutil
proc = psutil.Process(pid)
proc.terminate() # SIGTERM
try:
proc.wait(timeout=3) # wait up to 3 seconds
except psutil.TimeoutExpired:
proc.kill() # SIGKILL — last resort
# By name
for proc in psutil.process_iter(['pid', 'name']):
try:
if proc.info['name'] == 'chrome':
proc.terminate()
except (psutil.NoSuchProcess, psutil.AccessDenied):
passPython: os.kill (lower level)
import os, signal
os.kill(pid, signal.SIGTERM) # polite
os.kill(pid, signal.SIGKILL) # force
os.kill(pid, 0) # check if PID exists (no-op signal)Safe pattern: SIGTERM → wait → SIGKILL
def safe_kill(pid: int):
try:
proc = psutil.Process(pid)
proc.terminate() # try politely first
proc.wait(timeout=5)
except psutil.TimeoutExpired:
proc.kill() # force if still alive
except psutil.NoSuchProcess:
pass # already gone, that's fineSolution
import os
import signal
import time
import psutil
def kill_process(pid: int, force: bool = False) -> bool:
'''Send SIGTERM, wait 3s, then SIGKILL if still running.'''
try:
proc = psutil.Process(pid)
except psutil.NoSuchProcess:
return False # already gone
if force:
proc.kill() # SIGKILL immediately
return True
proc.terminate() # SIGTERM — polite, lets process clean up
try:
proc.wait(timeout=3) # wait up to 3 seconds
except psutil.TimeoutExpired:
proc.kill() # still alive — force kill
return True
def terminate_by_name(name: str) -> list[int]:
killed = []
for proc in psutil.process_iter(['pid', 'name']):
try:
if proc.info['name'] == name:
proc.terminate()
killed.append(proc.info['pid'])
except (psutil.NoSuchProcess, psutil.AccessDenied):
pass # process died or no permission — skip
return killed
def is_process_running(pid: int) -> bool:
try:
proc = psutil.Process(pid)
return proc.is_running() and proc.status() != psutil.STATUS_ZOMBIE
except psutil.NoSuchProcess:
return FalseTests
import psutil
import pytest
from unittest.mock import MagicMock, patch
def test_is_process_running_invalid_pid():
assert is_process_running(999999999) is False
def test_kill_process_nonexistent_returns_false():
result = kill_process(999999999)
assert result is False
def test_kill_process_uses_terminate_not_kill():
import inspect
src = inspect.getsource(kill_process)
assert 'terminate()' in src, 'Use .terminate() (SIGTERM) before .kill() (SIGKILL)'
def test_kill_process_uses_wait_before_kill():
import inspect
src = inspect.getsource(kill_process)
assert 'wait(' in src, 'Call proc.wait(timeout=N) after terminate() before escalating to kill()'
def test_terminate_by_name_handles_nosuchprocess():
# should not raise even if processes die during iteration
result = terminate_by_name('definitely_not_a_real_process_xyz')
assert result == []
def test_kill_process_returns_true_on_success():
mock_proc = MagicMock()
mock_proc.wait.return_value = None # exits cleanly within timeout
with patch('psutil.Process', return_value=mock_proc):
result = kill_process(12345)
assert result is True
mock_proc.terminate.assert_called_once()
def test_kill_process_escalates_to_kill_on_timeout():
mock_proc = MagicMock()
mock_proc.wait.side_effect = psutil.TimeoutExpired(seconds=3, pid=12345)
with patch('psutil.Process', return_value=mock_proc):
result = kill_process(12345)
assert result is True
mock_proc.terminate.assert_called_once()
mock_proc.kill.assert_called_once() # escalated because wait timed out
def test_is_process_running_returns_false_for_zombie():
mock_proc = MagicMock()
mock_proc.is_running.return_value = True
mock_proc.status.return_value = psutil.STATUS_ZOMBIE
with patch('psutil.Process', return_value=mock_proc):
assert is_process_running(12345) is False
def test_is_process_running_returns_true_for_live_process():
mock_proc = MagicMock()
mock_proc.is_running.return_value = True
mock_proc.status.return_value = psutil.STATUS_RUNNING
with patch('psutil.Process', return_value=mock_proc):
assert is_process_running(12345) is True