当前位置: 首页 > news >正文

(七)【Linux进程的创建、终止和等待】

1 进程创建

1.1 在谈fork函数

#include <unistd.h>  // 需要的头文件// 返回值:子进程中返回0,父进程返回子进程id,出错返回-1

调用fork函数后,内核做了下面的工作:

  1. 创建了一个子进程的PCB结构体、并拷贝一份相同的进程地址空间和页表(PCB结构体中的一个指针指向该空间)
  2. 子进程和父进程起初共享代码和数据,并且页表中的虚拟地址和物理地址的映射关系是一样的,所以也指向相同的物理空间。
  3. fork返回后将子进程添加到系统的进程列表中,由调度器调用(每个进程开始自己的旅程)
  4. 一旦其中任意一方尝试修改数据,那么就会发生写时拷贝,会开辟一块新的物理内存,然后改变页表的映射关系。

1.2 写时拷贝

  通常,父子代码共享,父子再不写入时,数据也是共享的,当任意一方试图写入,便以写时拷贝的方式各自一份副本。

1.3 fork函数存在的意义

fork函数常规用法:

  1. 一个父进程希望复制自己,使父子进程同时执行不同的代码段。例如,父进程等待客户端请求,生成子进程来处理请求
  2. 一个进程要执行一个不同的程序。例如子进程从fork返回后,调用exec函数。 (进程替换)

1.4 fork调用失败的原因

  1. 系统中有太多的进程
  2. 实际用户的进程数超过了限制

2 进程终止

思考为什么main函数要返回0?返回多少的意义是什么?

  • 成功只有一种情况,但是失败可以有无数的原因,所以main函数的本质是,进程运行时是否是正确的结果,如果不是,可以用不同的数字表示不同的出错原因。

进程退出场景:

  1. 代码运行完毕,结果正确
  2. 代码运行完毕,结果不正确
  3. 代码异常终止

2.1 运行完毕结果不正确

  • 正常终止(可以通过 echo $? 查看进程退出码): $?->保存最后一次进程退出的退出码
  1. 从main返回
  2. 调用exit
  3. _exit

2.1.1 main函数返回

问题:进程中,谁会关心我的运行情况呢?

  • 当然是父进程,其实main函数本质上也是一个被别人调用的函数,所以他return的结果其实是想告诉他的父进程自己的运行情况。

2.1.2 退出码概念

  • 父进程可以通过拿到子进程的退出码,不同的退出码分别代表的是不同的原因!

问题1:为什么需要有退出码呢?遇到问题直接printf输出一下错误原因不行吗?

  • 没有人规定代码程序必须得打印!比如说该错误并不是从显示器打印而是向网络写入。

问题2:错误码适合计算机去看,但是不适合人去看,所以我们是否可以将他转换成字符串的错误信息解读?

  • 可以,strerror函数,可以帮助我们将错误码信息转变为字符串的错误信息解读,这些退出码本质上就是错误码,由系统提供。
    在这里插入图片描述

我们可以将一些错误码对应的信息打印出来:

#include <stdio.h>    
#include "string.h"    int main() 
{ int i = 0;for(i = 0;i<200;i++){printf("%d:%s\n",i,strerror(i)); }return 0;
}

在这里插入图片描述

问题3:父进程为啥要关心子进程的运行状况呢?

  • 父进程创建子进程的目的就是为了让子进程执行和自己不一样的代码流来完成某些特定的任务,父进程本身也就是一个跑腿的,因为代码是用户写的,所以真正关心的是用户,用户需要知道子进程将自己的工作完成得怎样了。

问题4:全局变量erron是什么

  • 保存最后一次执行的错误码
    在这里插入图片描述
 #include <stdio.h>    #include "string.h"    #include <stdlib.h>    int main()    {    int ret = 0;
W>    char *p = (char*)malloc(1000*1000*1000*4);if(p == NULL)    {    
E>        printf("malloc error, %d:%s\n",errno,strerror(errno));    
E>        ret = errno;    }else    {    printf("malloc success\n");  }return ret; }

  这样的写法既可以直接在进程返回前知道错误码,然后再变成错误信息打印出来,并且也可以在进程结束后让父进程知道运行的情况

