这篇文章上次修改于 1008 天前,可能其部分内容已经发生变化,如有疑问可询问作者。
背景
系统调用被信号打断后, linux 默认不会重启系统调用,当然我们可以设置一些选项来重启一部分系统调用,但并不是所有的系统调用都可以被重启,比如我们经常使用的多路 I/O 复用模型 epoll 中的 epoll_wait 就是一个典型例子。
信号
信号可能会对程序的一般流程造成很大的破坏,因为它们本质上是异步的。当您在系统调用中被阻止(导致它们失败)或在执行用户空间指令(可能导致竞争条件)时,它们可能会发生。您可能必须仔细地为您的程序建模以补偿信号的异步性质,这是有经验的 Unix 程序员始终考虑的事情。
信号注意事项
以下是一些信号相关注意事项
- 对于体面的程序,您不能取消信号。它们是 Linux 下的现实。
- 您必须注意从信号处理程序中调用了哪些函数。您调用的函数必须是异步信号安全的。signal-safety(7)中列举了信号安全相关的函数
- 不应该从信号处理程序中调用不可重入函数,因为它们可以从主代码中调用,在执行过程中被中断。如果信号处理程序调用相同的函数,就会出现问题。
- 当程序被系统调用阻塞时出现信号时,系统调用会返回错误
EINTR
。但是,可以通过使用sigaction(2)函数设置信号处理程序时指定SA_RESTART
标志 来自动重新启动许多系统调用。- 不幸的是,有很多系统调用(如 epoll_wait、epoll_pwait、poll、ppoll、select、pselect、recv、send、和 nanosleep) 尽管
SA_RESTART
已指定,但永远不会重新启动。- 如果在设置信号处理程序和调用
pause
等待信号发生之间发生信号,则很容易“丢失”或错过信号,从而陷入等待信号的状态。
不可信号重启的系统调用
以下接口在被信号打断后永远不会重新启动,无论是否使用 SA_RESTART
,他们总是以错误 EINTR
失败:
- “输入”套接字接口,当已使用 setsockopt 在套接字上设置超时 (SO_RCVTIMEO):accept、recv、 recvfrom, recvmmsg (也有非空超时参数) 和 recvmsg。
- “输出”套接字接口,当已使用 setsockopt 在套接字上设置超时 (SO_RCVTIMEO):connect, send、sendto 和 sendmsg。
- 用于等待信号的接口:pause, sigsuspend, sigtimedwait 和 sigwaitinfo。
- 文件描述符复用接口:epoll_wait, epoll_pwait、poll、ppoll、select 和 pselect。
- SystemV IPC 接口:msgrcv、msgsnd、semop 和 semtimedop。
- sleep 接口:clock_nanosleep、nanosleep 和 usleep。
- io_getevents。
- 如果被中断, sleep 函数也永远不会重新启动处理程序,但成功返回还剩下的睡眠秒数。
设置自动重启标志
简单总结一下设置系统调用被信号中断时自动重启标志的方法 :
在安装信号捕捉函数时
struct sigaction sa;
sigemptyset(&sa.sa_mask);//清空信号掩码集 sigemptyset简单地将 初始化signalmask为空,这样就可以保证不会屏蔽任何信号。(也就是说,所有的信号都会被接收到)
sa.sa_flags = SA_RESTART;//设置重启标志
sa.sa_handler = sigalrm_handler;//指定信号捕捉函数名称
if (sigaction(SIGALRM, &sa, NULL) == -1) handle_error("sigaction");//注册信号捕捉函数
被信号打断的epoll_wait
#include <errno.h>
#include <netinet/in.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/epoll.h>
#include <unistd.h>
#define handle_error(msg) \
do \
{ \
perror(msg); \
exit(EXIT_FAILURE); \
} while (0)
#define MAX_EVENTS 10
struct epoll_event ev, events[MAX_EVENTS];
int listen_sock, conn_sock, nfds, epollfd;
/*
* Our signal handler simply prints a message and returns.
* */
void sigalrm_handler(int signo) { printf("Received SIGALRM\n"); }
/*
* This function is responsible for setting up the
* listening socket for the socket-based echo
* functionality. Pretty standard stuff.
* */
int setup_listening_socket(int port)
{
int sock;
struct sockaddr_in srv_addr;
sock = socket(PF_INET, SOCK_STREAM, 0);
if (sock == -1) handle_error("socket()");
int enable = 1;
if (setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &enable, sizeof(int)) < 0)
handle_error("setsockopt(SO_REUSEADDR)");
memset(&srv_addr, 0, sizeof(srv_addr));
srv_addr.sin_family = AF_INET;
srv_addr.sin_port = htons(port);
srv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
/* We bind to a port and turn this socket into a listening
* socket.
* */
if (bind(sock, (const struct sockaddr *)&srv_addr, sizeof(srv_addr)) < 0)
handle_error("bind()");
if (listen(sock, 10) < 0) handle_error("listen()");
return (sock);
}
/*
* Reads 1kb from the "in" file descriptor and
* writes it to the "out" file descriptor.
*/
void copy_fd(int in, int out)
{
char buff[1024];
int bytes_read;
bzero(buff, sizeof(buff));
bytes_read = read(in, buff, sizeof(buff) - 1);
if (bytes_read == -1)
handle_error("read");
else if (bytes_read == 0)
return;
write(out, buff, strlen(buff));
}
/*
* If a parsable number is passed as the 1st argument to the
* program, sets up SIGALRM to be sent to self the specified
* seconds later.
* */
void setup_signals(int argc, char **argv)
{
struct sigaction sa;
/* No argument passed, and so no signal will be delivered */
if (argc < 2)
{
printf("No alarm set. Will not be interrupted.\n");
return;
}
/* if a proper number is passed, we setup the alarm,
* else we return without setting one up.
* */
errno = 0;
long alarm_after = strtol(argv[1], NULL, 10);
if (errno) handle_error("strtol()");
if (alarm_after)
printf("Alarm after %ld seconds.\n", alarm_after);
else
{
printf("No alarm set. Will not be interrupted.\n");
return;
}
/*
* Setup a signal handler for the SIGALRM signal
* */
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_RESTART;
sa.sa_handler = sigalrm_handler;
if (sigaction(SIGALRM, &sa, NULL) == -1) handle_error("sigaction");
/*
* Let's send ourselves a SIGALRM signal specified
* seconds later.
* */
alarm(alarm_after);
}
/**
* Helper function to setup epoll
*/
void setup_epoll()
{
epollfd = epoll_create1(0);
if (epollfd == -1) handle_error("epoll_create1()");
}
/**
* Adds the file descriptor passed to be monitored by epoll
* */
void add_fd_to_epoll(int fd)
{
/* Add fd to be monitored by epoll */
ev.events = EPOLLIN;
ev.data.fd = fd;
if (epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &ev) == -1) handle_error("epoll_ctl");
}
int main(int argc, char *argv[])
{
/* Setup sigalrm if a number is passed as the first argument */
setup_signals(argc, argv);
/* Let's setup epoll */
setup_epoll();
/* Add stdin-based echo server to epoll's monitoring list */
add_fd_to_epoll(STDIN_FILENO);
/* Setup a socket to listen on port 5000 */
listen_sock = setup_listening_socket(5000);
/* Add socket-based echo server to epoll's monitoring list */
add_fd_to_epoll(listen_sock);
while (1)
{
/* Let's wait for some activity on either stdin or on the socket */
nfds = epoll_wait(epollfd, events, MAX_EVENTS, -1);
if (nfds == -1) handle_error("epoll_wait()");
/*
* For each of the file descriptors epoll says are ready,
* check which one it is the echo read data back.
* */
for (int n = 0; n < nfds; n++)
{
if (events[n].data.fd == STDIN_FILENO)
{
printf("stdin ready..\n");
copy_fd(STDIN_FILENO, STDOUT_FILENO);
}
else if (events[n].data.fd == conn_sock)
{
printf("socket data ready..\n");
copy_fd(conn_sock, conn_sock);
}
else if (events[n].data.fd == listen_sock)
{
/* Listening socket is ready, meaning
* there's a new client connection */
printf("new connection ready..\n");
conn_sock = accept(listen_sock, NULL, NULL);
if (conn_sock == -1) handle_error("accept()");
/* Add the connected client to epoll's monitored FDs list */
add_fd_to_epoll(conn_sock);
}
}
}
return 0;
}
运行:
gcc a.c
./a.out 5
当5秒后定时信号 SIGALRM
被信号捕捉函数处理之后, 返回 epoll_wait
时报错被中断的系统调用,程序结束运行。
这显然是对于服务器来说不能忍受的。
epoll_wait脆弱到被任何信号捕捉函数中断后,无法继续运行下去
注: 上面的代码启用了 自动重启
标志,但从结果来看并不能对 epoll_wait
生效。
因篇幅原因,故不对没有设置自动重启标志的 epoll_wait 测试,结果显而易见 epoll_wait 被中断了。
引入signalfd
如何避免循环 while(1){epoll_wait()}
,这里引出我们这篇文章的主角 signalfd
。
Linux 有一种机制可以将异步信号转换为可由epoll 监听的描述符。如果信号可以通过文件描述符传递,就像数据一样,那么现有的 I/O 多路复用机制epoll可以用来处理信号,就像我们处理代表本地文件或套接字的其他文件描述符一样。
如何将 signal
的异步中断转化为 signalfd
的一个事件,是我们接下来要考虑的问题
解决办法
#include <errno.h>
#include <netinet/in.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/epoll.h>
#include <sys/signalfd.h>
#include <sys/time.h>
#include <unistd.h>
#define handle_error(msg) \
do \
{ \
perror(msg); \
exit(EXIT_FAILURE); \
} while (0)
#define MAX_EVENTS 10
struct epoll_event ev, events[MAX_EVENTS];
int listen_sock, conn_sock, nfds, epollfd, sfd;
/**
* This function is responsible for setting up the
* listening socket for the socket-based echo
* functionality. Pretty standard stuff.
* */
int setup_listening_socket(int port)
{
int sock;
struct sockaddr_in srv_addr;
sock = socket(PF_INET, SOCK_STREAM, 0);
if (sock == -1) handle_error("socket()");
int enable = 1;
if (setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &enable, sizeof(int)) < 0)
handle_error("setsockopt(SO_REUSEADDR)");
memset(&srv_addr, 0, sizeof(srv_addr));
srv_addr.sin_family = AF_INET;
srv_addr.sin_port = htons(port);
srv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
/* We bind to a port and turn this socket into a listening
* socket.
* */
if (bind(sock, (const struct sockaddr *)&srv_addr, sizeof(srv_addr)) < 0)
handle_error("bind()");
if (listen(sock, 10) < 0) handle_error("listen()");
return (sock);
}
/**
* Reads 1kb from the "in" file descriptor and
* writes it to the "out" file descriptor.
*/
void copy_fd(int in, int out)
{
char buff[1024];
int bytes_read;
bzero(buff, sizeof(buff));
bytes_read = read(in, buff, sizeof(buff) - 1);
if (bytes_read == -1)
handle_error("read");
else if (bytes_read == 0)
return;
write(out, buff, strlen(buff));
}
/**
* If a parsable number is passed as the 1st argument to the
* program, sets up SIGALRM to be sent to self the specified
* seconds later.
* */
void setup_signals(int argc, char **argv)
{
sigset_t mask;
long alarm_after = 0;
/* No argument passed. Let's set a default interval of 5. */
if (argc < 2)
{
printf("No alarm set. Will default to 5 seconds.\n");
alarm_after = 5;
}
/* if a proper number is passed, we setup the alarm,
* else we return without setting one up.
* */
errno = 0;
if (alarm_after == 0)
{
alarm_after = strtol(argv[1], NULL, 10);
if (errno) handle_error("strtol()");
if (alarm_after)
printf("Alarm set every %ld seconds.\n", alarm_after);
else
{
printf("No alarm set. Will default to 5 seconds.\n");
return;
}
}
/*
* Setup SIGALRM to be delivered via SignalFD
* */
sigemptyset(&mask);
sigaddset(&mask, SIGALRM);
sigaddset(&mask, SIGQUIT);
/*
* Block these signals so that they are not handled
* in the usual way. We want them to be handled via
* SignalFD.
* */
if (sigprocmask(SIG_BLOCK, &mask, NULL) == -1) handle_error("sigprocmask");
sfd = signalfd(-1, &mask, 0);
if (sfd == -1) handle_error("signalfd");
/*
* Let's send ourselves a SIGALRM signal every specified
* seconds continuously.
* */
struct itimerval itv;
itv.it_interval.tv_sec = alarm_after;
itv.it_interval.tv_usec = 0;
itv.it_value = itv.it_interval;
if (setitimer(ITIMER_REAL, &itv, NULL) == -1) handle_error("setitimer()");
}
/**
* Helper function to setup epoll
*/
void setup_epoll()
{
epollfd = epoll_create1(0);
if (epollfd == -1) handle_error("epoll_create1()");
}
/**
* Adds the file descriptor passed to be monitored by epoll
* */
void add_fd_to_epoll(int fd)
{
/* Add fd to be monitored by epoll */
ev.events = EPOLLIN;
ev.data.fd = fd;
if (epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &ev) == -1) handle_error("epoll_ctl");
}
/**
* This is not a signal handler in the traditional sense.
* Signal handlers are invoked by the kernel asynchronously.
* Meaning, we have no control over when it's invoked and
* if we need to restart any system calls.
* This function is invoked from our epoll based event loop
* synchronously. Meaning, we have full control over when we
* invoke this function call. And we do not interrupt any
* system calls. This makes error handling much simpler in
* our programs.
*/
void handle_signals()
{
struct signalfd_siginfo sfd_si;
if (read(sfd, &sfd_si, sizeof(sfd_si)) == -1) handle_error("read()");
if (sfd_si.ssi_signo == SIGALRM)
printf("Got SIGALRM via SignalFD.\n");
else if (sfd_si.ssi_signo == SIGQUIT)
{
printf("Got SIGQUIT. Will exit.\n");
exit(0);
}
else
printf("Got unexpected signal!\n");
}
int main(int argc, char *argv[])
{
/* Let's setup epoll */
setup_epoll();
/* Add stdin-based echo server to epoll's monitoring list */
add_fd_to_epoll(STDIN_FILENO);
/* Setup a socket to listen on port 5000 */
listen_sock = setup_listening_socket(5000);
/* Add socket-based echo server to epoll's monitoring list */
add_fd_to_epoll(listen_sock);
/* Setup sigalrm+sigquit if a number is passed as the first argument */
setup_signals(argc, argv);
/* Add the SignalFD file descriptor to epoll's monitoring list */
add_fd_to_epoll(sfd);
while (1)
{
/* Let's wait for some activity on either stdin or on the socket */
nfds = epoll_wait(epollfd, events, MAX_EVENTS, -1);
if (nfds == -1) handle_error("epoll_wait()");
/*
* For each of the file descriptors epoll says are ready,
* check which one it is the echo read data back.
* */
for (int n = 0; n < nfds; n++)
{
if (events[n].data.fd == STDIN_FILENO)
{
printf("stdin ready..\n");
copy_fd(STDIN_FILENO, STDOUT_FILENO);
}
else if (events[n].data.fd == conn_sock)
{
printf("socket data ready..\n");
copy_fd(conn_sock, conn_sock);
}
else if (events[n].data.fd == listen_sock)
{
/* Listening socket is ready, meaning
* there's a new client connection */
printf("new connection ready..\n");
conn_sock = accept(listen_sock, NULL, NULL);
if (conn_sock == -1) handle_error("accept()");
/* Add the connected client to epoll's monitored FDs list */
add_fd_to_epoll(conn_sock);
}
else if (events[n].data.fd == sfd)
{
handle_signals();
}
}
}
return 0;
}
关键步骤
//注册signalfd(仅增加的部分步骤)
sigset_t mask;
int sfd = signalfd(-1, &mask, 0);
if (sfd == -1) handle_error("signalfd");
//将signalfd添加进epoll监听事件
add_fd_to_epoll(sfd);
//epoll对与signalfd的处理函数
void handle_signals()
{
struct signalfd_siginfo sfd_si;//新的结构体来分辨是何种信号
if (read(sfd, &sfd_si, sizeof(sfd_si)) == -1) handle_error("read()");
if (sfd_si.ssi_signo == SIGALRM)
printf("Got SIGALRM via SignalFD.\n");
else if (sfd_si.ssi_signo == SIGQUIT)
{
printf("Got SIGQUIT. Will exit.\n");
exit(0);
}
else
printf("Got unexpected signal!\n");
}
//影响了while(1)中的epoll_wait的处理流程,增加了判断
if (events[n].data.fd == sfd)
{
handle_signals();
}
至此我们学会了如何避免 epoll 被信号处理函数中断,增加了服务器的稳定性, congratulations!
文章来源
文章发自将signalfd加入epoll
[hide]微信推送[/hide]
没有评论