本文主要是介绍Linux - 三组 I/O 复用函数的比较,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
三组 I/O 复用函数的比较
- select
- poll
- epoll
select
/* According to POSIX.1-2001 */
#include <sys/select.h>/* According to earlier standards */
#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);void FD_CLR(int fd, fd_set *set);
int FD_ISSET(int fd, fd_set *set);
void FD_SET(int fd, fd_set *set);
void FD_ZERO(fd_set *set);
poll
#include <poll.h>int poll(struct pollfd *fds, nfds_t nfds, int timeout);
epoll
#include <sys/epoll.h>// 返回一个文件描述符, 标识内核事件表
int epoll_create(int size);
// 操作内核事件表
int epoll_ctl(int epfd, // epoll_create 的返回值int op, // 操作类型: 注册 修改 删除int fd, // 要操作的文件描述符struct epoll_event *event); // 事件类型
// 在一段超时时间内等待一组文件描述符上的事件
// 成功返回就绪的文件描述符的个数, 失败返回-1
int epoll_wait(int epfd, struct epoll_event *events,int maxevents, int timeout);
LT & ET 模式
LT 模式是默认的工作模式 , 此模式下的 epoll 相当于一个效率较高的 poll
当在 epoll 内核事件表中注册一个文件描述符上的 EPOLLET 事件时 , epoll 将以 ET 模式来操作该文件描述符 , ET 模式是 epoll 的高效工作模式
对于采用 LT 工作模式的文件描述符 , 当 epoll_wait 检测到其上有事件发生并将此事件通知应用程序后 , 应用程序可以不立即处理该事件 , 当下一次调用 epoll_wait 时 , 该事件还会被再次通告
对于采用 ET 工作模式的文件描述符 , 应用程序必须立刻处理该事件 , 因为后续的 epoll_wait 调用将不再通知这一事件
ET 模式在很大程度上降低了同一个事件被重复触发的次数 , 因此效率要比 LT 模式高
EPOLLONESHOT 事件
在多进程或多线程中 , 如果一个进程(或线程)读取完某个 socket 上的数据后开始处理这些数据 , 在这期间该 socket 上又有新的数据可读 (EPOLLIN 再次被触发) , 这时另一个进程(线程)就会被唤醒来读取这些新的数据 , 此时就出现了两个进程(线程)同时操作一个 socket 的局面
为了解决这个问题 , 让一个 socket 在任意时刻都只被一个进程(线程)处理 , 可以使用 epoll 的 EPOLLONESHOT 事件实现
对于注册了 EPOLLONESHOT 事件的文件描述符 , 系统最多只触发一个其上注册的事件(可读事件, 可写事件, 异常事件) , 并且只触发一次 , 除非使用 epoll_ctl 函数重置该文件描述符上注册的 EPOLLONESHOT 事件
所以当 一个线程处理完注册了 EPOLLONESHOT 事件的socket的时候 , 该线程应该立即重置这个 socket 上的 EPOLLONESHOT 事件 , 以确保下一次这个 socket 可读时 , 其 EPOLLIN 事件能被触发 , 能让其他线程有机会继续处理这个 socket
epoll 工作原理
当某⼀进程调用 epoll_create
函数时,Linux内核会创建⼀个 eventpoll
结构体,这个结构体中有两个成员与 epoll
的使用方式密切相关
struct eventpoll { ... .../*红⿊树的根节点,这颗树中存储着所有添加到epoll中的需要监控的事件*/ struct rb_root rbr; /*双链表中则存放着将要通过epoll_wait返回给⽤户的满⾜条件的事件*/ struct list_head rdlist; ... ...
} ;
每⼀个epoll对象都有⼀个独⽴的
eventpoll
结构体,⽤于存放通过epoll_ctl
函数向epoll对象中添加进来的事件
这些事件都会挂载在红⿊树中,如此重复添加的事件就可以通过红⿊树⽽⾼效的识别出来(红⿊树的插⼊时间效率是O(logN),其中N为树的⾼度)
⽽所有添加到epoll中的事件都会与设备(网卡)驱动程序建⽴回调关系也就是说 , 当响应的事件发⽣时会调⽤这个回调⽅法
这个回调⽅法在内核中叫eppollcallback
, 它会将发⽣的事件添加到rdlist
双链表中
在epoll中,对于每⼀个事件,都会建⽴⼀个epitem结构体
struct epitem
{struct rb_node rbn;//红黑树节点struct list_head rdllink;//双向链表节点struct epoll_filefd ffd; //事件句柄信息struct eventpoll *ep; //指向其所属的 eventpoll 对象struct epoll_event event; //期待发生的事件类型
}
当调⽤
epoll_wait
检查是否有事件发⽣时,只需要检查eventpoll
对象中的rdlist
双链表中是否有epitem
元素即可如果
rdlist
不为空,则把发⽣的事件复制到⽤户态,同时将事件数量返回给⽤户这个操作的时间复杂度是O(1)
相同点
都能同时监听多个文件描述符
都是通过某种结构体变量来告诉内核监听哪些文件描述符上的哪些事件 , 并使用该结构体类型的参数来获取内核处理的结果
不同点
select 的参数类型 fd_set
没有将文件描述符和事件绑定 , 只是一个文件描述符集合 , 因此 select 需要提供 三个此类型的参数来分别 传入和输出 可读 , 可写 , 异常等事件 , 这使得 select 不能处理更多类型的事件 , 又因为内核对 fd_set 集合的在线修改 , 下次调用 select 之前必须重置 这 3 个 fd_set 集合
poll 的参数类型 pollfd 将文件描述符和事件绑定 , 任何事件都被统一处理 , 并且内核每次修改的是 pollfd 结构体的 revents 成员 , 而 events 成员保持不变 , 因此下次调用 poll 时无需重置 pollfd 类型的事件集参数
epoll 适用于连接数量多 , 但活动连接较少的情况 , 因为当活动连接较多时 , 回调函数的触发过于频繁 , 其效率未必高于 select 和 poll
系统调用 | select | poll | epoll |
---|---|---|---|
事件集合 | 用户通过 3 个fd_set类型的参数分别传入可读, 可写, 异常事件, 内核通过对这些参数的在线修改来反馈其中的就绪事件 . 这使得每次调用 select 都要重置这 3 个参数 | 统一处理所有事件类型 , 因此只需要一个 事件集参数 . 用户通过 pollfd.events 传入事件 , 内核通过修改 pollfd.revents 反馈其中就绪的事件 | 内核通过一个事件表直接管理所有事件 . 每次调用 epoll_wait无需反复传入用户事件 . epoll_wait 的参数 events 仅用来反馈就绪的事件 |
应用程序索引就绪文件描述符的时间复杂度 | O(n) | O(n) | O(1) |
最大支持的文件描述符数量 | 有最大限制 | 65535 | 65535 |
工作模式 | LT 模式 | LT 模式 | LT / ET (高效模式) |
内核实现和工作效率 | 采用轮训方式检测就绪事件 , 算法的时间复杂度为 O(n) | 采用轮训方式检测就绪事件 , 算法的时间复杂度为 O(n) | 采用回调机制来检测就绪事件 , 算法时间复杂度为 O(1) |
这篇关于Linux - 三组 I/O 复用函数的比较的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!