news 2026/6/10 3:59:05

Linux---进程控制(1)(创建,终止,退出,等待)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Linux---进程控制(1)(创建,终止,退出,等待)

进程创建

写时拷贝(补充)

关于写时拷贝在上一篇博客里已经提到过了,只不过现在想补充一个点,fork之后,父子进程的代码和数据是共享的,一般来说,代码是只读的,数据是读写的,但是fork之后会特殊一点,代码依旧是只读的,数据也会变成只读,在父子进程没有任何一方要修改数据时,父子进程的代码和数据是映射到同一个物理内存空间的。假设现在有一方进程要修改共享数据了,OS会介入,因为现在数据是只读的,OS就会先检查一下是野指针,还是进程被挂起,数据不在物理内存里了.....如果都不是就说明是父子进程有一方要修改数据,就是要发生写时拷贝了,数据再从原来的读权限变成读写权限。

进程终止

进程终止的时候,内核会释放内核数据结构,代码和数据。注意,进程终止时候会先释放代码和数据,内核数据结构最后才释放,因为有僵尸进程。

你的进程终止会有几种情况?1.代码跑完,结果正确,2.代码跑完,结果不正确,3.代码没跑完,进程异常了。其中第三种情况会在后边的博客里体现,本篇只讨论前两种,你怎么知道你的结果对还是不对?答案:main函数的返回值,这个返回值其实也叫做进程的退出码,为0就说明进程是正常结束的,不为0就说明进程的结果不正确。除了0之外的数字都表示不同的含义,用于告诉OS非正常退出的具体原因,这个含义可以是日后你的公司自定义的,也可以是系统提供(规定)的。

echo $? 可以查看最近一个进程执行完的结果。($相当于指针解引用,?就代表一个变量)。

计算机适合看数字来知道错误原因,但是人不适合,因此strerror这个函数可以帮我们查看具体每一个退出码的含义。(下图只是片段),还有一个码叫做库函数调用失败的错误码,存在errno这个全局变量里边(C语言里边),错误码和退出码只是名字不一样,实际上意思都是一个意思,可以混用。

接下来简单说一下进程终止的第三种情况,代码没跑完,进程异常了。首先在这种情况下,return没有执行,退出码是没有意义的,那是什么原因导致进程异常了呢?答:被信号终止了。之前的博客里也提到过了,kill指令可以发出信号让进程终止,通过kill -l可以查看所有的信号编号(注意是从1开始的)。

综上:进程退出的3种情况可以再细化总结一下:

1.代码跑完(代码运行期间,没有收到信号),可以理解成收到了0信号(因为信号编号从1开始,0本身没意义) && return 0 -> 信号数字:0 && 退出码:0

2.信号数字:0 && 退出码:!0

3.信号数字:!0 && 退出码无意义

也就是说,进程执行的结果状态可以用两个数字表示,一个是信号数字,一个是退出码。用户本身不需要维护,当一个进程退出的时候,OS会把进程退出的详细信息写入到进程的task_struct里,所以,进程退出的时候,需要僵尸维护自己的退出状态。

进程退出

这里主要是讲的是进程终止的前两种情况。在不考虑异常的情况下,如何让进程退出?

1.main函数return

2.在任意地方调用exit,进程都会结束。(非main函数return,函数结束,非main函数exit,进程结束)。除此之外,还有一个系统调用_exit,功能跟exit一样,exit为最佳实践。

exit VS _exit:exit是库函数,_exit是系统调用,库函数是在上层,系统调用是在下层,exit终止进程之后会强制刷新缓冲区,但是_exit不会。因此可以得出一个结论,缓冲区和刷新缓冲区的操作一定不在内核里,缓冲区其实是C/C++维护的(具体之后说)。

进程等待

进程等待就是子进程退出之后,进入僵尸状态,等待父进程来回收它。进程等待是很有必要的,子进程进入僵尸状态之后,OS会保留它的内核数据结构,外界也杀不掉它,因为它已经死了,这就导致内核数据结构一直留在内存里,会有内存泄露的问题,所以等待是必须的,不等会有内存泄露问题。还有一种非必须情况,就是可能父进程要获取子进程的退出信息(退出码或者退出信号,这个也在内核数据结构里)。所以需要进程等待,解决子进程的僵尸问题。

