I/O 多路复用之select

来源:互联网 发布:bbc news软件下载 编辑:程序博客网 时间:2024/05/20 16:43

概述

Linux提供了三种 I/O 多路复用方案:select,poll和epoll。在这一篇博客里先讨论select, poll 在将下一篇中介绍,epoll是Linux特有的高级解决方案,将在接下来中介绍。

select()

select()系统调用提供了一种实现同步 I/O 多路复用的机制:

         #include <sys/select.h>

         int select (int n,
                          fd_set *readfds,
                          fd_set *writefds,
                          fd_set *exceptfds,
                          struct timeval *timeout);

         FD_CLR(int fd, fd_set *set);
         FD_ISSET(int fd, fd_set *set);
         FD_SET(int fd, fd_set *set);
         FD_ZERO(fd_set *set);

在给定的文件描述符 I/O 就绪之前并且还没有超出指定的时间限制,select()调用就会阻塞。

监视的文件描述符可以分为3类,分别等待不同的事件。对于 readfds 集中的文件描述符,监视是否有数据可读(即某个读操作是否可以无阻塞完成);对于 writefds 集中的文件描述符,监视是否有某个写操作可以无阻塞完成;对于 exceptfds 中的文件描述符,监视是否有发生异常,或者出现带外 (out-of-band) 数据(这些场景只适用于socket)。指定的集合可能是NULL,在这种情况下,select() 不会监视该事件。

成功返回时,每个集合都修改成只包含相应类型的 I/O 就绪的文件描述符。举个例子,假定 readfds 集中有两个文件描述符 7 和 9。当调用返回时,如果描述符 7 还在集合中,它在 I/O 读取时不会阻塞。如果描述符 9 不在集合中,它在读取时很可能会发生阻塞。(这里说的是“很可能”是因为在调用完成后,数据可能已经就绪了。在这种场景下,下一次调用 select() 就会返回描述符可用。)

第一个参数 n,其值等于所有集合中文件描述符的最大值加 1 。因此,select() 调用负责检查哪个文件描述符值最大,将该最大值 加 1 后传给第一个参数。

参数 timeout 是指向 timeval 结构体的指针,定义如下:

        #include <sys/time.h>
        struct timeval {
               long tv_sec;             /* seconds */
               long tv_usec;           /* microseconds */
        };

如果该参数不是NULL,在 tv_sec 秒 tv_usec 微妙后。select() 调用会返回,即使没有一个文件描述符处于 I/O 就绪状态。返回时,在不同的UNIX系统中,该结构体是未定义的,因此每次调用必须(和文件描述符集一起)重新初始化。实际上,当前Linux版本会自动修改该参数,把值修改成剩余的时间。因此,如果超时设置是 5 秒,在文件描述符可用之前已逝去了 3 秒,那么在调用返回时,tv.tv_sec 的值就是 2。

如果超时值都是设置成 0,调用会立即返回,调用时报告所有事件都挂起,而不会等待任何后续事件。

不是直接操作文件描述符集,而是通过辅助宏来管理。通过这种方式,UNIX系统可以按照所希望的方式来实现。不过,大多数系统把集合实现成位数组。

FD_ZERO 从指定集合中删除所有的文件描述符。每次调用 select() 之前,都应该调用该宏。

       fd_set writefds;
       FD_ZERO(&writefds);

FD_SET 向指定集中添加一个文件描述符,而 FD_CLR 则从指定集中删除一个文件描述符。

       FD_SET(fd, &writefds);           /* add 'fd' to the set */
       FD_CLR(fd, &writefds);           /* oops, remove 'fd' from the set */

设计良好的代码应该不需要使用FD_CLR,极少使用该宏。

FD_ISSET 检查一个文件描述符是否在给定集合中。如果在,则返回非0值,否则返回 0。当select() 调用返回时,会通过 FD_ISSET 来检查文件描述符是否就绪:

       if (FD_ISSET(fd, &readfds)) 
                                     /* 'fd' is readable without blocking */

由于文件描述符集是静态建立的,所以文件描述符数存在上限值,而且存在最大文件描述符值,这两个值都是由 FD_SETSIZE 设置。在Linux,该值是1024。

返回值和错误码

select() 调用成功时,返回三个集合中 I/O 就绪的文件描述符总数。如果给出了超时设置,返回值可能是 0 。出错时。返回 -1 ,并把 errno 值设置成如下值之一:

