一、fork初识
fork 知识点总结(极简版)
- 作用
-
创建一个新的子进程
-
调用一次,分裂成两个进程(父 + 子)
- 返回值规则(最重要)
-
父进程:返回 子进程 PID(>0)
-
子进程:返回 0
-
创建失败:返回 -1
- 执行特点
-
父子进程代码相同,从 fork 之后同时往下执行
-
谁先运行不确定(由系统调度决定)
-
子进程会复制父进程的地址空间(数据、栈、堆等)
-
现代系统用写时复制(Copy-on-Write),不立刻全复制
-
常见考点
-
fork 之后父子谁先跑不一定
-
子进程会继承:文件描述符、当前目录、信号处理方式等
-
子进程有自己的:PID、PPID、计时器
-
父子退出互不影响,但父进程不 wait 可能产生僵尸进程
-
一句话记忆
父返子PID,子返0,调用一次,两个进程。
二、写时拷贝
- 是什么
fork 创建子进程时,不立刻复制整个内存,而是让父子共享同一份物理内存,只有当要修改数据时,才真正复制一页。
- 为什么要用
-
传统 fork:直接复制全部数据,慢、浪费内存
-
COW:推迟复制、减少开销、提高效率
-
核心机制
-
fork 后,父子虚拟地址不同,但指向同一块物理内存
-
内存页设为 只读
-
谁要写,就触发缺页异常
-
操作系统这时才真正复制那一页,再让进程修改
-
特点
-
读时共享,写时复制
-
只复制要修改的页,不复制全部
-
让 fork 变得更快、更省内存
-
是 Linux 实现 fork 的标准方式
- 一句话记忆
fork 不复制,共享只读;谁写谁复制,一页一复制。
三、进程终止
进程终止会有几种情况呢?
(1)代码跑完,结果正确。
(2)代码跑完,结果不正确。
(3)代码没跑完,进程异常了。
return 0其实是进程的退出码。
进程终止的本质是释放系统资源,就是释放进程申请的相关内核数据结构和对应的数据和代码。
进程退出状态
- 进程退出的两种情况
- 正常退出:代码正常跑完,执行 return 0 或 exit(0) 。
- 此时 signumber = 0 (表示没有被信号终止)
- 退出码 exit_code 有意义(通常是 return 的值)
- 异常终止:进程在运行中收到信号(如 SIGKILL 、 SIGSEGV )被强制终止。
- 此时 signumber ≠ 0 (表示被信号终止)
- return 语句根本没机会执行,因此退出码没有意义
- 进程状态的表示
进程执行的最终结果,可以用两个整数完整描述:
- int sig :终止信号编号( 0 表示正常退出)
- int exit_code :退出码(仅在 sig = 0 时有意义)
在 Linux 中,父进程通过 wait() 或 waitpid() 获取的 status 变量,就是这两个信息的组合体,需要用宏(如 WIFEXITED , WEXITSTATUS , WIFSIGNALED , WTERMSIG )来解析。
- 关键结论
- return 没被执行 → 进程是被信号终止的,退出码无意义。
- signumber = 0 → 进程正常结束,退出码有效。
- signumber ≠ 0 → 进程被信号杀死,退出码无效。
两种正常的退出方式
1. 两种正常退出方式(不考虑信号等异常)
- 在 main 函数中使用 return
-
这是最标准的方式。 main 函数的返回值会作为进程的退出码(exit code)。
-
例如 return 0; 表示程序成功执行。
- 在程序的任意位置调用 exit() 函数
-
这个函数会直接终止整个进程,无论它在哪个函数里被调用。
-
它的参数就是进程的退出码,例如 exit(EXIT_SUCCESS); 。
**2. 关键区别(面试常考)
- return 在非 main 函数中:只会结束当前函数,进程继续运行。
- exit() 在任何函数中:都会直接结束整个进程。
- main 函数的 return :等价于调用 exit() ,会终止进程。**
- 与进程状态的关系
-
无论是 return 还是 exit() ,进程都是正常退出,此时 signumber = 0 ,退出码( exit_code )有效。
-
如果进程是被信号(如 SIGKILL )终止的,那么 return 和 exit() 都不会执行, signumber ≠ 0 ,退出码也无意义。
非main函数,return ,函数结束。非main函数,exit,进程结束。
四、进程等待
一、作用(为什么要用)
-
让父进程等待子进程退出
-
回收子进程的PCB资源,避免僵尸进程
-
获取子进程的退出状态
二、wait() 函数
头文件
#include <sys/wait.h>
函数原型
pid_t wait(int *wstatus);
功能
-
阻塞等待任意一个子进程退出
-
子进程退出后,回收它
-
把退出状态写到 wstatus 指向的位置
返回值
-
成功:返回退出的子进程 PID
-
失败:返回 -1(比如没有子进程)
特点
-
阻塞:不等到子进程退出就不返回
-
等待任意子进程
-
无法指定等待某个子进程
三、waitpid() 函数(更强大)
函数原型
pid_t waitpid(pid_t pid, int *wstatus, int options);
参数
- pid
-
pid > 0 :等待PID 等于 pid 的子进程
-
pid = -1 :等待任意子进程(和 wait 一样)
-
pid = 0 :等待同组任意子进程
-
pid < -1 :等待组 ID 等于 |pid| 的任意子进程
- wstatus
输出型参数,存退出状态,同 wait()。
- options
-
0 :阻塞等待
-
WNOHANG :非阻塞等待(有就收,没有立刻返回)
返回值
-
0:返回退出的子进程 PID
-
=0:非阻塞模式下,子进程还没退出
-
-1:出错
四、两个宏(必背!)
用来从 wstatus 里取出子进程信息:
- WIFEXITED(status)
- 子进程正常退出返回真
- WEXITSTATUS(status)
- 取出子进程的退出码(0~255)
示例:
if(WIFEXITED(status)){
printf("退出码:%d\n", WEXITSTATUS(status));
}
五、核心区别
-
wait 只能阻塞、等任意子进程
-
waitpid 可以:
-
指定等待某个子进程
-
选择阻塞 / 非阻塞
-
功能更强,是wait 的升级版
实例:
pid_t pid = fork();
if(pid == 0){
// 子进程
exit(123);
} else {
int status;
// 父进程等待
waitpid(pid, &status, 0);
// 或者 wait(&status);
}
如果父进程wait子进程,但是子进程就是没有退出,则父进程会阻塞再wait函数中。