图解嵌套进程

p27


fork() 函数详解

fork() 是一个系统调用,用于创建一个新进程。调用 fork() 后,会生成一个新的子进程,该子进程是父进程的几乎完整副本。这个函数的特点包括:调用一次,返回两次:(一次是在调用进程[父进程], 一次是在新创建的子进程[子进程的 PID 总是返回 0]当中)并发执行相同但独立的地址空间,以及共享文件描述符


1. fork() 的返回值

调用 fork() 时,它会返回不同的值给父进程和子进程:

  • 返回值为 0:表示这是在子进程中运行。
  • 返回值为子进程的 PID(进程 ID):表示这是在父进程中运行。
  • 返回 -1:表示 fork() 调用失败,通常是因为系统资源不足。

2. 代码详解

代码回顾

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>

int main()
{
    pid_t pid;
    int x = 1;

    pid = fork();  // 创建一个子进程
    if(pid == 0)   // 子进程
    {
        printf("child : x=%d\n", ++x);
        exit(0);  // 子进程结束
    }
    // 父进程
    printf("parent: x=%d\n", --x);
    exit(0);  // 父进程结束
}

执行流程

  1. fork() 创建一个子进程。

    • 在子进程中,fork() 返回 0,执行子进程代码块。
    • 在父进程中,fork() 返回子进程的 PID,继续执行父进程代码块。
  2. 子进程输出:

    • x 的初始值是 1,在子进程中执行 ++x,变为 2,打印:child : x=2
  3. 父进程输出:

    • 父进程中的 x 仍然是 1(和子进程中的 x 独立),执行 --x,变为 0,打印:parent: x=0

可能的输出顺序

由于父进程和子进程是并发执行的,输出顺序无法确定,可能是:

  • child : x=2
    parent: x=0

或:

  • parent: x=0
    child : x=2

3. fork() 的关键特点

(1) 调用一次,返回两次(“返回两次”指的是 fork() 函数在父进程和子进程中分别返回不同的值)

一次在父进程中返回子进程的 PID,另一次在子进程中返回 0

  • 父进程调用 fork() 后:
    • 返回值是子进程的 PID。
    • 父进程继续执行 fork() 后的代码。
  • 子进程调用 fork() 后:
    • 返回值是 0
    • 子进程从 fork() 后的代码开始执行。

这就是“调用一次,返回两次”的含义。

(2) 并发执行

  • 父进程和子进程在 fork()并发运行(实际顺序由操作系统调度器决定)。
  • 两个进程独立运行,互不干扰,但共享 CPU 时间。

(3) 相同但独立的地址空间

  • 子进程继承了父进程的内存内容
    • 父子进程的变量(如 x)在 fork() 后会有相同的初始值
    • 但是父子进程的内存空间是独立的,在其中一方修改变量不会影响另一方。
  • 实现细节
    • 现代操作系统通常采用写时复制(Copy-on-Write, COW)机制:
      • 父子进程最初共享相同的物理内存。
      • 当其中一个进程尝试修改共享内存时,操作系统会为其分配新的内存。

(4) 共享文件描述符

  • 父子进程共享文件描述符表(指向打开的文件)。
  • 影响
    • 如果父进程和子进程操作相同的文件,文件偏移量会受到影响。
  • 示例:

      #include <stdio.h>
      #include <unistd.h>
        
      int main() {
          FILE *fp = fopen("test.txt", "w");
          if (!fp) return 1;
        
          fork();
        
          fprintf(fp, "Hello\n"); // 父子进程都会写入
          fclose(fp);
          return 0;
      }
    

    输出可能是:

      Hello
      Hello
    

4. 总结

  • 调用一次,返回两次:父子进程的返回值不同。
  • 并发执行:父子进程独立运行,实际顺序由调度决定。
  • 相同但独立的地址空间:父子进程初始内存相同,但后续修改互不影响。
  • 共享文件描述符:父子进程共享打开的文件,可能影响文件操作的结果。

通过 fork(),Linux 实现了多进程的创建与管理,成为并发编程的重要工具。