2.1.3 库函数函数exit

  exit和return的区别:return和exit在main函数里是等价的,因为exit表示退出进程,而main函数恰好执行完return也会退出进程,但是return在其他函数中代表的是函数返回。

在这里插入图片描述

2.1.4 系统调用接口_exit

在这里插入图片描述
  _exit和exit的区别:一个是系统调用接口(更底层),一个是库函数,其实exit最后也会调用_exit, 但在调用exit之前,还做了其他工作:

  1. 执行用户通过 atexit或on_exit定义的清理函数。
  2. 关闭所有打开的流,所有的缓存数据均被写入(缓存被清理了)
  3. 调用_exit
#include <stdio.h>    
#include "unistd.h"    int main()    
{    printf("Hello HYQ");    sleep(1);    exit(11); 
}

上面的内容可以正常打印,但是换成_exit,就看不到打印内容。

  exit比_exit多做了一层最重要的工作就是刷新缓存,我们还可以得出另一个结论就是:缓冲区绝对不在内核区!!因为如果在内核区的话,系统调用的_exit在终止的时候也必然会把缓冲区刷新一下,因为现代操作系统不做任何浪费时间和空间的事情,所以肯定不是由内核维护缓存区,而是由用户区在维护!!(_exit压根看不到缓冲区,所以这个工作只能有exit去完成)

2.2 异常中止

  用退出码可以告诉父进程自己的执行情况,那如果是异常中止了呢??那就连运行完毕这个条件都完成不了,更别谈结果是否正确了,所以我们可以知道异常必然是最先需要被知道的!因为一旦异常了,一般代码都没跑完,即使跑完了,错误码也不能让人相信,此时退出码就没有意义了!

  打个比方:就好比我们平时考试一样,你考不好的时候大家会关心你为啥考不好,但如果你作弊了,性质就变了,即考得再好都让人觉得不可相信

所以进程结束后应该优先判断该进程是否异常了,然后才能确定退出码能不能用!

  类似除0、野指针这样的错误,最终会转化成一些硬件级别的信号来给操作系统。所以,父进程需要关心子进程为什么异常,以及发生何种异常,系统会通过信号来告诉我们的进程发生了异常!

3 进程等待

  进程等待是 Linux 中父进程通过 wait()waitpid() 系统调用等待子进程终止并回收其资源的过程。

3.1 为什么会有进程等待

  1. 避免僵尸进程(Zombie Process)

    • 当子进程终止后,内核会保留其退出状态(如返回值、终止信号等),直到父进程通过 wait() waitpid()读取。
    • 如果父进程不主动回收,子进程会一直占据进程表项(PID),形成僵尸进程,浪费系统资源
  2. 父子进程同步

    • 父进程可能需要等待子进程完成特定任务(例如计算、文件读写等)后才能继续执行。
    • 例如:Shell 执行命令时,必须等待子进程(命令)结束才能显示新的提示符。
  3. 获取子进程的退出状态

    • 父进程需要知道子进程是正常退出(通过 exit() 返回值)还是异常终止(如被信号杀死)。
    • 例如:通过 WEXITSTATUS(status) 获取子进程的退出码,或通过WIFSIGNALED(status)判断是否被信号终止。

3.2 进程等待的方法

在这里插入图片描述

3.2 wait方法

  1. wait() 的基本功能
    • 阻塞等待:父进程调用 wait() 时会暂停(阻塞),直到任意一个子进程终止。
    • 回收资源:内核会释放子进程的进程控制块(PCB),避免僵尸进程。
    • 获取状态:父进程可通过返回值拿到子进程的退出状态(如退出码、终止信号)。

3.2.1 函数原型

#include <sys/types.h>    // 头文件有两个
#include <sys/wait.h>pid_t wait(int *status);

返回值:

  • 成功:返回被终止的子进程的 PID
  • 失败:返回 -1(如没有子进程时调用 wait())。

参数:

  • 输出型参数,获取子进程退出状态,不关心则可以设置成为NULL

3.2.2 使用示例

