Linux高性能服务器编程(10)
信号是由用户、系统或者进程发送给目标进程的信息,以通知目标进程某个状态的改变或系统异常。Linux信号可由如下条件产生:
1.对于前台进程,用户可以通过输入特殊终端字符来给它发送信号。
2.系统异常。
3.系统状态变化。
运行kill命令或调用kill函数。
Linux信号概述
发送信号
Linux下,一个进程给其他进程发送信号的API是kill函数。其定义如下:
#include<sys/types.h>
#include<signal.h>
int kill(pid_t pid,int sig);
该函数把信号sig发送给目标进程;目标进程由pid参数指定,其可能的取值及含义如表
pid参数 | 含义 |
---|---|
pid > 0 | 信号发送给PID为pid的进程 |
pid = 0 | 信号发送给本进程组内的其他进程 |
pid = -1 | 信号发送给除init进程外的所有进程,但发送者需要拥有对目标进程发送信号的权限 |
pid < -1 | 信号发送给组ID为-pid的进程组中的所有成员 |
Linux定义的信号值都大于0,如果sig取值为0,则kill函数不发送任何信号。该函数成功时返回0,失败则返回-1并设置errno。几种可能的errno
errno | 含义 |
---|---|
EINVAL | 无效信号 |
EPERM | 该进程没有权限发送信号给任何一个目标进程 |
ESRCH | 目标进程或进程组不存在 |
信号处理方式
目标进程在收到信号时,需要定义一个接收函数来处理。信号处理函数的原型如下:
#include<signal.h>
typedef void (*__sighandler_t) (int);
信号处理函数只带有一个整型参数,该参数用来指示信号类型。信号处理函数应该是可重入的,否则很容易引发一些竞态条件。
除用户自定义信号处理函数外,bit/signum.h头文件中还定义可信号的两种其他处理方式——SIG_IGN和SIG_DEL:
#include<bit/signum.h>
#define SIG_DFL ((__sighandler_t) 0)
#define SIG_IGN ((__sighandler_t) 1)
SIG_IGN表示忽略目标信号,SIG_DFL表示使用信号的默认处理方式。信号的默认处理方式有如下几种:结束进程(Term)、忽略信号(Ign)、结束进程并生成核心转储文件(Core)、暂停进程(Stop),以及继续进程(Cont)。
Linux信号
Linux信号都定义在bits/signum.h头文件中,其中包括标准信号和POSIX实时信号。
信号 | 起源 | 默认行为 | 含义 |
---|---|---|---|
SIGHUP | POSIX | Term | 控制终端挂起 |
SIGINT | ANSI | Term | 键盘输入以中断进程(Ctrl+C) |
SIGQUIT | POSIX | Core | 键盘输入使进程退出(Ctrl+\) |
SIGILL | ANSI | Core | 非法指令 |
SIGTRAP | POSIX | Core | 断点陷阱,用于测试 |
SIGABRT | ANSI | Core | 进程调用abort函数时生成该信号 |
SIGIOT | 4.2 BSD | Core | 和SIGABRT相同 |
SIGBUS | 4.2 BSD | Core | 总线错误,错误内存访问 |
SIGFPE | ANSI | Core | 浮点异常 |
SIGKILL | POSIX | Trem | 终止一个进程。该信号不可被捕获或者忽略 |
SIGUSR1 | POSIX | Trem | 用户自定义信号之一 |
SIGSEGV | ANSI | Core | 非法内存段引用 |
SIGUSR2 | POSIX | Trem | 用户自定义信号之二 |
SIGPIPE | POSIX | Trem | 往读端被关闭的管道或者socket连接中写入数据 |
SIGALRM | POSIX | Trem | 由alarm或setitimer设置的实时闹钟超时引起 |
SIGTERM | ANSI | Trem | 终止进程。kill命令默认发送的信号就是SIGTERM |
SIGSTKFLT | Linux | Trem | 早期的Linux使用该信号来报告数学协处理器栈错误 |
SIGCLD | System V | Ign | 和SIGCHLD相同 |
SIGCHLD | POSIX | Ign | 子进程状态发生变化(退出或暂停) |
SIGCONT | POSIX | Cont | 启动被暂停的进程(Ctrl+Q)。如果进程未处于暂停状态,则信号被忽略 |
SIGSTOP | POSIX | Stop | 暂停进程(Ctrl+S)。该信号不可被捕获或者忽略 |
SIGTSTP | POSIX | Stop | 挂起进程(Ctrl+Z) |
SIGTTIN | POSIX | Stop | 后台进程试图从终端读取输入 |
SIGTTOU | POSIX | Stop | 后台进程试图往终端输入内容 |
SIGURG | 4.2 BSD | Ign | socket连接上接收到紧急数据 |
SIGXCPU | 4.2 BSD | Core | 进程的CPU使用时间超过其软限制 |
SIGXFSZ | 4.2 BSD | Core | 文件尺寸超过其软限制 |
SIGVTALRM | 4.2 BSD | Term | 与SIGALRM类似,不过它只统计本进程用户空间代码的运行时间 |
SIGPROF | 4.2 BSD | Term | 与SIGALRM类似,它同时统计用户代码和内核的运行时间 |
SIGWINCH | 4.3 BSD | Ign | 终端窗口大小发生变化 |
SIGPOLL | System V | Term | 与SIGIO类似 |
SIGIO | 4.2 BSD | Term | IO就绪,比如socket上发生的可读、可写事件。因为TCP服务器可触发 SIGIO的条件很多,故而SIGIO无法在TCP服务器中使用。 SIGIO信号可用在UDP服务器中,不过也非常少见 |
SIGPWR | System V | Term | 对于使用UPS的系统,当电池电量过低时,SIGPWR信号将被触发 |
SIGSYS | POSIX | Core | 非法系统调用 |
SIGUNUSED | Core | 保留,通常和SIGSYS效果相同 |
着重讲解与网络编程关系紧密的几个信号:SIGHUP、SIGPIPE、SIGURG.
中断系统调用
如果程序在执行处于阻塞状态的系统调用时接收到信号,并且我们为该信号设置了信号处理函数,则默认情况下系统调用将被中断,并且errno设置为EINTR。我们可以使用sigaction函数为信号设置SA_RESTART标志以自动重启被该信号中断的系统调用。
对于默认行为是暂停进程的信号(比如SIGSTOP、SIGTTIN),如果我们没有为它们设置信号处理函数,则它们也可以中断某些系统调用(比如connect、epoll_wait)。POSIX没有规定这种行为,这是Linux独有的。
信号函数
signal系统调用
要为一个信号设置处理函数,可以使用厦门的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的话)
signal系统调用出错时返回SIG_ERR,并设置errno
sigaction调用
设置信号处理函数的更健壮的接口是如下的系统调用:
#include<signal.h>
int sigaction(int sig,const struct sigaction* act, struct sigaction* oact);
sig参数要指出要捕获的信号类型,act参照指定新的信号处理方式,oact参数则输出信号先前的处理方式(如果不为NULL的话)。act和oact都是sigaction结构类型的指针,sigaction结构体描述了信号的处理细节,其定义如下:
struct sigaction
{
#ifdef __USE_POSIX199309
union
{
_sighandler_t sa_handler;
void (*sa_sigaction) (int , siginfo_t*,void*);
}
_sigaction_handler;
#define sa_handler __sigaction_handler.sa_handler
#define sa_sigaction __sigaction_handler.sa_sigaction
#else
_sighandler_t sa_handler;
#endif
_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 | 如果sigaction的sig参数是SIGCHLD,则设置该标志表示子进程暂停时不生成SIGCHLD信号 |
SA_NOCLDWAIT | 如果sigaction的sig参数是SIGCHLD,则设置该标志表示子进程结束时不产生僵尸进程 |
SA_SIGINFO | 使用sa_sigaction作为信号处理函数(而不是默认的sa_handler),它给进程提供更多相关的信息 |
SA_ONSTACK | 调用由sigaltstack函数设置地可选信号栈上的信号处理函数 |
SA_RESTART | 重新调用被该信号终止地系统调用 |
SA_NODEFER | 当接收到信号并进入其信号处理函数时,不屏蔽该信号。默认情况下,我们期望进程在处理一个信号时不再接收到同种信号,否则将引起一些竞态条件 |
SA_RESETHAND | 信号处理函数执行完后,恢复信号地默认处理方式 |
SA_INTERRUPT | 中断系统调用 |
SA_NOMASK | 同SA_NODEFER |
SA_ONESHOT | 同SA_RESETHAND |
SA_STACK | 同SA_ONSTACK |
sa_restorer已经过时。sigaction成功时返回0,失败则返回-1并设置errno。
信号集
信号集函数
Linux提供了如下一组函数来设置、修改、删除和查询信号集:
#include<signal.h>
int sigemptyset(sigset_t* _set) /* 清空信号集 */
int sigfillset(sigset_t* _set) /* 在信号集中设置所有信号 */
int sigaddset(sigset_t* _set,int _signo) /* 将信号_signo添加至信号集中 */
int sigdelset(sigset_t* _set,int _signo) /* 将信号_signo从信号集中删除 */
int sigismember(_const sigset_t* _set,int _signo) /* 测试_signo是否在信号集中 */
进程信号掩码
如下函数可以用于设置或者查看进程的信号掩码:
#include<signal.h>
int sigprocmask(int _how,_const sigset_t* _set,sigset_t* _oset);
_set参数指定新的信号掩码,_oset参数则输出原来的信号掩码。如果_set参数不为NULL,则_how参数指定设置进程信号掩码的方式,其可选值如表:
_how | 含义 |
---|---|
SIG_BLOCK | 新的进程信号掩码是其当前值和_set指定信号集的并集 |
SIG_UNBLOCK | 新的进程信号掩码是其当前值和~_set信号集的交集,因此_set指定的信号集将不被屏蔽 |
SIG_SETMASK | 直接将进程信号设置为_set |
如果_set为NULL,则进程信号掩码不变,此时我们仍然可以利用_oset参数来获得进程当前的信号掩码
sigprocmask成功时返回0,失败则返回-1并设置errno
被挂起的信号
设置进程信号掩码后,被屏蔽的信号将不能被进程接收。如果给进程发送一个被屏蔽的信号,则操作系统将该信号设置为进程的一个被挂起的信号。如果我们取消对被挂起信号的屏蔽,则它能立即被进程接收到。如下函数可以获得进程当前被挂起的信号集:
#include<signal.h>
int sigpending(sigset_t* set);
set参数用于保存被挂起的信号集。成功返回0,失败返回-1并设置errno。
网络编程相关信号
SIGHUP
触发条件:当挂起进程的控制终端时,SIGHUP被触发。
对于没有控制终端的网络后台程序而言,它们通常利用SIGHUP信号来强制重读配置文件。
SIGPIPE
触发条件:默认情况下,往一个读端关闭的管道或socket连接中写数据将引发SIGPIPE信号。程序接收到SIGPIPE默认行为是结束进程,我们不希望错误的写操作而导致程序退出。引起SIGPIPE信号的写操作将设置errno为EPIPE。
使用send函数的MSG_NOSIGNAL标志来禁止写操作触发SIGPIPE信号。这种情况下,我们应该使用send函数反馈的errno值来判断管道或者socket连接的读端是否已经关闭。
SIGURG
再Linux环境下,内核通知应用程序带外数据到达。