浅谈Linux中的信号处理机制(二)

时间:2023-03-08 16:05:42
浅谈Linux中的信号处理机制(二)

首先谢谢 @小尧弟 这位朋友对我昨天夜里写的一篇《浅谈Linux中的信号处理机制(一)》的指正,之前的题目我用的“浅析”一词,给人一种要剖析内核的感觉。本人自知功力不够,尚且不能对着Linux内核源码评头论足。以后的路还很长,我还是一步一个脚印的慢慢走着吧,Linux内核这座山,我才刚刚抵达山脚下。

好了,言归正传,我接着昨天写下去。如有错误还请各位看官指正,先此谢过。

上篇末尾,我们看到了这样的现象:send进程总共发送了500次SIGINT信号给rcv进程,但是实际过程中rcv只接受/处理了13次SIGINT的信号处理函数(signal-handler function)。究竟是rcv进程接受了500次SIGINT信号只执行了13次信号处理函数,还是rcv进程只接受了13次SIGINT信号然后执行了13次信号处理函数呢。我们不禁要问:信号去了哪儿呢?要搞清这个问题之前,我们还需了解一个叫做做信号集和信号屏蔽的知识点。

信号集

在处理信号相关的函数时,我们时常需要一种的特殊的数据结构来表示一组信号的集合,这样的集合我们称之为信号集,其数据类型表示为sigset_t,通常是用位掩码的形式来实现的。我的环境是CentOS7,其定义在/usr/include/bits/sigset.h中,具体如下:

/* A `sigset_t' has a bit for each signal. */

# define _SIGSET_NWORDS (1024 / (8 * sizeof (unsigned long int)))
typedef struct
{
unsigned long int __val[_SIGSET_NWORDS];
} __sigset_t;

#endif

在sigset.h同时也提供了一组函数(实际上用宏来实现的,感兴趣可以查阅sigset.h),用以实现对sigset_t类型数据的操作。其原型如下:

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);

除此之外Glibc还提供了另外三个非标准规定的函数:

int sigisemptyset(const sigset_t* set);

int sigandset(sigset_t* dest,sigset_t* left,sigset_t* right);

int sigorset(sigset_t* dest,sigset_t* left,sigset_t* right);

基本上看了原型之后这些函数的用法也就一目了然了,不需要浪费篇幅了。除此之外,我觉得的这些函数的实现还是值得一读的,是C语言中位运算学习的一个不错的demo。

信号屏蔽

在了解了信号集的基本概念之后,我们就可以知道继续了解其他与信号集相关的概念了,首先是信号屏蔽字。它定义了要阻塞递送到当前进程的信号集,每一个进程都有一个信号屏蔽字(signal mask)。如果你知道什么是权限屏蔽(umask)那么信号屏蔽字也很好理解。sigprocmask()函数可以检测和更改当前进程的信号屏蔽字。其原型:

int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);

当oldset是一个非空指针的话,调用sigprocmask之后,oldset便返回了之前的信号屏蔽字。set参数会结合how参数对当前的信号屏蔽字做出修改。(和之前一节提到过的一样有两个特殊的信号,你不可以屏蔽它们是:SIGKILL和SIGSTOP)具体规则是:

how 行为
SIG_BLOCK 设置进程的信号屏蔽字为当期信号屏蔽字和set的并集。set是新增的要屏蔽的信号集。
SIG_UNBLOCK 设置当前进程的信号屏蔽字为当前信号屏蔽字和set补集的交集,也就是当前信号屏蔽字减去set中的要解除屏蔽的信号集。set中是要解除屏蔽的信号集。
SIG_SETMASK 设置当前进程的信号屏蔽字为set信号集。

然而当set指向一个NULL时,那么how也就没有作用了。通常我们让set设置为NULL时,通过oldset获取当前的信号屏蔽字。

如果某个或多个信号在进程屏蔽了该信号的期间来到过一次或者多次,我们称这样的信号叫做未决的(pending)信号。那么在调用sigprocmask()解除这个信号屏蔽之后,该信号会在sigprocmask ()返回之前,递送给(SUSv3 规定至少传递一个信号)当前进程

进程维护了一个数据结构来保存未决的信号,我们可以通过sigpending()来获取哪些信号是未决的:

int sigpending(sigset_t *set);//return 0 on success,or -1 on error

set参数返回的便是未决的信号集。之后便可以通过使用sigismember()来判断,set中包含哪些信号。

到这里我们就可以解释上一篇末尾的问题了。因为Linux上signal()注册的信号处理函数在执行时,会自动的将当前的信号添加到进程的信号屏蔽字当中。当信号处理函数返回时,会恢复之前的信号屏蔽字。这意味着,当信号处理函数执行时,它不会递归的中断自身。

