Python os.fork Function
Last modified April 11, 2025
This comprehensive guide explores Python's os.fork function,
which creates child processes in Unix-like systems. We'll cover process
creation, parent-child relationships, and practical multiprocessing examples.
Basic Definitions
The os.fork function creates a new process by duplicating the
calling process. The new process is called the child, the original is parent.
It returns 0 in the child process and the child's PID in the parent process. On error, it raises OSError. This is a Unix-specific system call.
Basic Fork Example
This simple example demonstrates the basic usage of os.fork.
It shows how the same code runs in both parent and child processes.
import os
print("Before fork")
pid = os.fork()
if pid == 0:
print(f"Child process with PID: {os.getpid()}")
else:
print(f"Parent process with PID: {os.getpid()}, child PID: {pid}")
print("Both processes continue from here")
After fork, both processes continue execution from the same point. The if statement separates their behavior based on the return value.
The child gets PID 0, while the parent gets the child's actual PID. Both print statements will execute, but in different processes.
Fork with Different Tasks
This example shows how parent and child processes can perform different tasks. The child runs a calculation while the parent waits for it to finish.
import os
import time
def child_task():
print(f"Child process {os.getpid()} starting")
total = 0
for i in range(1000000):
total += i
print(f"Child calculated sum: {total}")
time.sleep(2)
print("Child exiting")
def parent_task(child_pid):
print(f"Parent process {os.getpid()} waiting for child")
_, status = os.waitpid(child_pid, 0)
print(f"Child exited with status: {status}")
pid = os.fork()
if pid == 0:
child_task()
os._exit(0) # Explicit exit
else:
parent_task(pid)
print("Parent continuing")
The child performs a CPU-intensive calculation while the parent waits using
os.waitpid. The child explicitly exits with os._exit.
Note the use of os._exit in the child to avoid running any cleanup
handlers that might be registered in the parent process.
Fork with Shared File Descriptors
This example demonstrates how file descriptors are shared between parent and child processes after a fork. Both can read and write the same file.
import os
file_path = "shared.txt"
# Create and write to file before fork
with open(file_path, "w") as f:
f.write("Initial content\n")
# Open file in append mode
f = open(file_path, "a+")
pid = os.fork()
if pid == 0:
# Child process
f.write(f"Child writing at position {f.tell()}\n")
f.flush()
f.seek(0)
print("Child read:", f.read())
f.close()
else:
# Parent process
f.write(f"Parent writing at position {f.tell()}\n")
f.flush()
os.waitpid(pid, 0) # Wait for child
f.seek(0)
print("Parent read:", f.read())
f.close()
Both processes share the same file descriptor and can interleave writes. The output shows how their operations affect the same file position.
Note the use of flush to ensure writes appear in the correct
order, and proper waiting to avoid race conditions.
Fork with Global Variables
This example shows how global variables behave after a fork. Each process gets its own copy of the variables, and changes aren't shared.
import os
counter = 0
pid = os.fork()
if pid == 0:
# Child process
counter += 1
print(f"Child counter: {counter}")
else:
# Parent process
counter += 2
print(f"Parent counter: {counter}")
os.waitpid(pid, 0)
print(f"Final counter: {counter}")
The child increments the counter by 1, the parent by 2. Each process has its own memory space, so changes don't affect the other process.
The "Final counter" output will be different in each process, demonstrating that they have independent copies of the variable.
Fork Bomb Example
This dangerous example demonstrates how uncontrolled forking can create a fork bomb. Warning: Running this may crash your system!
import os
import time
def fork_bomb():
while True:
try:
pid = os.fork()
if pid == 0: # Child
time.sleep(0.1)
else: # Parent
print(f"Created child {pid}")
except OSError as e:
print(f"Fork failed: {e}")
break
if __name__ == "__main__":
print("WARNING: This is a fork bomb!")
print("It will create processes until system resources are exhausted.")
print("Run at your own risk!")
# Uncomment to actually run
# fork_bomb()
This code creates child processes in an infinite loop, eventually exhausting system resources. It's included for educational purposes only.
The commented-out call prevents accidental execution. Never run actual fork bombs on production systems.
Fork with Signal Handling
This example shows how to handle signals in forked processes. The parent sends a signal to the child, which handles it with a custom handler.
import os
import signal
import time
def child_handler(signum, frame):
print(f"Child {os.getpid()} received signal {signum}")
pid = os.fork()
if pid == 0:
# Child process
signal.signal(signal.SIGUSR1, child_handler)
print(f"Child {os.getpid()} waiting for signal")
time.sleep(10) # Wait for signal
print("Child exiting")
os._exit(0)
else:
# Parent process
time.sleep(1) # Give child time to set up handler
print(f"Parent sending SIGUSR1 to child {pid}")
os.kill(pid, signal.SIGUSR1)
os.waitpid(pid, 0)
print("Parent exiting")
The child sets up a signal handler for SIGUSR1, then waits. The parent sends the signal after a delay. The handler prints a message when triggered.
Signal handling in child processes requires careful synchronization to ensure the handler is registered before the signal is sent.
Fork with Process Groups
This advanced example demonstrates creating a new process group with the child process, useful for managing groups of related processes.
import os
import time
pid = os.fork()
if pid == 0:
# Child becomes process group leader
os.setpgid(0, 0)
print(f"Child PID: {os.getpid()}, PGID: {os.getpgid(0)}")
time.sleep(5)
print("Child exiting")
os._exit(0)
else:
# Parent
print(f"Parent PID: {os.getpid()}, PGID: {os.getpgid(0)}")
print(f"Child PID: {pid}, Child PGID: {os.getpgid(pid)}")
os.waitpid(pid, 0)
print("Parent exiting")
The child calls os.setpgid to create a new process group. This
is useful when you want the child to be independent of the parent's group.
Process groups are particularly important for terminal control and signal delivery to groups of processes.
Security Considerations
- Resource limits: Forking too many processes can exhaust system resources
- Memory usage: Copy-on-write doesn't prevent eventual memory duplication
- Cleanup: Zombie processes must be properly reaped with wait
- Platform limits: Windows doesn't support os.fork
- Thread safety: Forking in multithreaded programs can be dangerous
Best Practices
- Always wait: Use os.wait or os.waitpid to clean up child processes
- Handle errors: Check for OSError when forking
- Use _exit: In children, use os._exit instead of sys.exit
- Limit forking: Consider multiprocessing module for complex cases
- Signal safety: Be careful with signals in forked processes
Source References
Author
List all Python tutorials.