EBADF        某个集合中存在非法文件描述符。
EINTR        等待时捕获了一个信号,可以重新发起调用。
EINVAL      参数 n 是负数,或者设置的超时时间值非法。
ENOMEN    没有足够的内存来完成该请求

select() 示例

#include <stdio.h>#include <sys/time.h>#include <sys/types.h>#include <unistd.h>#define TIMEOUT   5            /* select timeout to seconds */#define BUF_LEN   1024         /* read buffer in bytes */int main(void) {struct timeval tv;fd_set readfds;int    ret;/* Wait on stdin for input */FD_ZERO(&readfds);FD_SET(STDIN_FILENO, &readfds);/* Wait up to five seconds */tv.tv_sec = TIMEOUT;tv.tv_usec = 0;/* All right, now block */ret = select(STDIN_FILENO + 1,         &readfds,         NULL,         NULL,         &tv);if (ret == -1) {perror("select");return 1;} else if(!ret){printf("%d seconds eclapsed.\n", TIMEOUT);}/*  * Is our file descriptor ready to read? * (It must be, as it was the only fd that *  we provied and the call returned *  nonzero, but we will humor ourselves.) */    if (FD_ISSET(STDIN_FILENO, &readfds)) {    char buf[BUF_LEN];    int len;    /* guaranteed to not block */    len = read(STDIN_FILENO, buf, BUF_LEN);    if (len == -1) {    perror("read");    return 1;    }    if (len) {    buf[len] = '\0';    printf("read: %s\n", buf);    }    return 0;    }    fprintf(stderr, "This should not happen!\n");    return 1;}

用select() 实现可移植的sleep功能

在各个Unix系统中,相比微秒级的sleep功能,对select() 的实现更普遍,因此select() 调用常常被作为可移植的sleep实现机制:把所有三个集都设置成NULL,超时值设置为非NULL。如下:

       struct timeval tv;
       tv.tv_sec = 0;
       tv.tv_usec = 500;

       /* sleep for 500 microseconds */
       select(0, NULL, NULL, NULL, &tv);
Linux提供了更高精度的sleep机制。

pselect()

select()系统调用很流行,它最初是在 4.2BSD 中引入的,但是 POSIX 标准在 POSIX 1003.1g-2000 和后来的 POSIX 1003.1-2001中定义了自己的 pselect() 方法:

       #define _XOPEN_SOURCE 600
       #include <sys/select.h>

       int pselect(int n,
                         fd_set *readfds,
                         fd_set *writefds,
                         fd_set *exceptfds,
                         const struct timespec *timeout,
                         const sigset_t *sigmask);

       /* these are the same as those used by select() *
       FD_CLR(int fd, fd_set *set);
       FD_ISSET(int fd, fd_set *set);
       FD_SET(int fd, fd_set *set);
       FD_ZERO(fd_set *set);

pselect() 和 select() 存在三点区别:
  • pselect() 的timeout 参数使用了timespec 结构体,而不是 timeval 结构体。timespec 结构体使用秒和纳秒,而不是毫秒,从理论上讲更精确些。但是实际上,这两个结构体在毫秒精度上已经不可靠了。
  • pselect() 调用不会修改 timeout 参数。因此,在后续调用中,不需要重新初始化该参数。
  • select() 系统调用没有sigmask参数。当这个参数设置为NULL时,pselect() 的行为和select() 相同。
timespec 结构体定义如下:

      #include <sys/time.h>
      struct timespec {
             long tv_sec;         /* seconds */
             long tv_nsec;       /* nanoseconds */
      };

把pselect() 添加到UNIX工具箱主要原因是为了增加 sigmask 参数,该参数是为了解决文件描述符和信号之间等待出现竞争条件。假设信号处理程序设置了全局标志位(大部分如此),进程每次调用 select() 之前会检查该标志位。现在,假定在检查标志位和调用之间收到信号,应用可能会一直阻塞,永远都不会响应该信号。pselect() 提供了一组可阻塞信号,应用在调用时可以设置这些信号来解决这个问题。阻塞的信号要等到解除阻塞才会处理。一旦 pselect() 返回,内核会恢复老的信号掩码。

在Linux内核2.6.16之前,pselect() 还不是系统调用,而是由 glibc 提供的对 select() 调用的简单封装。该封装对出现竞争的风险最小化,但是没有完全的消除竞争。当真正引入了新的系统调用pselect() 之后,才彻底解决了这个竞争问题。

虽然和 select() 相比,pselect() 有一定的改进,但大多数应用还是使用 select(),有的是出于习惯,有的是为了更好的可移植性。






0 0
原创粉丝点击