Unix信号机制(下)

信号(下)

(一)时序竞态

在信号(上)里面讲解了信号基础的用法。但是考虑一下这样的场景,比如我用alarm函数定时3秒,但是在定时完成后,cpu调度去执行其它的进程了,过了4秒,才回到之前执行到的地方,但是alarm定时的时间已经过了,那么还没等到执行下一条语句,信号就先被处理了,可能导致程序的逻辑出现问题。这里用一个例子说明。
首先介绍一个函数
函数原型:int pause(void)
返回值:只有执行了一个信号处理程序并从其返回时,pause才返回。并且只会返回-1,并设置errno为EINTR。也就是说,没有失败的情况。
参数:无
头文件:#include <unistd.h>

这下我们就可以借助alarm()pause()完成一个我们自己编写的sleep函数了。

#include <stdio.h>
#include <unistd.h>
#include <signal.h>

typedef void (*sighandler_t)(int);
void sig_catch(int signo)           //处理函数
{
    printf("-----------catch\n");
}

unsigned int mysleep(unsigned int seconds)
{

    sighandler_t reback;
    reback = signal(SIGALRM, sig_catch);        //注册捕捉函数
    if(reback == SIG_ERR)       //调用失败
    {
        return seconds;         //返回剩余的秒数
    }
    alarm(seconds);             //定时
    pause();                    //挂起等待
    signal(SIGALRM, reback);    //恢复signal原来的默认处理动作
    return alarm(0);
}
int main()
{
    mysleep(5);
    return 0;
}

这段代码,在一个负载不是很严重的系统上运行一般不会出现问题。但是请注意之前说到的一个现象,当执行到定时那一行代码,准备执行pause()函数时,cpu调度到了另一个进程,用了6s,那么此时的定时期alarm定时的时间已经过了,信号一发送,就马上被处理掉,这个时候回到本进程,pause()函数就再也等不到alarm()函数发送的SIGALRM信号了,就会一直处于挂起状态。

既然信号是提前发送并处理了,那们我们可以用屏蔽信号的方法来阻止信号被预先处理,意思就是在执行pause函数之前把SIGALRM信号屏蔽了,然后在即将执行pause函数时,再解除屏蔽。

#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <stdlib.h>
void sig_catch(int signo)       //处理函数
{
    printf("-------catch\n");
}

int main()
{
    struct sigaction act, old_act;  //act代表新的处理动作,oldact代表旧的动作
    int ret;
    sigemptyset(&act.sa_mask);      //清空act中的信号屏蔽字
    act.sa_flags = 0;               //使用默认属性
    act.sa_handler = sig_catch;     //指定处理函数
    ret = sigaction(SIGALRM, &act, &old_act);   //注册捕捉
    if(ret == -1)
    {
        perror("sigaction error");
        exit(1);
    }

    sigset_t mask, old_mask;    //old_mask用来记录进程之前的信号屏蔽字
    sigemptyset(&mask);         //清空masl
    sigaddset(&mask, SIGALRM);  //将SIGALRM信号加入mask中
    sigprocmask(SIG_BLOCK, &mask, &old_mask);   //设置屏蔽SIGALRM

    alarm(5);       //定时开始
    sigprocmask(SIG_UNBLOCK, &mask, NULL);  //解除屏蔽
    pause();
    sigaction(SIGALRM, &old_act, NULL);
    sigprocmask(SIG_SETMASK, &old_mask, NULL);  //恢复进程之前的屏蔽字
    return 0;
}

仔细一看会发现一个问题,在解除屏蔽SIGALRM信号完成后,然后cpu就调度到了其它进程,这样的结果和之前一样,都会导致pause()接收不到任何信号,造成一直挂起。问题就出在解除屏蔽和挂起之间,只要能把这两个操作合成一个操作,那就不会导致有这样的现象出现了。而Unix中就有这样的一个函数。

函数原型:int sigsuspend(const sigset_t *mask) //挂起等待信号
返回值:总是返回-1,如果正常返回,会设置errno为EINTR
参数:mask是一个信号屏蔽字,里面屏蔽了需要等待的信号

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <signal.h>

void sig_catch(int signo)       //处理函数
{
    printf("-----------catch\n");
}

int main()
{
    /* 前面的步骤都一样 */
    struct sigaction act, old_act;
    sigemptyset(&act.sa_mask);
    act.sa_flags = 0;
    act.sa_handler = sig_catch;

    int ret = sigaction(SIGALRM, &act, &old_act);
    if(ret < 0)
    {
        perror("sigaction error");
        exit(1);
    }

    /* 和上段代码开始出现不同 */
    sigset_t mask, old_mask, sus_mask; //sus_mask是一个临时的信号屏蔽字,只在sigsuspend函数期间生效,并且该函数调用完之后,信号屏蔽字恢复为调用之前的值
    sigemptyset(&mask);
    sigaddset(&mask, SIGALRM);
    sigprocmask(SIG_BLOCK, &mask, &old_mask);       //屏蔽SIGALRM信号

    alarm(5);
    sus_mask = old_mask;    //将旧的信号屏蔽字赋给sus_mask
    sigdelset(&sus_mask, SIGALRM);  //为了防止之前的信号屏蔽字已经屏蔽了SIGALRM,所以保险起见,删掉SIGALRM

    sigsuspend(&sus_mask);  //传入sus_mask,等待信号

    sigaction(SIGALRM, &old_act, NULL); //恢复为默认的动作
    sigprocmask(SIG_SETMASK, &old_mask, NULL);  //恢复为之前的信号屏蔽字

    return 0;
}

这样就有效的避免了由于时序竞态造成的问题,要切记使用sigsuspend函数时传入的信号屏蔽字只是临时的,调用完毕后进程的信号屏蔽字会自动恢复成调用之前的样子。

(二).可/不可重入函数

在《UNIX环境高级编程》中,解释的比较清楚。以下是原话

进程捕捉到信号并对其进行处理时,进程正在执行的正常指令序列就被信号处理程序临时中断,它首先执行该信号处理程序中的指令。如果从信号处理程序返回(例如没有调用exit或longjmp),则继续执行在捕捉到信号时进程正在执行的正常指令序列(这类似于发生硬件中断时所做的)。但在信号处理程序中,不能判断捕捉到信号时进程执行到何处。如果进程正在执行malloc在其堆中分配另外的存储空间,而此时由于捕捉到信号而插入执行该信号处理程序,其中又调用malloc,这时会发生什么。。。。。。。。在malloc例子中,可能会对进程造成破坏,因为malloc通常为它所分配的存储区维护一个链表,而插入执行信号处理程序时,进程可能正在更改此链表。

也就是说,可重入函数要求在执行该函数的过程中,即使被中断了之后,再次回来继续执行,也不会造成异常,这就叫可重入函数。书中还列出了大部分可重入函数,并提出了三点是不可重入函数的特征:
1.使用了静态数据结构
2.调用了malloc或free
3.是标准I/O函数(标准I/O函数的很多实现都以不可重入方式使用全局数据结构)

所以我们需要避免捕捉到信号调用的函数是不可重入函数。

(三).SIGCHLD信号

这个信号会在1).子进程终止时。2).子进程接收到SIGSTOP信号停止时。3).子进程处在停止态,接收到SIGCONT后唤醒时。这几种情况产生。
而在子进程结束时,它的父进程会收到SIGCHLD信号,而该信号的默认处理动作就是忽略,所以我们可以利用这一点,对该信号进行捕捉,然后完成子进程的回收。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <signal.h>

void sys_err(char *str)
{
    perror(str);
    exit(1);
}

void do_sig_child(int signo)        //处理函数
{
    int status;
    pid_t pid;
    while((pid = waitpid(0, &status, WNOHANG)) > 0)  //关键之处,应该使用while而不是if
    {
        if(WIFEXITED(status))
            printf("---------child %d exit %d\n", pid, WEXITSTATUS(status));
        else if(WIFSIGNALED(status))
            printf("child %d cancel signal %d\n", pid, WTERMSIG(status));
    }
}

int main(void)
{
    pid_t pid;
    int i;

    struct sigaction act;

    act.sa_handler = do_sig_child;
    act.sa_flags = 0;
    sigemptyset(&act.sa_mask);
    sigaction(SIGCHLD, &act, NULL);     //注册捕捉函数

    for(i = 0; i < 10; i++)     //循环创建10个子进程
    {
        if((pid = fork()) == 0)
            break;
        else if(pid < 0)
            sys_err("fork");
    }
    if(pid == 0)        //子进程执行代码
    {
        int n = 1;
        while(n--)
        {
            printf("child ID %d\n", getpid());
        }
        return i+1; 
    }
    else if(pid > 0)    //父进程执行代码
    {

        while(1)
        {
            printf("Parent Id %d\n", getpid());
            sleep(1);
        }
    }
    return 0;
}

这段代码最关键的地方就是do_sig_child函数中为什么使用的是while而不是if,在上一篇博客中我们提到过,信号不支持排队,也就是说,假如有一个子进程死亡之后,正在处理该信号时,突然又有多个子进程死亡了,这样就会造成子进程的回收不完全,会遗漏,导致僵尸进程的产生,用while可以多次调用waitpid函数,让多个处于死亡的子进程统一进行回收,这样就不会造成遗漏了。

(四).信号传参

函数原型:int sigqueue(pid_t pid, int sig, const union sigval value)
返回值:成功返回0.失败返回-1,并设置errno
参数:pid:代表要发送给哪个进程,填进程号;sig:要发送的信号;value:携带的数据

union sigval{
    int sival_int;
    void *sival_ptr;
}

需要注意,不同的进程拥有不同的虚拟空间,所以传地址是没有意义的。

如果我们想要接收到传递的参数,那就应该使用sigaction函数中struct sigaction结构体中的另外一个成员,也就是void (*sa_sigaction)(int, siginfo_t *, void *),并且此时的sa_flags的值应为SA_SIGINFO而不是默认属性。

(五).中断系统调用

系统调用分为两类:慢速系统调用和其它系统调用
1.慢速系统调用:可能会使进程永久阻塞。例如wait read pause等
2.其它系统调用:比如fork getpid

慢速系统调用被中断的行为就如同之前测试的pause一样,必须接收到信号,并且该信号是被捕捉,而不是默认或者忽略。并且中断后返回-1,设置errno为EINTR。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值