ITPub博客

首页 > > > Linux signal 那些事儿(2)

Linux signal 那些事儿(2)

作者:u_453e1a12781dea 时间:2016-07-05 13:59:56 0 删除 编辑
作者:Bean_lee
来源:ChinaUnix技术博客,本文版权由Bean_lee所有,如需转载,请注明出处。
上一篇博文,基本算是给glibc的signal函数翻了个身。现在glibc的signal基本修正了传统的UNIX的一些弊端,我们说signal并没有我们想象的那么不堪。但是signal也有不尽人意的地方。比如信号处理期间,我们期望屏蔽某些信号,而不仅仅是屏蔽自身,这时候signal就不行了。
    
上面所说的都是signal的一些毛病,但是这些都不是致命的,致命的问题在于老的signal机制的不可靠。信号分成可靠性信号和非可靠性信号,并不是说用sigaction安装,用sigqueue发送的信号就是可靠性性信号,用signal安装,kill/tkill发送的信号就是非可靠性信号。这种理解是错误的。
    
信号值位于[SIGRTMIN,SIGRTMAX] 之间的信号,就是可靠信号,位于[SIGHUP,SIGSYS]之间信号,都是非可靠性信号,与安装函数是signal还是sigaction无关,与发送函数是kill还是sigqueue无关。
1~31之间的所有信号都称为不可靠信号,原因就在于信号不可排队,如果kernel发现同一个信号已经有挂起信号,当前信号就会被丢弃,就好象从来没有被发送过一样,无法引起信号的传递,也无法让进程执行信号处理函数。这种实现的机理,造成了这些信号的不可靠。
    
为了解决这个问题,Linux引入了实时信号,信号值在[32~64]区间内,或者称之为可靠信号。这种信号,kernel不会ignore,哪怕已经有了好多同一个信号,kernel会把新收到信号放入queue之中,等待被传递出去。
    
我参考了Linux Programming Interface 一书的例子,写了两个程序,一个是signal_receiver ,一个是signal_sender.
    
先看signal_receiver的code:  
manu@manu-hacks:~/code/c/self/signal$ cat signal_receiver.c 
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <string.h>
#include <errno.h>


static int sig_cnt[NSIG];
static volatile sig_atomic_t get_SIGINT = 0;

void handler(int signo)
{
    if(signo == SIGINT)
        get_SIGINT = 1;
    else
        sig_cnt[signo]++;
}

int main(int argc,char* argv[])
{
    int i = 0; 
    sigset_t blockall_mask ;
    sigset_t pending_mask ; 
    sigset_t empty_mask ; 
    printf("%s:PID is %ld\n",argv[0],getpid());

    
    for(i = 1; i < NSIG; i++)
    {
        if(i == SIGKILL || i == SIGSTOP)
            continue;

        if(signal(i,&handler) == SIG_ERR)
        {
            fprintf(stderr,"signal for signo(%d) failed(%s)\n",i,strerror(errno));
//            return -1;
        }
    }

    if(argc > 1)
    {
        int sleep_time = atoi(argv[1]);
        sigfillset(&blockall_mask);

        if(sigprocmask(SIG_SETMASK,&blockall_mask,NULL) == -1)
        {
            fprintf(stderr,"setprocmask to block all signal failed(%s)\n",strerror(errno));
            return -2;
        }

        printf("I will sleep %d second\n",sleep_time);

        sleep(sleep_time);
        if(sigpending(&pending_mask) == -1)
        {
            fprintf(stderr,"sigpending failed(%s)\n",strerror(errno));
            return -2;
        }

        for(i = 1 ; i < NSIG ; i++)
        {
            if(sigismember(&pending_mask,i))
            printf("signo(%d) :%s\n",i,strsignal(i));
        }

        sigemptyset(&empty_mask);
        if(sigprocmask(SIG_SETMASK,&empty_mask,NULL) == -1)
        {
            fprintf(stderr,"setprocmask to release all signal failed(%s)\n",strerror(errno));
            return -3;
        }
        
    }

    while(!get_SIGINT)
        continue ; //why not use pause ? I will explain later

    for(i = 1; i < NSIG ; i++)
    {
        if(sig_cnt[i] != 0 )
        {
            printf("%s:signal %d caught %d time%s\n",
                    argv[0],i,sig_cnt[i],(sig_cnt[i] >1)?"s":"");
        }
    }

    return 0;

}
因为我们知道,SIGKILL和SIGSTOP这两个信号是不能够定制自己的信号处理函数的,当然也不能block,原因很简单,OS或者说root才是final boss,必须有稳定终结进程的办法。假如所有的信号,进程都能ignore,OS如何终结进程?

这个signal_receiver会等待所有的信号,接收到某信号后,该信号的捕捉到的次数++,SIGINT会终结进程,进程退出前,会打印信号的捕捉统计。
    
