I/O多路复用
应用程序常常需要在多于一个文件描述符上阻塞。在不使用线程,尤其是独立处理每一个文件的情况下,进程无法在多个文件描述符上同时阻塞。尤其是对于网络应用程序而言,同时打开的多个套接字,会诱发潜在的问题。
非阻塞IO可以作为这个问题的一个解决方案,但这种方法效率较差。首先,进程要以某种不确定的方式不断发起I/O操作,直到某个打开的文件描述符准备好进行I/O。其次,如果程序可以睡眠的话将更加有效,可以让处理器进行其他工作,直到一个或更多文件描述符可以进行I/O时再唤醒。
- I/O多路复用
I/O多路复用允许应用在多个文件描述符上同时阻塞,并在其中某个可以读写时收到通知。
这时I/O多路复用就成了应用的关键所在,一般来讲I/O多路复用的设计遵循以下原则:
- I/O多路复用:当任何文件描述符准备好I/O时告诉我
- 在一个或更多文件描述符就绪前始终处于睡眠状态
- 唤醒:哪个准备好了?
- 在不阻塞的情况下处理所有I/O就绪的文件描述符。
- 返回第一步,重新开始。
Linux提供了三种I/O多路复用方案:select, poll和epoll。其中epoll是Linux特有的高级方法。
-
select( )
select( )系统调用提供了一种实现同步I/O多路复用的机制:
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
int select(int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
在指定的文件描述符准备好I/O之前或者超过一定的时间限制,select( )调用就会阻塞。
监测的文件描述符可以分为三类,分别等待不同的时间。
- 监测readfds集合中的fd,确认其中是否有可读数据(也就是说,读操作可以无阻塞的完成)
- 检测writefds集合中的fd,确认其中是否有一个写操作可以不阻塞地完成
- 监测exceptfds集合中的fd,确认其中是否有出现异常发生或者出现带外(out-of-band)数据(这种情况只适用于套接字)。
如果指定的集合为NULL,则seletc( )不对此类时间进行监视。
成功返回时,每个集合只包含对应类型的I/O就绪的文件描述符。
第一个参数n,等于所有集合中文件描述符的最大值加一。这样,select( )的调用者需要找到最大的文件描述符值,并将其加一后传给第一个参数。
最后的timeout参数是一个指向timeval结构体的指针,定义如下:
#include <sys/time.h>
struct timeval {
long tv_sec; /* seconds */
long tv_usec; /* microseconds */
};
如果这个参数不是NULL,即时此时没有文件描述符处于I/O就绪状态,select( )调用也将在tv_sec秒tv_usec微秒后返回。返回时,这个结构图的状态是未定义的。
- 较新版本的Linux会自动将该值改为剩余的时间。
- 如果时限中的两个值都是零,调用会立即返回,并报告调用时所有事件对应的文件描述符均不可用,且不等待任何后续事件。
三个集合中的fds并不直接操作,而是通过以下辅助宏来进行管理:
- FD_ZERO从指定的集合中移除所有文件描述符:
fd_set writefds;
FD_ZERO(&writefds);
- FD_SET想指定集合中添加一个文件描述符
FD_SET(fd, &writefds);
- FD_CLR从指定集合中移除一个文件描述符,设计良好的代码应该从不使用FD_CLR,一般来讲,很少使用该宏
FD_CLR(fd, &writefds);
- FD_ISSET测试一个文件描述符在不在给定集合中。如果在,返回一个非0值,不在,返回0。一般在select( )调用返回后使用FD_ISSET来检查一个文件描述符是否就绪。
if (FD_ISSET(fd, &readfds))
/* 'fd' is readable without blocking! */
- 由于文件描述符集合是静态建立的,所以对于文件描述符数量的上限和文件描述符的最大值均有限制。二者都由FD_SETSIZE设定。在Linux上,这个值是1024。
用select( )实现可移植的sleep( )
由于select( )在各种Unix系统中都很容易实现,相对于微妙级精度的睡眠机制来讲,经常将select( )做为一种可移植的微秒级的睡眠机制。
- 该方法通过将三个集合值设为空NULL,将超时值设置为non-NULL来实现。
struct timeval tv;
tv.tv_sec = 0;
tv.tv_usec = 500;
/* sleep for 500 microseconds */
select(0, NULL, NULL, NULL, &tv);
pselect( )
POSIX定义了自己的方法--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);
四个宏定义与select的相同。
pselect( )和select( )有三点不同:
- pselect( )的timeout参数使用了timespec结构。timespec使用秒和纳秒,而select的timeval使用秒和豪秒。理论上timespec更精准一些。
- pselect( )调用并不修改timeout参数。
- pselect( )比select( )多一个sigmask参数。当这个参数被设置为零时,pselect( )的行为等同于select( ).
- 添加pselect( )到Unix工具箱的主要原因是为了增加sigmask参数,以此来解决信号和等待文件描述符之间的竞争关系。
- pselect( )提供了一组可阻塞的信号,阻塞的信号直到解除阻塞才会被处理。
- 如果不考虑pselect( )中的改进,大多数应用会继续使用select( ),部分是出于习惯,其他则是考虑可移植性。
poll( )
poll( )系统调用是system V的I/O多路复用解决方案。
#include <sys/poll.h>
int poll(struct pollfd *fds, unsigned int nfds, int timeout);
poll( )使用一个简单的nfds个pollfd结构体构成的数组,fds指向该数组
#include <sys/poll.h>
struct pollfd {
int fd; /* file descriptor */
short events; /* requested events to watch */
short revents; /* returned events witnessed */
};
每个pollfd结构体指定监视单一的文件描述符。可以传递多个结构体,使得poll( )监视多个文件描述符。
- events是要监视的fd事件的一组位掩码,用户设置这个字段
- revents是发生在该fd上的事件的位掩码,内核在返回时设置这个字段
下面是合法的事件:
- POLLIN:没有数据可读
- POLLRDNORM:有正常数据可读
- POLLRDBAND:有优先数据可读
- POLLPRI:有高优先级数据可读
- POLLOUT:写操作不会阻塞
- POLLWRNORM:写正常数据不会阻塞
- POLLBAND:写优先数据不会阻塞
- POLLMSG:有一个SIGPOLL消息可用
另外,如下事件可能在revents中返回:
- POLLER:给出文件描述符上有错误
- POLLHUP:文件描述符上有挂起事件
- POLLNVAL:给出的文件描述符非法
POLLIN|POLLPRI等价于select( )的读事件,而POLLOUT|POLLWRBAND等价于select( )的写事件。POLLIN等价于POLLRDNORM|POLLRDBAND, 而POLLOUT等价于POLLWRNORM。
- 举例来说,监视一个文件描述符是否可读写,需设置events为POLLIN|POLLOUT。返回时,将在revents中检查是否有相应的标志。
- timeout参数指定在任何I/O就绪前需要等待时间的长度,以毫秒计。负值表示永远等待。一个零值表示调用立即返回,列出所有未准备好的I/O,但不等待任何其他事件。这种情况下,poll( )就如图其名,轮询一次后立即返回。
ppoll( ):
Linux提供了一个poll( )的近似调用——ppoll( )。ppoll( )和pselect( )同源,然而和pselect( )不同的是,ppoll( )是Linux的专有调用:
#define _GNU_SOURCE
#include <sys/poll.h>
int ppoll(struct pollfd *fds, nfds_t nfds, const struct timespec *timeout, const sigset_t *sigmask);
像pselect( )那样,timeout参数以秒和纳秒计指定了时限,而sigmask参数提供了一组等待处理的信号。
对比poll( )与select( ):
一般来说,poll( )系统调用优于select( ):
- poll( )无需使用者计算最大的文件描述符值加一和传递该参数
- poll( )应对较大值的fd时更具效率。
- 使用poll( )可以创建合适大小的数组,只需要监视一项或仅仅传递一个结构体。
- poll( )系统调用分离了输入events字段和输出revents字段,数组无需改变即而重用。
- select的timeout参数在返回时是未定义的。可移植的代码需要重新初始化它。然而pselect没有这个问题。
网友评论