#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>int main() {pid_t pid = fork();if (pid == 0) {// 子进程printf("Child PID: %d\n", getpid());sleep(2);exit(42); // 子进程退出码 42} else {// 父进程int status;pid_t child_pid = wait(&status); // 阻塞等待if (WIFEXITED(status)) {printf("Child %d exited with code: %d\n", child_pid, WEXITSTATUS(status));}}return 0;
}

输出:

Child PID: 12345
Child 12345 exited with code: 42

3.2.3 注意事项

  1. 僵尸进程:若父进程不调用 wait(),子进程终止后会一直保持僵尸状态,直到父进程结束(由 init 进程回收)。
  2. 多个子进程:wait() 每次只回收一个子进程,需循环调用以处理所有子进程。
  3. 非阻塞替代:用 waitpid(pid, &status, WNOHANG) 可实现非阻塞轮询。

常见问题1:父进程等待,我希望获取子进程的哪些信息呢?

  • 子进程的代码是否异常
  • 如果没有异常,结果对吗,不对的原因是什么

问题2:父进程为什么不定义全局变量的status,而必须用wait等系统调用来获取状态呢?

  • 用全局变量的话,因为进程具有独立性!!所以子进程再怎么去改自己的status,父进程都看不到!(虽然表面上是一份代码),所以这个过程比如要通过系统调用接口来让操作系统帮助我们获取子进程的一些数据!!(因为OS不相信任何人)

问题3:wait()的参数,int为什么被分为好几个部分?

  • 我们不仅需要知道是否发生异常,还需要知道退出状态,所以这个int需要拆分成bit位
    在这里插入图片描述
      低七位判断:是否异常 status&0x7F ,第八位是core dump标志(暂时不知道是干什么的),次8位判断退出原因 (status<<8)&0xFF。

补充:

  1. WIFEXITED(status) : 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出) 其实等价于status&0x7F
  2. WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)其实等价于(status<<8)&0xFF
  3. NULL:不关心子进程的状态

3.3 阻塞和非阻塞轮询

  阻塞等待(Blocking):当父进程调用 wait() 时,如果子进程尚未退出,父进程会一直挂起(即代码停止执行),直到子进程终止后才会继续。这种等待方式称为阻塞,因为它会卡住父进程的执行流,直到条件满足。这说明阻塞不仅仅发生在硬件I/O操作(如读写磁盘、网络请求),同样适用于进程间的同步等待。

  非阻塞轮询(Non-blocking Polling):如果父进程不想被卡住,可以使用 waitpid(pid, &status, WNOHANG),其中 WNOHANG 表示“不挂起”。这时,无论子进程是否退出,waitpid() 都会立即返回

  • 如果子进程已退出,则返回其状态;
  • 如果子进程仍在运行,则返回 0,父进程可以继续执行其他任务,稍后再检查。

总结:阻塞方式(如 wait())会暂停父进程,直到子进程结束;而非阻塞方式(如 waitpid(WNOHANG))允许父进程继续执行,通过轮询检查子进程状态。两者的选择取决于是否需要同步等待,还是希望父进程保持响应。

