1. fork() 是什么?

fork() 是一个在类Unix操作系统(包括Linux)中创建新进程的系统调用。它的独特之处在于:调用一次,返回两次

  • 它通过复制调用进程(称为父进程)来创建一个新的进程(称为子进程)。

  • 子进程几乎是父进程的完美副本。它会获得父进程代码段、数据段、堆、栈以及文件描述符表等的副本。

2. fork() 的工作流程和返回值

理解 fork() 的关键在于理解它的返回值:

  • 在父进程中fork() 返回新创建的子进程的进程ID(PID)(一个大于0的数字)。

  • 在子进程中fork() 返回 0

  • 如果出错(例如系统进程数达到上限):fork() 返回 -1

这个返回值是区分父进程和子进程执行流的唯一方法。

3. 一个简单示例

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

int main() {
    pid_t pid; // pid_t 是专门用于表示进程ID的数据类型

    printf("Before fork: [PID=%d]\n", getpid());

    // 调用 fork()
    pid = fork();

    // 从这里开始,代码会被两个进程执行
    if (pid < 0) {
        // 错误处理
        fprintf(stderr, "Fork failed!\n");
        return 1;
    } else if (pid == 0) {
        // 这是子进程的代码块
        printf("I am the CHILD process: [PID=%d], my parent's PID is [PPID=%d]\n", getpid(), getppid());
    } else {
        // 这是父进程的代码块
        printf("I am the PARENT process: [PID=%d], my child's PID is [%d]\n", getpid(), pid);
    }

    // 这个printf语句两个进程都会执行
    printf("This line is printed by both processes. [PID=%d]\n", getpid());

    return 0;
}

可能的输出结果:

Before fork: [PID=1234]
I am the PARENT process: [PID=1234], my child's PID is [1235]
This line is printed by both processes. [PID=1234]
I am the CHILD process: [PID=1235], my parent's PID is [PPID=1234]
This line is printed by both processes. [PID=1235]

4. fork() 的关键特性

  1. 写时复制(Copy-On-Write, COW)

    • 早期的Unix实现中,fork() 会立即复制父进程的整个地址空间,这通常很慢且低效。

    • 现代操作系统(如Linux)使用了“写时复制”技术。fork() 之后,父子进程共享相同的物理内存页。

    • 只有当其中一个进程尝试修改某个内存页时,操作系统才会为该进程创建一个该页的副本。这大大提高了 fork() 的效率,因为很多情况下子进程会立即调用 exec() 来执行新程序,从而避免了不必要的复制。

  2. 继承的文件描述符

    • 子进程会获得父进程所有打开的文件描述符的副本

    • 这意味着父子进程可以同时对同一个文件进行读写操作。标准输入(0)、标准输出(1)、标准错误(2)也被继承。

    • 这是一个非常强大的特性,常用于实现管道(pipe)和I/O重定向。

  3. 并发执行

    • fork() 之后,父进程和子进程的执行顺序是不确定的,由操作系统的进程调度器决定。

    • 如果你需要控制它们的执行顺序,需要使用进程间通信(IPC)机制,如信号(signal)、管道(pipe)、信号量(semaphore)等。

5. fork() 的典型用途

  1. 创建新进程

    • 这是最直接的用途,例如在Shell中,你每输入一个命令,Shell就会 fork() 一个子进程来执行它。

  2. 进程池

    • 服务器程序(如Web服务器)在启动时可能会 fork() 出一组子进程(进程池)来并发处理多个客户端请求。

  3. 协同工作

    • 父子进程可以通过进程间通信(IPC)协同完成一项复杂的任务。

6. fork() 与 exec() 的组合

这是Unix/Linux编程中最常见的模式之一:

  1. 首先,程序调用 fork() 创建一个自身的副本(子进程)。

  2. 然后,在子进程中,立即调用 exec() 系列函数(如 execlpexecvp)来加载并执行一个全新的程序exec() 会用新程序的代码和数据替换掉当前子进程的地址空间。

  3. 父进程则可以继续执行原有代码,或者等待子进程结束(使用 wait() 或 waitpid())。

示例:模拟 Shell 执行 ls -l

#include <unistd.h>
#include <sys/wait.h>
#include <stdio.h>

int main() {
    pid_t pid = fork();

    if (pid == 0) {
        // 子进程:执行 ls -l
        execlp("/bin/ls", "ls", "-l", NULL); // 如果成功,这行代码之后的都不会执行

        // 只有当 execlp 失败时,才会执行到这里
        perror("execlp failed");
        return 1;
    } else if (pid > 0) {
        // 父进程:等待子进程结束
        int status;
        waitpid(pid, &status, 0); // 等待特定的子进程
        printf("Child process finished.\n");
    } else {
        // fork 失败
        perror("fork failed");
        return 1;
    }
    return 0;
}

总结

特性 描述
目的 创建一个新的进程。
机制 通过复制调用进程(父进程)来实现。
返回值 父进程中返回子进程PID,子进程中返回0,出错返回-1。
核心技术 写时复制(Copy-On-Write),极大提升效率。
共享资源 继承父进程的代码、数据、堆栈、环境、打开的文件描述符等。
主要用途 创建新进程、实现进程池、与 exec() 配合运行新程序。

fork() 是理解Unix/Linux多任务编程的基石,虽然概念简单,但其与后续的 exec()wait(), 信号、IPC等机制的配合,构成了强大的进程管理能力。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