一、可靠性与不可靠性:
1. 不可靠信号
主要由以下两个问题导致不可靠问题的发生:
a. 进程每次处理信号后, 就会对信号的响应设置为默认动作;如果用户不希望这样操作,就要在信号处理函数结尾再调用一次signal,进行重装。
b. 信号会丢失。
Linux支持不可靠信号,信号值小于SIGRTMIN的都是不可靠的, 但是做了改进,在调用后,不用重新安装函数,信号会自动重装。Linux下的不可靠信号主要指信号丢失。
2. 可靠信号:
信号值位于SIGRTMIN和SIGRTMAX之间的信号都是可靠信号,可靠信号克服了信号可能丢失的问题。Linux在支持新版本的信号安装函数sigation()以及信号发送函数sigqueue()的同时,仍然支持早期的signal()信号安装函数,支持信号发送函数kill()。
注:不要有这样的误解:由sigqueue()发送、sigaction安装的信号就是可靠的。事实上,可靠信号是指后来添加的新信号(信号值位于SIGRTMIN及SIGRTMAX之间);不可靠信号是信号值小于SIGRTMIN的信号。信号的可靠与不可靠只与信号值有关,与信号的发送及安装函数无关。目前linux中的signal()是通过sigation()函数实现的,因此,即使通过signal()安装的信号,在信号处理函数的结尾也不必再调用一次信号安装函数。同时,由signal()安装的实时信号支持排队,同样不会丢失。
二、实时信号与非实时信号
SIGRTMIN(31)往后的都是实时信号,前32号信号已经有了预定义值,有了特定的用途和含义。 后32个信号表示实时信号。
非实时信号都不支持排队,都是不可靠信号;实时信号都支持排队,都是可靠信号。
三、响应:
三种方式:1、忽略,不做任何处理。2、捕捉,并调用处理函数。 3、执行缺省动作。
四、发送函数:
常用的有kill(), raise(), sigqueue(), alarm(), setitimer(),abort()。详见man手册...
五、信号的安装:
主要使用signal() 和 sigaction()函数。
1、signal()不支持信号传递信息,主要是用于前32种非实时信号的安装,而sigaction()是较新的函数(由两个系统调用实现:sys_signal以及sys_rt_sigaction),有三个参数,支持信号传递信息,主要用来与sigqueue()系统调用配合使用,当然,sigaction()支持非实时信号的安装,sigaction()优于signal()主要体现在支持信号带有参数。
2、两者都支持装填这样的处理函数:typedef void (*sighandler_t)(int);sigaction()还支持装填带有参数的处理函数:
void (*_sa_sigaction)(int,struct siginfo *, void *);下面是sigaction的结构体:
struct sigaction { union{ __sighandler_t _sa_handler; void (*_sa_sigaction)(int,struct siginfo *, void *); }_u sigset_t sa_mask; unsigned long sa_flags; void (*sa_restorer)(void); }很巧妙地使用union来支持两种不同的信号处理函数。带有参数的处理函数主要为实时信号准备,当然也支持旧的信号(0~31)。
再看看那个保存参数的结构体struct siginfo:
siginfo_t { int si_signo; /* 信号值,对所有信号有意义*/ int si_errno; /* errno值,对所有信号有意义*/ int si_code; /* 信号产生的原因,对所有信号有意义*/ union{ /* 联合数据结构,不同成员适应不同信号 */ //确保分配足够大的存储空间 int _pad[SI_PAD_SIZE]; //对SIGKILL有意义的结构 struct{ ... }... ... ... ... ... //对SIGILL, SIGFPE, SIGSEGV, SIGBUS有意义的结构 struct{ ... }... ... ... } }其中使用了一个_pad成员,为所有信号处理的结构体分配足够的空间,union会以成员中最大的那个来分配空间,这样处理我认为有这样的考虑:1、可以设定足够的大小,使得整个siginfo结构体对齐到cache line,提高cache命中。2、参数拷贝和传递时更加方便了,不管目前存放的参数是什么,有多大,要拷贝的话,直接使用_pad标签,一次性就可以全部拷贝过去。
那个sa_mask,是个很好用的东西,用于指定在信号处理程序执行的过程中,哪些喜好应当被阻塞。缺省情况下,当前信号本身被阻塞,防止信号的嵌套发送,除非指定SA_NODEFER或者SA_NOMASK标志位。这个标记位,在一定程度上解决了信号丢失的问题,被阻塞的信号并不是被丢失掉,而是在当前信号处理过之后,继续处理。只不过不打断当前的处理过程而已。
sa_flags中包含了许多标志位,包括刚刚提到的SA_NODEFER及SA_NOMASK标志位。另一个比较重要的标志位是SA_SIGINFO,当设定了该标志位时,表示信号附带的参数可以被传递到信号处理函数中,因此,应该为sigaction结构中的sa_sigaction指定处理函数,而不应该为sa_handler指定信号处理函数,否则,设置该标志变得毫无意义。即使为sa_sigaction指定了信号处理函数,如果不设置SA_SIGINFO,信号处理函数同样不能得到信号传递过来的数据,在信号处理函数中对这些信息的访问都将导致段错误(Segmentation fault)
信号作为异步通知,安全保障比较少, 在开发应用程序的时候,尽量不要使用信号来传递参数,
六、进一步讨论sigaction:信号集及信号操作函数:
信号集其实是一个位图:
typedef struct {unsigned long sig[_NSIG_WORDS];} sigset_t;
linux所支持的所有信号可以全部或部分的出现在信号集中,主要与信号阻塞相关函数配合使用,支持以下的操作函数来操作信号集合:
int sigemptyset(sigset_t *set); int sigfillset(sigset_t *set);int sigaddset(sigset_t *set, int signum)int sigdelset(sigset_t *set, int signum);int sigismember(const sigset_t *set, int signum);sigemptyset(sigset_t *set)初始化由set指定的信号集,信号集里面的所有信号被清空;sigfillset(sigset_t *set)调用该函数后,set指向的信号集中将包含linux支持的64种信号;sigaddset(sigset_t *set, int signum)在set指向的信号集中加入signum信号;sigdelset(sigset_t *set, int signum)在set指向的信号集中删除signum信号;sigismember(const sigset_t *set, int signum)判定信号signum是否在set指向的信号集中。
七、信号阻塞与信号未决:
使用函数sigprocmask()阻塞信号的传递,只是延迟信号的到达。信号会在解除阻塞后继续传递。这种情况往往需要在信号程序和其它程序共享全局变量时,如果全局变量的类型不是sig_atomic_t类型,当一部分程序恰好读、写到变量过程中,产生某个信号,而信号程序里会改变该变量,那么就会产生混乱。为了避免这种混乱,提供程序的可靠性,必须在操作这类变量前阻塞信号,操作完成后恢复信号的传递。
每个进程都有一个用来描述哪些信号递送到进程时将被阻塞的信号集,该信号集中的所有信号在递送到进程后都将被阻塞。我们称正在阻塞的信号的集合为信号掩码(signal mask)。每个进程都有自己的信号掩码,创建子进程时子进程将继承父进程的信号掩码。我们可以通过修改当前的信号掩码来改变信号的阻塞情况。
int sigprocmask(int how, const sigset_t *set,sigset_t *oldset),该函数用来检查和改变调用进程的信号掩码,其中的how参数指出信号掩码改变的方式,必须是下面的值之一:
SIG_BLOCK,阻塞set中包含的信号。意思是说把set中的信号加到当前的信号掩码中去,新的信号掩码是set和旧信号掩码的并集。
SIG_UNBLOCK,解除set中信号的阻塞,从当前信号掩码中去除set中的信号。
SIG_SETMASK,设置信号掩码,既按照set中的信号重新设置信号掩码。
最后一个参数是进程原来的信号集。如果你只需要改变信号的阻塞情况而不需要关心原来的值,可以传递NULL指针给函数。如果你希望什么也不改变,只是想获得当前信号掩码的信息,那么把set设置成NULL,old中返回当前的设置。
使用sigpending(sigset_t *set)可以获得当前已经送到进程,但是被阻塞的所有信号,在set指向的信号集中返回结果。是一个用于查询的函数,暂时没有想到好的用处。
sigsuspend(const sigset_t *mask);用于在接收到某个信号之前, 临时用mask替换进程的信号掩码, 并暂停进程执行,直到收到信号为止。sigsuspend 返回后将恢复调用之前的信号掩码。信号处理函数完成后,进程将继续执行。该系统调用始终返回-1,并将errno设置为EINTR。
signsuspend的操作是原子的(要么都执行,要么一点都不执行),主要做了一下工作:
(1) 设置新的mask阻塞当前进程;
(2) 收到信号,调用该进程设置的信号处理函数;
(3) 待信号处理函数返回后,恢复原先mask;
(4) sigsuspend返回。
八、信号的生命周期:
1、信号"诞生"。信号的诞生指的是触发信号的事件发生(如检测到硬件异常、定时器超时以及调用信号发送函数kill()或sigqueue()等)。
2、信号在目标进程中"注册";进程的task_struct结构中有关于本进程中未决信号的数据成员:
sigpending结构体中,signal域保存所有待处理信号的集合,每个信号占一位。list域指向sigqueue链表,对于非实时信号(1-31),每个信号在链表中只能拥有一个sigqueue;对于实时信号(32-63),如果接收到多个相同的信号,每个信号都会在链表中拥有一个sigqueue。
信号在进程中注册指的就是信号值加入到进程的未决信号集中(sigpending结构的第二个成员sigset_t signal),并且信号所携带的信息被保留到未决信号信息链的某个sigqueue结构中。只要信号在进程的未决信号集中,表明进程已经知道这些信号的存在,但还没来得及处理,或者该信号被进程阻塞。
注:
当一个实时信号发送给一个进程时,不管该信号是否已经在进程中注册,都会被再注册一次,因此,信号不会丢失,因此,实时信号又叫做"可靠信号"。这意味着同一个实时信号可以在同一个进程的未决信号信息链中占有多个sigqueue结构(进程每收到一个实时信号,都会为它分配一个结构来登记该信号信息,并把该结构添加在未决信号链尾,即所有诞生的实时信号都会在目标进程中注册);
当一个非实时信号发送给一个进程时,如果该信号已经在进程中注册,则该信号将被丢弃,造成信号丢失。因此,非实时信号又叫做"不可靠信号"。这意味着同一个非实时信号在进程的未决信号信息链中,至多占有一个sigqueue结构(一个非实时信号诞生后,(1)、如果发现相同的信号已经在目标结构中注册,则不再注册,对于进程来说,相当于不知道本次信号发生,信号丢失;(2)、如果进程的未决信号中没有相同信号,则在进程中注册自己)。
3、信号在进程中的注销。在目标进程执行过程中,会检测是否有信号等待处理(每次从系统空间返回到用户空间时都做这样的检查)。如果存在未决信号等待处理且该信号没有被进程阻塞,则在运行相应的信号处理函数前,进程会把信号在未决信号链中占有的结构卸掉。是否将信号从进程未决信号集中删除对于实时与非实时信号是不同的。对于非实时信号来说,由于在未决信号信息链中最多只占用一个sigqueue结构,因此该结构被释放后,应该把信号在进程未决信号集中删除(信号注销完毕);而对于实时信号来说,可能在未决信号信息链中占用多个sigqueue结构,因此应该针对占用sigqueue结构的数目区别对待:如果只占用一个sigqueue结构(进程只收到该信号一次),则应该把信号在进程的未决信号集中删除(信号注销完毕)。否则,不应该在进程的未决信号集中删除该信号(信号注销完毕)。
进程在执行信号相应处理函数之前,首先要把信号在进程中注销。
4、信号生命终止。进程注销信号后,立即执行相应的信号处理函数,执行完毕后,信号的本次发送对进程的影响彻底结束。
注:
1)信号注册与否,与发送信号的函数(如kill()或sigqueue()等)以及信号安装函数(signal()及sigaction())无关,只与信号值有关(信号值小于SIGRTMIN的信号最多只注册一次,信号值在SIGRTMIN及SIGRTMAX之间的信号,只要被进程接收到就被注册)。
2)在信号被注销到相应的信号处理函数执行完毕这段时间内,如果进程又收到同一信号多次,则对实时信号来说,每一次都会在进程中注册;而对于非实时信号来说,无论收到多少次信号,都会视为只收到一个信号,只在进程中注册一次。
评论
查看更多