如何优雅地处理给予EMFILE并关闭连接的accept()?

bin*_*y01 7 c sockets

当进程用完文件描述符时,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;
}
Run Code Online (Sandbox Code Playgroud)

使用有限数量的文件描述符编译并运行此代码:

gcc srv.c
ulimit -n 10
strace -t ./a.out 2>&1 |less
Run Code Online (Sandbox Code Playgroud)

在另一个控制台中,我跑了

 telnet localhost 65432 &
Run Code Online (Sandbox Code Playgroud)

在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.
Run Code Online (Sandbox Code Playgroud)

基本上在这一点上:

  • 代码将尽可能快地调用accept(),无法一遍又一遍地接受相同的TCP连接,从而搅拌CPU.
  • 客户端将保持连接状态(因为TCP握手在应用程序接受连接之前完成),并且客户端不会获得有问题的信息.

所以,

  1. 有没有办法强制TCP连接导致accept()无法关闭(例如,客户端可以快速通知,也许可以尝试其他服务器)

  2. 当出现这种情况时(或者完全防止这种情况),防止服务器代码进入无限循环的最佳做法是什么?

PSk*_*cik 4

您可以在程序开头留出一个额外的 fd 并跟踪 EMFILE 条件:

int reserve_fd;
_Bool out_of_fd = 0;

if(0>(reserve_fd = dup(1)))
    err("dup()");
Run Code Online (Sandbox Code Playgroud)

然后,如果您遇到 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;
}
Run Code Online (Sandbox Code Playgroud)

完整示例:

#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;
}
Run Code Online (Sandbox Code Playgroud)

如果你是多线程的,那么我想你需要一个围绕 fd 生成函数的锁,并在关闭额外的 fd (同时期望接受最终连接)时获取它,以防止备用槽被另一个线程填充。

所有这些只有在以下情况下才有意义:1)侦听套接字未与其他进程共享(可能尚未达到其 EMFILE 限制)并且 2)服务器处理持久连接(因为如果不这样做,那么您' re 很快就会关闭一些现有的连接,为您的下一次尝试释放一个 fd 插槽accept)。