抄了一个小故事,帮助理解非阻塞轮询:

  故事一:假设你还有三天就要C语言考试了,但是你不以为然,就先玩了两天,当第三天的时候,你慌了,因为你平时上课没听而且啥也不懂,所以你找了一个班里的努力型学霸小张(喜欢学习并且做了很多笔记) 于是你走到楼下,但是你又懒得上去,于是你就打电话给小张“你能不能跟我去图书馆帮我复习几个小时,顺便教教我把笔记借我看看呗” 小张说:“好,但是我现在笔记还有几页没看完,你再楼下等等我,我等会就下去……” 然后你就把电话挂了,然后过了5分钟,你发现小张还没下来,然后你又打了电话,但是小张还是说等会就下去,就这样你打了十多个电话,终于小张下来了,于是你们开开心心地去往图书馆了。

  在这个过程中,你就是用户,你打电话的过程就是系统调用的过程,而小张就是操作系统,当你打电话询问小张的这个过程其实就是向操作操作系统询问:“你当前的状态准备好了没有?(检查状态)” 小张说等会就下来,于是你挂电话 ,其实就是你检查不成功,先结束系统调用(系统调用立马返回) 这就是非阻塞!! 而你一直给小张打电话其实就是轮询 (不断询问 有while循环)所以加在一起就是非阻塞轮询!

  故事二:最后你考过了,你很开心,但是数据结构老师又告诉你明天要考试,你又没听,于是你又想到了小张,但是历史的经验告诉你肯定得打很多电话,上次手机都打欠费了。于是这次你换了一个思路,在小张告诉你,需要再等一会的时候,你就要求他不要挂断电话,直到下楼的时候再挂,这样我可以随时知道你的情况。

   这个过程其实就是阻塞!! 也就是系统调用会卡住,会被链接到子进程的一个阻塞队列中等待。

  故事三:你又过了,你特别开心,但是操作系统明天又要考试了,于是你给小张打电话,但是你也不知道小张不会立马下来,所以你自己也带了本书,在等小张的时候,闲着没事,可以自己先看会书。

  这个过程想描述的是,非阻塞轮询相比较于阻塞来说,可以多做一些自己的事情,比如说我可以做一些检查的工作!

如果父进程在非阻塞轮询时可以做什么事,如果这件事任务太重到时没时间等怎么办?

  一般来说这种事都是一些比较轻的工作,因为我们核心的任务是等待子进程,所以一般来说都是做一些检查之类的简单任务。

3.4 waitpid解读

  waitpid 是 Linux 系统中用于父进程等待子进程结束的系统调用,是wait函数的增强版本,功能更强大、更灵活。

3.4.1 函数原型

#include <sys/types.h>
#include <sys/wait.h>pid_t waitpid(pid_t pid, int *status, int options);

3.4.2 参数说明

参数含义
PID指定要等待的子进程ID
status用于存储子进程的退出状态,可通过宏进一步分析(如WIFEXITED等)
options控制等待行为的选项,如是否阻塞等待

pid 的几种常用取值:

参数含义
>0等待进程号等于 pid 的子进程
= -1等待任何一个子进程(等同于 wait() 的行为)

status 的处理,使用以下宏对 status 变量进行解析:

含义
WIFEXITED(status)如果子进程正常退出,返回非0
WEXITSTATUS(status)获取子进程的退出码(仅在 WIFEXITED 为真时有效)
NULL不关心子进程的状态

options 常用选项:

参数含义
0默认阻塞,直到子进程结束
WNOHANG非阻塞,如果没有子进程退出则立即返回

返回值:

  • 正常返回:子进程的 PID
  • 如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0;
  • 如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在;(调用出错,比如:等待的不是自己的子进程)
#include <stdio.h>
#include <stdlib.h>   // exit
#include <unistd.h>   // waitpid
#include <sys/wait.h>int main() {pid_t pid = fork();if (pid == 0) {// 子进程printf("Child process PID: %d\n", getpid());sleep(2);exit(42);  // 子进程退出码为42} else if (pid > 0) {// 父进程int status;pid_t result = waitpid(pid, &status, 0);  // 阻塞等待子进程if (result > 0 && WIFEXITED(status)) {printf("Child exited with code %d\n", WEXITSTATUS(status));}} else {perror("fork failed");}return 0;
}

3.5 多进程的代码逻辑

  1. 子进程管理与等待
    • 当创建了多个子进程后,父进程可以使用 waitpid 的第一个参数设为 -1,表示等待任意一个子进程退出。为了避免遗漏,建议使用一个宏或常量定义子进程数量,方便后续统计。父进程不能在等待到一个子进程后就立即 break,而是应该维护一个计数器,每当成功等待并回收一个子进程,就将计数加一,直到所有子进程都被回收完毕。
  2. 进程调度与执行顺序
    • 虽然我们可以通过 fork 循环快速创建多个子进程,但这些子进程到底谁先执行,是由操作系统调度器决定的,我们无法控制。但有一点是确定的:父进程一定是最后一个退出的。因为它需要负责回收所有子进程的资源,避免僵尸进程的产生。这是父进程在多进程模型中的责任和义务。
  3. 多进程代码的三大核心:创建、退出与等待
    • 进程创建:通常通过循环调用 fork() 来生成多个子进程。
    • 进程终止:每个子进程在完成自己的任务后应当及时使用 exit() 或 return 退出。
    • 进程等待:父进程必须等待所有子进程的结束。若使用阻塞方式(不加选项的 waitpid),父进程会一直挂起直到某个子进程退出;若采用非阻塞方式(如加 WNOHANG),则需要通过 while 循环不断轮询判断子进程状态,直到全部退出。无论哪种方式,父进程都必须负责回收全部子进程资源。
