如何遍历fd_set?

17
我想知道是否有一种简单的方法可以迭代fd_set?我希望这样做是为了不必循环遍历所有连接的套接字,因为select()会更改这些fd_set仅包括我感兴趣的。我也知道直接访问不打算直接访问的类型的实现通常是一个坏主意,因为它可能在不同的系统上有所不同。然而,我需要某种方式来解决这个问题,而我已经没有任何想法。所以,我的问题是:

如何迭代fd_set?如果这是一个非常糟糕的做法,除了循环遍历所有连接的套接字之外,还有其他解决“问题”的方法吗?

谢谢


6
这并不一定意味着“全部连接”。你可以将你的一部分已连接套接字传递给select,然后在select返回后只对该子集使用FD_ISSET。另外,遍历所有套接字是否存在实际问题?除非你要处理成千上万个已连接的套接字,否则循环可能需要非常短的时间。 - Rakis
1
同意Rakis的观点。这似乎是一件效率低下的事情,但在大多数情况下并非如此。循环所需的时间将被处理集合中的一个FD所需的时间所淹没。 - Duck
1
@Andreas:10000个打开的连接,所有事务都按顺序处理? - Potatoswatter
迭代和循环是相等的,对吧?所以你的问题是“如何在不使用循环的情况下进行循环?”这里的目标是什么?我不理解,因为——你不想使用FD_ISSET……但由于select会删除文件描述符……你想要遍历整个集合?——此外,我同意Rakis和Duck的观点。 - default
2
@Andreas: 这里出现了瓶颈。除了FD_ISSET太慢之外,还有其他可能出错的地方。只需将连接分配给多个调度线程即可。 - Potatoswatter
显示剩余10条评论
8个回答

12

在调用select()之前,您必须填充一个fd_set结构体,不能直接传入原始的std::set套接字。然后,select()会相应地修改fd_set,删除任何未“设置”的套接字,并返回剩余的套接字数量。您必须遍历生成的fd_set,而不是遍历您的std::set。因为生成的fd_set只包含准备就绪的“设置”套接字,所以没有必要调用FD_ISSET(),例如:

fd_set read_fds;
FD_ZERO(&read_fds);

int max_fd = 0;

read_fds.fd_count = connected_sockets.size();
for( int i = 0; i < read_fds.fd_count; ++i ) 
{
    read_fds.fd_array[i] = connected_sockets[i];
    if (read_fds.fd_array[i] > max_fd)
      max_fd = read_fds.fd_array[i];
}

if (select(max_fd+1, &read_fds, NULL, NULL, NULL) > 0)
{ 
    for( int i = 0; i < read_fds.fd_count; ++i ) 
        do_socket_operation( read_fds.fd_array[i] ); 
} 

FD_ISSET()在使用select()进行错误检查时更常用,例如:

fd_set read_fds;
FD_ZERO(&read_fds);

fd_set error_fds;
FD_ZERO(&error_fds);

int max_fd = 0;

read_fds.fd_count = connected_sockets.size();
for( int i = 0; i < read_fds.fd_count; ++i ) 
{
    read_fds.fd_array[i] = connected_sockets[i];
    if (read_fds.fd_array[i] > max_fd)
      max_fd = read_fds.fd_array[i];
}

error_fds.fd_count = read_fds.fd_count;
for( int i = 0; i < read_fds.fd_count; ++i ) 
{
    error_fds.fd_array[i] = read_fds.fd_array[i];
}

if (select(max_fd+1, &read_fds, NULL, &error_fds, NULL) > 0)
{ 
    for( int i = 0; i < read_fds.fd_count; ++i ) 
    {
        if( !FD_ISSET(read_fds.fd_array[i], &error_fds) )
            do_socket_operation( read_fds.fd_array[i] ); 
    }

    for( int i = 0; i < error_fds.fd_count; ++i ) 
    {
        do_socket_error( error_fds.fd_array[i] ); 
    }
} 

