3种I/O多路复用的方法
一共有3中常见的I/O多路复用技术,I/O多路复用我的理解就是可以同时监听多个 fd 上是否可读或者可写,多与 socket 系列系统调用配合使用。这三种常见的技术分别是 select
,poll
,epoll
,其实也可以理解成这是3个系统调用,只是每一种都有很多配套的函数使用。
select
select 函数原型:
#include<sys/select.h>
int select(int nfds,fd_set* readfds,fd_set* writefds,fd_set* exceptfds,struct timeval* timeout);
select 参数:
ndfs
指的是监听的所有 fd 里面最大值+1,保证内核不用每次都检查大于这个ndfs
的fd
fd_set
是一个 fd 的集合,定义为:
//每个ulong型可以表示多少个bit
#define __NFDBITS (8 * sizeof(unsigned long))
//socket最大取值为1024
#define __FD_SETSIZE 1024
//bitmap一共有1024个bit,共需要多少个ulong
#define __FDSET_LONGS (__FD_SETSIZE/__NFDBITS)
typedef struct {
unsigned long fds_bits [__FDSET_LONGS]; //用ulong数组来表示bitmap
} __kernel_fd_set;
typedef __kernel_fd_set fd_set;
fd_set
能容纳最多由FD_SETSIZE
指定,这个常量在 Linux 下为 1024。和fd_set
对应的有几个操作函数:
#include<sys/select.h>
void FD_ZERO(fd_set *fdset); //清除fdset的所有位
void FD_SET(int fd,fd_set *fdset); //设置fdset的位fd
void FD_CLR(int fd,fd_set *fdset); //清除fdset的位fd
int FD_ISSET(int fd,fd_set *fdset); //测试fdset的位fd是否被设置
select 里面的readfds
,writefds
,exceptfds
代表的是用户希望 select 监听的 fd,当 select 返回的时候,这三个fd_set
里面还为真的 fd 就是已经就绪的文件。也就是 select 会直接在输入的三个参数上面直接修改。
select 返回:
- 返回-1表示有错误
- 返回 0 表示没有 fd 就绪,但是
select
已经超时 - 返回一个正整数代表有几个 fd 就绪,readfds 和 writefds 会重复计算
pselect
pselect 是 select 的升级版,唯一的区别是前者带有一个信号屏蔽字,函数原型为:
int pselect(int maxfdp1, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, const struct timespec *tsptr, const sigset_t *sigmask);
poll
poll 函数原型
#include <poll.h>
int poll(struct pollfd fdarray[], nfds_t nfds, int timeout);
poll 参数
其中 pollfd 的结构如下:
struct pollfd {
int fd;
short events;
short revents;
};
events 是你对 fd 感兴趣的事件,而 revents 是这个 fd 在 poll 返回后,实际发生的事件。
其中 events 和 revents 可以取下面的几种组合(按位或):
POLLIN POLLRDNORM POLLRDBAND POLLPRI
POLLOUT POLLWRNORM POLLWRBAND
POLLERR POLLHUP POLLNVAL
后三个只能存在在 revents 里,而不能在 events 中进行设置。在 poll 返回后,只需要检查每个 pollfd 的 revents 是否等于0,如果不等于0,那么再和 events 按位与即可。
timeout 的单位是 毫秒。
poll 返回值
成功时,poll 返回结构体中 revents 域不为 0 的文件描述符个数;如果在超时前没有任何事件发生,poll 返回 0;
失败时,poll 返回 -1,并设置 errno 为下列值之一:
EBADF:一个或多个结构体中指定的文件描述符无效。
EFAULT:fds 指针指向的地址超出进程的地址空间。
EINTR:请求的事件之前产生一个信号,调用可以重新发起。
EINVAL:nfds 参数超出 PLIMIT_NOFILE 值。
ENOMEM:可用内存不足,无法完成请求。
epoll
epoll 是 Linux 独有的一种技术,因此APUE中并没有讲到,epoll最大的优势是,每次都会返回发生事件的 fd,这样就不用像 select 和 poll 那样在所有传入的 fd 里遍历。
epoll 所有函数原型
int epoll_create(int size);
int epoll_create1(int flags);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
int epoll_pwait(int epfd, struct epoll_event *events, int maxevents, int timeout, const sigset_t *sigmask);
epoll 中用到的结构体
typedef union epoll_data {
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;
struct epoll_event {
uint32_t events;
epoll_data_t data;
}
其中epoll_event
的 events 可以是 EPOLLIN EPOLLOUT EPOLLRDHUP EPOLLPRI EPOLLERR EPOLLHUP EPOLLET EPOLLONESHOT EPOLLWAKEUP 的 按位或 结果
对几个主要的 event 介绍:
- EPOLLIN 和 EPOLLOUT 是读写就绪
- EPOLLRDHUP 是 Stream socket peer closed connection, or shut down writing half of connection. (This flag is especially useful for writing simple code to detect peer shutdown when using Edge Triggered monitoring.)
- EPOLLERR 是说对应的 fd 出现错误了,epoll_wait 会总是监听这个时间,因此不需要注册
- EPOLLHUP 也是 epoll_wait 会一直监听的一个事件, Hang up happened on the associated file descriptor,例如一个channel,比如pipe或者stream socket,当 peer 关闭了 channel 的它那一端。
- EPOLLET 是将 epoll_wait 的出发改为 ET 模式,默认为 LT 模式
- EPOLLONESHOT 指的是,当一个事件发生后,就会将 epoll_wait 里面的对应 fd disable掉,此时需要 epoll_ctl + EPOLL_CTL_MOD 重新配置 fd
epoll 的各个函数介绍
- epoll_create 和 epoll_create1 创造一个 epoll instance,返回的就是 epfd,这个 epfd 后面也需要 close 来关闭,epoll_create1 的 flags 参数唯一可选的参数是设置 EPOLL_CLOEXEC, 即对 epfd 设置 close-on-exec 标志
- epoll_ctl 的 op 参数可以是 EPOLL_CTL_ADD EPOLL_CTL_MOD EPOLL_CTL_DEL 三种
- epoll_wait 会返回触发的事件个数,每个事件被存储在 events 中,而最多返回 maxevents 个事件,也就是是说 maxevents 是 events 的大小保证。timeout 单位是 微秒,而当 timeout 设置为 -1 的时候,就会始终阻塞。
Level-triggered 和 edge-triggered
用 man 里面的解释就是,edge-triggered mode delivers events only when changes occur on the monitored file descriptor
select, poll 和 epoll 的一个简单对比 (来源:http://blog.csdn.net/lishenglong666/article/details/45536611)
通过以上的分析可以看出,poll和select的实现基本是一致,只是用户到内核传递的数据格式有所不同,
select和poll即使只有一个描述符就绪,也要遍历整个集合。如果集合中活跃的描述符很少,遍历过程的开销就会变得很大,而如果集合中大部分的描述符都是活跃的,遍历过程的开销又可以忽略。
epoll的实现中每次只遍历活跃的描述符(如果是水平触发,也会遍历先前活跃的描述符),在活跃描述符较少的情况下就会很有优势,在代码的分析过程中可以看到epoll的实现过于复杂并且其实现过程中需要同步处理(锁),如果大部分描述符都是活跃的,epoll的效率可能不如select或poll。(参见epoll 和poll的性能测试 http://jacquesmattheij.com/Poll+vs+Epoll+once+again)
select能够处理的最大fd无法超出FDSETSIZE。
select会复写传入的fd_set 指针,而poll对每个fd返回一个掩码,不更改原来的掩码,从而可以对同一个集合多次调用poll,而无需调整。
select对每个文件描述符最多使用3个bit,而poll采用的pollfd需要使用64个bit,epoll采用的 epoll_event则需要96个bit
如果事件需要循环处理select, poll 每一次的处理都要将全部的数据复制到内核,而epoll的实现中,内核将持久维护加入的描述符,减少了内核和用户复制数据的开销。