目录
一、什么是IO多路复用
把标准输入、套接字等都看成IO的一路,IO多路复用就是在任何一路I/O有事件发生时,都能通知应用程序去处理响应的事件。
所谓的“事件”包括:
(1)标准输入文件描述符准备好可读;
(2)监听套接字准备好,新连接建立成功;
(3)已连接套接字准备好可写;
(4)发生超时事件,如一个I/O事件等待超过10秒。
二、怎么实现IO多路复用
最简单的实现大概如下:
fd_array = {....};
while (true) {
for (auto i : fd_array) { // 遍历fd数组
read/write(fd_array[i], ...);
}
sleep(1);
}
使用一个for循环,去不断的遍历fd数组,读写到了就去处理,否则就sleep,不过这里的fd必须设置成非阻塞的,不然会被卡死。
但是这样有很多弊端,比如在sleep的时候可能有事件发生,但如果不sleep,就太耗费CPU了,最好是方法是由内核监听,监听到了事件通知我们,Linux内核提供了三种方式:
1、select
函数原型:
int select(int maxfd, fd_set *readset, fd_set *writeset, fd_set *exceptset, const struct timeval *timeout);
返回:若有就绪描述符则为其数目,若超时则为0,若出错则为-1
缺点:所支持的fd个数有限,默认最大值是1024;
2、poll
函数原型:
int poll(struct pollfd *fds, unsigned long nfds, int timeout);
返回值:若有就绪描述符则为其数目,若超时则为0,若出错则为-1
特点:突破了文件描述符最大值的限制,而且每次检测完不会修改原来的传入值,pollfd结构体如下:
struct pollfd {
int fd; /* file descriptor */
short events; /* events to look for */
short revents; /* events returned */
};
其中events是待检测的事件,检测出来的结果保存到revents里,poll返回的是准备就绪的fd数目,然后还需要遍历revents数组,才能找到对应就绪的fd。
3、epoll
与前两个相比,epoll的性能最好,因为select和poll都需要遍历数组,才能找到就绪的fd,而epoll_wait返回的直接是就绪的fd数组,所以无用功最少,epoll函数主要有三个:
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);
epoll_create用来创建一个epoll实例,这里的size只要大于0即可,后面可以动态增加或删除,
epoll_ctl返回值: 若成功返回0;若返回-1表示出错
epoll_ctl负责fd的增删改,管理这些fd使用的数据结构是红黑树,所以增加或删除的效率很高;
epoll_wait返回值: 成功返回的是一个大于0的数,表示事件的个数;返回0表示的是超时时间到;若出错返回-1.
如何做到有事件发生立即能感知到呢?是通过设置poll回调。
这里的poll是文件系统针对fd定制的监听事件的机制,通过poll机制可以实现当fd准备就绪后,让底层硬件回调的时候自动把这个fd相关的结构体放到指定队列(就绪列表),并唤醒操作系统,然后只需要遍历就绪列表,就能返回所有已经就绪的fd数组。
所以如果一个”文件“(Linux一切皆文件)所在的文件系统没有实现poll接口,就用不了epoll机制。
目前常见的文件系统,如:ext2、ext4、xfs都没有实现poll接口,除了常见的网络套接字socket外还有几种fd,可以使用epoll机制:
(1)eventfd:用来做事件通知,无法传输数据;
(2)timerfd:定时器事件,到时间点触发可读事件。
除了高效的通知机制外,epoll还有个很好的机制,epoll支持边缘触发(edge-triggered),而poll和select都是条件触发(level-triggered)。
条件触发的意思是只要满足事件的条件,比如有数据需要读,就一直不断地把这个事件传递给用户;而边缘触发的意思是只有第一次满足条件的时候才触发,之后就不会再传递同样的事件了。
参考资料:https://mp.weixin.qq.com/s/0ooBuspqJTR9MSMmqK9_rQ