当进程用完文件描述符时,accept()将失败并将errno
设置为EMFILE。
但是,已接受的基础连接未关闭,因此似乎无法通知客户端应用程序代码无法处理连接。
问题是在用完文件描述符时接受TCP连接的正确行动是什么。
以下代码演示了我想学习如何最好地处理的问题(注意这只是用于演示问题/问题的示例代码,而不是生产代码)
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
static void err(const char *str)
{
perror(str);
exit(1);
}
int main(int argc,char *argv[])
{
int serversocket;
struct sockaddr_in serv_addr;
serversocket = socket(AF_INET,SOCK_STREAM,0);
if(serversocket < 0)
err("socket()");
memset(&serv_addr,0,sizeof serv_addr);
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr= INADDR_ANY;
serv_addr.sin_port = htons(6543);
if(bind(serversocket,(struct sockaddr*)&serv_addr,sizeof serv_addr) < 0)
err("bind()");
if(listen(serversocket,10) < 0)
err("listen()");
for(;;) {
struct sockaddr_storage client_addr;
socklen_t client_len = sizeof client_addr;
int clientfd;
clientfd = accept(serversocket,(struct sockaddr*)&client_addr,&client_len);
if(clientfd < 0) {
continue;
}
}
return 0;
}
使用有限数量的文件描述符编译并运行此代码:
gcc srv.c
ulimit -n 10
strace -t ./a.out 2>&1 |less
在另一个控制台中,我运行
telnet localhost 65432 &
在accept()失败之前需要多次:
strace的输出显示了这种情况:
13:21:12 socket(AF_INET, SOCK_STREAM, IPPROTO_IP) = 3
13:21:12 bind(3, {sa_family=AF_INET, sin_port=htons(6543), sin_addr=inet_addr("0.0.0.0")}, 16) = 0
13:21:12 listen(3, 10) = 0
13:21:12 accept(3, {sa_family=AF_INET, sin_port=htons(43630), sin_addr=inet_addr("127.0.0.1")}, [128->16]) = 4
13:21:19 accept(3, {sa_family=AF_INET, sin_port=htons(43634), sin_addr=inet_addr("127.0.0.1")}, [128->16]) = 5
13:21:22 accept(3, {sa_family=AF_INET, sin_port=htons(43638), sin_addr=inet_addr("127.0.0.1")}, [128->16]) = 6
13:21:23 accept(3, {sa_family=AF_INET, sin_port=htons(43642), sin_addr=inet_addr("127.0.0.1")}, [128->16]) = 7
13:21:24 accept(3, {sa_family=AF_INET, sin_port=htons(43646), sin_addr=inet_addr("127.0.0.1")}, [128->16]) = 8
13:21:26 accept(3, {sa_family=AF_INET, sin_port=htons(43650), sin_addr=inet_addr("127.0.0.1")}, [128->16]) = 9
13:21:27 accept(3, 0xbfe718f4, [128]) = -1 EMFILE (Too many open files)
13:21:27 accept(3, 0xbfe718f4, [128]) = -1 EMFILE (Too many open files)
13:21:27 accept(3, 0xbfe718f4, [128]) = -1 EMFILE (Too many open files)
13:21:27 accept(3, 0xbfe718f4, [128]) = -1 EMFILE (Too many open files)
... and thousands upon thousands of more accept() failures.
基本上就在这一点上:
所以,
有没有办法强制TCP连接导致accept()无法关闭(例如,客户端可以快速通知并可能尝试其他服务器)
在出现这种情况时(或完全防止这种情况),防止服务器代码进入无限循环的最佳做法是什么?
答案 0 :(得分:3)
您可以在程序开头留出额外的fd并跟踪EMFILE条件:
int reserve_fd;
_Bool out_of_fd = 0;
if(0>(reserve_fd = dup(1)))
err("dup()");
然后,如果您点击EMFILE条件,您可以关闭reserve_fd
并使用其插槽接受新连接(然后您将立即关闭):
clientfd = accept(serversocket,(struct sockaddr*)&client_addr,&client_len);
if (out_of_fd){
close(clientfd);
if(0>(reserve_fd = dup(1)))
err("dup()");
out_of_fd=0;
continue; /*doing other stuff that'll hopefully free the fd*/
}
if(clientfd < 0) {
close(reserve_fd);
out_of_fd=1;
continue;
}
完整示例:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
static void err(const char *str)
{
perror(str);
exit(1);
}
int main(int argc,char *argv[])
{
int serversocket;
struct sockaddr_in serv_addr;
serversocket = socket(AF_INET,SOCK_STREAM,0);
if(serversocket < 0)
err("socket()");
int yes;
if ( -1 == setsockopt(serversocket, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(int)) )
perror("setsockopt");
memset(&serv_addr,0,sizeof serv_addr);
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr= INADDR_ANY;
serv_addr.sin_port = htons(6543);
if(bind(serversocket,(struct sockaddr*)&serv_addr,sizeof serv_addr) < 0)
err("bind()");
if(listen(serversocket,10) < 0)
err("listen()");
int reserve_fd;
int out_of_fd = 0;
if(0>(reserve_fd = dup(1)))
err("dup()");
for(;;) {
struct sockaddr_storage client_addr;
socklen_t client_len = sizeof client_addr;
int clientfd;
clientfd = accept(serversocket,(struct sockaddr*)&client_addr,&client_len);
if (out_of_fd){
close(clientfd);
if(0>(reserve_fd = dup(1)))
err("dup()");
out_of_fd=0;
continue; /*doing other stuff that'll hopefully free the fd*/
}
if(clientfd < 0) {
close(reserve_fd);
out_of_fd=1;
continue;
}
}
return 0;
}
如果你是多线程的,那么我想你需要锁定fd生成函数并在关闭额外的fd(同时期望接受最终连接)时接受它以防止填充备用插槽另一个主题。
如果1)侦听套接字未与其他进程共享(可能尚未达到其EMFILE限制),并且2)服务器处理持久连接(因为如果没有,然后你必须很快关闭一些现有的连接,释放一个fd插槽,以便下一次尝试accept
)。
答案 1 :(得分:2)
<强>问题强>
如果达到最大文件描述符数,则无法接受客户端连接。这可以是进程限制(errno EMFILE
)或全局系统限制(errno ENFILE
)。客户端没有立即注意到这种情况,并且他认为服务器接受了连接。如果太多这样的连接堆积在套接字上(当积压运行时),服务器将停止发送syn-ack数据包,连接请求将在客户端超时(这可能是一个非常令人讨厌的延迟)
文件描述符数量
当然可以在受到攻击时扩展两个限制。对于进程范围限制,使用setrlimit(RLIMIT_NOFILE, ...)
,系统范围限制sysctl()
是要调用的命令。两者都可能需要root权限,第一个只能提高硬限制。
但是,文件描述符限制通常有充分的理由来防止系统资源过度使用,因此这不适用于所有情况。
从EMFILE中恢复
一种选择是在收到sleep(n)
后实施EMFILE
,一秒钟就足以通过过于频繁地调用accept()
来阻止额外的系统负载。这可能有助于处理短暂的连接突发。
但是,如果情况不能很快恢复正常,则应采取其他措施(例如,如果必须连续5次调用sleep()或类似情况)。
在这种情况下,建议关闭服务器套接字。所有挂起的客户端连接将立即终止(通过接收RST数据包),并且客户端可以使用其他服务器(如果适用)。此外,没有接受新的客户端连接,但立即拒绝(连接被拒绝),而不是在套接字保持打开时可能发生的超时。
争用发布后,可以再次打开服务器套接字。对于EMFILE
情况,只有当这些连接低于某个阈值时,才需要跟踪打开的客户端连接数并重新打开服务器套接字。在系统范围内,没有一般的答案,可能只是尝试一段时间后或使用/ proc文件系统或系统工具如lsof
来找出争用何时停止。
答案 2 :(得分:2)
我读过的一个解决方案是保留一个“备用”文件描述符,当你超过fd容量时,你可以使用它来接受并立即关闭新连接。例如:
int sparefd = open("/dev/null", O_RDONLY);
然后,当accept
返回EMFILE
时,您可以:
close(sparefd); // create an available file descriptor
int newfd = accept(...); // accept a new connection
close(newfd); // immediately close the connection
sparefd = open("/dev/null", O_RDONLY); // re-create spare
它并不完全优雅,但它可能比在某些情况下关闭侦听套接字要好一些。要小心,如果你的程序是多线程的,那么另一个线程可能会在你释放后立即“声明”备用fd;没有简单的方法可以解决这个问题(“硬”方法是在可能使用文件描述符的每个操作周围放置一个互斥锁。)