一、进程的创建
1、fork 函数初识
在 Linux 中 fork 函数是非常重要的函数,它从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程。
进程调用 fork,当控制转移到内核中的 fork 代码后,内核做:
- 分配新的内存块和内核数据结构给子进程
- 将父进程部分数据结构内容拷贝至子进程,页表代码和数据可以是完全一样的
- 添加子进程到系统进程列表当中
fork 返回,开始调度器调度
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main() {
printf("pid:%d Before!\n", getpid());
fork();
printf("pid:%d After!\n", getpid());
return 0;
}
fork 之前父进程独立执行,fork 之后,父子两个执行流分别执行。
注意 fork 之后,谁先执行完完全由调度器决定。
2、fork 函数返回值
子进程返回 0,父进程返回的是子进程的 pid。
3、写时拷贝
父子代码共享,父子再不写入时,数据也是共享的,当任意一方试图写入,便以写时拷贝的方式各自一份副本。
写时拷贝本质是写的时候再用,是一种延时申请,按需申请。
无论是父进程还是子进程,如果想要写入,会将父进程中可写的部分改成只读,子进程继承时也是只读状态,暂时是只读状态。针对这种情况,操作系统不做异常处理,如果想要写入数据,会将页表对应的区域重新映射,然后进行写时拷贝,这样就能访问原来可写的区域。
4、创建多个进程
#include <unistd.h>
#include <stdlib.h>
#define N 5
void RunChild() {
int cnt = 5;
while(cnt) {
printf("I am child:%d,ppid:%d\n", getpid(), getppid());
sleep(1);
cnt--;
}
}
int main() {
for(int i = 0; i < N; i++) {
pid_t id = fork();
if(id == 0) {
RunChild();
exit(0);
}
}
sleep(1000);
return 0;
}
5 次循环结束后,父进程没有结束,子进程终止成为僵尸进程。父子进程谁先运行由调度器决定。
二、进程终止
1、进程退出场景
- 代码运行完毕,结果正确
- 代码运行完毕,结果不正确
- 代码异常终止
结果是否正确采用进程的退出码来进行判定。
2、进程常见退出方法
成功只有 1 种可能,但失败有多个理由。
(1)正常终止(可以通过 echo $? 查看进程退出码):
echo $?
表示最近一次进程退出时的退出码。可以通过观察退出码来判断进程是否正常结束。
- 从 main 返回
在 C 语言中,程序返回 0 中的 0 表示进程的退出码,表征程序的运行结果是否正确,0->success。main 函数的返回值的本质表示进程运行完成时是否是正确的结果,如果不是,可以使用不同的数字表示不同的出错原因。
进程中断父进程会关心程序的运行情况,用户可以根据错误码来找出程序中的错误。
可以改变 return 的返回值
第二次调用 echo $? 返回值成为 0。当第 2 次输出时,程序变成了 echo 命令,echo 上次执行是正确的,所以退出码为 0。
strerror
将错误码转换成错误码描述。
示例 1
系统提供的错误码和错误码描述是有对应关系的。错误码用来表征错误原因,错误码描述展现更详细的错误信息。
示例 2
可以自己定义错误码。
errno
最近一次的错误码。
示例
3、代码异常
本质可能就是代码没有跑完。进程的退出码无意义,不关心退出码了。进程出现了异常,本质是进程收到了对应的信号。
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
int main() {
int* p = NULL;
*p = 100;
return 0;
}
访问野指针,进程抛出异常显示段错误,对应第 11 号。
验证
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
int main() {
while(1) {
printf("Hello world,pid:%d\n", getpid());
sleep(1);
}
return 0;
}
第 11 个信号是进程出现了段错误。
4、exit 和 return 的区别
return
exit
exit 在任意地方被调用,都表示调用进程直接退出,return 只表示当前函数返回,没有退出进程。
5、_exit
终止进程。
6、exit 和 _exit 的区别
exit
_exit
exit 在结束之后还做了以下工作:
- 执行用户通过 atexit 或 on_exit 定义的清理函数
- 关闭所有打开的流,所有的缓存数据均被写入
- 调用 _exit
exit 是库函数,_exit 是系统调用。 printf 函数先把数据写入缓冲区中,合适的时候进行刷新。这个缓冲区绝对不在内核中。如果缓冲区在内核中,exit 和 _exit 都会刷新,但实际 _exit 没有刷新缓冲区。
三、进程等待
1、必要性
- 子进程退出,父进程如果不管不顾,就可能造成'僵尸进程'的问题,进而造成内存泄漏。
- 僵尸进程无法被杀死,需要通过进程等待来杀掉它,进而解决内存泄漏问题。
- 需要得到子进程的退出情况,即知道布置给子进程的任务子进程的任务完成的怎么样,是可以选择的。
2、定义
通过系统调用 wait/waitpid,来进行对子进程进行状态检测与回收的功能。
3、回收
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
int main() {
pid_t id = fork();
if(id < 0) {
perror("fork");
return 1;
} else if(id == 0) {
int cnt = 5;
while(cnt) {
printf("I am child,pid:%d,ppid:%d,cnt:%d\n", getpid(), getppid(), cnt);
cnt--;
sleep(1);
}
exit(0);
} else {
while(1) {
printf("I am father,pid:%d,ppid:%d\n", getpid(), getppid());
sleep(1);
}
}
return 0;
}
子进程退出后一直成为僵尸状态。父进程通过调用 wait/waitpid 来进行僵尸进程回收问题。
4、wait
wait 是系统调用接口,等待进程直到进程的状态发生改变。
1 个进程
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <sys/types.h>
int main() {
pid_t id = fork();
if(id < 0) {
perror("fork");
return 1;
} else if(id == 0) {
int cnt = 5;
while(cnt) {
printf("I am child,pid:%d,ppid:%d,cnt:%d\n", getpid(), getppid(), cnt);
cnt--;
sleep(1);
}
exit(0);
} else {
int cnt = 10;
while(cnt) {
printf("I am father,pid:%d,ppid:%d,cnt:%d\n", getpid(), getppid(), cnt);
cnt--;
sleep(1);
}
pid_t ret = wait(NULL);
if(ret == id) {
printf("wait success,ret:%d\n", ret);
}
}
return 0;
}
当父进程的循环结束之后,子进程被回收。 在子进程成为僵尸状态以后,父进程等待是必须的。wait 等待任意一个子进程退出。
多个进程
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <sys/types.h>
#define N 10
void RunChild() {
int cnt = 5;
while(cnt) {
printf("I am child,pid:%d,ppid:%d\n", getpid(), getppid());
sleep(1);
cnt--;
}
}
int main() {
for(int i = 0; i < N; i++) {
pid_t id = fork();
if(id == 0) {
RunChild();
exit(0);
}
printf("create child process:%d success\n", id);
}
sleep(10);
for(int i = 0; i < N; i++) {
pid_t id = wait(NULL);
if(id > 0) {
printf("wait %d success\n", id);
}
}
sleep(5);
return 0;
}
wait 当任意一个子进程退出的时候,wait 回收子进程。 如果任意一个子进程不退出,父进程默认在 wait 的时候,调用这个系统调用的时候,也就不返回,默认叫做阻塞状态。
5、waitpid
当 waitpid 回收子进程时返回的是子进程的 pid。
status
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <sys/types.h>
int main() {
pid_t id = fork();
if(id < 0) {
perror("fork");
return 1;
} else if(id == 0) {
int cnt = 5;
while(cnt) {
printf("I am child,pid:%d,ppid:%d,cnt:%d\n", getpid(), getppid(), cnt);
cnt--;
sleep(1);
}
exit(1);
} else {
int cnt = 10;
while(cnt) {
printf("I am father,pid:%d,ppid:%d,cnt:%d\n", getpid(), getppid(), cnt);
cnt--;
sleep(1);
}
int status = 0;
pid_t ret = waitpid(id, &status, 0);
if(ret == id) {
printf("wait success,ret:%d,status:%d\n", ret, status);
}
}
return 0;
}
父进程等待,期望获得子进程的代码是否异常,如果没有异常,结果对吗,不对是因为什么。
int 类型总共有 32 个比特位,目前只考虑低 16 位。
上面代码中,status 是 256 的原因是因为子进程的 exit 为 1 即退出码为 1,00000000 00000000 00000001 00000000,化为十进制就是 2^8 为 256。
如果低 7 位是否为 0,如果为 0 则进程没有收到信号,则代码没有异常。
上面的代码中如果 status 为全局变量,因为父子进程具有独立性,父进程无法得到子进程的数据。父进程要拿子进程的状态数据,只能通过 wait 等系统调用来得到子进程的代码和数据。
验证
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <sys/types.h>
int main() {
pid_t id = fork();
if(id < 0) {
perror("fork");
return 1;
} else if(id == 0) {
int cnt = 5;
while(cnt) {
printf("I am child,pid:%d,ppid:%d,cnt:%d\n", getpid(), getppid(), cnt);
cnt--;
sleep(1);
}
exit(11);
} else {
int cnt = 10;
while(cnt) {
printf("I am father,pid:%d,ppid:%d,cnt:%d\n", getpid(), getppid(), cnt);
cnt--;
sleep(1);
}
int status = 0;
pid_t ret = waitpid(id, &status, 0);
if(ret == id) {
printf("wait success,ret:%d,exit sig:%d,exit code:%d\n", ret, status&0x7F, (status>>8)&0xFF);
}
}
return 0;
}
6、原理
waitpid 是操作系统提供的接口,子进程退出时会将接收的信号以及 main 函数的返回值返回到 status 中,父进程通过 waitpid 得到子进程的相关信息来回收子进程。 父进程在等待时只能等待自己的子进程,不能等待其余进程,否则会等待失败。
7、WIFEXITED 和 WEXITSTATUS
可以使用 WIFEXITED 和 WEXITSTATUS 来检测进程是否正常退出。
1 个进程
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <sys/types.h>
int main() {
pid_t id = fork();
if(id < 0) {
perror("fork");
return 1;
} else if(id == 0) {
int cnt = 5;
while(cnt) {
printf("I am child,pid:%d,ppid:%d,cnt:%d\n", getpid(), getppid(), cnt);
cnt--;
sleep(1);
}
exit(11);
} else {
int cnt = 10;
while(cnt) {
printf("I am father,pid:%d,ppid:%d,cnt:%d\n", getpid(), getppid(), cnt);
cnt--;
sleep(1);
}
int status = 0;
pid_t ret = waitpid(id, &status, 0);
if(ret == id) {
if(WIFEXITED(status)) {
printf("process success,code exit:%d\n", WEXITSTATUS(status));
} else {
printf("process fail\n");
}
}
}
return 0;
}
多个进程
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <sys/types.h>
#define N 10
void RunChild() {
int cnt = 5;
while(cnt) {
printf("I am child,pid:%d,ppid:%d\n", getpid(), getppid());
sleep(1);
cnt--;
}
}
int main() {
for(int i = 0; i < N; i++) {
pid_t id = fork();
if(id == 0) {
RunChild();
exit(i);
}
printf("create child process:%d success\n", id);
}
sleep(10);
for(int i = 0; i < N; i++) {
int status = 0;
pid_t id = waitpid(-1, &status, 0);
if(id > 0) {
printf("wait %d success,exit code:%d\n", id, WEXITSTATUS(status));
}
}
sleep(5);
return 0;
}
多个子进程被父进程回收。
Linux 的进程也是一棵多叉树结构,父进程只对直系的子进程直接负责。
8、options
阻塞方式,当 options 为 0 的时候为阻塞方式。waitpid 会导致父进程进入阻塞状态。
(1)WNOHANG
在等待过程中采用非阻塞等待。
(2)非阻塞轮询
非阻塞轮询是一种在程序中定期检查某个状态或资源是否就绪的机制,其核心特点是不会阻塞当前程序的执行流程。 每次检查操作不会'卡住'程序,如果目标未就绪,检查会立即返回,允许程序继续执行其他任务,而不是一直等待到目标就绪。
(3)代码演示
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <sys/types.h>
int main() {
pid_t id = fork();
if(id < 0) {
perror("fork");
return 1;
} else if(id == 0) {
int cnt = 5;
while(cnt) {
printf("I am child,pid:%d,ppid:%d,cnt:%d\n", getpid(), getppid(), cnt);
cnt--;
sleep(1);
}
exit(11);
} else {
int status = 0;
while(1) {
pid_t ret = waitpid(id, &status, WNOHANG);
if(ret > 0) {
if(WIFEXITED(status)) {
printf("process success,code exit:%d\n", WEXITSTATUS(status));
} else {
printf("process fail\n");
}
break;
} else if(ret < 0) {
printf("wait fail\n");
break;
} else {
printf("子进程还没有退出,再等等...\n");
}
sleep(1);
}
}
sleep(3);
return 0;
}
注意 在 while 循环中必须添加 sleep(1) 这一语句。原因是,若缺少这个睡眠操作,程序会进入不间断的轮询状态,持续不断地查询子进程是否退出。这种高频次的查询会导致 CPU 资源被大量占用,进而可能造成程序运行出现卡顿现象。
通过进程等待可以保证父进程是多进程当中最后一个退出的进程。 父进程可以在等待子进程返回时做一些简单级的任务。但是父进程的核心是等待子进程返回,即延迟回收子进程,统一回收子进程。
四、总结
本文带你掌握了 fork 原理、写时拷贝、进程终止方式,以及 wait/waitpid 回收僵尸进程的方法 - - - 这些是 Linux 系统编程的基础,为后续进程通信、线程管理铺路。建议多实操修改代码加深理解。


