Linux下的信号机制

2017-04-06

之前在看LinuxThreads线程模型的时候,看到该模型是通过信号实现线程间的同步,当时没有多想,直接当做信号量了,现在想起来真是汗颜……后来想想并不是那么回事,于是,就有了今天这篇博文!

其实关于信号的文章,网上有很多,写的也很好,而笔者仅仅是想把自己的想法记录下来,一来帮助自己捋顺思路,二来说不定还可以帮助他人理解,如有错误,还请指正!


一、总体介绍

Linux的信号在实现机制上根中断很类似,以至于有文献把linux信号作为软中断,这当然在一定程度上是说的过去,但是笔者不建议这么做,因为虽然二者的实现机制又相同之处,但是二者所处理的事务千差万别;况且软中断在Linux中是一个专有名词,比如中断处理下半部常作为软中断的方式存在,而软中断的记录根CPU相关而根某个进程无关,这里强行把信号作为软中断对于初学者的理解也有害无益,至少应该明确说明区别,想当初笔者本科时由老师提到异常就是软中断,这概念因为先入为主,在笔者后来看到真正的软中断时迟迟不能接受。至于软中断,可参考笔者另一篇博文。

  当然把信号作为软中断也并非空穴来风,自然有其道理。首先,二者均是作为异步事件的处理方式。相对于操作系统来讲,中断作为异步方式存在已经众人皆知,信号也不例外。但是信号相对的,是进程,而并非操作系统。说到这里,还有文献说进程其实就是一个虚拟机,当然仅仅是广义上的。那么从这个角度,信号像是把中断扩展到更小的单元上的一个方式。

二、信号的实现方式

  之前说到,信号是相对于进程存在的,在进程结构体task_struct结构中,信号相关的字段如下:

/* 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;

 同一个进程中的所有线程共享一个signal_struct结构和sighand_struct结构,即这两个结构都是进程相关的。而之前咱们也说过,Linux中线程在内核中同样也是由task_struct代表的,下面几个字段都是针对单个线程的,blocked字段记录当前线程对信号的屏蔽情况,其类型早期是unsigned long类型,x86架构下就是32位,而现在升级成一个结构,其大小根据具体的信号数量确定,当前最大信号数量为64,以后还可能增加。

#define _NSIG        64
#define _NSIG_BPW    32
#define _NSIG_WORDS    (_NSIG / _NSIG_BPW)
#define _NSIG 64
typedef struct { unsigned long sig[_NSIG_WORDS]; } sigset_t;

利用宏定义扩展位图。每位对应一个信号,当某个信号对应的位为1时,表明该信号暂时被屏蔽,但这并不意味着信号不能产生,恰恰相反,信号依然可以被传递到当前线程,只是当前线程对信号不做处理,把信号阻塞或称为挂起。待到该信号不被屏蔽了,那么该信号就可以得到处理了。挂起的信号记录在pending字段,sigpending结构如下:

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

 所有产生的信号都会被挂入这个list为表头的链表中,这样产生多个信号也不会丢失。signal还是做链表中信号标记,表明当前有哪些类型的信号未处理。链表中节点结构是sigqueue,只是需要注意,这里信号分为可靠信号和不可靠信号。二者的区别暂时不介绍,不可靠信号是早期支持的32个信号,从0-31,而可靠信号是大于31的信号。不可靠信号在投递还是采取先前的方式,这是为了和之前保持兼容,即signal位图中每个位代表一个信号,且一个信号只有一个sigqueue结构与之对应;当目前存在未处理的信号,再次受到这个信号就会忽略。而可靠信号位图中也有与之对应的位,而且每收到一个信号就生成一个sigqueue结构挂入链表,sigqueue结构如下。

struct sigqueue {
    struct list_head list;
    int flags;
    siginfo_t info;
    struct user_struct *user;
};

三、信号的处理

信号的处理时机主要由两个:

1、从内核空间返回到用户空间前夕。

2、进程位于内核空间被唤醒。

对于信号的处理主要由两种处理方式,用户可以对信号定义自己的处理函数,也可以采用系统默认的处理函数。在上述task_struct结构中有sighand_struct类型的字段 sighand,记录了信号的处理函数

struct sighand_struct {
    atomic_t        count;
    struct k_sigaction    action[_NSIG];
    spinlock_t        siglock;
    wait_queue_head_t    signalfd_wqh;
};

第一个count应该指action表项的数目,第二个action和每一个信号对应,一个表项记录一个信号的处理函数,当为0时表示采用系统默认的处理方式。注意,在signal_struct中并没有设置锁,源码注释解释为一个共享的signal_struct必定有一个sighand_struct结构与之对应,对sighand_struct加锁即可实现对signal_struct保护。

struct sigaction {
    __sighandler_t sa_handler;
    unsigned long sa_flags;
    sigset_t sa_mask;        /* mask last for extensibility */
};

sa_handler指向一个处理函数,sa_flags记录信号处理时的一些设置,可以有如下值

  • SA_ONSTACK:表明要使用已经注册的新栈,而不是使用进程自身的栈。
  • SA_RESTART:设置在信号被中断后重启。
  • SA_NOCLDSTOP:当该位设置时,在子进程stop时不产生SIGCHLD信号。
  • SA_RESETHAND:当调用信号处理函数时,将信号的处理函数重置为缺省值SIG_DFL
  • SA_RESTART:如果信号中断了进程的某个系统调用,则系统自动启动该系统调用
  • SA_NODEFER :一般情况下, 当信号处理函数运行时,内核将阻塞该给定信号。但是如果设置了 SA_NODEFER标记, 那么在该信号处理函数运行时,内核将不会阻塞该信号

sa_mask就代表在处理当前信号时,可以选择性的屏蔽一些信号。相当于即时有效的。

在用户空间可以调用signal和sigaction 函数给一个信号设置处理函数,SIGKILL和SIGSTOP信号除外,这两个信号必须采用系统默认的函数。当一个信号被设置handler时,已经挂起的该类信号被丢弃。关于signal和sigaction的区别这里不做详细介绍。对于sa_handler还可以是SIG_DFL和SIG_IGN两个值,前者为0表示采取默认的处理方式。后者为1,表示忽略该信号。

参考资料:

LInux3.10.1内核源码

原文地址:https://www.cnblogs.com/ck1020/p/6675193.html