ZetCode

Python os.fork 函数

上次修改时间:2025 年 4 月 11 日

这份全面的指南探讨了 Python 的 os.fork 函数,该函数在类 Unix 系统中创建子进程。我们将介绍进程创建、父子关系和实际的多进程示例。

基本定义

os.fork 函数通过复制调用进程来创建一个新进程。 新进程被称为子进程,原始进程被称为父进程。

它在子进程中返回 0,在父进程中返回子进程的 PID。 出错时,它会引发 OSError。 这是一个 Unix 特定的系统调用。

基本 Fork 示例

这个简单的示例演示了 os.fork 的基本用法。 它展示了相同的代码如何在父进程和子进程中运行。

basic_fork.py
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 执行不同的任务

此示例显示了父进程和子进程如何执行不同的任务。 子进程运行计算,而父进程等待它完成。

fork_tasks.py
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 之后,父进程和子进程之间如何共享文件描述符。 两者都可以读取和写入同一文件。

fork_file.py
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 之后的行为。 每个进程都获得变量的自己的副本,并且更改不会共享。

fork_globals.py
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 炸弹。 警告:运行此代码可能会导致您的系统崩溃!

fork_bomb.py
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 进程中处理信号。 父进程向子进程发送信号,子进程使用自定义处理程序对其进行处理。

fork_signals.py
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 进程组

这个高级示例演示了使用子进程创建一个新的进程组,这对于管理相关的进程组很有用。

fork_process_group.py
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 以创建一个新的进程组。 当您希望子进程独立于父进程的组时,这很有用。

进程组对于终端控制和向进程组传递信号尤其重要。

安全注意事项

最佳实践

资料来源

作者

我的名字是 Jan Bodnar,我是一位充满激情的程序员,拥有丰富的编程经验。 自 2007 年以来,我一直在撰写编程文章。 迄今为止,我撰写了超过 1,400 篇文章和 8 本电子书。 我拥有超过十年的编程教学经验。

列出所有 Python 教程