验证:父进程等待能解决子进程的僵尸问题。我们用到了wait系统调用,用来回收子进程(waitpid函数的功能完全覆盖wait,所以waitpid是最佳实践)。它的参数你先不要管,我们就先当它是NULL,然后如果等待成功了,就返回子进程的PID,等待失败就返回-1。

(下边代码意思:子进程运行总时间为5s,父进程总共休眠10s,在子进程运行完还要休眠5s,这个5s可以看到子进程的僵尸状态,子进程运行完再等5s之后,父进程等待子进程成功,然后父进程自己退出。)

1 #include<stdio.h> 2 #include<unistd.h> 3 #include<stdlib.h> 4 #include<sys/wait.h> 5 #include<sys/types.h> 6 #include<unistd.h> 7 8 int main() 9 { 10 pid_t id = fork(); 11 if(id == 0) 12 { 13 //child 14 int cnt = 5; 15 while(cnt--) 16 { 17 printf("我是子进程:pid为%d\n", getpid()); 18 sleep(1); 19 } 20 exit(0); 21 } 22 else 23 { 24 sleep(10); 25 //parent---fork给父进程返回子进程的pid 26 pid_t rid = wait(NULL); 27 if(rid == id) 28 { 29 printf("等待成功:pid%d", getpid()); 30 } 31 exit(0); 32 } 33 34 return 0; 35 }

由观察结果能看出来,当父进程一回收,子进程立马从僵尸状态变成死亡状态了(死亡状态看不到,查不出来就表示死亡了)。

对于wait系统调用还想说一点,就是如果父进程wait子进程,但是子进程就是没有退出,则父进程会阻塞在wait函数中(阻塞等待)。

验证:如何获取子进程的退出信息。用waitpid函数。先来看看waitpid函数的相关信息。注:pid==-1,options==0时,waitpid和wait在功能上没有任何区别。

pid_ t waitpid(pid_t pid, int *status, int options);

返回值:

当正常返回的时候waitpid返回收集到的⼦进程的进程ID

如果设置了选项WNOHANG,⽽调⽤中waitpid发现没有已退出的⼦进程可收集,则返回0

如果调⽤中出错,则返回-1,这时errno会被设置成相应的值以指⽰错误所在(比如说:父进程等待的不是自己子进程就会调用出错)

参数:

pid:

Pid=-1,等待任⼀个⼦进程。与wait等效。

Pid>0.等待其进程ID与pid相等的⼦进程。

status: 输出型参数

WIFEXITED(status): 若为正常终⽌⼦进程返回的状态,则为真(查看进程是否是正常退出,信号数字是否为0)

WEXITSTATUS(status): 若WIFEXITED⾮零,提取⼦进程退出码(查看进程的退出码)

options:

默认为0,表⽰阻塞等待

WNOHANG: 若pid指定的⼦进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该⼦进程的ID。

重点来说一下第二个参数status,status参数是一个输出型参数,是获取子进程退出信息的,子进程退出有3种情况,就是上文进程退出那个标题里说的那3种,也就是说子进程的退出信息,本质上是要得到子进程退出的时候的那两个数字(信号数字和退出码)。那一个int类型的status变量怎么能同时得到两个数字呢?一个int类型4个字节,32bit,其中前16个bit不考虑,主要是考虑后16个bit,后16bit中,后8位表示信号编号,前8位表示退出码。(注:因为后8位信号位有1位表示core dump,因此实际上信号位有7位数字)。

接下来来看一个案例,验证一下这个事。

