Python os.killpg Function
Last modified April 11, 2025
This comprehensive guide explores Python's os.killpg function,
which sends signals to process groups. We'll cover signal types, process
groups, and practical examples of process management.
Basic Definitions
The os.killpg function sends a signal to a process group. It
requires the process group ID (pgid) and a signal number as parameters.
Key parameters: pgid (process group ID), sig (signal number). On success, returns None. Raises OSError on failure (invalid pgid or permissions).
Sending SIGTERM to a Process Group
This basic example demonstrates sending SIGTERM to terminate all processes in a group. SIGTERM allows graceful shutdown unlike SIGKILL.
import os
import signal
import time
from multiprocessing import Process
def worker():
print(f"Worker PID: {os.getpid()} running")
time.sleep(60)
if __name__ == "__main__":
# Create a new process group
os.setpgrp()
# Start child processes
children = [Process(target=worker) for _ in range(3)]
for p in children:
p.start()
print(f"Process group ID: {os.getpgid(0)}")
input("Press Enter to terminate process group...")
# Send SIGTERM to entire process group
os.killpg(os.getpgid(0), signal.SIGTERM)
print("Sent SIGTERM to process group")
This creates a process group with worker processes. When triggered, it sends SIGTERM to all processes in the group. Each process can handle the signal.
Note we use os.getpgid(0) to get our own process group ID (0 means current).
Graceful Shutdown with SIGTERM
This example shows proper signal handling for graceful shutdown. Processes can clean up resources before exiting when receiving SIGTERM.
import os
import signal
import sys
import time
from multiprocessing import Process
def cleanup():
print(f"{os.getpid()}: Performing cleanup...")
time.sleep(1) # Simulate cleanup
print(f"{os.getpid()}: Cleanup complete")
def signal_handler(signum, frame):
print(f"{os.getpid()}: Received signal {signum}")
cleanup()
sys.exit(0)
def worker():
signal.signal(signal.SIGTERM, signal_handler)
print(f"Worker {os.getpid()} running")
while True:
time.sleep(1)
if __name__ == "__main__":
os.setpgrp()
children = [Process(target=worker) for _ in range(3)]
for p in children:
p.start()
print(f"Main PID: {os.getpid()}, PGID: {os.getpgid(0)}")
input("Press Enter to terminate...")
os.killpg(os.getpgid(0), signal.SIGTERM)
for p in children:
p.join()
print("All processes terminated gracefully")
Each worker registers a SIGTERM handler for cleanup. When killpg sends SIGTERM, all processes perform cleanup before exiting. The main process waits for them.
This pattern is common in servers and long-running processes needing cleanup.
Forced Termination with SIGKILL
SIGKILL cannot be caught or ignored. This example shows using it to forcefully terminate unresponsive processes in a group.
import os
import signal
import time
from multiprocessing import Process
def unresponsive_worker():
print(f"Unresponsive worker {os.getpid()} running")
while True:
time.sleep(1)
# Simulate ignoring SIGTERM
pass
if __name__ == "__main__":
os.setpgrp()
children = [Process(target=unresponsive_worker) for _ in range(3)]
for p in children:
p.start()
print(f"PGID: {os.getpgid(0)}")
input("First try SIGTERM (press Enter)...")
# Try graceful termination first
os.killpg(os.getpgid(0), signal.SIGTERM)
time.sleep(2) # Give processes time to exit
# Check if any children still alive
alive = any(p.is_alive() for p in children)
if alive:
print("Processes not responding to SIGTERM")
input("Press Enter to force kill with SIGKILL...")
os.killpg(os.getpgid(0), signal.SIGKILL)
for p in children:
p.join(timeout=0.1)
print("All processes terminated")
This first attempts graceful shutdown with SIGTERM. If processes don't exit, it follows up with SIGKILL. SIGKILL immediately terminates processes.
Use SIGKILL sparingly as it doesn't allow cleanup and may leave resources locked.
Different Process Groups
This demonstrates killing a specific process group rather than the current one. We create two separate groups and terminate one selectively.
import os
import signal
import time
from multiprocessing import Process
def group_member(name):
print(f"{name} PID: {os.getpid()}, PGID: {os.getpgid(0)}")
while True:
time.sleep(1)
if __name__ == "__main__":
# Create first process group
p1 = Process(target=group_member, args=("Group1",))
p1.start()
time.sleep(0.1) # Ensure process starts
# Create second process group
p2 = Process(target=group_member, args=("Group2",))
p2.start()
time.sleep(0.1)
# Get their PGIDs
pgid1 = os.getpgid(p1.pid)
pgid2 = os.getpgid(p2.pid)
print(f"Group1 PGID: {pgid1}, Group2 PGID: {pgid2}")
input("Press Enter to kill Group1...")
os.killpg(pgid1, signal.SIGTERM)
p1.join()
print("Group1 terminated, Group2 still running")
input("Press Enter to exit...")
os.killpg(pgid2, signal.SIGTERM)
p2.join()
We create two independent process groups and terminate one while keeping the other running. Each Process creates its own group by default on Unix.
This shows how to target specific groups rather than just the current one.
Error Handling
This example demonstrates proper error handling when using os.killpg, including permission checks and invalid process group scenarios.
import os
import signal
import errno
def safe_killpg(pgid, sig):
try:
os.killpg(pgid, sig)
print(f"Successfully sent signal {sig} to group {pgid}")
except ProcessLookupError:
print(f"Process group {pgid} does not exist")
except PermissionError:
print(f"Permission denied to signal group {pgid}")
except OSError as e:
if e.errno == errno.ESRCH:
print(f"No such process group {pgid}")
else:
print(f"Error signaling group {pgid}: {e}")
if __name__ == "__main__":
# Test with various scenarios
safe_killpg(0, signal.SIGTERM) # Current process group
safe_killpg(999999, signal.SIGTERM) # Non-existent group
safe_killpg(1, signal.SIGTERM) # Init process (usually permission denied)
The safe_killpg function handles common error cases gracefully. It checks for non-existent groups, permission issues, and other potential errors.
Proper error handling is crucial when managing processes as conditions may change between checking and signaling.
Signal Propagation
This example shows how signals propagate to child processes in a group and demonstrates different signal behaviors.
import os
import signal
import time
from multiprocessing import Process
def child(signal_name):
print(f"Child {os.getpid()} waiting for {signal_name}")
while True:
time.sleep(1)
if __name__ == "__main__":
os.setpgrp()
signals = [
(signal.SIGTERM, "SIGTERM"),
(signal.SIGINT, "SIGINT"),
(signal.SIGHUP, "SIGHUP")
]
# Create a child for each signal type
children = [Process(target=child, args=(name,)) for _, name in signals]
for p in children:
p.start()
print(f"Main PID: {os.getpid()}, PGID: {os.getpgid(0)}")
for sig, name in signals:
input(f"Press Enter to send {name}...")
os.killpg(os.getpgid(0), sig)
time.sleep(0.5) # Allow time for signal handling
for p in children:
p.join(timeout=1)
print("Done")
This creates multiple child processes and sends different signals to the group. Each child would typically handle these signals differently in a real scenario.
Signals like SIGTERM terminate by default, while others like SIGHUP may have different default actions or be caught by handlers.
Process Group Creation
This advanced example demonstrates creating and managing custom process groups, then signaling specific groups.
import os
import signal
import time
from multiprocessing import Process
def group_worker(name, pgid):
# Create new session and process group
os.setsid()
print(f"{name} PID: {os.getpid()}, PGID: {os.getpgid(0)}")
while True:
time.sleep(1)
if __name__ == "__main__":
# Create two separate process groups
group1 = Process(target=group_worker, args=("Group1",))
group1.start()
time.sleep(0.1)
group2 = Process(target=group_worker, args=("Group2",))
group2.start()
time.sleep(0.1)
# Get their PGIDs
pgid1 = os.getpgid(group1.pid)
pgid2 = os.getpgid(group2.pid)
print(f"Main PGID: {os.getpgid(0)}")
print(f"Group1 PGID: {pgid1}, Group2 PGID: {pgid2}")
input("Press Enter to terminate Group1...")
os.killpg(pgid1, signal.SIGTERM)
group1.join()
input("Press Enter to terminate Group2...")
os.killpg(pgid2, signal.SIGTERM)
group2.join()
print("All groups terminated")
Each worker creates its own session and process group using os.setsid(). This allows completely independent process groups that can be signaled separately.
This pattern is useful for managing sets of related processes independently from other groups in the system.
Security Considerations
- Permissions: Need appropriate privileges to signal process groups
- Orphaned processes: Be careful not to kill unintended groups
- Signal safety: Async-signal-safe functions only in handlers
- Race conditions: Process groups may change between check and signal
- Cross-platform: Behavior differs between Unix and Windows
Best Practices
- Graceful first: Try SIGTERM before SIGKILL for cleanup
- Error handling: Always handle potential errors from killpg
- Minimal privileges: Run with least privileges needed
- Document signals: Clearly document signal handling in code
- Test thoroughly: Signal timing can be tricky to test
Source References
Author
List all Python tutorials.