实时信号

早期Unix系统只定义了32种信号。POSIX.1b定义了一组额外的实时信号(为了兼容之前的应用,而不是修改以前的传统信号)。实时信号的特点,《Linux系统编程手册》上有一段总结的很是全面:

  • Realtime signals provide an increased range of signals that can be used for application-defined purposes. Only two standard signals are freely available for application-defined purposes: SIGUSR1 and SIGUSR2.
  • Realtime signals are queued. If multiple instances of a realtime signal are sent to a process, then the signal is delivered multiple times. By contrast, if we send further instances of a standard signal that is already pending for a process, that signal is delivered only once.
  • When sending a realtime signal, it is possible to specify data (an integer or pointer value) that accompanies the signal. The signal handler in the receiving process can retrieve this data.
  • The order of delivery of different realtime signals is guaranteed. If multiple different realtime signals are pending, then the lowest-numbered signal is delivered first. In other words, signals are prioritized, with lower-numbered signals having higher priority. When multiple signals of the same type are queued, they are delivere—along with their accompanying data—in the order in which they were sent.

根据第二点,我们可以将上篇的博客末尾的SIGINT改成SIGRTMIN+5(当然这里随意,只要是实时信号,Linux上kill()也是可以发送实时信号的),然后重复昨天的测试,我们会惊喜的发现,rcv进程“不出意外”地接受并处理了500次信号处理函数。

那么如何通过发送实时信号时传递数据呢?别着急,还得掌握一个系统调用sigaction()。

sigaction()系统调用

之前我们已经解除了signal()函数,sigaction()是另外一种选择,它功能更加强大,兼容性更好,任何时候我们都应优先考虑使用sigaction(),即使signal()更加简单灵活。其函数原型:

int sigaction(int signum, const struct sigaction *act,struct sigaction *oldact);//Return 0 on success,or -1 on error

与sigprocmask类似地,oldact返回之前的信号设置,act用来设置新的信号处理。signum自然不用解释,这是要处理的信号。这个函数的关键之处就是struct sigaction这个和函数同名的结构体。当然要使用sigaction()还是得从struct sigaction入手,它的定义:

struct sigaction {

union {
        void (*sa_handler)(int);                                 
        void (*sa_sigaction)(int, siginfo_t *, void *);

}__sigaction_handler;                                  //Address of handler
    sigset_t sa_mask;                                        //Signals blocked during the handler invocation
    int sa_flags;                                                //Flags controlling handler invocation
    void (*sa_restorer)(void);                             //Restore,not use
};

sa_mask是一组信号集,当调用信号处理函数之前会将这组信号集添加到进程的信号屏蔽字中,直到信号处理函数返回。利用sa_mask参数,我们可以指定一组信号,让我们的信号处理函数不被这些信号打断。与前面的signal()一样,默认还是会把引发信号处理函数的信号,自动的添加到进程的信号屏蔽字中的。sa_flags参数,如果有经验的话,我们不难猜到这肯定是一组选项,毕竟身经百战了嘛。那我们就来看看这组选项是什么意思:

sa_flags 说明
SA_INTERRUPT 由此信号中断的系统调用不会自动重启。
SA_NOCLDSTOP

当signum为SIGCHLD时,当因接受一信号的子进程停止或者恢复时,将不会产生此信号(有点绕).但是子进程终止时,仍会产生此信号。

(If sig is SIGCHLD, don’t generate this signal when a child process is stopped or resumed as a consequence of receiving a signal.)

 SA_NOCLDWAIT 当signum为SIGCHLD时,子进程终止时不会转化为僵尸进程。此时调用wait(),则阻塞到所有子进程都终止,才返回-1,errno被视之为ECHILD。 
 SA_NODEFER 捕获该信号的时候,不会在执行信号处理函数之前将该信号自动添加到进程的信号屏蔽字中。 
 SA_ONSTACK 调用信号处理函数时,使用sigaltstack()安装的备用栈。 
 SA_RESETHAND  当捕获该信号时,会在调用信号处理函数之前将信号处理函数设置为默认值SIG_DFL,并清除SA_SIGINFO标志。
 SA_RESTART  被此信号中断的系统调用,会自动重启。
SA_SIGINFO 调用信号处理函数时附带了额外的数据要处理,具体见下文。

