信号是由用户、系统或者进程发送给目标进程的信息,以通知目标进程某个状态的改变或系统异常。Linux 信号可由如下条件产生:
服务器程序必须处理(或至少忽略)一些常见的信号,以免异常终止。
Linux 下,一个进程给其他进程发送信号的 API 是 kill 函数。其定义如下:
#include
#include
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 函数不发送任何信号。但将 sig 设置为 0 可以用来检测目标进程或进程组是否存在,因为检查工作总是在信号发送之前就执行。不过这种检测方式是不可靠的:
errno | 含义 |
---|---|
EINVAL | 无效信号 |
EPERM | 该进程没有权限发送信号给任何一个目标进程 |
ESRCH | 目标进程或进程组不存在 |
目标进程在收到信号时,需要定义一个接收函数来处理之:
#include
typedef void(*__sighandler_t)(int);
信号处理函数只带有一个整型参数,该参数用来指示信号类型。信号处理函数应该是可重入的,否则很容易引发一些竞态条件。所以在信号处理函数中严禁调用一些不安全的函数。除了用户自定义信号处理函数外,bits/signum.h 头文件中还定义了 信号的两种其他处理方式:SIG_IGN 和 SIG_DEL:
#include
#define SIG_DFL((__sighandler_t)0)
#define SIG_IGN((__sighandler_t)1)
Linux 的可用信号都定义在 bits/signum.h 头文件中,其中包括标准信号和 POSIX 实时信号:
信号 | 起源 | 默认行为 | 含义 |
---|---|---|---|
SIGHUP | POSIX | Term | 控制终端挂起 |
SIGNIT | 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 | Term | 种植一个进程,该信号不可被捕获或忽略 |
SIGUSRI | POSIX | Term | 用户自定义信号之一 |
SIGSEGV | ANSI | Core | 非法内存段引用 |
SIGUSR2 | POSIX | Term | 用户自定义信号之二 |
SIGPIPE | POSIX | Term | 往读端被关闭的管道或者 socket 连接中写数据 |
SIGALRM | POSIX | Term | 由 alarm 或 setitimer 设置的实时闹钟超时引起 |
SIGTERM | ANSI | Term | 终止进程,kill 命令默认发送的信号就是 SIGTERM |
SIGSTKFLT | Linux | Term | 早期的 Linux 使用该信号来报告数学协处理器栈错误 |
SIGCLD | System Ⅴ | 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.2 BSD | Ign | 终端窗口大小发生变化 |
SIGPOLL | System Ⅴ | Term | 与 SIGIO 类似 |
SIGIO | 4.2 BSD | Term | IO 就绪,比如 socket 上发生可读、可写事件。因为 TCP 服务器可触发多 SIGIO 的条件很多,故而 SIGIO 无法在 TCP 服务器中使用。SIGIO 信号可用在 UDP 服务器中 |
SIGPWR | System Ⅴ | Term | 对于使用 UPS(Uninterruptable Power Supply)的系统,当电池电量过低时,SIGPWR 信号将被触发 |
SIGSYS | POSIX | Core | 非法系统调用 |
SIGUNUSED | Core | 保留,通常和 SIGSYS 效果相同 |
如果程序在执行处于阻塞状态的系统调用时接收到信号,并且我们为该信号设置了信号处理函数,则默认情况下系统调用将被中断,并且 errno 被设置为 EINTR 。可以使用 sigaction 函数为信号设置 SA_RESTART 标志以自动重启被该信号中断的系统调用。
对于默认行为是暂停进程的信号(比如 SIGSTOP 、SIGTTIN),如果我们没有为它们设置信号处理函数,则它们也可以中断某些系统调用(比如 connect 、epoll_wait)。POSIX 没有规定这种行为,这是 Linux 独有的。
要为一个信号设置处理函数,可以使用下面的 signal 系统调用:
#include
_sighandler_t signal(int sig, _sighandler_t _handler)
设置信号处理函数的更健壮的接口是如下的系统调用:
#include
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);
};
int sigaction(int sig, const struct sigaction *act, struct sigaction *oact);
选项 | 含义 |
---|---|
SA_NOCLDSTOP | 如果 sigaction 的 sig 参数是 SIGCHLD ,则设置该标志表示子进程暂停时不生成 SIGCHLD 信号 |
SA_NOCLDWAIT | 如果 sigaction 的 sig 参数是 SIGHLD ,则设置该标志标识子进程结束时不产生僵尸进程 |
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 |
前文提到,Linux 使用数据结构 sigset_t 来表示一组信号。其定义如下:
#include
#define_SIGSET_NWORDS(1024/(8*sizeof(unsigned long int)))
typedef struct {
unsigned long int__val[_SIGSET_NWORDS];
}__sigset_t;
由该定义可见,sigset_t 实际上是一个长整型数组,数组的每个元素的每个位表示一个信号。这种定义方式和文件描述符集 fd_set 类似。Linux 提供了如下一组函数来设置、修改、删除和查询信号集:
#include
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是否在信号集中*/
前文提到,我们可以利用 sigaction 结构体的 sa_mask 成员来设置进程的信号掩码。此外,如下函数也可以用于设置或查看进程的信号掩码:
#include
int sigprocmask(int_how, _const sigset_t *_set, sigset_t *_oset);
_how 参数 | 含义 |
---|---|
SIG_BLOCK | 信的进程信号掩码是其当前值和 _set 指定信号集的并集 |
SIG_UNBLOCK | 信的进程信号掩码是当前值和 ~_set 信号集的交集,因此 _set 指定的信号集将不被屏蔽 |
SIG_SETMASK | 直接将进程信号掩码设置为 _set |
设置进程信号掩码后,被屏蔽的信号将不能被进程接收。如果给进程发送一个被屏蔽的信号,则操作系统将该信号设置为进程的一个被挂起的信号。如果我们取消对被挂起信号的屏蔽,则它能立即被进程接收到。如下函数可以获得进程当前被挂起的信号集:
#include
int sigpending(sigset_t *set);
信号是一种异步事件:信号处理函数和程序的主循环是两条不同的执行路线。很显然,信号处理函数需要尽可能快地执行完毕,以确保该信号不被屏蔽(前面提到过,为了避免一些竞态条件,信号在处理期间,系统不会再次触发它)太久。一种典型的解决方案是:把信号的主要处理逻辑放到程序的主循环中,当信号处理函数被触发时, 它只是简单地通知主循环程序接收到信号,并把信号值传递给主循环,主循环再根据接收到的信号值执行目标信号对应的逻辑代码。信号处理函数通常使用管道来将信号“传递”给主循环:信号处理函数往管道的写端写入信号值,主循环则从管道的读端读出该信号值。那么主循环怎么知道管道上何时有数据可读呢?这很简单,我们只需要使用 I/O 复用系统调用来监听管道的读端文件描述符上的可读事件。如此一来,信号事件就能和其他 I/O 事件一样被处理,即统一事件源。
很多优秀的 I/O 框架库和后台服务器程序都统一处理信号和 I/O 事件,比如 Libevent I/O 框架库和 xinetd 超级服务。下面给出了统 一事件源的一个简单实现:
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define MAX_EVENT_NUMBER 1024 // 最大事件个数
static int pipefd[2]; // 全局管道
int setnonblocking(int fd) // 设置非阻塞状态
{
int old_option = fcntl(fd, F_GETFL);
int new_option = old_option | O_NONBLOCK;
fcntl(fd, F_SETFL, new_option);
return old_option;
}
void addfd(int epollfd, int fd)
{
epoll_event event; // 创建事件变量
event.data.fd = fd; // 存储数据fd
event.events = EPOLLIN | EPOLLET; // 期待可读事件、边缘触发模式
epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &event); // 往事件表中注册fd上的事件event
setnonblocking(fd); // 设为非阻塞
}
void sig_handler(int sig) // 信号处理函数
{
int save_errno = errno; // 保留原来的errno,在函数最后恢复,以保证函数的可重入性
int msg = sig;
send(pipefd[1], (char *)&msg, 1, 0); // 将信号值写入管道,以通知主循环
errno = save_errno;
}
void addsig(int sig) // 设置信号的处理函数
{
struct sigaction sa; // 处理信号的结构体类型
memset(&sa, '\0', sizeof(sa)); // 初始化
sa.sa_handler = sig_handler; // 指定信号处理函数为sig_handler
sa.sa_flags |= SA_RESTART; // 设置程序收到信号时的行为,SA_RESTART表示重新调用被信号终止的系统调用
sigfillset(&sa.sa_mask); // sa_mask表示进程的信号掩码,指定哪些信号不能发送给本进程,此语句表示在信号集中设置所有信号,即给mask设定所有信号
assert(sigaction(sig, &sa, NULL) != -1); // sigaction系统调用,sig是要捕获的信号,sa指定了信号处理方式
}
int main(int argc, char *argv[])
{
// 处理参数、地址
if (argc <= 2)
{
printf("usage:%s ip_address port_number\n", basename(argv[0]));
return 1;
}
const char *ip = argv[1];
int port = atoi(argv[2]);
int ret = 0;
struct sockaddr_in address;
bzero(&address, sizeof(address));
address.sin_family = AF_INET;
inet_pton(AF_INET, ip, &address.sin_addr);
address.sin_port = htons(port);
int listenfd = socket(PF_INET, SOCK_STREAM, 0);
assert(listenfd >= 0);
ret = bind(listenfd, (struct sockaddr *)&address, sizeof(address));
if (ret == -1)
{
printf("errno is%d\n", errno);
return 1;
}
ret = listen(listenfd, 5);
assert(ret != -1);
epoll_event events[MAX_EVENT_NUMBER]; // 初始化epoll事件
int epollfd = epoll_create(5); // 创建内核事件表
assert(epollfd != -1);
addfd(epollfd, listenfd); // 添加事件
ret = socketpair(PF_UNIX, SOCK_STREAM, 0, pipefd); // 使用socketpair创建双向管道,注册pipefd[0]上的可读事件
assert(ret != -1);
setnonblocking(pipefd[1]); // 管道非阻塞
addfd(epollfd, pipefd[0]); // 添加事件
// 设置一些信号的处理函数
addsig(SIGHUP); // 控制终端挂起
addsig(SIGCHLD); // 子进程状态发生变化(退出或暂停)
addsig(SIGTERM); // 终止进程,kill 命令默认发送的信号就是 SIGTERM
addsig(SIGINT); // 中断信号
bool stop_server = false; // 关闭服务器标志
while (!stop_server) // 主循环
{
int number = epoll_wait(epollfd, events, MAX_EVENT_NUMBER, -1); //-1是timeout,表示永远阻塞
if ((number < 0) && (errno != EINTR))
{
printf("epoll failure\n");
break;
}
for (int i = 0; i < number; i++)
{
int sockfd = events[i].data.fd;
// 如果就绪的文件描述符是listenfd,则处理新的连接
if (sockfd == listenfd)
{
struct sockaddr_in client_address;
socklen_t client_addrlength = sizeof(client_address);
int connfd = accept(listenfd, (struct sockaddr *)&client_address, &client_addrlength);
addfd(epollfd, connfd); // 将本次连接加入内核事件表
}
// 如果就绪的文件描述符是pipefd[0],则处理信号
else if ((sockfd == pipefd[0]) && (events[i].events & EPOLLIN)) // 来自管道的事件,并且为可读类型
{
int sig;
char signals[1024];
ret = recv(pipefd[0], signals, sizeof(signals), 0); // 接收数据
if (ret == -1)
continue;
else if (ret == 0)
continue;
else
{
// 因为每个信号值占1字节,所以按字节来逐个接收信号。我们以SIGTERM为例,来说明如何安全地终止服务器主循环
for (int i = 0; i < ret; ++i)
{
switch (signals[i])
{
case SIGCHLD:
case SIGHUP:
{
continue;
}
case SIGTERM:
case SIGINT:
{
stop_server = true; // 设置关闭服务器标志
}
}
}
}
}
else
{
}
}
}
printf("close fds\n");
close(listenfd);
close(pipefd[1]);
close(pipefd[0]);
return 0;
}
当挂起进程的控制终端时,SIGHUP 信号将被触发。对于没有控制终端的网络后台程序而言,它们通常利用 SIGHUP 信号来强制服务器重读配置文件。一个典型的例子是 xinetd 超级服务程序。
xinetd 程序在接收到 SIGHUP 信号之后将调用 hard_reconfig 函数(见 xinetd 源码),它循环读取 /etc/xinetd.d/ 目录下的每个子配置文件,并检测其变化。如果某个正在运行的子服务的配置文件被修改以停止服务,则 xinetd 主进程将给该子服务进程发送 SIGTERM 信号以结束它。如果某个子服务的配置文件被修改以开启服务,则 xinetd 将创建新的 socket 并将其绑定到该服务对应的端口上。
默认情况下,往一个读端关闭的管道或 socket 连接中写数据将引发SIGPIPE信号。我们需要在代码中捕获并处理该信号,或者至少忽略它,因为程序接收到 SIGPIPE 信号的默认行为是结束进程,而我们绝对不希望因为错误的写操作而导致程序退出。引起 SIGPIPE 信号的写操作将设置 errno 为 EPIPE 。
可以使用 send 函数的 MSG_NOSIGNAL 标志来禁止写操作触发 SIGPIPE 信号。在这种情况下,应该使用 send 函数反馈的 errno 值来判断管道或者 socket 连接的读端是否已经关闭。
此外,也可以利用 I/O 复用系统调用来检测管道和 socket 连接的读端是否已经关闭。以 poll 为例,当管道的读端关闭时,写端文件描述符上的 POLLHUP 事件将被触发;当 socket 连接被对方关闭时,socket 上的 POLLRDHUP 事件将被触发。
在 Linux 环境下,内核通知应用程序带外数据到达主要有两种方法:一种是 I/O 复用技术,select 等系统调用在接收到带外 数据时将返回,并向应用程序报告 socket 上的异常事件;另外一种方法就是使用 SIGURG 信号。