4

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

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

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);
}
4

2 回答 2

3

更新/警告:在试图证明这个答案适用时,我发现它可能不适用。我运行了测试并得到了延迟,而 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类型的实际实现。它有一个分裂的人格。在内核中, whereselect被实现,它被视为可变长度的位数组。第一个参数 toselect用于决定数组的结束位置。如果您调用select(2048, ...)内核,将期望每个非 NULLfd_set *指向一个 256 字节(2048 位)的数组。

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

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

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

有办法解决这个问题。我见过旧代码#define FD_SETSIZE somebignumber在包含定义fd_set. 我不知道在哪些操作系统上有效;我刚刚尝试过,glibc 似乎忽略了它。

更好的可能性是执行类似旧的“struct hack”之类的操作,在这种情况下,您分配的 struct 具有比它更多的内存sizeof,并且额外的内存可用作数组中的额外元素,该数组是该结构的最后一个成员。

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

现在你当然需要记住当你完成它时释放它,并传递rfds而不是&rfdstoselectFD_*宏,并做你自己的memset而不是FD_ZERO,并希望内核实现不会改变,因为你现在是真实的和它很亲密。但它工作......现在。

使用 poll 实际上可能是正确的答案。

于 2012-08-09T07:18:48.133 回答
1

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

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

"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 次,我们就被卡住了。

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

于 2012-08-10T04:07:38.983 回答