Linux信号-可重入函数&竞态条件与sigsuspend函数&SIGCHLD信号
一. 可重入函数
1. 定义的简单介绍
当main函数在调用一个函数如insert函数还未返回时,由于信号中断在执行信号的自定义行为时,再次调用insert函数,这就叫
重入
。如果insert函数访问的是一个全局链表,有可能因为重入导致结果错乱,这样的函数就叫
不可重入函数
。反之,如果insert函数只访问自己的局部变量或参数,则称为
可重入函数。因为局部变量保存在各自的栈帧结构中,互不影响。
如果一个函数满足以下条件之一,则是不可重入的:
(1)调用了malloc或者free,因为malloc也是用全局链表来管理堆的;
(2)调用了标准I/O库函数。标准I/O库的很多实现都是以不可重入的方式使用全局数据结构。
二. 竞态条件
在上一篇博客的最后,我实现的mysleep函数中,其实还是存在一点点的问题:
mysleep函数实现博客(mysleep函数实现链接)
问题在设置完闹钟与调用pause函数挂起等待时,若在设置完闹钟之后,内核调度了优先级更高的进程取代了当前进程去执行,等n秒之后,闹钟响了,内核发送SIGALRM信号给当前进程,处于未决状态。等若干时间之后更高优先级的进程被执行完了,内核切换回来这个进程,在返回用户态之前发现有信号处于未决状态,所以返回用户态直接执行信号处理函数,执行完信号处理函数之后进程自动返回内核态。此时无信号需要处理,进程返回用户态继续执行主控制流程语句,即调用pause函数让进程挂起等待直至信号递达。可是,SIGALRM信号已经处理过了,还等什么呢?
(这里不考虑程序中还设置有其他闹钟,恰好在我们想要的时间响起)
出现这个问题的根本原因就是,系统运行的时序并不像我们写程序时所预想的那样。虽然alarm(t)之后紧接着就是pause(),但是我们无法保证pause()一定会在调用alarm(t)的t秒之内被调用。由于异步事件在任何时间都有可能会发生(这里的异步事件指的是有更高级的进程),如果在写程序时我们考虑的不够周密,就有可能由于时序问题导致错误,这叫做竞态条件。
那mysleep函数中出现的问题我们怎么解决呢?
我们可以在调用pause()函数之前,屏蔽SIGALRM信号,使得它不能递达。就比如说:
(1)屏蔽SIGALRM信号;(2)alarm(t);(3)解除对SIGALRM信号的屏蔽;(4)pause( )
这样实现的原理没有问题,但是在(3)(4)之间也有可能当前进程会被内核切走,从而出现相同的问题。所以我们的解决办法是将要将(3)(4)两步合并成为一个原子操作就好了,这就是sigsuspend函数的功能。
三. sigsuspend函数
(1)函数原型:
![](https://i-blog.csdnimg.cn/blog_migrate/5af27b0679255ddccb7aeeddacef44b5.png)
(2)函数功能:如上所说,sigsuspend函数包含了pause函数挂起等待功能,同时解决了竞态条件的问题,在对时序要求严格的情况下,我们都应该采用sigsuspend而不是pause。
(3)参数:mask参数指定了进程的信号屏蔽字
(4)返回值:和pause函数一样,sigsuspend没有成功的返回值,只有执行了一个信号处理函数之后sigsuspend才会返回,返回值为-1,errno设置为EINTR。
(5)说明:使用该函数时,可以通过指定mask来临时解除对某个信号的屏蔽,然后挂起等待,当sigsuspend返回时,进程的信号屏蔽字恢复为原来的值。如果原来对该信号是屏蔽的,从sigsuspend函数返回后仍是屏蔽的。
四. 修改mysleep函数
将上篇博客中的mysleep函数我们可以修改,利用sigsuspend函数实现代码如下:
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
void handler(int num)
{
;
}
unsigned int mysleep(unsigned int t)
{
struct sigaction act,oact;//act为要设置闹钟信号的相关信息,oact保存闹钟信号
的原有相关信息
sigset_t new,old;
act.sa_handler = handler;
act.sa_flags = 0;
sigemptyset(&act.sa_mask);//处理闹钟信号时,不屏蔽其他信号
sigaction(SIGALRM,&act,&oact);//捕捉闹钟信号
sigemptyset(&new);//初始化信号集
sigaddset(&new,SIGALRM);//将闹钟信号在信号集内设为有效信号
sigprocmask(SIG_BLOCK,&new,&old);//屏蔽闹钟信号
alarm(t);//t秒之后向进程发送闹钟信号
sigdelset(&new,SIGALRM);//将信号集中闹钟信号置为无效
sigsuspend(&new);//解除对闹钟信号的屏蔽,并使进程挂起等待
unsigned int ret = alarm(0);//取消闹钟,返回闹钟剩余秒数
sigaction(SIGALRM,&oact,NULL);//恢复闹钟的默认处理动作
sigprocmask(SIG_UNBLOCK,&old,NULL);//取消对闹钟信号的屏蔽
return ret;//返回闹钟剩下的时间
}
int main()
{
while(1)
{
printf("hi,i am youngmay\n");
mysleep(1);
}
return 0;
}
代码执行效果如下:
![](https://i-blog.csdnimg.cn/blog_migrate/94a992cf6e633a40bc40c6a48c8751f2.png)
四. SIGCHLD信号
在进程一章有说过,父进程要等待子进程退出,了解子进程的退出信息以及清理回收子进程。
点击打开链接 在父进程等待子进程退出时,父进程可以有阻塞式等待或者是非阻塞式等待两种方式,但是这两种方式其实工作效率都较低。
其实子进程在终止的时候会给父进程发SIGCHLD信号,该信号的默认处理动作是忽略,父进程可以自定义SIGCHLD信号的处理函数,这样父进程只需要专心处理自己的事情,而不用关心子进程了。子进程在终止时,会通知父进程,父进程只要在信号处理函数中调用函数等待清理子进程即可。但是需要注意的一点是在自定义函数中,我们用来等待子进程退出时,要轮询式访问。若是阻塞式等待,进程可能会一直阻塞在自定义信号捕捉函数中,这样也会使得效率较低。
事实上,我们可以在父进程调用sigaction将SIGCHLD信号的处理动作设置为SIG_IGN。这样fork出来的子进程在终止时,会自动清理掉,不会产生僵尸进程,也不会通知父进程。但是该方法在Linux下适用,但是不保证在其他UNIX系统上都可用。
实现代码如下:
#include <stdio.h> #include <unistd.h> #include <sys/types.h> #include <sys/wait.h> #include <stdlib.h> void handler(int num)//信号处理函数 { pid_t id; //这里如果不用轮询式访问,进程可能会一直阻塞在当前自定义函数,同样也使得效率低下 while((id = waitpid(-1, NULL ,WNOHANG)) > 0)//确保所有子进程都被回收 { printf("wait child success: %d\n",id); } printf("child is quit! pid %d\n",getpid()); }
int main() { pid_t id = fork(); struct sigaction act,oact; act.sa_handler = handler; act.sa_flags = 0; sigaction(SIGCHLD,&act,&oact); if(id < 0)//创建子进程失败 { perror("fork"); return -1; } else if(id == 0)//child { printf("child: pid is %d\n",getpid()); sleep(3); exit(1); } else//father { while(1) { //handler(SIGCHLD); printf(" I am father\n"); sleep(1); } } return 0; }
运行结果如下:
![](https://i-blog.csdnimg.cn/blog_migrate/45076e249d35c29eddf4a141901e2972.png)
我们可以看到,父进程在做自己的事,仅在子进程退出时,调用了自定义函数将子进程回收了。
实现代码如下:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>
void handler(int num)//信号处理函数
{
pid_t id;
//这里如果不用轮询式访问,进程可能会一直阻塞在当前函数
while((id = waitpid(-1, NULL ,WNOHANG)) > 0)//确保所有子进程都>被回收
{
printf("wait child success: %d\n",id);
}
printf("child is quit! pid %d\n",getpid());
}
int main()
{
pid_t id = fork();
struct sigaction act,oact;
act.sa_handler = SIG_IGN;
act.sa_flags = 0;
sigaction(SIGCHLD,&act,&oact);
if(id < 0)
{
perror("fork");
return -1;
}
else if(id == 0)//child
{
printf("child: pid is %d\n",getpid());
sleep(3);
exit(1);
}
else//father
{
while(1)
{
printf(" I am father\n");
sleep(1);
}
}
return 0;
}
运行结果如下:
![](https://i-blog.csdnimg.cn/blog_migrate/0d0616d269c560da33bda6caddf81464.png)
检测进程状态如下:
![](https://i-blog.csdnimg.cn/blog_migrate/fef2e77c0948e77fd56caabe33e76815.png)
可以看到,当子进程退出时不会有僵尸状态出现,而且不会通知父进程,在进程运行界面,父进程不会接收到子进程的退出消息,只会做自己的事情。