可重入函数,竞态条件与sigsuspend函数,SIGCHLD信号

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)函数原型:
(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;
}          

代码执行效果如下:

四. 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; }

运行结果如下:
        我们可以看到,父进程在做自己的事,仅在子进程退出时,调用了自定义函数将子进程回收了。

实现代码如下:
#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;
}                       

运行结果如下:
        检测进程状态如下:
        可以看到,当子进程退出时不会有僵尸状态出现,而且不会通知父进程,在进程运行界面,父进程不会接收到子进程的退出消息,只会做自己的事情。

猜你喜欢

转载自blog.csdn.net/lycorisradiata__/article/details/80143586