如果进程有参数,表示sleep时间,signal_receiver会先屏蔽所有信号(当然,SIGKILL和SIGSTOP并不能被真正屏蔽)。然后sleep 一段时间后,取消信号屏蔽。我们可以想象,在信号屏蔽期间,我们收到的信号,都会在kernel记录下来,但是并不能delivery,这种信号称之挂起信号。如果在sleep期间或者说信号屏蔽期间,我收到SIGUSR1 这个信号1次和10000次,对内核来说,都是没差别的,因为后面的9999次都会被ignore掉。SIGUSR1属于不可靠信号,位图表示有没有挂起信号,有的话,直接ignore,没有的话,则记录在kernel。
    
然后我们看下,signal_sender: 
manu@manu-hacks:~/code/c/self/signal$ cat signal_sender.c 
#include <stdio.h>
#include <stdlib.h>
#include <getopt.h>
#include <signal.h>
#include <string.h>
#include <errno.h>

void usage()
{
    fprintf(stderr,"USAGE:\n");
    fprintf(stderr,"--------------------------------\n");
    fprintf(stderr,"signal_sender pid signo times\n");
}

int main(int argc,char* argv[])
{
    pid_t pid = -1 ;
    int signo = -1;
    int times = -1;
    int i ; 


    if(argc < 4 )
    {
        usage();
        return -1;
    }
    
    pid = atol(argv[1]);
    signo = atoi(argv[2]);
    times = atoi(argv[3]);

    if(pid <= 0 || times < 0 || signo <1 ||signo >=64 ||signo == 32 || signo ==33)
    {
        usage();
        return -1;
    }

    printf("pid = %ld,signo = %d,times = %d\n",pid,signo,times);

    for( i = 0 ; i < times ; i++)
    {
        if(kill(pid,signo) == -1)
        {
            fprintf(stderr, "send signo(%d) to pid(%ld) failed,reason(%s)\n",signo,pid,strerror(errno));
            return -2;
        }
    }
    fprintf(stdout,"done\n");
    return 0;

}
signal_sender需要三个参数,pid signo times,就是向拿个进程发送什么信号多少次的意思。如 signal_sender 1234 10 10000,含义是向pid=1234的 进程发送10号信号(SIGUSR1),连续发送10000次。

有这两个进程,我们就可以实验了  。 
manu@manu-hacks:~/code/c/self/signal$ ./signal_receiver &

[1] 23416
manu@manu-hacks:~/code/c/self/signal$ ./signal_receiver:PID is 23416
signal for signo(32) failed (Invalid argument)
signal for signo(33) failed (Invalid argument)

manu@manu-hacks:~/code/c/self/signal$ ./signal_sender 23416 10 10000
pid = 23416,signo = 10,times = 10000
done
manu@manu-hacks:~/code/c/self/signal$ sleep 20 ; ./signal_sender 23416 2 1
pid = 23416,signo = 2,times = 1
done
./signal_receiver:signal 10 caught 2507 times
[1]+ Done ./signal_receiver
signal_receiver等待signal的来临,singal_sender向其发送SIGUSR1 10000次,然后sleep 20秒,确保sig_receiver处理完成。但是我们发现,其实一共才caught信号SIGUSR1  2507次,7000多次的发送都丢失了,所以我们称SIGUSR1 是非可靠信号,存在丢信号的问题。
    
我们让可靠信号参战,看下效果:
manu@manu-hacks:~/code/c/self/signal$ ./signal_receiver &
[1] 26067
./signal_receiver:PID is 26067
signal for signo(32) failed (Invalid argument)
signal for signo(33) failed (Invalid argument)
manu@manu-hacks:~/code/c/self/signal$ ./signal_sender 26067 10 10000
pid = 26067,signo = 10,times = 10000
done
manu@manu-hacks:~/code/c/self/signal$ ./signal_sender 26067 36 10000
pid = 26067,signo = 36,times = 10000
done
manu@manu-hacks:~/code/c/self/signal$ ./signal_sender 26067 2 1
pid = 26067,signo = 2,times = 1
done
./signal_receiver:signal 10 caught 2879 times
./signal_receiver:signal 36 caught 10000 times
[1]+ Done ./signal_receiver
可靠性信号36,发送10000次,signal_receiver全部收到,不可靠性信号10,共收到2879次。这个数字是不可预期的,取决于内核进程的调度。
    
这个如果还不够直观,我们在比较一次,让signal_receiver先屏蔽所有信号一段时间,如30s,然后解除屏蔽。
manu@manu-hacks:~/code/c/self/signal$ ./signal_receiver 30 &
[1] 27639
manu@manu-hacks:~/code/c/self/signal$ ./signal_receiver:PID is 27639
signal for signo(32) failed (Invalid argument)
signal for signo(33) failed (Invalid argument)
I will sleep 30 second

manu@manu-hacks:~/code/c/self/signal$ ./signal_sender 27639 10 10000
pid = 27639,signo = 10,times = 10000
done
manu@manu-hacks:~/code/c/self/signal$ ./signal_sender 27639 36 10000
pid = 27639,signo = 36,times = 10000
done
manu@manu-hacks:~/code/c/self/signal$ 
manu@manu-hacks:~/code/c/self/signal$ signo(10) :User defined signal 1
signo(36) :Real-time signal 2

