Linux 进程管理:创建、终止与回收全流程解析
Linux 进程管理的核心流程,涵盖进程创建(fork 函数原理及返回值)、写时拷贝机制、进程终止场景与方法(exit/_exit/return 区别)、以及进程等待(wait/waitpid 解决僵尸进程)。通过代码示例和原理分析,帮助读者理解父子进程关系、状态检测及资源回收,掌握系统编程基础。

Linux 进程管理的核心流程,涵盖进程创建(fork 函数原理及返回值)、写时拷贝机制、进程终止场景与方法(exit/_exit/return 区别)、以及进程等待(wait/waitpid 解决僵尸进程)。通过代码示例和原理分析,帮助读者理解父子进程关系、状态检测及资源回收,掌握系统编程基础。

在 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 之后,谁先执行完完全由调度器决定。
子进程返回 0,父进程返回的是子进程的 pid。
父子代码共享,父子再不写入时,数据也是共享的,当任意一方试图写入,便以写时拷贝的方式各自一份副本。
写时拷贝本质是写的时候再用,是一种延时申请,按需申请。
无论是父进程还是子进程,如果想要写入,会将父进程中可写的部分改成只读,子进程继承时也是只读状态,暂时是只读状态。针对这种情况,操作系统不做异常处理,如果想要写入数据,会将页表对应的区域重新映射,然后进行写时拷贝,这样就能访问原来可写的区域。
#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 种可能,但失败有多个理由。
(1)正常终止(可以通过 echo $? 查看进程退出码):
echo $?
表示最近一次进程退出时的退出码。可以通过观察退出码来判断进程是否正常结束。
在 C 语言中,程序返回 0 中的 0 表示进程的退出码,表征程序的运行结果是否正确,0->success。main 函数的返回值的本质表示进程运行完成时是否是正确的结果,如果不是,可以使用不同的数字表示不同的出错原因。
进程中断父进程会关心程序的运行情况,用户可以根据错误码来找出程序中的错误。
可以改变 return 的返回值
第二次调用 echo $? 返回值成为 0。当第 2 次输出时,程序变成了 echo 命令,echo 上次执行是正确的,所以退出码为 0。
strerror
将错误码转换成错误码描述。
示例 1
系统提供的错误码和错误码描述是有对应关系的。错误码用来表征错误原因,错误码描述展现更详细的错误信息。
示例 2
可以自己定义错误码。
errno
最近一次的错误码。
示例
本质可能就是代码没有跑完。进程的退出码无意义,不关心退出码了。进程出现了异常,本质是进程收到了对应的信号。
#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 个信号是进程出现了段错误。
return
exit
exit 在任意地方被调用,都表示调用进程直接退出,return 只表示当前函数返回,没有退出进程。
终止进程。
exit
_exit
exit 在结束之后还做了以下工作:
exit 是库函数,_exit 是系统调用。 printf 函数先把数据写入缓冲区中,合适的时候进行刷新。这个缓冲区绝对不在内核中。如果缓冲区在内核中,exit 和 _exit 都会刷新,但实际 _exit 没有刷新缓冲区。
通过系统调用 wait/waitpid,来进行对子进程进行状态检测与回收的功能。
#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 来进行僵尸进程回收问题。
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 的时候,调用这个系统调用的时候,也就不返回,默认叫做阻塞状态。
当 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)&);
}
}
;
}
waitpid 是操作系统提供的接口,子进程退出时会将接收的信号以及 main 函数的返回值返回到 status 中,父进程通过 waitpid 得到子进程的相关信息来回收子进程。 父进程在等待时只能等待自己的子进程,不能等待其余进程,否则会等待失败。
可以使用 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 {
();
}
}
}
;
}
多个进程
#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 ;
}
多个子进程被父进程回收。
Linux 的进程也是一棵多叉树结构,父进程只对直系的子进程直接负责。
阻塞方式,当 options 为 0 的时候为阻塞方式。waitpid 会导致父进程进入阻塞状态。
在等待过程中采用非阻塞等待。
非阻塞轮询是一种在程序中定期检查某个状态或资源是否就绪的机制,其核心特点是不会阻塞当前程序的执行流程。 每次检查操作不会'卡住'程序,如果目标未就绪,检查会立即返回,允许程序继续执行其他任务,而不是一直等待到目标就绪。
#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 (ret < ) {
();
;
} {
();
}
sleep();
}
}
sleep();
;
}
注意 在 while 循环中必须添加 sleep(1) 这一语句。原因是,若缺少这个睡眠操作,程序会进入不间断的轮询状态,持续不断地查询子进程是否退出。这种高频次的查询会导致 CPU 资源被大量占用,进而可能造成程序运行出现卡顿现象。
通过进程等待可以保证父进程是多进程当中最后一个退出的进程。 父进程可以在等待子进程返回时做一些简单级的任务。但是父进程的核心是等待子进程返回,即延迟回收子进程,统一回收子进程。
本文带你掌握了 fork 原理、写时拷贝、进程终止方式,以及 wait/waitpid 回收僵尸进程的方法 - - - 这些是 Linux 系统编程的基础,为后续进程通信、线程管理铺路。建议多实操修改代码加深理解。

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online
将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online