1 #include<stdio.h> 2 #include<unistd.h> 3 #include<stdlib.h> 4 #include<sys/types.h> 5 #include<sys/wait.h> 6 7 int main() 8 { 9 pid_t id = fork(); 10 if(id == 0) 11 { 12 //child 13 int cnt = 5; 14 while(cnt--) 15 { 16 printf("我是子进程PID: %d\n", getpid()); 17 } 18 exit(1); 19 } 20 else 21 { 22 //parent 23 int status = 0; 24 pid_t rid = waitpid(id, &status, 0); 25 if(rid == id) 26 { 27 printf("等待成功,status: %d\n", status); 28 } 29 exit(0); 30 } 31 return 0; 32 }

上文代码里,子进程的退出码为1,父进程里,waitpid的参数status获得了1,然后最终打印结果是256,原因就是上文说过的,由于父进程是正常return终止,因此后8位信号位为全0,也就是00000000,前8位退出码为1,也就是00000001,最终status的二进制表示为0000000100000000,转化成10进制就是256。根据status还可以得到子进程的退出码或者信号数字。退出码:(status >> 8) & 0xFF(11111111),信号数字:status & 0x7F(01111111)(信号位只有7位),注意这里的0xFF和0x7F也是int类型,有32个bit位,因为C语言里进行运算时有隐式类型转换。

父进程通过waitpid,是如何得到子进程的退出信息的?子进程退出后会进入Z状态,OS会保留Z状态时进程的PCB,PCB里的exit_code和exit_signal里会保存子进程的退出信息,waitpid的第一个参数是子进程的PID,凭借PID找到对应的子进程,并且读取子进程PCB里的信息。那如果waitpid的第一个参数为-1呢?为-1表示获取任何一个子进程的退出信息,在父进程的task_struct里边有一个子进程链表,一个兄弟进程链表,通过便利它们就可以获取到进程状态为Z的进程的退出信息了。

上文我们获取status里子进程的退出码和信号数字是用的位运算的方式,除此之外,waitpid还给我们提供了许多的宏,比较重要的如下:(上文介绍waitpid那里截图下来的),WIFEXITED(status)这个宏里是用来检测信号是否为0,WEXITSTATUS(status)是用来获取子进程的退出码。说实话就是这两个宏就是系统帮我们把位操作给搞完了,不用我们自己搞了。

之前一直忽略了waitpid的第三个参数,上文所有的代码都是传的默认值0,当传0的时候,其实就是让父进程阻塞等待子进程退出,接收到子进程的退出信息之后父进程才会执行自己之后的代码。这样其实会占用资源,因为父进程自己也是一个进程,但是它就光等了,没有干自己的事情,有点浪费,因此waitpid的第三个参数也给了我们第二种选择,WNOHANG(等待不要宕机了),传这个参数时父进程是会非阻塞等待的,非阻塞等待的意思就是调用waitpid检测子进程是否退出时如果子进程没有退出,父进程不会阻塞在那里,而是继续执行它自己后边的代码,然后在执行过程中,父进程会非阻塞轮询(自己实现的,waitpid不会帮你干,它只会检测一次,再要检测要重新调用)。

结合以上全部关于waitpid这个系统调用的解释,我们整合一下完整版代码如下。(用到了几乎所有的waitpid的返回值以及参数的值等信息)。

1 #include<stdio.h> 2 #include<unistd.h> 3 #include<stdlib.h> 4 #include<sys/types.h> 5 #include<sys/wait.h> 6 7 int main() 8 { 9 pid_t id = fork(); 10 if(id < 0)//创建失败 11 { 12 perror("fork"); 13 exit(1); 14 } 15 else if(id == 0) 16 { 17 //child 18 int cnt = 5; 19 while(cnt--) 20 { 21 printf("我是子进程PID: %d\n", getpid()); 22 sleep(1); 23 } 24 exit(0); 25 } 26 else 27 { 28 //parent 29 //写成循环才能实现非阻塞轮询的效果 30 while(1) 31 { 32 int status = 0; 33 //单次调用waitpid的作用只有两个,非阻塞检测和回收子进程 34 pid_t rid = waitpid(id, &status, WNOHANG); 35 if(rid > 0)//子进程已经退出 36 { 37 if(WIFEXITED(status)) 38 { 39 printf("等待成功, 子进程PID:%d, 退出信息:%d, 退出码:%d\n", rid, status, WEXITSTATUS(status)); 40 } 41 else 42 { 43 printf("异常退出\n"); 44 } 45 break; 46 } 47 else if(rid == 0)//子进程未退出 48 { 49 printf("子进程正在运行,父进程还需等待\n"); 50 usleep(100000); 51 } 52 else//检测失败 53 { 54 perror("waitpid"); 55 break; 56 } 57 } 58 } 59 return 0; 60 }

