Linux信号概述
信号是由用户、系统或者进程发送给目标进程的信息,以通知目标进程某个状态的改变或系统异常。
Linux信号可由如下条件产生:
对于前台进程,用户可以通过输人特殊的终端字符来给它发送信号。比如输入Ctrl+C 通常会给进系统异常。
比如浮点异常和非法内存段访问。
系统状态变化。比如 alarm定时器到期将引起SIGALRM信号。
运行kill命令或调用kill函数。
发送信号
#include<sys/types.h>
#include<signal.h>
int kill(pid_t pid,int sig);
kill函数的pid参数以及意义
pid参数 | 含义 |
pid>0 | 信号发送给PID为pid的进程 |
pid=0 | 信号发送给本进程组内的所有进程 |
pid=-1 | 信号发送给楚init进程外所有进程,但发送者需要拥有对目标进程发送信号的权限 |
pid<-1 | 信号发送给组ID为-pid的进程组中所有成员 |
Linux定义的信号值都大于0,如果sig取值为0,则kill函数不发送任何信号。但将sig设置为О可以用来检测目标进程或进程组是否存在,因为检查工作总是在信号发送之前就执行。不过这种检测方式是不可靠的。一方面由于进程PID的回绕,可能导致被检测的PID不是我们期望的进程的PID:
另一方面,这种检测方法不是原子操作。
函数成功返回0,失败则返回errno,几种可能如下表格
error | 含义 |
EINVAL | 无效的信号 |
EPERM | 该进程没有权限发送信号给任何一个目标进程 |
ESRCH | 目标进程或进程组不存在 |
信号处理方式
接受信号就要处理信号
#include<signal.h>
typedef void (*_sighandler_t)(int);
#include<bits/signum.h>
#define SIG_DFL ((_sighandler_t) 0);
#define SIG_ING ((_sighandler_t) 1);
信号处理函数只带有一个整型参数,该参数用来指示信号类型。信号处理函数应该是可重入的,否则很容易引发一些竞态条件。所以在信号处理函数中严禁调用一些不安全的函数。
SIG_IGN表示忽略目标信号,SIG_DFL表示使用信号的默认处理方式。信号的默认处理方式有如下几种:结束进程(Term)、忽略信号(Ign)、结束进程并生成核心转储文件( Core)、暂停进程( Stop),以及继续进程(Cont)。
中断系统的调用
如果程序在执行处于阻塞状态的系统调用时接收到信号,并且我们为该信号设置了信号处理函数,则默认情况下系统调用将被中断,并且errno被设置为EINTR。我们可以使用sigaction函数(见后文)为信号设置SA_RESTART标志以自动重启被该信号中断的系统调用。
对于默认行为是暂停进程的信号(比如 SIGSTOP、SIGTTIN),如果我们没有为它们设置信号处理函数,则它们也可以中断某些系统调用(比如 connect、epoll_wait)。POSIX没有规定这种行为,这是Linux独有的。
信号函数
signal的系统调用
#include<signal.h>
_sighandler_t signal(int sig,_sighandler_t _handler);
sig 参数指出要捕获的信号类型。_handler参数是_sighandler_t类型的函数指针,用于指定信号sig 的处理函数。
signal函数成功时返回一个函数指针,该函数指针的类型也是_sighandler_t。这个返回值是前一次调用signal函数时传入的函数指针,或者是信号sig对应的默认处理函数指针SIG_DEF(如果是第一次调用signal 的话)。
sigaction
#include<siganl.h>
int sigaction(int sig,const struct sigaction* act,struct sigaction* oact);
struct sigaction {
void (*sa_handler)(int);
void (*sa_sigaction)(int, siginfo_t *, void *);
sigset_t sa_mask;
int sa_flags;
void (*sa_restorer)(void);
};
该结构体中的sa_hander成员指定信号处理函数。sa_mask成员设置进程的信号掩码(确切地说是在进程原有信号掩码的基础上增加信号掩码),以指定哪些信号不能发送给本进程。sa_mask 是信号集sigset_t (_sigset_t的同义词)类型,该类型指定一组信号。关于信号集,我们将在后面介绍。sa_flags 成员用于设置程序收到信号时的行为,其可选值如表下图所示。
选项 | 含义 |
SA_NOCLDSTOP | 设置成表示该子进程暂停时不生成SIGCHLD |
SA_NOCLDWAIT | 子进程结束时不产生僵尸进程 |
SA_SIGINFO | 使用sa_sigaction作为信号处理函数(而不是默认sa_hanler),提供更多的信息 |
SA_ONSTACK | 调用由sigaltstack函数设置的可选信号栈上的信号函数 |
SA_RESTART | 重新调用被信号终止的系统调用 |
SA_NODEFER | 当接收到信号并进入其信号处理函数,不屏蔽信号,默认情况下,我们期望在进程处理一个信号时不在接受到同种信号 |
SA_RESETHAND | 信号处理函数执行完,恢复信号的默认处理方式 |
SA_INTERRUPT | 中断系统调用 |
SA_NOMSAK | 同SA_NODEFER |
SA_ONESHOT | 同SA_RESETHAND |
SA_STACK | 同SA_ONSTACK |
sa_restorer成员已经过时啦
信号集
信号集函数
信号集原型:
#include <signal.h>
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);
进程信号掩码
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
_set参数指定新的信号掩码,_oset参数则输出原来的信号掩码(如果不为NULL的话)。如果_set参数不为NULL,则_how参数指定设置进程信号掩码的方式,
_how参数 | 含义 |
SIG_BLOCK | 新的进程信号就是当前值和_set指定信号的并集 |
SIG_UNBLOCK | 新的进程信号就是当前值和_set指定信号的交集 |
SIG_SETMAsk | 直接将进程信号掩码设置成_set |
注意:如果_set为NULL,则进程信号掩码不变,此时我们仍然可以利用_oset参数来获得进程当前的信号集
被挂起的信号
设置进程信号掩码后,被屏蔽的信号将不能被进程接收。如果给进程发送一个被屏蔽的信号,则操作系统将该信号设置为进程的一个被挂起的信号。如果我们取消对被挂起信号的屏蔽,则它能立即被进程接收到。如下函数可以获得进程当前被挂起的信号集:
#include<signal.h>
int sigpending(sigset_t* set);
set参数用于保存被挂起的信号集。显然,进程即使多次接收到同一个被挂起的信号,sigpending 函数也只能反映一次。并且,当我们再次使用sigprocmask使能该挂起的信号时,该信号的处理函数也只被触发一次。
关于信号和信号集,Linux还提供了很多有用的API,这里就不一一介绍了。需要提醒读者的是,要始终清楚地知道进程在每个运行时刻的信号掩码,以及如何适当地处理捕获到的信号。在多进程、多线程环境中,我们要以进程、线程为单位来处理信号和信号掩码。我们不能设想新创建的进程、线程具有和父进程、主线程完全相同的信号特征。比如,fork调用产生的子进程将继承父进程的信号掩码,但具有一个空的挂起信号集。
同一事件源
信号是一种异步事件:信号处理函数和程序的主循环是两条不同的执行路线。很显然,信号处理函数需要尽可能快地执行完毕,以确保该信号不被屏蔽(前面提到过,为了避免一些竞态条件,信号在处理期间,系统不会再次触发它〉太久。
一种典型的解决方案是;把信号的主要处理逻辑放到程序的主循环中,当信号处理函数被触发时,它只是简单地通知主循环程序接收到信号,并把信号值传递给主循环,主循环再根据接收到的信号值执行目标信号对应的逻辑代码。信号处理函数通常使用管道来将信号“传递”给主循环﹔信号处理函数往管道的写端写入信号值,主循环则从管道的读端读出该信号值。那么主循环怎么知道管道上何时有数据可读呢﹖这很简单,我们只需要使用IO复用系统调用来监听管道的读端文件描述符上的可读事件。如此一来,信号事件就能和其他I/O事件一样被处理,即统一事件源。
网络编程相关的信号
SIGHUP
控制终端被挂起
当挂起进程的控制终端时,SIGHUP信号将被触发。对于没有控制终端的网络后台程序而言,它们通常利用SIGHUP信号来强制服务器重读配置文件。一个典型的例子是xinetd超级服务程序。
SIGPIPE
往读端被关闭的管道写入数据或socket连接写数据
默认情况下,往一个读端关闭的管道或socket连接中写数据将引发SIGPIPE信号。我们需要在代码中捕获并处理该信号,或者至少忽略它,因为程序接收到SIGPIPE信号的默认行为是结束进程,而我们绝对不希望因为错误的写操作而导致程序退出。引起SIGPIPE信号的写操作将设置errno为EPIPE。
这个博客,我们可以使用send函数的MSG_NOSIGNAL标志来禁止写操作触发
SIGURG
检测带外数据到达
附录:LINUX信号大全