manu@manu-hacks:~/code/c/self/signal$ ./signal_sender 27639 2 1
pid = 27639,signo = 2,times = 1
done
./signal_receiver:signal 10 caught 1 time
./signal_receiver:signal 36 caught 10000 times
[1]+ Done ./signal_receiver 30
这个比较反差比较大,不可靠signal10 共收到1次,可靠性信号36 共caught到10000次。原因就在于sigprocmask将所有的信号都屏蔽了,造成所有的信号都不能delivery。对1~31的信号,内核发现已经有相应的挂起信号,则ignore到新来的信号。但是可靠性信号则不同,会添加队列中去,尽管已经有了相同的信号。需要注意的是,signal pending有上限,并不能无限制的发:
manu@manu-hacks:~/code/c/self/signal$ ulimit -a
core file size (blocks, -c) 0
data seg size (kbytes, -d) unlimited
scheduling priority (-e) 0
file size (blocks, -f) unlimited
pending signals (-i) 15408
max locked memory (kbytes, -l) 64
max memory size (kbytes, -m) unlimited
open files (-n) 1024
pipe size (512 bytes, -p) 8
POSIX message queues (bytes, -q) 819200
real-time priority (-r) 0
stack size (kbytes, -s) 8192
cpu time (seconds, -t) unlimited
max user processes (-u) 15408
virtual memory (kbytes, -v) unlimited
file locks (-x) unlimited
 我发送100万,最终会收到15408个可靠信号:
manu@manu-hacks:~/code/c/self/signal$ ./signal_receiver 30 &
[1] 16488
manu@manu-hacks:~/code/c/self/signal$ ./signal_receiver:PID is 16488
signal for signo(32) failed (Invalid argument)
signal for signo(33) failed (Invalid argument)
I will sleep 30 second

manu@manu-hacks:~/code/c/self/signal$ ./signal_sender 16488 36 1000000
pid = 16488,signo = 36,times = 1000000
done
manu@manu-hacks:~/code/c/self/signal$ signo(36) :Real-time signal 2

manu@manu-hacks:~/code/c/self/signal$ ./signal_sender 16488 2 1
pid = 16488,signo = 2,times = 1
done
./signal_receiver:signal 36 caught 15408 times
[1]+ Done ./signal_receiver 30

内核是怎么做到的?

上图是内核中signal相关的数据结构。其中task_struct中有sigpending类型的成员变量pending
struct task_struct {
    volatile long state; /* -1 unrunnable, 0 runnable, >0 stopped */
    void *stack;
    atomic_t usage;
    unsigned int flags; /* per process flags, defined below */
    unsigned int ptrace;
        ...
        ...
/* signal handlers */
    struct signal_struct *signal;
    struct sighand_struct *sighand;

    sigset_t blocked, real_blocked;
    sigset_t saved_sigmask; /* restored if set_restore_sigmask() was used */
    struct sigpending pending;

       ...
}

struct signal_struct {
      atomic_t     sigcnt;
      atomic_t     live;
      int     nr_threads;
      ...
      ...
      /* shared signal handling: */
      struct sigpending    shared_pending;
      ...
}

struct sigpending {
    struct list_head list;
    sigset_t signal;
};

#define _NSIG        64

#ifdef __i386__
# define _NSIG_BPW    32
#else
# define _NSIG_BPW    64
#endif

#define _NSIG_WORDS    (_NSIG / _NSIG_BPW)

typedef unsigned long old_sigset_t;        /* at least 32 bits */

typedef struct {
    unsigned long sig[_NSIG_WORDS];
} sigset_t;
task_struct中的pending,和signal->shared_pending都是记录挂起信号的数据结构,读到此处,你可能会迷惑,为何有两个这样的结构。这牵扯到thread与信号的一些问题,我们此处简化,就认为是一个就好,后面讲述线程与信号关系的时候,再展开。


我们看到了,kill也好,tkill也罢,最终都走到了_send_signal.当然了kill系统调用根据pid的情况会分成多个分支如pid >0 pid = 0 pid=-1;pid < 0&pid !=-1,总之了,我的图只绘制了pid >0 的分支。tkill也有类似情况。


注:由于文章长度过长,省略“kernel是怎么做到的非可靠信号和可靠信号”和“那么15408的限制是在哪里呢” 两个问题,点击“阅读原文”,查看完整演示!

我们看到,legacy_queue就是用来判断是否是非可靠信号(signo低于32),并且相同signo值已经存在在挂起信号之中,如果是,直接返回。

而对于可靠信号,会分配一个sigqueue的结构,然后讲sigqueue链入到sigpending结构的中链表中。从而就不会丢失信号。当然对pending信号的总数作了限制,限制最多不可超过15408.当然了这个值是可以修改的:

参考文献:
1 Linux programming interface
2 深入理解linux内核

3 linux kernel 3.8.0 内核源码 


点击“阅读原文”查看完整演示!

原文作者:;如有侵权,请联系管理员删除。

请登录后发表评论 登录
全部评论

注册时间:2019-07-17

  • 博文量
    1982
  • 访问量
    17894