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

Linux : 进程控制

目录

前言

一 再识fork()函数

1.fork()函数的几个补充点

2.fork()函数创建子进程失败

二 进程终止

1. main()函数的返回值

2. 进程终止的常见做法 

​编辑

3._exit()和exit()函数的使用

4.进程终止后,内核做了什么?

三 进程等待

1.进程等待的必要性

2.进程等待的方法

3. 获取子进程的status

🐳3.1waitpid的调用

 🐊3.2 宏:WEXITSTATUS()和WIFEXITED():


前言

我么在Linux:进程概念-CSDN博客这一章节中认识使用fork()函数来创建子进程,在Linux : 进程地址空间-CSDN博客这一篇详细介绍了父子进程的代码和数据共享的问题(写时拷贝)但是我们之前的实验都是为了演示子进程创建成功的现象,并没有说明子进程创建失败是什么结果。在Linux :进程状态-CSDN博客中我们提到了许多进程状态,其中提到了僵尸进程,这是一种在子进程退出时没有被父进程读取所产生的一种状态。所以在这篇文章中,我们再深入认识一下进程的终止和进程的等待


一 再识fork()函数

1.fork()函数的几个补充点

  • 在用fork函数创建子进程成功之后,子进程和父进程共享的是一份代码和数据,但是代码一般都是只读的,不能被修改,数据可以被修改,所以会发生写时拷贝。
  • 子进程创建成功之后,为什么子进程从fork()函数之后的代码开始执行,而不是从头开始?在CPU中存在一个寄存器叫程序计数器(eip),这个寄存器的作用是保存当前正在执行的指令的下一条指令,当子进程创建成功之后,父进程的程序计数器的内容会拷贝给子进程,因此子进程就会知道上次执行到哪一个地方,便会接着向下执行。

2.fork()函数创建子进程失败

  fork()函数创建失败的原因是跟操作系统中的进程的数量和进程所占用的资源有关。当操作系统      中的进程数量太多,如果接着创建进程会使操作系统管理不过来,或者当前操作系统中所剩的内    存资源不够再创建出一个进程来,这些情况下都会创建进程失败。


二 进程终止

我们在Linux :进程状态-CSDN博客中提到了进程终止,即在进程被正确回收之后,这里我们再进行详细介绍一下细节,首先我们需要明白一个进程的正常退出有几种情况:

  • 代码正常跑完,运行结果正确
  • 代码正常跑完,运行结果错误
  • 代码没有跑完,进程异常退出

1. main()函数的返回值

从我们开始编写第一个C/C++程序开始,我们在主函数的最后都要写上一行代码 return 0;相信我们大家都曾对它产生过疑问,以当时的知识储备我们只能理解表面,我们只知道,return 0表示的程序正常跑完,运行结果正确;非零表示程序正常跑完,运行结果不正确。那么这个零和非零究竟是什么呢?

return返回的这个数字是进程的退出码,其中0表示的是正常退出,结果正确,其他的非零就分别代表着结果运行不正确的原因,且退出码是返回给当前进程的父进程进行接收的,父进程需要知道自己子进程退出或者终止的原因,从而回收子进程的资源。

❶退出码 

我们之前在学习进程PCB的时候,知道每一个进程的task_struct中存储了许多和进程相关的属性和信息,其中就包括了退出码,如下:

那么怎么获取到进程的退出码呢?答:echo $?命令 

如上图,我们使用echo $? 命令,可以看到退出码为16,可是下图再执行echo $? 命令时候,为什么退出码变成0呢?

🎲因为$?永远记录最近一个进程 执行完毕对应的退出码。

第一个$?--------------->(main->return 16)

第二个$?--------------->(对应进程&?的退出码0)

$?是一个进程

❷退出码意义 

下面我们利用strerror函数将错误代码转换为相应的错误消息字符串打印出来看看。

2. 进程终止的常见做法 

㊀ :在主函数(main)中使用return返回,main函数是程序的入口,在主函数中使用return表示程序的结束(非主函数中只表示函数的调用结束,并不是程序的结束)。

