授课语音

使用 fork 系统调用创建进程


1. fork 系统调用概述

fork 是一个系统调用,用于在 Unix-like 操作系统中创建一个新进程。它会复制当前进程(父进程),并返回两次:一次在父进程中,返回新创建子进程的进程ID;另一次在子进程中,返回0。父子进程在此后并行运行,互相独立。

1.1 fork 的基本原理

fork 系统调用会创建一个与父进程几乎完全相同的子进程。子进程拥有与父进程相同的代码、数据、堆栈和打开的文件描述符,但它们拥有独立的进程ID、内存空间和进程表项。

  • 父进程:调用 fork 的进程,返回值为子进程的进程ID。
  • 子进程:由 fork 创建的新进程,返回值为0。

2. fork 的行为

2.1 父进程与子进程的区分

fork 被调用时,操作系统会进行以下操作:

  • 复制父进程的内存空间。
  • 创建一个新的进程ID(PID)给子进程。
  • 子进程继承父进程的所有资源,如打开的文件描述符、信号处理方式等。

2.2 返回值

  • 在父进程中,fork 返回子进程的进程ID(大于0)。
  • 在子进程中,fork 返回0。

2.3 父子进程的独立性

父子进程在执行过程中是独立的。它们可以并发执行,并且有各自独立的进程控制块(PCB)。修改子进程的数据不会影响父进程的数据,反之亦然。


3. 使用 fork 创建进程

3.1 基本语法

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

int main() {
    pid_t pid = fork();  // 创建子进程

    if (pid < 0) {
        // 错误处理:fork失败
        perror("Fork failed");
        return 1;
    } else if (pid == 0) {
        // 子进程的代码
        printf("This is the child process. PID: %d\n", getpid());
    } else {
        // 父进程的代码
        printf("This is the parent process. PID: %d, Child PID: %d\n", getpid(), pid);
    }

    return 0;
}

3.2 代码解析

  • fork():创建一个新进程,并返回进程ID。父进程中返回子进程的ID,子进程中返回0。
  • getpid():返回当前进程的进程ID。
  • pid_t pidpidfork 函数的返回值类型,表示进程ID。

3.3 输出示例

This is the parent process. PID: 1234, Child PID: 1235
This is the child process. PID: 1235
  • 父进程和子进程的 PID 会不同。
  • 输出的顺序可能不同,因为父子进程是并发执行的。

4. fork 的常见问题

4.1 子进程的资源继承

  • 文件描述符:父进程打开的文件描述符会被子进程继承,但文件描述符指向的文件是父子进程共享的,文件的读写操作会影响父子进程。
  • 环境变量:子进程会继承父进程的环境变量,但它可以修改自己的环境变量。
  • 内存空间:父子进程有独立的地址空间,修改一个进程的内存不会影响另一个进程。

4.2 僵尸进程(Zombie Process)

当子进程执行完毕并退出时,它会将退出状态传给父进程。如果父进程未调用 waitwaitpid 函数来收集子进程的退出状态,子进程将会变为“僵尸进程”,直到父进程清理它为止。

4.3 孤儿进程(Orphan Process)

如果父进程在子进程执行之前退出,子进程会被 init 进程(进程ID为1)收养。孤儿进程的父进程是 init


5. fork 的进阶用法:exec 系列函数

子进程创建后,通常会使用 exec 系列函数来加载新的程序,从而实现程序的替换。forkexec 常常一起使用:

  • exec 系列函数:用来加载一个新的程序,替换当前进程的内存空间。
  • 常见的 exec 函数execlexecpexecv 等。

5.1 示例:使用 forkexec 执行新的程序

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

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

    if (pid < 0) {
        perror("Fork failed");
        return 1;
    } else if (pid == 0) {
        // 子进程执行新的程序
        execlp("/bin/ls", "ls", "-l", NULL);
        // 如果 execlp 调用成功,这一行代码不会被执行
        perror("Exec failed");
    } else {
        // 父进程代码
        printf("This is the parent process. PID: %d\n", getpid());
    }

    return 0;
}

5.2 代码解析

  • 在子进程中,execlp 会替换当前进程的代码并执行 /bin/ls 程序,输出目录列表。
  • 如果 execlp 调用失败,会打印错误信息。

6. 总结

  • fork 是 Unix-like 操作系统中创建新进程的核心系统调用。
  • 父进程和子进程通过返回值来区分,父进程返回子进程的进程ID,子进程返回0。
  • 父子进程之间独立执行,但它们继承了很多资源,如文件描述符、环境变量等。
  • 在使用 fork 时,要注意管理僵尸进程和孤儿进程。
  • forkexec 常常结合使用,通过 exec 系列函数实现进程的替换。
去1:1私密咨询

系列课程: