我是靠谱客的博主 高大煎饼,最近开发中收集的这篇文章主要介绍深入研究signal和sigaction,觉得挺不错的,现在分享给大家,希望可以做个参考。

概述

信号通常使用一个无符号长整数(32位)中的比特位表示各种不同的信号。因此最多可以表示32个不同的信号。signal()和sigaction()的功能比较类似,都是更改信号原处理句柄(handler,或称为处理程序)。但signal()就是内核操作上述传统信号的方式,在某些特殊时刻可能会造成信号丢失。

之前介绍过signal()函数,signal()函数的返回值是一个无返回值且具有一个整型参数的函数指针,是默认的处理方式。并且在新句柄被调用执行过一次后,信号处理句柄又会被恢复成默认处理句柄值SIG_DFL。
在linux0.11源码中,include/signal.h文件中,默认句柄SIG_DFL和忽略处理句柄SIG_IGN的定义是:

#define SIG_DFL     ((void (*)(int))0)
#define SIG_IGN     ((void (*)(int))1)

signal()函数不可靠的原因在于当信号已经发生而进入自己设置的信号处理函数中,但在重新再一次设置自己的处理句柄之前,在这段时间内有可能又有同样的信号发生了。但是此时系统已经把处理句柄设置成默认值。因为就有可能造成信号丢失。
为了防止信号的丢失,sigaction()函数是比signal()函数更安全的选择。如果坚持要用signal(),可以在自己的信号处理函数开头再重新调用signal()函数做相同的处理。
sigaction()函数比signal()函数更可靠的原因是,在信号处理函数正在处理时,被捕捉的信号在处理期间会被自动屏蔽,并且用struct sigaction结构体中的void (*sa_handler)(int)成员修改了信号的处理方式之后,除非再改回来,否则就一直使用修改之后的处理函数。

下面附上signal()函数在内核中的系统调用代码及注释:

int sys_signal(int signum, long handler, long restorer)
{
    struct sigaction tmp;   //用来代替当前进程对signum的处理方式以及信号屏蔽字

    if (signum<1 || signum>32 || signum==SIGKILL)   //当signum不合法时直接返回-1,SIGKILL不能被改变
        return -1;
    tmp.sa_handler = (void (*)(int)) handler;   //设置句柄为传入的handler
    tmp.sa_mask = 0;                            //屏蔽字设置为什么都不屏蔽
    tmp.sa_flags = SA_ONESHOT | SA_NOMASK;      //设置属性,SA_ONESHOT即使用一次就恢复到默认值,SA_NOMASK表示设置不启用位图屏蔽信号,即这个时候可以允许多个相同的信号一起触发。
    tmp.sa_restorer = (void (*)(void)) restorer;//这个成员起到内核用来保护恢复处理函数的作用
    handler = (long) current->sigaction[signum-1].sa_handler;   //把当前进程的信号处理函数保存到handler中
    current->sigaction[signum-1] = tmp;     //把当前进程原来的该信号的处理方式等替换成我们新设置的
    return handler;     //返回默认处理动作
}

看完了是不是有种原来如此的感觉,接下来是sigaction()函数在内核中的系统调用代码及注释:

int sys_sigaction(int signum, const struct sigaction * action,
    struct sigaction * oldaction)
{
    struct sigaction tmp;       //首先也声明一个中间变量

    if (signum<1 || signum>32 || signum==SIGKILL)   //如果signum不合法直接返回-1,并且SIGKILL不能被改变
        return -1;
    tmp = current->sigaction[signum-1];     //将中间变量的值赋成signum对应的信号的相关属性
    //在信号的sigaction结构中设置新的处理动作
    get_new((char *) action,        
        (char *) (signum-1+current->sigaction));
    //这个函数的作用是:如果需要将以前的处理方式传出的话,就把原来的处理方式保存到oldaction中
    if (oldaction)
        save_old((char *) &tmp,(char *) oldaction);
    //如果允许信号在处理中接收到本信号就令屏蔽码为0,否则设置屏蔽本信号 
    if (current->sigaction[signum-1].sa_flags & SA_NOMASK)
        current->sigaction[signum-1].sa_mask = 0;
    else
        current->sigaction[signum-1].sa_mask |= (1<<(signum-1));
    return 0;
}

这样我们就了解了signal()函数和sigaction()函数在内核中的实现。但是还有一个更底层的问题,一个信号是怎么中断当前进程,进入到内核并设置处理函数,然后再处理掉的呢。
在内核中,实现这个功能的函数是do_signal()函数,源码及注释如下:

void do_signal(long signr,long eax, long ebx, long ecx, long edx,
    long fs, long es, long ds,
    long eip, long cs, long eflags,
    unsigned long * esp, long ss)
{
    unsigned long sa_handler;
    long old_eip=eip;       //cs:eip确定了用户程序中下一条指令执行的位置,old_eip=eip很明显是将用户程序中原来即将执行的位置保存起来
    struct sigaction * sa = current->sigaction + signr - 1;     //这行代码其实和之前的tmp = current->sigaction[signum-1];这行类似,只不过这里是用的指针,指向的current->sigaction+signr-1,对照着想一下就能理解
    int longs;
    unsigned long * tmp_esp;    //看到esp就知道一定是和栈顶指针有关的

    sa_handler = (unsigned long) sa->sa_handler;    //将当前进程的处理句柄赋值给sa_handler
    if (sa_handler==1)      //1代表SIG_IGN(忽略),如果还是以前的处理动作,就可以直接返回了
        return;
    if (!sa_handler) {      //如果是SIG_DEL默认处理
        if (signr==SIGCHLD) //如果对应的是SIGCHLD(子进程给父进程发的信号,默认处理动作是忽略)
            return;
        else                //否则终止进程的执行,do_exit()函数是exit()函数的内部实现
            do_exit(1<<(signr-1));
    }
    if (sa->sa_flags & SA_ONESHOT)  //如果该信号句柄只需执行一次(sa_flags其实是位掩码,所以能进行相与操作来判断),则将该句柄置空
        sa->sa_handler = NULL;
    *(&eip) = sa_handler;       //将用户调用系统调用的代码指针eip指向该信号处理句柄
    longs = (sa->sa_flags & SA_NOMASK)?7:8; //如果允许信号自己的处理句柄接收到自己,就也需要把进程的阻塞码压入堆栈,7或者8代表栈要下移的值(以4字节为单位)。
    //将栈顶指针向下扩展7/8个字长(用来存放调用信号句柄的参数等)并检查内存使用情况
    *(&esp) -= longs;       
    verify_area(esp,longs*4);
    // 在用户堆栈中从下到上存放 sa_restorer, 信号 signr, 屏蔽码 blocked(如果 SA_NOMASK 置位),
    // eax, ecx, edx, eflags 和用户程序原代码指针。
    tmp_esp=esp;
    put_fs_long((long) sa->sa_restorer,tmp_esp++);
    put_fs_long(signr,tmp_esp++);
    if (!(sa->sa_flags & SA_NOMASK))
        put_fs_long(current->blocked,tmp_esp++);
    put_fs_long(eax,tmp_esp++);
    put_fs_long(ecx,tmp_esp++);
    put_fs_long(edx,tmp_esp++);
    put_fs_long(eflags,tmp_esp++);
    put_fs_long(old_eip,tmp_esp++);
    current->blocked |= sa->sa_mask;    //进程的屏蔽码添加上sa_mask上的码位
}

这里引用《linux内核完全注释》书中的一张图,可以很好的理解do_signal函数的工作过程。

这里写图片描述

最后

以上就是高大煎饼为你收集整理的深入研究signal和sigaction的全部内容,希望文章能够帮你解决深入研究signal和sigaction所遇到的程序开发问题。

如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(87)

评论列表共有 0 条评论

立即
投稿
返回
顶部