【问题标题】:select and accept delay in Linux在 Linux 中选择并接受延迟
【发布时间】:2012-08-05 02:28:36
【问题描述】:

我创建了一个简单的应用程序来使用 select() 和 accept() 接受 IPv4 TCP 连接。

我使用 python 脚本来测试它。它依次打开 100 个连接。即:

for i in range(100):
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    print s.connect((IP, PORT))
    s.send("Test\r\n")

我观察到我的应用程序在第一个 X 连接后卡在 select() 中 2 秒。 来自 strace 的输出:

1344391414.452208 select(30, [3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29], NULL, NULL, NULL) = 1 (in [3])
1344391416.742843 accept(3, 0, NULL)    = 30

我的代码如下。知道我做错了什么吗?

#include <assert.h>
#include <errno.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <syslog.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/select.h>

int
fd_create (void)
{
    int fd;
    int set = true;
    struct sockaddr_in addr;

    fd = socket(AF_INET, SOCK_STREAM, 0);

    setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &set, sizeof(set));

    memset(&addr, 0, sizeof(addr));
    addr.sin_family = AF_INET;
    addr.sin_port = htons(1999);
    addr.sin_addr.s_addr = INADDR_ANY;

    bind(fd, (struct sockaddr *)&addr, sizeof(addr));

    listen(fd, 1024);

    return (fd);
}

int
fd_echo (int fd)
{
    int n;
    char buffer[128 + 1];

    while ((n = recv(fd, buffer, 128, 0)) > 0);

    return (n);
}

int
main (void)
{
    int listen_fd;
    fd_set working;
    fd_set master;
    int max_fd;
    int i;
    int new_fd;
    int rc;
    int con;

    FD_ZERO(&master);
    listen_fd = fd_create();
    fcntl(listen_fd, F_SETFL, fcntl(listen_fd, F_GETFL) | O_NONBLOCK);

    max_fd = listen_fd;
    printf("%d\n", listen_fd);
    FD_SET(listen_fd, &master);
    con = 0;
    for (;;) {
    memcpy(&working, &master, sizeof(fd_set));
    select(max_fd + 1, &working, NULL, NULL, NULL);

    for (i = 0; i <= max_fd; i++) {
        if (FD_ISSET(i, &working)) {
        if (i == listen_fd) {
            while ((new_fd = accept(i, NULL, NULL)) >= 0) {
            fcntl(new_fd, F_SETFL, fcntl(new_fd, F_GETFL) | O_NONBLOCK);
            FD_SET(new_fd, &master);
            if (max_fd < new_fd) {
                max_fd = new_fd;
            }
            printf("New connection %d (%d)\n", new_fd, ++con);
            }
            if ((new_fd == -1) && (errno != EAGAIN && errno != EWOULDBLOCK)) {
            return(0);
            }
        } else {
            rc = fd_echo(i);
            if ((rc == 0) ||
            ((rc == -1) && ((errno != EAGAIN && errno != EWOULDBLOCK)))) {
            close(i);
            FD_CLR(i, &master);
            }
        }
        }
    }
    }
    return (0);
}

【问题讨论】:

  • 你怎么知道你的脚本在这两秒钟内生成了连接?
  • 您的 fd_echo() 方法无法编译。真正的代码是什么样子的?
  • user315052。好问题!实际上它不会产生连接。因为连接是按顺序进行的。我猜连接要么失败 connect() 要么 connect() 有 2 秒的延迟。
  • EJP。对不起,我猜在复制粘贴过程中出现了一些错误。修好了。
  • 实际上这不是 Linux 特定的。在 sun/solaris 中也会发生。

标签: c linux sockets select


【解决方案1】:

更新/警告:在尝试证明此答案适用时,我发现它可能不适用。我运行了测试并得到了延迟,而 max_fd 没有超过 300。我也得到了 poll() 的延迟。所以我尝试了 tcpdump 并且有重传。看起来即使 127.0.0.1 这么快就可以丢包了。在这里留下答案,因为这是一个真正的问题,即使它不是最紧迫的问题。

所以这涉及到很多文件描述符,它适用于 poll 而不是 select。有了这些线索,我可以看到解释:你已经超出了FD_SETSIZE 的限制。

来自POSIX的官方声明是(参考FD_ZERO/FD_SET/FD_CLR/FD_ISSET):

如果 fd 参数小于 0 或大于或等于 FD_SETSIZE,或者 fd 不是有效的文件描述符,或者任何参数是具有副作用的表达式,则这些宏的行为是未定义的。

