在上一章说到用kill发送内核信号中的其中一种给进程,进而进行通信
这一章来讨论通过信号量来改变进程的状态
我们单独的建立两个进程,然后用raise函数杀死进程,查看状态
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<stdlib.h>
#include<signal.h>
int main(void)
{
pid_t pid;
pid=fork();
if(pid>0)
{
printf("I am parent process !\n");
sleep(5);
while(1);
}
if(pid==0)
{
printf("I am chlid process !\n");
printf("raise before !\n");
raise(SIGTSTP);
printf("raise after !\n");
exit(0);
}
return 0;
}
其运行结果以及前后进程的状态如下:
运行前的状态:
运行后的状态:
R+代表正在运行当中,T+代表暂停状态,而Z+代表僵尸状态
接下来我们做一点小改动,然后我们来看看结果有什么变化
在父进程中加入判断是否退出 waitpid(pid,NULL,WNOHANG) 第三个参数WNOHANG代表非阻塞
如果没有退出就用kill命令进行终止进程
#include<unistd.h>
#include<sys/types.h>
#include<stdlib.h>
#include<signal.h>
int main(void)
{
/*
printf("raise before !");
raise(9);//相当于exit(1);一样,库缓存,没有换行符不输出
printf("raise after !\n");
*/
pid_t pid;
pid=fork();
if(pid>0)
{
printf("I am parent process !\n");
sleep(5);
if(waitpid(pid,NULL,WNOHANG)==0)
{
printf("子进程没有退出!\n");
kill(pid,9);
}
while(1);
}
if(pid==0)
{
printf("I am chlid process !\n");
printf("raise before !\n");
raise(SIGTSTP);
printf("raise after !\n");
exit(0);
}
return 0;
}
运行后的状态为:
我们将没有退出的子进程杀死以后没有回收资源, 用wait(NULL);来回收资源 ,不然子进程就变成了僵尸进程
僵尸进程处理方式
任何一个子进程(init除外)在exit()之后,并非马上就消失掉,而是留下一个称为僵尸进程(Zombie)的数据结构,等待父进程处理。这是每个子进程在结束时都要经过的阶段。如果子进程在exit()之后,父进程没有来得及处理,这时用ps命令就能看到子进程的状态是“defunct”。如果父进程能及时处理,可能用ps命令就来不及看到子进程的僵尸状态,但这并不等于子进程不经过僵尸状态。如果父进程在子进程结束之前退出,则子进程将由init接管。init将会以父进程的身份对僵尸状态的子进程进行处理。所以孤儿进程不会占资源,僵尸进程会占用资源危害系统。我们应当避免僵尸进程的出现。
解决方式如下:
1):一种比较暴力的做法是将其父进程杀死,那么它的子进程,即僵尸进程会变成孤儿进程,由系统来回收。但是这种做法在大多数情况下都是不可取的,如父进程是一个服务器程序,如果为了回收其子进程的资源,而杀死服务器程序,那么将导致整个服务器崩溃,得不偿失。显然这种回收进程的方式是不可取的,但其也有一定的存在意义
2):SIGCHLD信号处理
我们都知道wait函数是用来处理僵尸进程的,但是进程一旦调用了wait,就立即阻塞自己,由wait自动分析是否当前进程的某个子进程已经退出,如果让它找到了这样一个已经变成僵尸的子进程,wait就会收集这个子进程的信息,并把它彻底销毁后返回;如果没有找到这样一个子进程,wait就会一直阻塞在这里,直到有一个出现为止。我们先来看看wait函数的定义
于调用wait之后,就必须阻塞,直到有子进程结束,所以,这样来说是非常不高效的,我们的父进程难道要一直等待你子进程完成,最后才能执行自己的代码吗?难道就不能我父进程执行自己的代码,你子进程什么时候完成我就什么时候去处理你,不用一直等你?当然是有这种方式了。
实际上当子进程终止时,内核就会向它的父进程发送一个SIGCHLD信号,父进程可以选择忽略该信号,也可以提供一个接收到信号以后的处理函数。对于这种信号的系统默认动作是忽略它。我们不希望有过多的僵尸进程产生,所以当父进程接收到SIGCHLD信号后就应该调用 wait 或 waitpid 函数对子进程进行善后处理,释放子进程占用的资源。
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<assert.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<signal.h>
void deal_child(int num)
{
printf("deal_child into\n");
wait(NULL);
}
int main()
{
signal(SIGCHLD,deal_child);
pid_t pid=fork();
int i;
if(pid==0)
{
printf("child is running\n");
sleep(2);
printf("child will end\n");
}
else
{
sleep(1); //让子进程先执行
printf("parent is running\n");
sleep(10); //一旦被打断就不能再进入睡眠
printf("sleep 10 s over\n");
sleep(5);
printf("sleep 5s over\n");
}
exit(0);
}
进行测试后确定了是在父进程睡眠10s时子进程结束,父进程接收到了SIGCHLD信号,调用了deal_child函数,释放了子进程的PCB后又回到自己本身的代码中执行。我们看看运行结果
说到这里,我们再来看看signal函数(不是阻塞函数)
signal(参数1,参数2);
参数1:我们要进行处理的信号。系统的信号我们可以再终端键入 kill -l查看(共64个)。其实这些信号是系统定义的宏。
参数2:我们处理的方式(是系统默认还是忽略还是捕获)。
eg: signal(SIGINT ,SIG_ING ); //SIG_ING 代表忽略SIGINT信号
eg:signal(SIGINT,SIG_DFL); //SIGINT信号代表由InterruptKey产生,通常是CTRL +C或者是DELETE。发送给所有ForeGroundGroup的进程。 SIG_DFL代表执行系统默认操作,其实对于大多数信号的系统默认动作是终止该进程。这与不写此处理函数是一样的。
我们也可以给参数2传递一个信号处理函数的地址,但是这个信号处理函数需要其返回值为void,并且默认自带一个int类型参数
这个int就是你所传递的第一个信号参数的值(你用kill -l可以查看)
我们测试了一下,如果创建了5个子进程,但是销毁的时候仍然有两个仍是僵尸进程,这又是为什么呢?
这是因为当5个进程同时终止的时候,内核都会向父进程发送SIGCHLD信号,而父进程此时有可能仍然处于信号处理的deal_child函数中,那么在处理完之前,中间接收到的SIGCHLD信号就会丢失,内核并没有使用队列等方式来存储同一种信号
所以为了解决这一问题,我们需要调用waitpid函数来清理子进程。
void deal_child(int sig_no)
{
for (;;) {
if (waitpid(-1, NULL, WNOHANG) == 0)
break;
}
}
最后呢至于什么是孤儿进程
一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。孤儿进程将被init进程(进程号为1)所收养,并由init进程对它们完成状态收集工作。
子进程死亡需要父进程来处理,那么意味着正常的进程应该是子进程先于父进程死亡。当父进程先于子进程死亡时,子进程死亡时没父进程处理,这个死亡的子进程就是孤儿进程。
但孤儿进程与僵尸进程不同的是,由于父进程已经死亡,系统会帮助父进程回收处理孤儿进程。所以孤儿进程实际上是不占用资源的,因为它终究是被系统回收了。不会像僵尸进程那样占用ID,损害运行系统。
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<assert.h>
#include<sys/types.h>
int main()
{
pid_t pid=fork();
if(pid==0)
{
printf("child ppid is %d\n",getppid());
sleep(10); //为了让父进程先结束
printf("child ppid is %d\n",getppid());
}
else
{
printf("parent id is %d\n",getpid());
}
exit(0);
}
从执行结果来看,此时由pid =4168
父进程创建的子进程,其输出的父进程pid = 1,说明当其为孤儿进程时被init进程回收,最终并不会占用资源
这就是为什么要将孤儿进程分配给init进程。
对了还有一个alarm函数忘记介绍了,alarm函数是用于设置闹钟的,到指定时间之后去执行Linux内核捕捉的signal信号的服务函数
老样子上代码:
#include<stdio.h>
#include<stdlib.h>
#include<sys/types.h>
#include<unistd.h>
void my_fun(int pid_num)
{
int i=0;
for(;i<10;i++)
{
printf("%d the pid_num=%d \n",i,pid_num);
}
}
int main(void)
{
int i=0;
signal(14,my_fun);
alarm(5);
for(;i<10;i++)
{
printf("%d \n",i);
sleep(1);
}
return 0;
}
运行结果如下:
闹钟设置5秒之后去执行服务函数,如果没有用signal函数捕捉,主进程将被终止