3种I/O多路复用的方法

一共有3中常见的I/O多路复用技术,I/O多路复用我的理解就是可以同时监听多个 fd 上是否可读或者可写,多与 socket 系列系统调用配合使用。这三种常见的技术分别是 selectpollepoll,其实也可以理解成这是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的实现中,内核将持久维护加入的描述符,减少了内核和用户复制数据的开销。

posted on 2018-01-24 21:03  daghlny  阅读(732)  评论(0编辑  收藏  举报

导航