select ,poll,epoll 都是多路 io 复用的机制, i/o 多路复用就通过一种机制, 可以监视多个描述符, 一旦某个描述符就绪(一般是读就绪或者写就绪), 能够通知乡音的程序进行相应的读写操作. 但 select poll epoll 本质上都是同步 I/O, 因为他们都需要在读写事件就绪后自己负责进行读写, 也就是说这个读写过程是阻塞的(即就绪后切换到内核态进行吧数据从内核拷贝到空间), 而异步 I/O 则无需自己负责读写.
与多进程多线程技术相比. I/O 多路复用技术的最大优势是减小系统开销, 系统不必创建, 维护进程 / 线程
- select
- int select(int maxfdp1,fd_set *readset,fd_set *writeset,fd_set *exceptset,const struct timeval *timeout)
返回值: 就绪描述符的数目, 超时返回 0, 出错返回 - 1
maxfdp1 表示指定的检测描述字个数.
中间的三个 readset writeset exceptset 指定我们要让内核测试读, 写, 异常的描述字(不感兴趣设为 null).
struct fd_set 可以理解为一个集合, 存放文件描述符
timeval 最多等待时间 (null 永远等待下去)
fd_set 的数据结构, 实际上是一 long 类型的数组, 每一个数组元素都能与一打开的文件 handler(不管是 socket handler, 还是其他文件或命名管道或设备句柄)建立联系的工作由程序员完成, 当调用 select()时, 由内核根据 IO 状态修改 fd_set 的内容, 由此来通知执行了 select()的进程哪一 socket 或文件发生了可读或可写事件.
poll
1, 基本知识
poll 的机制与 select 类似, 与 select 在本质上没有多大差别, 管理多个描述符也是进行轮询, 根据描述符的状态进行处理, 但是 poll 没有最大文件描述符数量的限制. poll 和 select 同样存在一个缺点就是, 包含大量文件描述符的数组被整体复制于用户态和内核的地址空间之间, 而不论这些文件描述符是否就绪, 它的开销随着文件描述符数量的增加而线性增大.
2,poll 函数
- # include <poll.h>
- int poll ( struct pollfd * fds, unsigned int nfds, int timeout);
pollfd 结构体定义如下:
- struct pollfd {
- int fd; /* 文件描述符 */
- short events; /* 等待的事件 */
- short revents; /* 实际发生了的事件 */
- } ;
- epoll
1, 基本知识
epoll 是在 2.6 内核中提出的, 是之前的 select 和 poll 的增强版本. 相对于 select 和 poll 来说, epoll 更加灵活, 没有描述符限制. epoll 使用一个文件描述符管理多个描述符, 将用户关系的文件描述符的事件存放到内核的一个事件表中, 这样在用户空间和内核空间的 copy 只需一次.
2,epoll 接口
epoll 操作过程需要三个接口, 分别如下:
- #include <sys/epoll.h>
- int epoll_create(int size);
- 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);
- (1) int epoll_create(int size);
创建一个 epoll 的句柄, size 用来告诉内核这个监听的数目一共有多大. 这个参数不同于 select()中的第一个参数, 给出最大监听的 fd+1 的值. 需要注意的是, 当创建好 epoll 句柄后, 它就是会占用一个 fd 值, 在 Linux 下如果查看 / proc / 进程 id/fd/, 是能够看到这个 fd 的, 所以在使用完 epoll 后, 必须调用 close()关闭, 否则可能导致 fd 被耗尽.
(2)int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
epoll 的事件注册函数, 它不同与 select()是在监听事件时告诉内核要监听什么类型的事件 epoll 的事件注册函数, 它不同与 select()是在监听事件时告诉内核要监听什么类型的事件, 而是在这里先注册要监听的事件类型. 第一个参数是 epoll_create()的返回值, 第二个参数表示动作, 用三个宏来表示:
EPOLL_CTL_ADD: 注册新的 fd 到 epfd 中;
EPOLL_CTL_MOD: 修改已经注册的 fd 的监听事件;
EPOLL_CTL_DEL: 从 epfd 中删除一个 fd;
第三个参数是需要监听的 fd, 第四个参数是告诉内核需要监听什么事, struct epoll_event 结构如下:
- struct epoll_event {
- __uint32_t events; /* Epoll events */
- epoll_data_t data; /* User data variable */
- };
events 可以是以下几个宏的集合:
EPOLLIN : 表示对应的文件描述符可以读(包括对端 SOCKET 正常关闭);
EPOLLOUT: 表示对应的文件描述符可以写;
EPOLLPRI: 表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
EPOLLERR: 表示对应的文件描述符发生错误;
EPOLLHUP: 表示对应的文件描述符被挂断;
EPOLLET: 将 EPOLL 设为边缘触发 (Edge Triggered) 模式, 这是相对于水平触发 (Level Triggered) 来说的.
EPOLLONESHOT: 只监听一次事件, 当监听完这次事件之后, 如果还需要继续监听这个 socket 的话, 需要再次把这个 socket 加入到 EPOLL 队列里
(3) int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
等待事件的产生, 类似于 select()调用. 参数 events 用来从内核得到事件的集合, maxevents 告之内核这个 events 有多大, 这个 maxevents 的值不能大于创建 epoll_create()时的 size, 参数 timeout 是超时时间(毫秒, 0 会立即返回,-1 将不确定, 也有说法说是永久阻塞). 该函数返回需要处理的事件数目, 如返回 0 表示已超时.
3, 工作模式
epoll 对文件描述符的操作有两种模式: LT(level trigger)和 ET(edge trigger).LT 模式是默认模式, LT 模式与 ET 模式的区别如下:
LT 模式: 当 epoll_wait 检测到描述符事件发生并将此事件通知应用程序, 应用程序可以不立即处理该事件. 下次调用 epoll_wait 时, 会再次响应应用程序并通知此事件.
ET 模式: 当 epoll_wait 检测到描述符事件发生并将此事件通知应用程序, 应用程序必须立即处理该事件. 如果不处理, 下次调用 epoll_wait 时, 不会再次响应应用程序并通知此事件.
ET 模式在很大程度上减少了 epoll 事件被重复触发的次数, 因此效率要比 LT 模式高. epoll 工作在 ET 模式的时候, 必须使用非阻塞套接口, 以避免由于一个文件句柄的阻塞读 / 阻塞写操作把处理多个文件描述符的任务饿死.
来源: http://www.bubuko.com/infodetail-3016788.html