sa_restorer和名字一样为保留参数,不需要使用。最后我们要看的是__sigaction_handler,这是一个联合体(当然啦,这是废话)。sa_handler和sa_sigaction都是信号处理函数的指针,所以一次只能选择两者中的一个。如果sa_mask中设置了SA_SIGINFO位那么就按照void (*sa_sigaction)(int, siginfo_t *, void *)的形式的函数调用信号处理函数,否则使用 void (*sa_handler)(int)这样的函数。下面我们再来看一看sa_sigaction这个函数:

void sa_sigaction(int signum, siginfo_t* info, void* context);

siginfo_t是一个结构体,其结构和实现相关,我的CentOS7系统上是这样的:

siginfo_t {
    int si_signo; /* Signal number */
    int si_errno; /* An errno value */
    int si_code; /* Signal code */
    int si_trapno; /* Trap number that caused hardware-generated signal (unused on most architectures) */
    pid_t si_pid; /* Sending process ID */
    uid_t si_uid; /* Real user ID of sending process */
    int si_status; /* Exit value or signal */
    clock_t si_utime; /* User time consumed */
    clock_t si_stime; /* System time consumed */
    sigval_t si_value; /* Signal value */
    int si_int; /* POSIX.1b signal */
    void *si_ptr; /* POSIX.1b signal */
    int si_overrun; /* Timer overrun count; POSIX.1b timers */
    int si_timerid; /* Timer ID; POSIX.1b timers */
    void *si_addr; /* Memory location which caused fault */
    long si_band; /* Band event (was int in glibc 2.3.2 and earlier) */
    int si_fd; /* File descriptor */
    short si_addr_lsb; /* Least significant bit of address (since Linux 2.6.32) */
}

每个字段的含义后边都加了清晰的注释,但是还有一个参数使我们需要特别注意的,其中si_value字段用来接收伴随着信号发送过来的数据,其类型是一个sigval_t的联合体,其定义(我的系统是在路径/usr/include/bits/siginfo.h 上):

# define __have_sigval_t 1

/* Type for data associated with a signal. */
typedef union sigval
{
    int     sival_int;
    void* sival_ptr;
} sigval_t;
#endif

在实际编程中,到底选择sival_int还是sival_ptr字段,还是取决于你的应用程序。但是由于指针的作用范围只能在进程的内部,如果发送一个指针到另一个进程一般没有什么实际的意义。

基本上写到这里,我们就可以使用sigaction()进行信号处理的demo了,但是这里我们先不急着写,留到下一节一并写了。

使用sigqueue()

之前我们提到了发送实时信号时可以附带数据,kill(),raise()等函数的参数注定他们无法附带更多的数据,这里我们要认识一个新的函数sigqueue()专门用于在发送信号的时候,附加传递额外的数据。

int sigqueue(pid_t pid, int sig, const union sigval value);//Return 0 on success ,or -1 on error

前两个参数和kill()一致,但是不同于kill(),这里不能将pid只能是单个进程,而不像kill()那样丰富的用法。value的类型便是在上边提及的sigval_t,于是就清晰了:发送进程在这里发送的value在接受进程中通过信号处理函数sa_sigaction中的siginfo_t info参数就可以拿到了。

一个处理实时信号信号简单的demo,处理信号端代码catch.c:

#include <signal.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h> void sighandler(int sig,siginfo_t* info,void* context)
{
printf("Send process pid = %ld,receive a data :%d\n",info->si_pid,info->si_value.sival_int);
} int main()
{
printf("pid = %ld\n",(long)getpid());
struct sigaction act;
act.sa_flags = SA_SIGINFO;
sigemptyset(&act.sa_mask);
act.sa_sigaction = sighandler;
if(sigaction(SIGRTMIN+5,&act,0) == -1)
exit(-1);
pause();
}

发送信号端send.c:

#include <unistd.h>
#include <stdio.h>
#include <signal.h>
#include <string.h> int main(int argc,char* argv[])
{
printf("Send process pid = %ld\n",(long)getpid());
union sigval value;
value.sival_int = 5435620;
pid_t pid = (pid_t)atol(argv[1]);
sigqueue(pid,SIGRTMIN+5,value);
}

  运行结果如图所示,在sa_sigaction中成功拿到了发送进程的进程id以及传送的数据:

浅谈Linux中的信号处理机制(二)

当然由于夜深了,这个demo写的还是比较简单的,基本我们使用已经没有任何障碍了。

准备把有关信号的知识点总结完的,一写出来,才发现信号这部分的知识点真是多,而且牵扯到好多细节方面的东西,看来这个任务今晚完不成了,明天继续吧。

如果您发现我的博文有错误之处,烦请您指正,我先在此谢过!联系邮箱baixiangcpp@gmail.com。