#include <stdio.h>      // 标准输入输出函数
#include <stdlib.h>     // 提供 exit() 等函数
#include <unistd.h>     // 提供 fork(), sleep(), getpid() 等系统调用
#include <sys/wait.h>   // 提供 waitpid(), WIFEXITED 等宏定义#define NUM_CHILDREN 5  // 宏定义子进程数量,便于统一管理int main() {pid_t pid;int i;// 1. 循环创建多个子进程for (i = 0; i < NUM_CHILDREN; i++) {pid = fork();  // fork 创建新进程if (pid < 0) {// 创建失败perror("fork failed");exit(EXIT_FAILURE);} else if (pid == 0) {// 子进程逻辑printf("子进程 %d 启动,PID = %d\n", i + 1, getpid());// 模拟任务处理(sleep 秒数不同,以区分子进程结束顺序)sleep(1 + i);printf("子进程 %d 退出,PID = %d\n", i + 1, getpid());// 用 exit() 主动退出,并返回一个退出码exit(i + 1);  // 返回 i+1 作为子进程的退出码}// 父进程继续循环创建下一个子进程}// 2. 父进程等待所有子进程退出并回收资源,避免僵尸进程int status;             // 用于存储子进程的退出状态int exited = 0;         // 已退出子进程数量计数器pid_t wpid;while (exited < NUM_CHILDREN) {// waitpid(-1, ...) 表示等待任意一个子进程wpid = waitpid(-1, &status, 0);  // 阻塞等待if (wpid > 0) {exited++;  // 成功等待到一个子进程退出if (WIFEXITED(status)) {// 正常退出printf("父进程:子进程 PID = %d 正常退出,退出码 = %d\n", wpid, WEXITSTATUS(status));} else if (WIFSIGNALED(status)) {// 被信号中断退出printf("父进程:子进程 PID = %d 被信号终止,信号编号 = %d\n", wpid, WTERMSIG(status));} else {// 其他异常情况printf("父进程:子进程 PID = %d 异常退出\n", wpid);}} else {// waitpid 出现错误(一般不会到这里,除非系统出错)perror("waitpid error");break;}}// 所有子进程已退出,父进程也准备退出printf("父进程:所有子进程已回收完毕,父进程退出。\n");return 0;
}

相关文章:

  • C语言基础(09)【数组的概念 与一维数组】
  • 【Linux】shell的条件判断
  • linux信号详解
  • 用Python实现一个简单的远程桌面服务端和客户端
  • LCA(最近公共祖先)与树上差分
  • debian12.9或ubuntu,vagrant离线安装插件vagrant-libvirt,20250601
  • Java流【全】
  • 【计网】第六章(网络层)习题测试
  • Cesium快速入门到精通系列教程三:添加物体与3D建筑物
  • linux系统中防火墙的操作
  • 进阶日记(一)大模型的本地部署与运行
  • vue3常用组件有哪些
  • BFS入门刷题
  • STM32——CAN总线
  • 飞牛fnNAS存储空间模式详解
  • P4549 【模板】裴蜀定理
  • Linux --进程状态
  • 利用多进程定时播放,关闭音乐播放器
  • 2025 年 AI 技能的全景解析
  • Hilbert曲线
  • 软件开发培训机构课程/东莞百度seo排名
  • 新手如何做网站维护/如何进行网站性能优化?
  • 网站建设 上海/引流黑科技app
  • 用腾讯云做淘宝客网站视频下载/跨国网站浏览器
  • 免费网站免费无遮挡/app广告联盟平台
  • 建设银行网站登录不/营销团队公司