(从 http://pubs.opengroup.org/onlinepubs/9699919799/functions/select.html)

要真正了解发生了什么,您必须比官方规范更深入地了解fd_set 类型的实际实现。它有一个分裂的人格。在实现select 的内核中,它被视为可变长度的位数组。 select 的第一个参数用于决定数组的结束位置。如果您调用select(2048, ...),内核将期望每个非NULL fd_set * 指向一个256 字节(2048 位)的数组。

但在用户空间中,fd_set 是一个固定大小的结构。大小是FD_SETSIZE bits,在我的系统上是 1024,可能你的也是。 FD_SET 和其他宏基本上只是对数组元素进行赋值,只是它们稍微复杂一些,因为它们必须处理作为单个位的概念数组元素。因此,如果您的文件描述符之一是 1024 并且您尝试 FD_SET 它,您已经完成了相当于

int array[1024];
array[1024] = 1;

换句话说,你破坏了fd_set之后内存中的所有内容,导致后来发生了奇怪的事情。

有办法解决这个问题。在包含定义fd_set 的标头之前,我已经看到了执行#define FD_SETSIZE somebignumber 的旧代码。我不知道在哪些操作系统上有效;我刚试了一下,glibc似乎忽略了它。

更好的可能性是做一些类似旧的“struct hack”的事情,你可以分配一个比sizeof 更多内存的结构,额外的内存可以用作最后一个数组中的额外元素结构的成员。

fd_set *rfds = malloc(128+sizeof *foo); /* can hold fds up to FD_SETSIZE+128*8-1 */

现在你当然需要记住在完成后释放它,并将 rfds 而不是 &amp;rfds 传递给 selectFD_* 宏,并执行你自己的 memsetFD_ZERO,并希望内核实现不会改变,因为你现在真正很喜欢它。但它工作......现在。

使用民意调查实际上可能是正确的答案。

【讨论】:

  • 我确实确保在我的测试中我永远不会超过 1024 限制。事实上,我从未使用超过 1000 个连接。我认为你的更新是正确的。必须是数据包速率限制或其他东西。今天,即使使用 select() 我也无法重现,所以它与 poll() 一起使用的声明可能不是 100% 正确的。
  • 啊.. 我认为您的更新完美地解释了这种情况。 IE。 SYN 数据包被内核丢弃,应用程序在选择时休眠,然后重新传输 SYN(2 秒后?)并且应用程序唤醒。好一个!谢谢!
  • 你是对的:“# netstat -s | grep drop”给出“187 SYNs to LISTEN sockets dropped”。但是我想知道为什么它们会被丢弃...
  • 运行 systemtap 和 dropwatch 脚本,数据包似乎被丢弃在“tcp_v4_do_rcv()”中。在该函数内部,它们很可能被丢弃在“struct sock *nsk = tcp_v4_hnd_req(sk, skb);”处“如果(!nsk)”“转到丢弃;”。那么可能 tcp_v4_hnd_req() 失败了?
  • OK 最后更新... 很抱歉发送垃圾邮件。现在验证在 FreeBSD 中也可以看到相同的行为。天知道我用这么几行代码做错了什么:-)
【解决方案2】:

所以进一步调试内核...

数据包在“tcp_v4_syn_recv_sock()”中被丢弃,因为“sk_acceptq_is_full(sk)”返回 true。

“sk->sk_ack_backlog”为11,配置的“sk->sk_max_ack_backlog”为10。(我们在listen()命令中设置)

(根据 EJP 注释更新。) 所以我猜正在发生的事情是:

connect() 上的客户端阻塞。一个 SYN 被发送到服务器。内核获取 SYN 发送 SYN/ACK。客户端取回 SYN/ACK 并解除阻塞,然后 a) 发送 ACK 和 b) 新的 SYN/ACK。

服务器收到 ACK 并将连接放入 backlog。

这样做 10 次,我们就卡住了。

好家伙。没有你的帮助是不会理解的。谢谢!!

【讨论】:

  • 这很简单。在阻塞模式下,connect() 发送一个 SYN,等待一个 SYN/ACK,发送一个 ACK​​,然后返回。在非阻塞模式下,connect() 行为可能会有所不同,我不同意您在任何指定它的地方查找文档。在服务器端,接收到 SYN 并发送 SYN/ACK,将半格式套接字放置在队列中,当 ACK 到达时,它将从那里移动到积压队列。同时,accept() 实质上阻塞了积压队列,或者在非阻塞模式下,如果可选择的侦听套接字在积压队列中有项目,则 select() 返回。
猜你喜欢
  • 2017-04-23
  • 2015-09-10
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-10-26
  • 1970-01-01
  • 2014-01-25
  • 2016-08-26
相关资源
最近更新 更多