在使用select()或pselect()时,是否有任何平台在使用结构体复制(fd_set)时会导致问题?

18

select()pselect()系统调用会修改它们的参数('fd_set *'参数),因此输入值告诉系统要检查哪些文件描述符,返回值告诉程序员当前可用的文件描述符。

如果您要重复调用相同一组文件描述符,则需要确保每次调用都有一份新的描述符副本。显而易见的方法是使用结构体复制:

fd_set ref_set_rd;
fd_set ref_set_wr;
fd_set ref_set_er;
...
...code to set the reference fd_set_xx values...
...
while (!done)
{
    fd_set act_set_rd = ref_set_rd;
    fd_set act_set_wr = ref_set_wr;
    fd_set act_set_er = ref_set_er;
    int bits_set = select(max_fd, &act_set_rd, &act_set_wr,
                          &act_set_er, &timeout);
    if (bits_set > 0)
    {
        ...process the output values of act_set_xx...
    }
 }

(编辑以删除不正确的struct fd_set引用 - 如'R..'所指出的。)

我的问题:

  • 是否有任何平台在进行结构复制的fd_set值时不安全,如所示?

我担心会有隐藏的内存分配或其他意外的情况。 (有宏/函数FD_SET(),FD_CLR(),FD_ZERO()和FD_ISSET()来掩盖应用程序中的内部内容。)

我可以看到MacOS X(Darwin)是安全的;其他基于BSD的系统很可能也是安全的。 您可以通过在答案中记录您知道安全的其他系统来提供帮助。

(我对fd_set在打开超过8192个文件描述符的情况下的工作效果有一些担忧-默认的最大打开文件数仅为256,但最大数量为“无限”。 此外,由于结构体为1 KB,则复制代码不是可怕的有效,但是在每个周期运行通过文件描述符列表来重新创建输入掩码也不一定有效。 也许当您打开这么多文件描述符时,您不能执行select(),尽管那时您最有可能需要此功能。)


这里有一个相关的SO问题-询问“poll() vs select()”,它涉及到与此问题不同的一组问题。


请注意,在MacOS X上 - 并且可能是BSD更一般 - 有一个FD_COPY()宏或函数,具有有效原型:

  • extern void FD_COPY(const restrict fd_set *from, restrict fd_set *to);

在尚未提供的平台上模拟值得考虑。

5个回答

10

由于struct fd_set只是一个普通的C结构体,所以应该都没问题。我个人不喜欢使用=运算符进行结构体复制,因为我曾经在许多平台上工作过,这些平台没有访问正常编译器内部函数的权限。在我的看来,明确地使用memcpy()而不是让编译器插入函数调用是更好的选择。

根据C规范第6.5.16.1节简单赋值(为了简洁而编辑):

以下情况之一应成立:

...

  • 左操作数具有与右操作数类型兼容的有资格或无资格版本的结构或联合类型;

...

简单赋值(=)中,将右操作数的值转换为赋值表达式的类型,并替换由左操作数指定的对象中存储的值。

如果正在存储到对象的值从重叠任何方式的第一个对象读取,则重叠必须是精确的,并且两个对象必须具有相容的有资格或无资格版本类型;否则,行为未定义。

所以,只要struct fd_set是实际上一个普通的Cstruct,你就能成功。但它确实取决于您的编译器是否发出一些代码来执行它,或依赖于结构体赋值的任何memcpy()内部函数。如果您的平台由于某种原因无法链接到编译器的内部函数库,则可能无法工作。

如果您有更多打开文件描述符而无法适合struct fd_set中,您将需要一些技巧。Linuxman页面说:

fd_set是一个固定大小的缓冲区。当使用小于0或者等于或大于FD_SETSIZEfd值执行FD_CLR()FD_SET()时,会导致未定义的行为。此外,POSIX要求fd必须是有效的文件描述符。
正如下面提到的那样,在所有系统上证明代码的安全性可能不值得。 FD_COPY()就是为这种情况提供的,并且通常可以保证: FD_COPY(&fdset_orig, &fdset_copy)将已分配的&fdset_copy文件描述符集替换为&fdset_orig的副本。

1
但是假设有人变得花哨起来,将指向分配的位数组的指针存储在其中...那么复制结构将复制指针而不是指向的数据。 结构体可以被复制; 所有结构体都可以被复制。 但是,这样做可能会出现问题。 我认为这不太可能成为一个问题,但我看不到 POSIX 中排除此类问题的措辞 - 除非宏 FD_SETSIZE 被暗示为常量并且... - Jonathan Leffler
1
@Jonathan,说得好。有时人们可能会做出疯狂的实现选择。我将编辑我的答案并提到FD_COPY - Carl Norum
1
显然,其他人也遇到了类似的问题 - 所以他们发明了FD_COPY()。不幸的是,它不在POSIX 2008标准或许多其他平台上 - 包括旧版Linux(内核2.6.9;glibc 2.3.4)。 - Jonathan Leffler
@Jonathan:我在我的答案中解释了为什么只要 fd_set 在标准 C 下实现,就永远不会使用已分配的内存。 - R.. GitHub STOP HELPING ICE

