Linux进程管理:fork与vfork深度解析

在 Linux 系统编程中,进程的创建是并发和多任务处理的基础fork()vfork() 是两个用于创建新进程的核心系统调用。


目录

一、fork() 系统调用

1. 功能概述

2. 基本原型

3. 示例代码:演示 fork()

4. 编译运行

二、vfork() 系统调用

1. 功能概述

2. 基本原型

3. 示例代码:演示 vfork()

4. 编译运行

三、fork() vs vfork()

四、写时复制(Copy-on-Write)

工作原理:

五、僵尸进程与孤儿进程

1. 僵尸进程(Zombie Process)

示例:产生僵尸进程

解决方法:

2. 孤儿进程(Orphan Process)

示例:产生孤儿进程

六、总结知识点图解(知识树状图)

七、课后练习建议


一、fork() 系统调用

1. 功能概述

fork() 是最常用的创建子进程的方式。它会复制当前进程(父进程)的地址空间、堆栈、寄存器状态等,生成一个几乎完全相同的子进程。

  • 返回值不同 是区分父子进程的关键:
    • 在父进程中返回 子进程的 PID
    • 在子进程中返回 0
    • 若出错返回 -1

2. 基本原型

#include 

pid_t fork(void);

3. 示例代码:演示 fork()

#include 
#include 
#include 

int main() {
    pid_t pid;

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

    pid = fork();  // 创建子进程

    if (pid < 0) {
        perror("Fork failed");
        return 1;
    } else if (pid == 0) {
        // 子进程执行
        printf("Child process: PID = %d, PPID = %d\n", getpid(), getppid());
    } else {
        // 父进程执行
        printf("Parent process: PID = %d, Child PID = %d\n", getpid(), pid);
    }

    return 0;
}

4. 编译运行

gcc fork_example.c -o fork_example
./fork_example

输出示例(顺序可能不同):

Before fork: Parent process (PID = 12345)
Parent process: PID = 12345, Child PID = 12346
Child process: PID = 12346, PPID = 1

注意:由于父进程与子进程是并发执行的,所以输出顺序可能不确定。


二、vfork() 系统调用

1. 功能概述

vfork()fork() 的一个变种,用于更高效的子进程创建,尤其适用于立即调用 exec() 执行新程序的情况。

它的关键特性是:

  • 不复制父进程的地址空间
  • 共享父进程的内存空间
  • 子进程先运行,直到调用 _exit() 或 exec() 后,父进程才继续执行

2. 基本原型

#include 

pid_t vfork(void);

3. 示例代码:演示 vfork()

#include 
#include 
#include 
#include 

int main() {
    pid_t pid;

    printf("Before vfork: Parent process (PID = %d)\n", getpid());

    pid = vfork();  // 创建子进程

    if (pid < 0) {
        perror("vfork failed");
        return 1;
    } else if (pid == 0) {
        // 子进程执行
        printf("Child process: PID = %d, PPID = %d\n", getpid(), getppid());
        _exit(0);  // 必须使用 _exit(),不能使用 exit()
    } else {
        // 父进程等待子进程结束后继续
        printf("Parent process: PID = %d, Child PID = %d\n", getpid(), pid);
    }

    return 0;
}

4. 编译运行

gcc vfork_example.c -o vfork_example
./vfork_example

输出示例:

Before vfork: Parent process (PID = 12345)
Child process: PID = 12346, PPID = 1
Parent process: PID = 12345, Child PID = 12346

三、fork() vs vfork()

特性/对比项 fork() vfork()
地址空间复制 完全复制父进程地址空间 不复制,共享父进程内存
性能开销 较大(复制页表等) 极小(仅创建轻量进程)
是否并发执行 否,子进程先执行
使用场景 需要独立地址空间时(如长期运行) 立即调用 exec() 执行新程序
返回后是否继续执行 子进程必须调用 _exit() 或 exec()
可否修改数据 安全(写时复制机制) 危险,会影响父进程数据

四、写时复制(Copy-on-Write)

现代 Linux 内核在实现 fork() 时采用了 写时复制(Copy-on-Write, COW) 技术,使得 fork() 的性能大大提升。

工作原理:

  • 父子进程初始共享物理内存页。
  • 当任一进程尝试修改内存时,内核才会为该页创建副本。

这使得 fork() 在大多数情况下性能接近于 vfork()


五、僵尸进程与孤儿进程

1. 僵尸进程(Zombie Process)

当子进程结束但父进程尚未调用 wait()waitpid() 获取其退出状态时,该子进程就进入僵尸状态。

示例:产生僵尸进程
#include 
#include 

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

    if (pid == 0) {
        // 子进程执行完毕
        return 0;
    }

    sleep(60);  // 父进程休眠,不回收子进程
    return 0;
}

运行期间查看僵尸进程:

ps aux | grep defunct

解决方法:

父进程应始终调用 wait()waitpid() 来回收子进程资源。


2. 孤儿进程(Orphan Process)

如果父进程在子进程之前结束,则子进程成为孤儿进程,由 init 进程(PID=1)接管。

示例:产生孤儿进程
#include 
#include 

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

    if (pid == 0) {
        sleep(10);  // 等待父进程结束
        printf("Child process: My parent is now %d\n", getppid());
    }

    return 0;  // 父进程先退出
}

输出示例:

Child process: My parent is now 1

六、总结知识点图解(知识树状图)

进程创建(fork、vfork)
│
├── fork()
│   ├── 创建子进程,复制父进程地址空间
│   ├── 返回值:0(子)、PID(父)、-1(失败)
│   └── Copy-on-Write 提升性能
│
├── vfork()
│   ├── 不复制地址空间,共享内存
│   ├── 子进程必须调用 _exit() 或 exec()
│   └── 子进程先运行,父进程暂停
│
├── fork() vs vfork()
│   ├── 复制方式
│   ├── 并发执行
│   └── 应用场景
│
├── 僵尸进程
│   ├── 子进程结束,未被回收
│   └── 使用 wait() / waitpid() 回收
│
└── 孤儿进程
    ├── 父进程提前终止
    └── 被 init 进程接管

七、课后练习建议

  1. 编写一个程序,使用 fork() 创建多个子进程,并让它们打印各自的 PID。
  2. 修改上面的程序,确保父进程使用 wait() 正确回收所有子进程。
  3. 尝试使用 vfork() 替换 fork(),观察行为差异。
  4. 编写一个程序故意制造僵尸进程,然后使用 ps 查看并分析如何避免。
  5. 使用 strace 跟踪 fork() 和 vfork() 的系统调用行为。
strace -f ./your_program

你可能感兴趣的:(Linux系统编程,linux,运维,服务器)