为什么Linux不接受()返回EINTR?

环境:一个类似RedHat的发行版,2.6.39内核,glibc 2.12。

我完全认为,如果accept()正在进行的时候发送了一个信号,接受应该会失败,留下errno == EINTR。 但是,我不这样做,我想知道为什么。 下面是示例程序和strace输出。

#include <stdio.h> #include <sys/types.h> #include <sys/socket.h> #include <signal.h> #include <errno.h> #include <arpa/inet.h> #include <string.h> static void sigh(int); int main(int argc, char ** argv) { int s; struct sockaddr_in sin; if ((s = socket(AF_INET, SOCK_STREAM, 0))<0) { perror("socket"); return 1; } memset(&sin, 0, sizeof(struct sockaddr_in)); sin.sin_family = AF_INET; if (bind(s, (struct sockaddr*)&sin, sizeof(struct sockaddr_in))) { perror("bind"); return 1; } if (listen(s, 5)) { perror("listen"); } signal(SIGQUIT, sigh); while (1) { socklen_t sl = sizeof(struct sockaddr_in); int rc = accept(s, (struct sockaddr*)&sin, &sl); if (rc<0) { if (errno == EINTR) { printf("accept restarted\n"); continue; } perror("accept"); return 1; } printf("accepted fd %d\n", rc); close(rc); } } void sigh(int s) { signal(s, sigh); unsigned char p[100]; int i = 0; while (s) { p[i++] = '0'+(s%10); s/=10; } write(1, "sig ", 4); for (i--; i>=0; i--) { write(1, &p[i], 1); } write(1, "\n", 1); } 

strace输出:

 execve("./accept", ["./accept"], [/* 57 vars */]) = 0 <skipped> socket(PF_INET, SOCK_STREAM, IPPROTO_IP) = 3 bind(3, {sa_family=AF_INET, sin_port=htons(0), sin_addr=inet_addr("0.0.0.0")}, 16) = 0 listen(3, 5) = 0 rt_sigaction(SIGQUIT, {0x4008c4, [QUIT], SA_RESTORER|SA_RESTART, 0x30b7e329a0}, {SIG_DFL, [], 0}, 8) = 0 accept(3, 0x7fffe3e3c500, [16]) = ? ERESTARTSYS (To be restarted) --- SIGQUIT (Quit) @ 0 (0) --- rt_sigaction(SIGQUIT, {0x4008c4, [QUIT], SA_RESTORER|SA_RESTART, 0x30b7e329a0}, {0x4008c4, [QUIT], SA_RESTORER|SA_RESTART, 0x30b7e329a0}, 8) = 0 write(1, "sig ", 4sig ) = 4 write(1, "3", 13) = 1 write(1, "\n", 1 ) = 1 rt_sigreturn(0x1) = 43 accept(3, ^C <unfinished ...> 

就在我要发布这个时,strace输出中的“SA_RESTART”标志引起了我的注意。 signal(2)手册页说,signal() “…使用提供BSD语义的标志调用sigaction(2)…”从glibc2开始。

SA_RESTART标志“…使得某些系统调用可以跨信号重新启动…” ,这隐藏了从用户重新开始呼叫的过程。 所以,这不是具体的接受(),其他一些系统调用也受到影响,而不是有清晰的清单。

因此,如果您需要对来自可能在系统调用中阻塞的线程的信号作出反应,则应该使用sigaction()来设置您的信号处理程序,而不是signal()。 下面是修改后的示例程序,完全可以作为参考。

 #include <stdio.h> #include <sys/types.h> #include <sys/socket.h> #include <signal.h> #include <errno.h> #include <arpa/inet.h> #include <string.h> static void sigh(int); static struct sigaction sa; int main(int argc, char ** argv) { int s; struct sockaddr_in sin; if ((s = socket(AF_INET, SOCK_STREAM, 0))<0) { perror("socket"); return 1; } memset(&sin, 0, sizeof(struct sockaddr_in)); sin.sin_family = AF_INET; if (bind(s, (struct sockaddr*)&sin, sizeof(struct sockaddr_in))) { perror("bind"); return 1; } if (listen(s, 5)) { perror("listen"); } memset(&sa, 0, sizeof(struct sigaction)); sa.sa_handler = sigh; sigemptyset(&sa.sa_mask); sigaction(SIGQUIT, &sa, 0); while (1) { socklen_t sl = sizeof(struct sockaddr_in); int rc = accept(s, (struct sockaddr*)&sin, &sl); if (rc<0) { if (errno == EINTR) { printf("accept restarted\n"); continue; } perror("accept"); return 1; } printf("accepted fd %d\n", rc); close(rc); } } void sigh(int s) { sigaction(SIGQUIT, &sa, 0); unsigned char p[100]; int i = 0; while (s) { p[i++] = '0'+(s%10); s/=10; } write(1, "sig ", 4); for (i--; i>=0; i--) { write(1, &p[i], 1); } write(1, "\n", 1); } 

和strace:

 execve("./accept", ["./accept"], [/* 57 vars */]) = 0 socket(PF_INET, SOCK_STREAM, IPPROTO_IP) = 3 bind(3, {sa_family=AF_INET, sin_port=htons(0), sin_addr=inet_addr("0.0.0.0")}, 16) = 0 listen(3, 5) = 0 rt_sigaction(SIGQUIT, {0x400994, [], SA_RESTORER, 0x30b7e329a0}, NULL, 8) = 0 accept(3, 0x7fffb626be90, [16]) = ? ERESTARTSYS (To be restarted) --- SIGQUIT (Quit) @ 0 (0) --- rt_sigaction(SIGQUIT, {0x400994, [], SA_RESTORER, 0x30b7e329a0}, NULL, 8) = 0 write(1, "sig ", 4) = 4 write(1, "3", 13) = 1 write(1, "\n", 1) = 1 rt_sigreturn(0x1) = -1 EINTR (Interrupted system call) write(1, "accept restarted\n", 17) = 17 accept(3, 

在Unix网络编程的书中,有一段说:

我们使用术语“慢速系统调用”来描述accept ,并且我们使用这个术语用于任何可以永久阻止的系统调用。 也就是说,系统调用不需要返回。 大多数网络功能都属于这一类。 例如,如果没有连接到服务器的客户端,则不能保证服务器的accept调用将永远返回。 同样,如果客户端永远不会为服务器发送回应消息,图5.3中read服务器调用将永远不会返回。 慢速系统调用的其他例子是管道和终端设备的读写。 一个明显的例外是磁盘I / O,通常返回给调用者(假设没有灾难性的硬件故障)。

这里应用的基本规则是当一个进程在一个缓慢的系统调用中被阻塞,并且进程捕获一个信号并且信号处理程序返回时,系统调用可以返回一个EINTR错误。 一些内核自动重启一些中断的系统调用。 为了可移植性,当我们编写一个捕获信号的程序(大多数并发服务器捕获SIGCHLD )时,我们必须为缓慢的系统调用准备返回EINTR 。 可移植性问题是由之前使用的限定词“can”和“some”引起的,POSIX SA_RESTART标志的支持是可选的。 即使一个实现支持SA_RESTART标志,并不是所有中断的系统调用都会自动重新启动。 例如,大多数伯克利派生的实现永远不会自动重新启动select,而其中一些实现从不重新启动acceptrecvfrom