非阻塞等待和阻塞等待哪一种更高效一点呢?你问等待效率高,是不是比的是等待时间的长或者短,而不管是非阻塞等待还是阻塞等待,等待的时间都由子进程决定,所以其实不存在谁效率高的问题,只不过是阻塞等待在等待期间什么都没干,非阻塞等待在等待期间可以干其他的事情,它把等待的时间利用了起来。接下来来说说非阻塞等待能干啥其他的事情?(下边的截图是基于上边最终版完整代码多出部分的截图,目的仅为让我们知道非阻塞等待的时候是可以干其他事情的)。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/6/10 3:58:12

制造业电子数据交换EDI软件落地|五大落地实施全流程

在智能制造全面普及、供应链全球化协同的时代&#xff0c;EDI电子数据交换已经成为制造业企业对接头部品牌、稳定承接订单、实现数字化合规的硬性准入标准。无论是消费电子、汽车零部件、精密制造还是通用机械行业&#xff0c;上下游供需协同早已告别人工传单、表格核对、线下对…

作者头像 李华
网站建设 2026/6/10 3:57:05

12-Hooks 上篇:五种事件 + 实用模板 —— 让 AI 自动执行你的脚本

Hooks 上篇&#xff1a;五种事件 实用模板 —— 让 AI 自动执行你的脚本 Skills 让 AI 学会了你的工作流&#xff0c;MCP 给 AI 装上了手脚。但还有一个问题没解决&#xff1a;你希望某些事在特定时机自动发生——AI 编辑文件后自动格式化、提交前自动跑测试、启动时自动加载环…

作者头像 李华
网站建设 2026/6/10 3:56:58

工商业储能系列: 主动均衡之集中式主动均衡<双向隔离DCDC+开关矩阵>

前言 在锂电池储能系统中&#xff0c;电芯之间存在难以避免的容量和内阻差异&#xff0c;形成“木桶效应”——最差的那节电芯限制了整个模组的可用容量&#xff0c;并加速整体衰减。主动均衡技术正是为了解决这一问题而生。 集中式主动均衡属于主动均衡的一种主流技术路线&a…

作者头像 李华
网站建设 2026/6/10 3:55:35

还在死磕 写空格?这4招让HTML空格代码原地起飞

怎样于网页里呈现空格吗? 这看上去好像是个简易的问题, 然而事实上, 它关联到诸多不一样的方法以及技巧。在这篇文章当中, 我们会介绍几种常见的技巧用以呈现空格, 涵盖实体字符、CSS样式、HTML标签以及代码。使用实体字符首种办法乃是运用实体字符, 其为一种特殊的HTML字符, …

作者头像 李华
网站建设 2026/6/10 3:54:30

【Spring Boot + MyBatis|第3篇】统一返回结果 Result 类设计

前言上一篇我们学习了 Controller 中常见的三种参数接收方式&#xff1a;RequestParam、PathVariable、RequestBody。这一篇继续学习一个项目中非常常见的设计&#xff1a;统一返回结果类 Result。在 Spring Boot 项目中&#xff0c;如果每个接口返回的数据格式都不一样&#x…

作者头像 李华
网站建设 2026/6/10 3:52:10

基于UCIe与3DIC Compiler的高效多芯片设计与IP集成

基于UCIe与3DIC Compiler的高效多芯片设计与IP集成 随着高性能计算、人工智能、汽车电子和移动终端等应用对计算能力与功耗效率要求的不断提升,芯片设计正加速从传统的单片式SoC向多芯片(multi-die)架构演进。通过2.5D/3D先进封装技术,设计者可以将多个异构或同质芯片(又…

作者头像 李华