㊁:在程序的任何地方使用exit()函数,exit()函数表示终止程序,在程序的任意位置调用它都可以让程序终止。

3._exit()和exit()函数的使用

➊ 首先用man 2 exit命令查看_exit()

➋ 我们分别使用exit()函数和_exit()函数退出一个程序看看区别

exit:可以看到进程正常退出且正常打印。

_exit()

 所以区别显而易见了,一个在进程退出的时候打印了,一个没有打印。

我们知道,在使用printf语句进行打印的时候,打印的内容并不会立马刷新到外设,首先会暂存到缓冲区中,在上面的程序中,我们并没有使用‘\n’直接刷新缓冲区。当我们使用exit()函数时,缓冲区的东西能刷新到显示器上,但是使用_exit()函数时却不会。这表明,exit()函数在终止程序的时候会刷新缓冲区,_exit()函数在终止程序的时候不会刷新缓冲区。

exit/_exit/return区别 

  • exit()是C语言的接口,_exit()属于系统调用。
  • exit()属于正常的退出进程,它可以用于任意的函数中,通过调用它使得整个进程退出。且通过返回一个退出码将控制权交还给操作系统。
  • exit()最后也会调用_exit()函数来直接终止程序,但是在调用_exit()之前,它还会干点其他的事情,比如会执行用户定义的清理函数和刷新缓冲区关闭对应的流等。
  • _exit()是直接终止程序,不返回任何状态给操作系统。
  • return语句用于退出函数和方法,并将控制权返回到调用该函数或者方法的位置。
  • return语句只能用于函数或方法的内部,而不能使整个进程退出。当函数或方法中的所有代码都执行完毕或者遇到return语句,该函数或方法的执行将结束,控制权返回给调用者。

再谈return 

上面提到的return是一种更加常见的退出进程的方法,执行return n;语句等同于执行了exit(n),因为调用main()函数的运行的时候函数会将main()的返回值当作exit的参数。

4.进程终止后,内核做了什么?

我们知道,一个进程是由自己的内核数据结构和进程代码和数据组成的,当一个进程退出的时候,首先自己的进程状态会变成Z状态,也就是所谓的僵尸状态,此时需要等其父进程对其进程回收,由终止我们知道其需要给其父进程返回退出码,这个进程的状态马上变成X状态,其父进程的相关代码会回收(释放)其数据和相关代码,因为当该进程终止的时候,其数据和代码已经没有任何意义了


三 进程等待

1.进程等待的必要性

  • 之前讲过,子进程退出,父进程如果不管不顾,就可能造成‘僵尸进程’的问题,进而造成内存泄漏。
  • 另外,进程一旦变成僵尸状态,kill -9 也无能为力,因为谁也没有办法 杀死一个已经死去的进程。 最后,父进程派给子进程的任务完成的如何,我们需要知道。如子进程运行完成,结果对还是不对, 或者是否正常退出。
  • 父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息

2.进程等待的方法

wait方法

wait()返回值: 成功则返回被等待进程pid,失败返回-1。 

从运行结果我们能看出,刚开始子进程和父进程都是休眠状态(S+),因为要打印,需要与显示器交互,所以处于休眠状态,

当子进程exit(0)退出的时候,父进程还在,此时子进程变成僵尸状态(Z+),等待父进程回收读取退出信息

wait() 之后,子进程被父进程成功读取

状态变成X,此时只有父进程状态

父进程sleep 5 秒之后,父进程会被bash回收,所以最后,父进程的状态也不存在了。

并且我们可以看到wait的返回值是子进程的pid.

waitpid()方法

 waitpid()包含三个参数,但是其返回值和wait()调用相同,若返回值 > 0 返回值即为等待的子进程的PID; 若返回值 = -1, 即为调用失败, errorno会被设置为错误提示码; 先来看它的三个参数:

  • pid_t pid:这里传入的是被等待进程的pid,即指定特定的pid让父进程等待。当传入-1时,waitpid()与wait()的作用相同,都是等待任意的子进程。

  • int *status:一个指针参数(输出型参数),此参数的作用是向外部传递信息,当 waitpid()此函数等待到一个退出的子进程的时候,子进程的退出信息会被存储到 status指针指向的内容中。这两个调用都是系统调用。
    int options:更像是一个配置参数,通过传递不同的参数,可以让waitpid()有着不同的工作模式。当传入0时,表示阻塞等待,即waitpid()的工作模式就像是阻塞一样, 如果其等待的子进程还未退出, 那么父进程就会一直阻塞等着子进程退出, 在子进程退出之前, 父进程一直处于阻塞状态, 不会做其他事情, 就只是等着子进程退出。

