目录
进程创建
进程终止
进程退出码
进程终止的方式
进程等待
进程等待的方式
status概述
总结
上期我们学习了Linux中进程地址空间的概念,至此进程的所有基本概念已经全部学习完成,今天我们将开始学习进程相关的操作。
进程创建
进程创建其实之前我们已经讨论过,进程创建有两种方式。
1.程序编译之后形成可执行程序,运行可执行程序,可创建进程。
2.使用fork函数创建子进程。
#include<stdio.h>
#include<unistd.h>
int a = 100;
int main()
{
if(fork()==0)
{
printf("#############我是子进程更改前a的值等于%d ###########\n",a);
a = 200;
printf("#############我是子进程更改后a的值等于%d 地址为%p \n",a,&a);
}
else{
printf("##############我是父进程,a的值等于%d 地址为%p \n",a,&a);
sleep(2);
}
return 0;
}
创建子进程的目的就是让子进程去做跟父进程不一样的事情(任务)。在fork函数执行之后,会去执行fork函数之后的代码,因为子进程会继承父类的pcb,pcb中有pc指针,用于记录进程的上下文数据,正是因为如此,子进程拥有和父类一样的pc指针,而且子进程和父进程是共享代码的,所以子进程会去执行fork函数之后的代码。
进程终止
什么是进程终止呢?进程终止其实就是进程退出,进程退出总共有三种情况。
1.代码运行完毕,结果正确。
2.代码运行完毕,结果不正确。
3.代码异常终止。
那么既然进程退出总共有三种情况,那么我们怎样去分辨这三种情况呢?其实这就涉及到了下一个概念------进程退出码。
进程退出码
直接给出进程退出码的概念。
进程退出码:一个整型变量,在进程退出时返回,返回0为代码运行完毕,结果正确;返回非0(非0有很多种可能,每一个可能为一种错误的原因)为代码运行完毕,结果不正确;当代码异常终止时,进程退出码是没有任何意义的。
通过一个场景为大家熟悉一下进程退出码。比如学校考试,进程退出码可以看做考试成绩,满分只有一种,但是非满分有很多种,用满分对应退出码为0,非满分对应退出码我为非0,当考场作弊时,分数已经没有了任何意义,对应为代码异常终止,进程退出码无意义。
进程终止的方式
进程终止有三种方式。
1.使用main函数作为返回。
#include<stdio.h>
int main()
{
printf("hello world!\n");
return 0;
}
上述代码大家并不陌生,大家可能经常会无脑写return 0,但是有多少人想过为什么要写return 0呢。这个return 0其实就是进程退出的标志,返回0证明代码运行结束结果正确,进程退出。可以使用echo $?查看当前进程的退出码。
当然,除过return 0 之外我们可以自己返回任意退出码。我们将return 0改为了return 1.
2.使用exit函数进行进程退出,函数的参数为进程退出码。
#include<stdio.h>
#include<unistd.h>
int main()
{
printf("hello world!\n");
//return 1;
exit(0);
}
通过运行结果可知,exit可以进行进程的退出,退出码也与我们设置的一样。
3.使用_exit函数进行进程退出,函数的参数为进程退出码。
#include<stdio.h>
#include<unistd.h>
int main()
{
printf("hello world!\n");
_exit(1);
}
通过运行结果可知,exit可以进行进程的退出,退出码也与我们设置的一样。
那么问题来了,exit函数和_exit函数都可以进行进程退出,那么它们的区别是什么呢?我们以下述代码为大家解释。
1.exit函数。
#include<stdio.h>
#include<unistd.h>
int main()
{
printf("hello world!");
exit(0);
}
2._exit函数。
#include<stdio.h>
#include<unistd.h>
int main()
{
printf("hello world!");
_exit(0);
}
上述代码,除了函数不同之外,其它部分全部相同,为什么运行结果不一样呢,exit函数打印出了对应的结果,而_exit函数没有打印对应的结果。这究竟是为什么呢?
其实,这也就是exit函数和_exit函数的区别,因为上述打印代码都没有带‘\n’,所以在打印的时候时不会刷新缓冲区,所以要最终在显示器上打印出来,就必须刷新缓冲区。两个函数只有exit会在函数运行结束时刷新缓冲区,而_exit不会刷新缓冲区,所以就有了第一份代码打印对应的结果,而第二份代码不去打印。
进程等待
我们知道任何进程在退出时都会先转为Z状态(僵尸态),因为僵尸态的进程是已经死亡的进程,操作系统无法杀死已经死亡的进程,所以当系统中存在大量的僵尸进程时,就会占用大量的系统资源,造成资源的浪费。那么究竟如何避免系统产生大量的僵尸进程呢?进程等待就是最好的一个解决方式。
我们知道僵尸进程的产生原因就是子进程退出时,产生大量的退出信息而父进程不对这些信息进行处理所造成的,所以要避免产生僵尸进程,就要使用进程等待的方式,让父进程去处理子进程产生的退出信息,从而避免僵尸进程的产生。
进程等待的方式
1.wait函数。
wait函数由两个返回值,返回值>0,意味着等待成功,有子进程退出,返回值为子进程的pid。返回值<0,意味着等待失败。
参考下述代码。
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/wait.h>
int main()
{
pid_t id = fork();
if(id == 0)
{
//child
int cnt=5;
while(cnt){
printf("child[%d] is running,cnt is %d \n",getpid(),cnt);
sleep(1);
cnt--;
}
}
else{
//parent
sleep(10);
//父进程开始进行等待
int ret=wait(NULL);
if(ret>0)
{
printf("父进程等待成功!\n");
}
else{
printf("父进程等待失败!\n");
}
sleep(10);
}
整个执行逻辑是,让父进程先休眠10s,在父进程休眠期间,子进程进行打印,打印完成之后,子进程退出,但是由于父进程在休眠,所以父进程不对子进程的退出信息进行处理,所以子进程变成了僵尸进程,但是当父进程睡眠10s被唤醒之后,就开始进行等待,将子进程的退出信息进行了处理,此时子进程就从僵尸态变成了死亡状态,从Z变X状态。然后只剩父进程,父进程继续休眠10s,父进程退出,然后后进程数量变成了0。运行结果如下。
运行结果符合预期。
子进程状态由S变成了Z状态,符合预期。
然后由Z变成了X状态,符合预期。
最终系统进程变成了0,符合预期。
2.waitpid函数。
waitpid有三个返回值,返回值>0,意味着父进程等待成功,返回值为子进程的pid,返回值=0,意味着等待成功,但是没有子进程退出,返回值<0,意味着等待失败。
waitpid函数的功能与wait函数的功能类似。二者的区别仅仅是参数的区别。
wait中的status和waitpid中的status是同一个参数,意义相同,所以我们重点介绍waitpid的三个参数。
第一个参数pid:当pid为-1时,意味着父进程等待任意一个子进程,即与wait函数功能一致。当pid不为-1时,则表明等待pid为当前值的进程。
第二个参数status: 一个输出型参数。
我们知道父进程等待其实就是获取子进程的退出信息,子进程有什么退出信息呢。进程退出我们知道有三种情况,前两种都是代码执行完毕,进程正常退出,所以我们用退出码可以识别进程的退出状态。最后一种是异常退出,异常退出我们用是否存在信号来识别。综上子进程退出的信息无非就有两种进程的退出码和信号。所以父进程通过这两种信息来判断子进程的退出状态。
status概述
status可以看做是一个整型,所以总共有32个比特位,但是我们往往不关心高16位,我们只关心低16位。图示如下:
低16位中的次低8位用于存放子进程的退出码,低8位的0-6位存放信号信息,第7位是core dump标志,这个我们后期在为大家讲解。
所以父进程判断子进程是否是正常退出时,先判断信号为是否为0,如果为0意味着正常退出,则去获取退出码,判断运行结果是否正确。
那么具体的这些退出码信息和信号信息是怎样写入status中的呢?
其实在子进程退出时,它的退出信息(退出码和信号)全部保存在了其pcb中,父进程从子进程的pcb中获取到了退出码信息和信号信息,最终让status和退出码信息和信号信息进行与操作和移位操作,使得退出码信息和信号信息存储在了status中,最终可以通过status获得子进程的退出码和信号信息。但是移位和与操作比较麻烦,操作系统给了两个函数用于获取这两个信息。WIFEXITED(status)用于判断进程是否正常退出,为真则正常退出,否则为异常退出,WEXITSTATUS(status)用于获取子进程的退出码。
第三个参数options:即用于指定父进程是阻塞等待还是非阻塞等待。为0则为阻塞等待,为WNOHANG则为非阻塞等待。
所谓阻塞等待和非阻塞等待其实就是,在等待的过程中父进程是否可以干自己的事情。若为阻塞状态,则意味着父进程的pcb被加载到了等待队列中,父进程不能被cpu运行,所以父进程也就不能干其它的事;若为非阻塞状态,意味着父进程的pcb仍运行队列中,可以被cpu调度,所以可以执行其它事情。
代码如下。
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/wait.h>
int main()
{
pid_t id = fork();
if(id == 0)
{
//child
int cnt=5;
while(cnt){
printf("child[%d] is running,cnt is %d \n",getpid(),cnt);
sleep(1);
cnt--;
}
}
else{
//parent
sleep(10);
//父进程开始进行等待
// int ret=wait(NULL);
int status = 0;
int ret=waitpid(-1,&status,0);
if(ret== 0)
{
//等待成功,但是没有子进程退出
printf("父进程等待成功,没有子进程退出!\n");
}
else if(ret > 0){
//等待成功,有子进程退出
printf("等待成功,子进程pid为:%d\n",ret);
}
else
{
//等待失败
printf("父进程等待失败!\n");
}
sleep(10);
}
}
运行结果如下:
运行结果符合预期。
子进程有由S状态转为Z状态。符合预期。
子进程由Z状态转为X状态,只剩一个父进程,符合预期。
父进程休眠结束,进程退出,进程数量变为0,符合预期。
总结
进程相关的操作已经基本完成,只剩下进程替换,进程替换我们下期单独来讲,因为进程替换相对比较重要。本期的主要内容为进程创建,进程退出,进程等待的操作,大家要掌握相关的概念以及实现这些操作要使用的函数接口以及每个参数的具体含义,特别是进程等待的第二个方法waitpid的参数。
本期内容到此结束^_^