9
首先,没有名为struct fd_set的结构体。它只是被称为fd_set。但是,在POSIX中要求它是一个结构体类型,因此复制是明确定义的。
其次,在标准C中,fd_set对象不可能包含动态分配的内存,因为没有任何函数/宏需要在返回之前使用任何函数/宏来释放它。即使编译器有预先VLA扩展的alloca(一种基于堆栈的分配),fd_set也不能使用在堆栈上分配的内存,因为程序可能会将指向fd_set的指针传递给另一个使用FD_SET的函数等,而分配的内存将在返回到调用方时失效。只有如果C编译器提供了某些析构函数的扩展,fd_set才能使用动态分配。
总之,似乎安全的做法是只是分配/memcpy fd_set对象,但要确保,我会这样做:
#ifndef FD_COPY
#define FD_COPY(dest,src) memcpy((dest),(src),sizeof *(dest))
#endif

或者仅仅:
#ifndef FD_COPY
#define FD_COPY(dest,src) (*(dest)=*(src))
#endif

如果系统提供了 FD_COPY 宏,您将使用该宏,仅在缺少它时才退回到理论上可能不安全的版本。


感谢指出问题中的错误 - 我会修复它。 - Jonathan Leffler
1
如果你想把指针传递给fd_sets(这是其他FD_*()函数所使用的),那么FD_COPY宏就会存在问题。例如,在FD_COPY(pFdSet1,pFdSet2)中,sizeof(pFdSet1)通常将是4,因为pFdSe3t1是一个指向fd_set的指针。请在“sizeof(dest)”处替换为“sizeof(fd_set)”。 - JimKleck
2
请注意,答案的第一个参数为“dest”,而BSD实现的第一个参数为“src”。 - denis

3
您说得对,POSIX并不保证复制fd_set一定“有效”。 尽管我个人没有听说过任何不起作用的地方,但我也从未进行过试验。
您可以使用替代方案poll()(这也是 POSIX)。 它的工作方式与 select()非常相似,唯一的区别是输入/输出参数不是不透明的(不包含指针),因此裸体的memcpy将起作用,并且它的设计完全消除了制作“请求文件描述符”结构副本的需要(因为“请求事件”和“返回事件”存储在不同的字段中)。
您也正确地推断出select()(和poll())并不适合大量文件描述符 - 这是因为每次函数返回时,必须遍历每个文件描述符以测试是否有活动。 解决这个问题的方法是各种非标准接口(例如 Linux 的 epoll(),FreeBSD 的 kqueue),如果发现您有延迟问题,则可能需要研究它们。

2

我对MacOS X、Linux、AIX、Solaris和HP-UX进行了一些研究,得出了一些有趣的结果。我使用了以下程序:

#if __STDC_VERSION__ >= 199901L
#define _XOPEN_SOURCE 600
#else
#define _XOPEN_SOURCE 500
#endif /* __STDC_VERSION__ */

#ifdef SET_FD_SETSIZE
#define FD_SETSIZE SET_FD_SETSIZE
#endif

#ifdef USE_SYS_TIME_H
#include <sys/time.h>
#else
#include <sys/select.h>
#endif /* USE_SYS_TIME_H */

#include <stdio.h>

int main(void)
{
    printf("FD_SETSIZE = %d; sizeof(fd_set) = %d\n", (int)FD_SETSIZE, (int)sizeof(fd_set));
    return 0;
}

每个平台都编译了两次:

cc -o select select.c
cc -o select -DSET_FD_SETSIZE=16384

在某个平台上,HP-UX 11.11,我必须添加-DUSE_SYS_TIME_H才能编译。我对FD_COPY进行了可视化检查,只有MacOS X似乎包含它,并且必须通过确保未定义_POSIX_C_SOURCE或定义_DARWIN_C_SOURCE来激活它。

AIX 5.3

  • 默认的FD_SETSIZE为65536
  • 可以重新调整FD_SETSIZE参数
  • 没有FD_COPY

HP-UX 11.11

  • 没有sys/select.h头文件 - 使用sys/time.h代替
  • 默认的FD_SETSIZE为2048
  • 可以重新调整FD_SETSIZE参数
  • 没有FD_COPY

HP-UX 11.23

  • 具有sys/select.h
  • 默认的FD_SETSIZE为2048
  • 可以重新调整FD_SETSIZE参数
  • 没有FD_COPY

Linux (kernel 2.6.9, glibc 2.3.4)

  • 默认的FD_SETSIZE为1024
  • 不能重新调整FD_SETSIZE参数
  • 没有FD_COPY

MacOS X 10.6.2

  • 默认的FD_SETSIZE为1024
  • 可以重新调整FD_SETSIZE参数
  • 如果不要求严格的POSIX兼容性或指定了_DARWIN_C_SOURCE,则定义FD_COPY

Solaris 10 (SPARC)

  • 32位默认的FD_SETSIZE为1024,64位默认为65536
  • 可以重新调整FD_SETSIZE参数
  • 没有FD_COPY

显然,对程序进行微小修改即可自动检查FD_COPY:

#ifdef FD_COPY
    printf("FD_COPY is a macro\n");
#endif

确保其可用并不是一件容易的事情;您最终需要进行手动扫描并找出如何触发它。

在所有这些机器上,看起来可以通过结构复制来复制fd_set,而不会遇到未定义行为的风险。


1

我没有足够的声望将此作为评论添加到caf的答案中,但是有一些库可以抽象出非标准接口,例如epoll()kqueue。其中一个是libevent,另一个是libev。我认为GLib也有一个与其主循环相关的库。


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