Linux支持3中I/O复用,分别是select、poll、epoll,下面分别进行介绍。
1、select
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
- nfds:定被监听的文件描述符的总数,设置为监听的所有文件描述符中最大值加1,因为文件描述符是从0开始计数的。
- readfds、writefds、exceptfds:分别指向可读、可写、异常事件对应的文件描述符集合。程序员通过这3个参数向该调用传入自己感兴趣的文件描述符。函数返回时,内核将修改它们来通知应用程序哪些文件描述符已经就绪。原型如下:
#define XFD_SETSIZE 256
#define FD_SETSIZE XFD_SETSIZE
typedef long fd_mask;
#define NBBY 8
#define NFDBITS (sizeof(fd_mask) * NBBY)
#define howmany(x,y) (((x)+((y)-1))/(y))
#if defined(BSD) && BSD < 198911
typedef struct fd_set
{
fd_mask fds_bits[howmany(FD_SETSIZE, NFDBITS)];
} fd_set;
#endif
fds_bit共占据32字节,即256位。该数组的每个元素的每一位(bit)标记一个文件描述符。fd_set能容纳的文件描述符由FD_SETSIZE指定,显然这限制了select()能同时处理的文件描述符的总数。该系统调用还提供了一些列宏方便程序员实现位操作:
void FD_CLR(int fd, fd_set *set); //清零set中的fd位
int FD_ISSET(int fd, fd_set *set); //测试set中的fd位是否被设置
void FD_SET(int fd, fd_set *set); //设置fd中的fd位
void FD_ZERO(fd_set *set); //清零set中所有位
- timeout: 设置函数的超时时间。它是一个timeval的普通(非const)指针,内核可以修改此参数以告诉应用程序函数阻塞等待了多久。不过内核返回的该值不能完全信任,比如调用失败时timeout的值是不确定的。timeout有三种可能:1)永远等下去,此时timeout设置为NULL;2)等待一段时间;3)根本不等待,值为0.
- 返回值:执行成功时返回就绪的文件描述符的总数;若在超时时间内没有任何文件描述符就绪,select()将返回0;失败将返回-1并设置errno。若在select()阻塞等待期间程序收到信号,将立即返回-1并设置errno为EINTR。
select的优缺点:
- 所能监听的最大描述符数为1024
- 函数返回后只能进行轮询哪个描述符已准备好
- 需要维护一个用来存放大量fd的数据结构,这样会使得用户空间和内核空间在传递该结构时复制开销大
2、poll
#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
- fds:该参数是一个pollfd结构体类型的指针,它指定所有程序员感兴趣的文件描述符上发生的可读、可写、异常等事件。既然它是一个结构体指针,就可以指向该类型的数组。pollfd结构体的原型为:
struct pollfd {
int fd; /* 文件描述符 */
short events; /* 注册的事件 */
short revents; /* 实际发生的事件,由内核填充 */
};
fd成员指定文件描述符。event成员告诉内核要监听fd上的哪些事件,它可以是一系列事件的按位或。revents成员由内核修改,以通知应用程序fd上实际发生了哪些事件。event的取值为:
- nfds:fds数组成员的的个数由参数nfds指定。显然,这个比select()的设计要灵活一点: 用户可以监测任意多数目文件描述符。
- timeout:该参数指定函数的超时事件,单位为毫秒。当timeout为-1时,poll调用将一直阻塞直到监听的目标事件发生;当timeout为0时,poll()调用立即返回。
- 返回值:执行成功时返回就绪的文件描述符的总数;若在超时时间内没有任何文件描述符就绪,poll()将返回0;失败将返回-1并设置errno。若在select()阻塞等待期间程序收到信号,将立即返回-1并设置errno为EINTR。
poll的优缺点:
- 相比select,无最大监听描述符的限制,但是仍然需要轮询哪个描述符是否准备好。
- 大量的fd的数组被整体复制于用户态和内核地址空间之间。
- poll还有一个特点是“水平触发”,如果报告了fd后,没有被处理,那么下次poll时会再次报告该fd。
3、epoll:
(1)创建一个epoll的句柄
int epoll_create(int size):
成功则返回 epoll 专用的文件描述符epfd,失败返回 -1。size用来告诉内核这个监听的数目一共有多大。这个参数不同于select()中的第一个参数,给出最大监听的fd+1的值。需要注意的是,当创建好epoll句柄后,它就是会占用一个fd值,在linux下如果查看/proc/进程id/fd/,是能够看到这个fd的,所以在使用完epoll后,必须调用close()关闭,否则可能导致fd被耗尽。
(2)epoll的事件注册
它不同与se