Python os.fork 函数
上次修改时间:2025 年 4 月 11 日
这份全面的指南探讨了 Python 的 os.fork 函数,该函数在类 Unix 系统中创建子进程。我们将介绍进程创建、父子关系和实际的多进程示例。
基本定义
os.fork 函数通过复制调用进程来创建一个新进程。 新进程被称为子进程,原始进程被称为父进程。
它在子进程中返回 0,在父进程中返回子进程的 PID。 出错时,它会引发 OSError。 这是一个 Unix 特定的系统调用。
基本 Fork 示例
这个简单的示例演示了 os.fork 的基本用法。 它展示了相同的代码如何在父进程和子进程中运行。
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")
fork 之后,两个进程都从同一点继续执行。 if 语句根据返回值来区分它们的行为。
子进程得到 PID 0,而父进程得到子进程的实际 PID。 两个 print 语句都将执行,但在不同的进程中。
Fork 执行不同的任务
此示例显示了父进程和子进程如何执行不同的任务。 子进程运行计算,而父进程等待它完成。
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")
子进程执行 CPU 密集型计算,而父进程使用 os.waitpid 等待。 子进程使用 os._exit 显式退出。
请注意在子进程中使用 os._exit 以避免运行可能在父进程中注册的任何清理处理程序。
Fork 共享文件描述符
此示例演示了在 fork 之后,父进程和子进程之间如何共享文件描述符。 两者都可以读取和写入同一文件。
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()
两个进程共享相同的文件描述符,并且可以交错写入。 输出显示了它们的操作如何影响相同的文件位置。
请注意使用 flush 确保写入以正确的顺序显示,并进行适当的等待以避免竞争条件。
Fork 共享全局变量
此示例显示了全局变量在 fork 之后的行为。 每个进程都获得变量的自己的副本,并且更改不会共享。
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}")
子进程将计数器递增 1,父进程递增 2。 每个进程都有自己的内存空间,因此更改不会影响另一个进程。
每个进程中的“最终计数器”输出将不同,这表明它们具有该变量的独立副本。
Fork 炸弹示例
这个危险的例子演示了不受控制的 forking 如何创建 fork 炸弹。 警告:运行此代码可能会导致您的系统崩溃!
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()
此代码在一个无限循环中创建子进程,最终耗尽系统资源。 它仅出于教育目的而包含。
注释掉的调用可防止意外执行。 永远不要在生产系统上运行实际的 fork 炸弹。
Fork 信号处理
此示例显示了如何在 fork 进程中处理信号。 父进程向子进程发送信号,子进程使用自定义处理程序对其进行处理。
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")
子进程为 SIGUSR1 设置一个信号处理程序,然后等待。 父进程在延迟后发送信号。 触发时,处理程序会打印一条消息。
子进程中的信号处理需要仔细的同步,以确保在发送信号之前注册处理程序。
Fork 进程组
这个高级示例演示了使用子进程创建一个新的进程组,这对于管理相关的进程组很有用。
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")
子进程调用 os.setpgid 以创建一个新的进程组。 当您希望子进程独立于父进程的组时,这很有用。
进程组对于终端控制和向进程组传递信号尤其重要。
安全注意事项
- 资源限制: Forking 过多的进程可能会耗尽系统资源
- 内存使用情况: 写时复制并不能阻止最终的内存复制
- 清理: 僵尸进程必须使用 wait 正确回收
- 平台限制: Windows 不支持 os.fork
- 线程安全: 在多线程程序中进行 Forking 可能是危险的
最佳实践
- 始终等待: 使用 os.wait 或 os.waitpid 清理子进程
- 处理错误: Forking 时检查 OSError
- 使用 _exit: 在子进程中,使用 os._exit 而不是 sys.exit
- 限制 forking: 对于复杂的情况,请考虑使用 multiprocessing 模块
- 信号安全: 在 fork 进程中小心信号
资料来源
作者
列出所有 Python 教程。