前言
前面我们学习了进程地址空间,进程状态,进程优先级以及进程地址空间等等的相关概念。那么操作系统既然作为进程的管理者,那么就意味这操作系统掌握这进程的生杀大权。 而操作系统的对于进程的生杀大权,我们就称之为进程控制。下面我们就来正式谈一下进程控制相关的内容。
进程创建
方式
首先,在Linux下我们有两种新建进程的方式:
1. ./可执行程序
2.使用fork系统调用
理解
而对于第二种使用fork方式创建进程,操作系统主要做了如下的两件事:
1.申请物理内存和创建内核数据结构给子进程
2.以父进程为模板,把对应的数据拷贝到子进程的内核数据结构(task_struct)中
3.把新建立的子进程加入到运行队列中去。
这就是大致的进程创建的整个过程。相对而言,进程创建的过程没有什么太大的理解的难的地方。
进程终止
进程退出的三态
首先对于任何一份代码,我们最终的结果无非就只有三种:
1.代码跑完,结果正确
2.代码跑完,结果不正确
3.代码跑到一半,崩溃退出。
而对于第三种情况,我们暂时先不予讨论。对于1和2,显而易见的是代码都执行完毕,但是结果是不正确的!那么这个时候,用户就会想要知道这个进程的错误原因是什么!所以这个时候就会需要一个信息来标记这个错误的原因! 而这个就是错误码存在的必要性!而这个错误码我们也叫做退出码。通常是一个整数来代表对应的错误信息。
退出码
一个进程一般是通过一下两个方式提供退出码的:
1.main函数的返回值
2.在任何一个地方调用_exit或者exit函数,通过设置_exit或exit参数的值来设置返回值
从学习C语言开始,我们都会默默在最后写上一个return 0,在学习语言的时候。我们没办法很好地去理解main函数返回值的意义。那么今天我们就可以很好地来回答这个问题:main函数的返回值是用来表示这个进程的退出信息的!而对应的退出信息是要被该进程的父进程(或者是操作系统)读取,并让对应的父进程进行子进程的善后工作。 那么要注意:非main函数对应的返回值不能代表进程退出信息!这点要特别注意! 但是有的时候会出现一种需求,当进程执行到非main函数内部出现了不能允许的错误,此时进程需要退出,不能通过return方式返回退出码。那么系统也提供了两个退出进程的函数,它们分别是:
C语言提供的:void exit(int status);–>头文件<stdlib.h>
系统调用:void _exit(int status);—>头文件<unistd.h>
下面我们就来简单的通过一段代码演示一下使用exit设置退出码:
/*
* 使用exit和_exit终止进程
* */
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
void func()
{
printf("func(),我将要退出\n");
//设置进程退出码为1
exit(1);
}
int main()
{
func();
return 0;
}
而在Linux系统中,最近一次进程的退出码的信息被设置在一个叫做?的环境变量里面,我们可以使用echo命令获取对应的进程退出码。
#获取进程退出码信息
echo $?
可以看到这里确实用的就是我们设置的退出码。那么对应的_exit的作用也是类似,这里就不接着演示了。值得一提的是,exit和_exit有一点小小的区别,exit在退出进程的时候会刷新对应的缓冲区里的数据,但是_exit不会。
接下来我们通过一段简单的代码来看一看效果。
/*
* 使用exit和_exit终止进程
* */
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
/*
*演示exit和_exit的区别
*
* */
void test1()
{
//不带\n,这句信息会暂时放在缓冲区中,不会立即刷新
printf("测试exit\n");
printf("我是exit");
sleep(3);
exit(1);
}
int main()
{
test1();
return 0;
}
接下来我们替换成_exit试一试会有什么效果
/*
* 使用exit和_exit终止进程
* */
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
/*
*演示exit和_exit的区别
*
* */
void test1()
{
//不带\n,这句信息会暂时放在缓冲区中,不会立即刷新
printf("测试_exit\n");
printf("我是_exit");
sleep(3);
_exit(1);
}
int main()
{
test1();
return 0;
}
exit的底层实现就是使用的这个_exit,而后面我们使用进程退出的函数的时候多数情况下调用的也是这个exit。这个_exit权当了解即可。
进程等待
我们讲完了进程终止的基本的流程。那么接下来,一个进程进入了僵尸状态,那么必然等待着父进程要对其进行回收和善后工作处理。那么接下来我们就来介绍父进程进行善后处理的方式---->进程等待
先来看一看对应的两个系统接口—>wait()和waitpid()
那么接下来我们对应来看一看对应的代码
/*
* 使用exit和_exit终止进程
* */
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
/*
* 进程等待,使用父进程调用wait接口
* */
int main()
{
int cnt=5;
pid_t id=fork();
if(id==0)
{
//child
while(cnt)
{
printf("我是子进程,我还有%d秒退出\n",cnt--);
sleep(1);
}
exit(2);
}
else
{
//parent
while(1)
{
printf("我是父进程,我等待子进程退出\n");
//这里先设置NULL,后面我们在详细介绍这个参数的作用
int ret=wait(NULL);
if(ret>0)
{
printf("等待成功\n");
break;
}
sleep(1);
}
sleep(3);
}
return 0;
}
而对于wait这个系统调用,实际上可研究的内容不是特别多,这个wait调用针对的任意的子进程!而waitpid这个系统调用接口才是我们需要重点了解和介绍的!
首先我们来看waitpid的函数的结构组成:
pid_t waitpid(pid_t pid, int *status, int options);
第一个参数:pid代表的是你需要等待那一个子进程退出,而如果设置pid=-1就代表等待任意子进程退出!
第二个参数:status表示对应的子进程退出的状态
第三个参数:等待方式,后面会详谈
返回值:等待成功就返回对应的子进程pid,出错就返回-1
那么接下来我们就来用一用waitpid
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
/*waitpid系统调用接口的使用*/
int main()
{
int cnt=5;
pid_t id=fork();
if(id==0)
{
//child
while(cnt)
{
printf("我是子进程,我还有%d秒退出\n",cnt--);
sleep(1);
}
exit(1);
}
else
{
//parent
while(1)
{
printf("我是父进程,我要等待子进程退出\n");
pid_t ret=waitpid(id,NULL,0);
if(ret>0)
{
printf("等待成功\n");
break;
}
}
sleep(3);
}
return 0;
}
我们可以看到,使用waitpid的效果和wait是差不多的。接下来我们就要重点来讲解第二个参数status.
见名知义,status的作用就是获取对应的进程退出的状态。而这里之所以使用指针参数,根本原因就是我们需要输出退出码信息!也就是status是一个输出型的参数!而接下来我们就要来谈一谈Linux进程退出信息码的构成!
我们知道,在Linux下,如果一个进程失控了。那么我们就要使用ctrl+c来终止它!那么这个ctrl+c的本质到底是什么呢?除此之外,我们还有可以终止进程的方式吗?答案是有的!在Linux下,一些进程没有正常结束的原因本质是因为收到了信号!
#在linux下,收到信号的进程会被强行终止
#除了操作系统自动检测到异常发出信号终止,我们也可以自己发出信号终止进程--->使用kill命令
kill -l #查看对应的信号表
注意,Linux下没有0号信号!
那么接下来我们就来看一看Linux下进程退出信息的组成:
接下来我们就通过代码来看一看是不是真的是这样组成的!
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
/*
* 验证Linux退出信息码的组成
* */
int main()
{
int t=5;
pid_t id=fork();
if(id==0)
{
//child
while(1)
{
printf("我是子进程,我还有%d秒退出\n",t--);
sleep(1);
if(!t)
break;
}
//退出码设置奇特一点
exit(123);
}
else
{
//parent
while(1)
{
//等待子进程退出
printf("我是父进程,我等待子进程退出\n");
//获取退出码信息
int status=0;
pid_t ret=waitpid(id,&status,0);
if(ret>0)
{
//status>>8位取到第16位的高8位,按位与全1不变,status右移不影响status
//status&全1不影响第7位
printf("等待成功,退出码是%d,退出信号是%d\n",(status>>8)&0xFF,status&0x7F);
break;
}
}
}
return 0;
}
从运行结果不难可以看出,退出码确实是低16位的高8位。而由于进程没有收到信号,所以这里的信号部分是0,接下来我们来尝试使用信号来终止进程。
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
/*验证信号是低7位*/
int main()
{
pid_t id=fork();
if(id==0)
{
//child
while(1)
{
printf("我是子进程,我永远不会退出,我的pid是%d\n",getpid());
sleep(1);
}
}
else
{
//parent
while(1)
{
printf("我是父进程,我等待子进程退出,子进程的pid是%d\n",getpid());
int status=0;
pid_t ret=waitpid(id,&status,0);
if(ret>0)
{
printf("等待成功,子进程退出码是%d,退出信号是%d\n",(status>>8)&0xFF,status&0X7F);
break;
}
sleep(1);
}
}
return 0;
}
从代码就可以看出,子进程永远不会退出了,那么这个时候要想终止子进程就只能用发送信号的方式来终止进程了。这里我选择发送11号信号
从实验现象也可以得出低7位的数据确实代表的就是退出信号。可能就有细心的小伙伴就会发现,这里的进程退出码是0。但实际上这里的进程退出码已经没有任何的意义了!因为一旦进程收到了信号,意味着进程多数情况下已经是出现异常了,属于第三种代码跑不完的情况!所以收到信号的条件下,进程退出码已经没有任何意义了!
WIFEXITED和WEXITSTATUS
前面我们谈到了退出码的组成。但是对于实际用户来说,前面获取退出码的成本太高!还需要用户理解退出码的底层结构。而操作系统本身就是为了用户而服务的!所以操作系统也提供了对应的两个宏来判断是否退出和提取退出码,这两个宏叫做WIFEXITED和WEXITSTASTUS
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
/*
* 使用系统的宏进行退出判断和退出码提取
* */
int main()
{
pid_t id=fork();
int t=5;
if(id==0)
{
//child
while(1)
{
printf("我是子进程,我还有%d秒退出\n",t--);
if(!t)
break;
sleep(1);
}
exit(134);
}
else
{
//parent
while(1)
{
printf("我是父进程,我等待子进程退出,子进程的pid是%d\n",id);
int status=0;
waitpid(id,&status,0);
if(WIFEXITED(status))
{
printf("子进程退出成功,退出码是%d\n",WEXITSTATUS(status));
break;
}
sleep(1);
}
}
return 0;
}
关于这两个宏的作用我们就介绍到这里。
WNOHANG
接下来我们来看waitpid的第三个参数—>option:这个参数的作用就是指定父进程的等待方式,那么我们要如何理解这个等待方式呢?
首先我们看默认设置为0的是如何等待的
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
int main()
{
pid_t id=fork();
int t=5;
if(id==0)
{
//child
while(1)
{
printf("我是子进程,我还有%d秒退出\n",t--);
if(!t)
break;
sleep(1);
}
exit(134);
}
else
{
//parent
while(1)
{
printf("我是父进程,我等待子进程退出,子进程的pid是%d\n",id);
int status=0;
waitpid(id,&status,0);
printf("能走到这里说明我没有干等子进程退出\n");
if(WIFEXITED(status))
{
printf("等待成功\n");
break;
}
sleep(1);
}
}
运行结果如下:
可以看到,当我们把option设置成0的时候,父进程就在干等子进程退出了,没有执行后续的代码! 对应到现实生活中就是:你去银行办理业务,银行人员在哪里翻看你的账单记录和身份信息,这个时候你就在那里静静等着银行人员处理. 但是还有这样一种情况:当你在和朋友联系的时候,你的朋友暂时有事离开但是他叫你不要挂电话.在这段期间,你觉得闲着没事干就继续做你自己的事情了.也就是没有干等着. 对应到我们的父进程讲option设置成0就是干等着,专业术语叫做阻塞等待,而第二种就是非阻塞等待. 只需要把对应参数设置成WNOHANG即可!
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
int time=6;
/*WNOHANG非阻塞轮询
* */
int main()
{
pid_t id=fork();
if(id==0)
{
//child
while(time)
{
printf("我是子进程,我还有%d秒后就要退出了\n",time--);
sleep(1);
}
exit(1);
}
else
{
//parent
while(1)
{
printf("我是父进程,我在等待子进程退出\n");
int status=0;
int ret=waitpid(id,&status,WNOHANG);
printf("能够打印我说明父进程没有干等子进程退出!\n");
sleep(2);
if(ret>0)
{
printf("父进程等待成功,退出码是%d,退出信号是%d\n",(status>>8)&0xFF,status&0x7F);
break;
}
sleep(1);
}
}
return 0;
}
从效果来看:WNOHANG这组运行起来确实是没有无用功得阻塞下去等待子进程的退出。
而讲到阻塞,我们的脑海中就要浮现出一系列的操作系统背后做的操作
1.将处于运行队列上的进程取下,放入阻塞队列
2.改变进程状态:R->S
而这里的阻塞,可以理解为等待某种软件资源就绪的阻塞。而这里的软件资源就绪就说的是子进程退出!
而当我们把等待选项设置为WNOHANG的时候,对应的waitpid的调用的返回值就十分有讲究了。我们来看官方文档对waitpid返回值的解读:
If WNOHANG was specified in options and there were no children in a waitable state, then waitid() returns 0 immediately
and the state of the siginfo_t structure pointed to by infop is unspecified. To distinguish this case from that where a
child was in a waitable state, zero out the si_pid field before the call and check for a nonzero value in this field
after the call returns.
大致的意思就是:如果等待方式设置成为WNOHANG,那么当等待的子进程没有退出的时候,这个函数就会立即返回0,继续执行后续的代码!而等待成功的时候则会立刻返回对应的子进程id!
以上就是本篇博客的主要内容,如有不足之处还望指出。希望大家一起进步。