7.2. select()-同步 I/O 多工
这个函数有点奇怪,不过它很好用。看看下面这个情况:如果你是一个 server,而你想要 listen 正在进来的连接,如同不断读取已建立的连接 socket 一样。 你说:没问题,只要用 accept() 及一对 recv() 就好了。 慢点,老兄!如果你在 accept() call 时发生了 blocking 该怎麽办呢?你要如何同时进行 recv() 呢? "那就使用 non-blocking socket!" 不行!你不会想成为浪费 CPU 资源的罪人吧。 嗯,那有什麽好方法吗? select() 授予你同时监视多个 sockets 的权力,它会告诉你哪些 sockets 已经有数据可以读取丶哪些 sockets 已经可以写入,如果你真的想知道,还会告诉你哪些 sockets 触发了例外。 即使 select() 相当有可移植性,不过却是监视 sockets 最慢的方法。一个比较可行的替代方案是 libevent [24] 或者其它类似的方法,封装全部的系统相依要素,用以取得 socket 的通知。 好了,不罗唆,下面我提供了 select() 的原型:
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
int select(int numfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);这个函数以 readfds丶writefds 及 exceptfds 监视 file descriptor(文件描述符)的 "sets(组)"。如果你想要知道你是否能读取 standard input(标准输入)及某个 sockfd socket descriptor,只要将 file descriptor 0 与 sockfd 新增到 readfds set 中。numfds 参数应该要设置为 file descriptor 的最高值加 1。在这个例子中,应该要将 numfds 设置为 sockfd+1,因为它必定大於 standard input(0)。 当 select() 回传时,readfds 会被修改,用来反映你所设置的 file descriptors 中,哪些已经有数据可以读取,你可以用下列的FD_ISSET() macro(宏)来取得这些可读的 file descriptors。 在继续谈下去以前,我想要说说该如何控制这些 sets。 每个 sets 的型别都是 fd_set,下列是用来控制这个型别的 macro:
FD_SET(int fd, fd_set *set); 将 fd 新增到 set。
FD_CLR(int fd, fd_set *set); 从 set 移除 fd。
FD_ISSET(int fd, fd_set *set); 若 fd 在 set 中,返回 true。
FD_ZERO(fd_set *set); 将 set 整个清为零。最後,这个令人困惑的 struct timeval 是什麽东西呢? 好,有时你不想要一直花时间在等人家送数据给你,或者明明没什麽事,却每 96 秒就要印出 "运行中 ..." 到终端(terminal),而这个 time structure 让你可以设置 timeout 的周期。 如果时间超过了,而 select() 还没有找到任何就绪的 file descriptor 时,它会回传,让你可以继续做其它事情。 struct timeval 的栏位如下:
struct timeval {
int tv_sec; // 秒(second)
int tv_usec; // 微秒(microseconds)
};只要将 tv_sec 设置为要等待的秒数,并将 tv_usec 设置为要等待的微秒数。是的,就是微秒,不是毫秒。一毫秒有 1,000 微秒,而一秒有 1,000 毫秒。所以,一秒就有 1,000,000 微秒。 为什麽要用 "usec(微秒)" 呢? "u"看起来很像我们用来表示 "micro(微)"的希腊字母 μ(Mu)。还有,当函数回传时,会更新 timeout,用以表示还剩下多少时间。这个行为取决於你所使用的 Unix 而定。
译注: 因为有些系统平台的 select() 会修改 timeout 的值,而有些系统不会,所以如果要重复调用 select() 的话,每次都应该要重新设置 timeout 的值,以确保程序的行为可以符合预期。
哇!我们有微秒精度的计时器了! 是的,不过别依赖它。无论你将 struct timeval 设置的多小,你可能还要等待一小段 standard Unix timeslice(标准 Unix 时间片段)。 另一件有趣的事:如果你将 struct timeval 的栏位设置为 0,select() 会在轮询过 sets 中的每个 file descriptors 之後,就马上 timeout。如果你将 timeout 参数设置为 NULL,它就永远不会 timeout,并且陷入等待,直到至少一个 file descriptor 已经就绪(ready)。如果你不在乎等待时间,就在调用 select() 时将 timeout 参数设置为 NULL。 下列的代码片段 [25] 等待 2.5 秒後,就会出现 standard input(标准输入)所输入的东西:
如果你用一行缓冲区(buffer)的终端,那麽你从键盘输入数据後应该要尽快按下 Enter,否则程序就会发生 timeout。 你现在可能在想,这个方法用在需要等待数据的 datagram socket 上很好,而且你是对的:应该是不错的方法。 有些系统会用这个方式来使用 select(),而有些不行,如果你想要用它,你应该要参考你系统上的 man 使用手册说明看是否会有问题。 有些系统会更新 struct timeval 的时间,用来反映 select() 原本还剩下多少时间 timeout;不过有些却不会。如果你想要程序是可移植的,那就不要倚赖这个特性。[如果你需要追踪剩下的时间,可以使用 gettimeofday(),我知道这很令人失望,不过事实就是这样。] 如果在 read set 中的 socket 关闭连接,会怎样吗? 好的,这个例子的 select() 回传时,会在 socket descriptor set 中说明这个 socket 是 "ready to read(就绪可读)"的。而当你真的用recv() 去读取这个 socket 时,recv() 则会回传 0 给你。这样你就能知道是 client 关闭连接了。 再次强调 select() 有趣的地方:如果你正在 listen() 一个 socket,你可以将这个 socket 的 file descriptor 放在 readfds set 中,用来检查是不是有新的连接。 我的朋友阿,这就是万能 select() 函数的速成说明。 不过,应观众要求,这里提供个有深度的范例,毫无疑问地,以前的简单范例和这个范例的难易度会有显着差距。不过你可以先看看,然後读後面的解释。 程序 [26] 的行为是简单的多用户聊天室 server,在一个窗口中运行 server,然後在其它多个窗口使用 telnet 连接到 server["telnet hostname 9034"]。当你在其中一个 telnet session 中输入某些文字时,这些文字应该会在其它每个窗口上出现。
我说过在代码中有两个 file descriptor sets:master 与 read_fds。前面的 master 记录全部现有连接的 socket descriptors,与正在 listen 新连接的 socket descriptor 一样。 我用 master 的理由是因为 select() 实际上会改变你传送过去的 set,用来反映目前就绪可读(ready for read)的 sockets。因为我必须在在两次的 select() calls 期间也能够持续追踪连接,所以我必须将这些数据安全地储存在某个地方。最後,我再将 master 复制到read_fds,并接着调用 select()。 可是这不就代表每当有新连接时,我就要将它新增到 master set 吗?是的! 而每次连接结束时,我们也要将它从 master set 中移除吗?是的,没有错。 我说过,我们要检查 listen 的 socket 是否就绪可读,如果可读,这代表我有一个待处理的连接,而且我要 accept() 这个连接,并将它新增到 master set。同样地,当 client 连接就绪可读且 recv() 返回 0 时,我们就能知道 client 关闭了连接,而我必须将这个 socket descriptor 从 master set 中移除。 若 client 的 recv() 返回非零的值,因而,我能知道 client 已经收到了一些数据,所以我收下这些数据,并接着到 master 清单,并将数据送给其它已连接的每个 clients。 我的朋友们,以上对万能 select() 函数的概述,这真是不简单的事情。 另外,这里有个福利:一个名为 poll 的函数,它的行为与 select() 很像,但是在管理 file descriptor sets 时用不一样的系统,你可以看看 poll。 参考资料 [24] http://www.monkey.org/~provos/libevent/ [25] http://beej.us/guide/bgnet/examples/select.c [26] http://beej.us/guide/bgnet/examples/selectserver.c
Last updated