在 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()
是最常用的创建子进程的方式。它会复制当前进程(父进程)的地址空间、堆栈、寄存器状态等,生成一个几乎完全相同的子进程。
#include
pid_t fork(void);
#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;
}
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()
是 fork()
的一个变种,用于更高效的子进程创建,尤其适用于立即调用 exec()
执行新程序的情况。
它的关键特性是:
#include
pid_t vfork(void);
#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;
}
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() |
vfork() |
---|---|---|
地址空间复制 | 完全复制父进程地址空间 | 不复制,共享父进程内存 |
性能开销 | 较大(复制页表等) | 极小(仅创建轻量进程) |
是否并发执行 | 是 | 否,子进程先执行 |
使用场景 | 需要独立地址空间时(如长期运行) | 立即调用 exec() 执行新程序 |
返回后是否继续执行 | 是 | 子进程必须调用 _exit() 或 exec() |
可否修改数据 | 安全(写时复制机制) | 危险,会影响父进程数据 |
现代 Linux 内核在实现 fork()
时采用了 写时复制(Copy-on-Write, COW) 技术,使得 fork()
的性能大大提升。
这使得 fork()
在大多数情况下性能接近于 vfork()
。
当子进程结束但父进程尚未调用 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()
来回收子进程资源。
如果父进程在子进程之前结束,则子进程成为孤儿进程,由 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 进程接管
fork()
创建多个子进程,并让它们打印各自的 PID。wait()
正确回收所有子进程。vfork()
替换 fork()
,观察行为差异。ps
查看并分析如何避免。strace
跟踪 fork()
和 vfork()
的系统调用行为。strace -f ./your_program