3. 获取子进程的status

上面我们讲到wait和waitpid,都有一个status参数,该参数是一个输出型参数,由操作系统填充。如果传递NULL,表示不关心子进程的退出状态信息。status不能简单的当作整形来看待,可以当作位图来看待,如下图(只研究status低16比特位)

也就是说,waitpid()使用的第二个参数是一个int类型变量的地址 ,当waitpid()等待到一个子进程后,传入的地址所指向的变量的低16位中,低八位表示子进程的退出信号,高八位表示子进程退出码。 并且,当子进程是正常退出时,表示退出信号的八位为0;当子进程被某种信号所杀时,表示退出码的八位为0. 

关于退出信号和退出信号的计算,退出码((status>>8)&0xFF),退出信号(status&0x7F),(status>>8)&0xFF表示的是取status中的第二个八位数,即从第9到第16位的数字,这部分的数字代表退出进程的退出码。退出信号的计算是status&0x7F,但是最高位是一个单独的core_dump标志,暂时忽略。

✍注: 当进程退出码为0时, 往往意味着进程正常退出.而此时子进程接收到信号而退出, 肯定是不属于正常退出的范围的.而子进程的退出码依旧是0?是否与退出信号冲突了?当然没有, 当退出信号不为0时, 退出码就没有意义了, 可以忽略不看

🐳3.1waitpid的调用

程序跑完,结果不正确(退出信号为0,退出码非零)

退出信号为0,说明代码跑完,退出码为10,说明结果不对(这里代码exit(10)抽象认为某种原因结果不对)。 

 程序跑完,结果正确(退出信号为0,退出码为零)

代码没跑完,程序出异常了,这个时候不需要关注退出码结果了

运行结果

 🐊3.2 宏:WEXITSTATUS()和WIFEXITED()

父进程中的status变量接收到数据之后, 其实不需要通过手动位运算来计算出子进程的退出码和退出信号.其实而可以通过两个宏:

WIFEXITED(status): 若子进程正常退出(即程序跑完了,退出信号为0),则为真。

WEXITSTATUS(status): 提取子进程退出码,判断运行结果对不对。

exit code:10说明程序跑完了,正常退出,退出码为10

相关文章:

  • 时序约束整理
  • Spring 框架学习
  • winform中chart控件解决显示大量曲线数据卡顿方法——删旧添新法
  • 基于大模型预测的巨细胞病毒视网膜炎诊疗全流程研究报告
  • Docker从入门到精通
  • iOS18.0 iPad适配问题-tabbar
  • 贪心算法简介(greed)
  • Go语言为什么运行比Java快
  • 软著申请流程图
  • Netty基础—3.基础网络协议一
  • java中的队列Queue
  • mysql之主从切换
  • 207、【图论】孤岛的总面积
  • 【Godot4.2】Vector2向量插值的应用
  • DeepSeek 多模态大模型 Janus-Pro 本地部署教程
  • Android中的FragmentTransaction是什么以及如何使用它
  • 机器学习项目实战——信用评分与贷款风险评估(主页有源码)
  • Spring Boot整合MapStruct教程
  • Tomato 文件包含漏洞靶机 通关攻略
  • LeetCode Hot100刷题——对称二叉树
  • 做外汇的官方网站/网络营销的六大特征
  • java web是做网站的吗/域名买卖交易平台
  • 彩票网站多少钱可以做/青岛seo网络推广
  • b站不收费观看/怎么制作网站平台
  • 做外贸电商网站/网络营销八大职能
  • 淮安哪里做网站/随州网络推广