选择手册中提到:“nfds是三个集合中任意一个描述符的最高编号加1。” 使用最高编号而不是计数 - MaPePeR
@RemyLebeau,如果您设置error_fds.fd_count = read_fds.fd_count;,那么在if(select...)语句中使用只有1个for循环会更好,我猜测检查read_fdserror_fdsFD_ISSET存在一些错误。(即FD_ISSET(read_fds)根本没有被检查) - RIscRIpt
在此示例中,检查 FD_ISSET(read_fds.fd_array[i], &read_fds) 是多余的,因为循环已经遍历了“设置”的 read_fds 的项。该循环在处理每个“可读”项目之前检查它是否也不在 error_fds 中。要同时使用单个循环处理两个 fd_set,需要不同的循环逻辑: for (int fd = 0; fd <= max_fd; ++fd) { if (FD_ISSET(fd, &error_fds) {...} else if (FD_ISSET(fd, &read_fds)) {...} },但这在所有平台上都无法移植(尤其是 Windows)。 - Remy Lebeau
当然,直接循环遍历 fd_count 也不是真正的可移植性。fd_set 应该是不透明的,你真的不应该直接访问它的元素。FD_...() 宏是为了隐藏这些细节而设计的。 - Remy Lebeau
我会选择遍历“connected_sockets”,将每个套接字传递给“FD_ISSET()”,而不是使用“for (int fd = 0; fd <= max_fd; ++fd)”循环。 - Remy Lebeau

7

Select函数将文件描述符对应的位设置在集合中,所以如果你只对其中几个感兴趣(并且可以忽略其他的),就不需要遍历所有的文件描述符,只需要测试那些你感兴趣的文件描述符即可。

if (select(fdmax+1, &read_fds, NULL, NULL, NULL) == -1) {
   perror("select");
   exit(4);
}

if(FD_ISSET(fd0, &read_fds))
{
   //do things
}

if(FD_ISSET(fd1, &read_fds))
{
   //do more things
}

编辑
以下是fd_set结构体:

typedef struct fd_set {
        u_int   fd_count;               /* how many are SET? */
        SOCKET  fd_array[FD_SETSIZE];   /* an array of SOCKETs */
} fd_set;

其中,fd_count是设置的套接字数量(因此,您可以使用此进行优化),fd_array是位向量(大小为FD_SETSIZE * sizeof(int),这取决于机器)。 在我的机器上,它是64 * 64 = 4096。

所以,您的问题本质上是:在大小约为4096位的位向量中找到1的位位置的最有效方法是什么?

我想在这里澄清一件事:
“循环遍历所有连接的套接字”并不意味着实际读取/处理连接。FD_ISSET()仅检查分配给连接的文件描述符号码处的fd_set中的位是否设置。如果效率是您的目标,那么这不是最有效的方法吗?使用启发式算法?

请告诉我们这种方法的问题在哪里,以及您试图使用替代方法实现什么。


也谢谢你。但请看我的评论,也许我没有明确地解释我不想采取的方法。 - Andreas
3
如果这不是正确的答案或者你不想要这个答案,为什么它被标记为答案呢? - Greg Domjan
由于两个原因。a) 修改提供了我正在寻找的信息 b) 我改变了主意,因此答案变得相关。 - Andreas
5
fd_set的定义取决于操作系统。Linux的fd_set没有fd_count成员。 - Conspicuous Compiler
如果性能比可移植性更重要,x86_64指令集具有一些指令,可以在机器字中进行非常快速的位扫描。因此,只需构建一个简单的汇编函数来执行扫描即可。 - Mark K Cowan
FD_SETSIZE * sizeof(int) 没有任何意义。据我所知,FD_SETSIZE 是位图中文件描述符的数量,没有理由将其乘以魔法常数。此外,伪 C 语言定义位数组只会更加混淆。 - Pavel Šimerda

4

这相当简单:

for( int fd = 0; fd < max_fd; fd++ )
    if ( FD_ISSET(fd, &my_fd_set) )
        do_socket_operation( fd );

谢谢您的回答。请查看我的评论以澄清我想做什么。 - Andreas

4
这种循环是select()接口的限制。底层实现中的fd_set通常是一个位集,这意味着查找套接字需要扫描位。正是因为这个原因,创建了几个替代接口,但不幸的是,它们都是特定于操作系统的。例如,Linux提供了epoll,它返回仅处于活动状态的文件描述符列表。FreeBSD和Mac OS X都提供kqueue,可以实现同样的结果。

1
请参考Beej的网络指南第7.2节“7.2.select()——同步I/O多路复用”,使用FD_ISSET。
简而言之,您必须遍历fd_set以确定文件描述符是否准备好进行读取/写入...

谢谢回答。我知道这是标准方法,但我想偏离它,请看看我在自己的帖子上的评论。 - Andreas

0

我认为使用select()调用效率不高。 "The C10K problem"中的信息仍然有效。

您需要一些特定于平台的解决方案:

或者您可以使用事件库来隐藏平台细节libev


0

我认为你试图做的事情不是一个好主意。

首先,它是系统相关的,但我相信你已经知道了。

其次,在内部级别,这些集合被存储为整数数组,而fds则被存储为设置位。现在根据select的man页面,FD_SETSIZE为1024。 即使您想迭代并获取您感兴趣的fd,您也必须循环该数字以及一堆位操作的混乱。 因此,除非您正在等待超过FD_SETSIZE fd的选择,否则我认为这是不可能的,这不是一个好主意。

哦等等!!无论如何都不是一个好主意。


0

ffs() 可以在 POSIX 或 4.3BSD 上用于比特迭代,但它期望 int(long 和 long long 版本是 glibc 扩展)。当然,你必须检查 ffs() 是否像 strlen 和 strchr 一样被优化。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接