高性能跨平台网络IO(Reactor、epoll、iocp)总结

今天听了公司内部的讲座,对于之前关于IO一些模模糊糊的地方有了一些新的感想以及体会,故此总结一下。

一、IO模型:Reactor和Proactor

高性能跨平台网络IO(Reactor、epoll、iocp)总结

 高性能跨平台网络IO(Reactor、epoll、iocp)总结

 Reactor框架工作模式为:用户注册事件,而后Reactor框架监听该事件,当数据到达后,通知用户,而后用户自己完成事件处理。因此用户只需向Reactor提供fd即可。

Proactor框架工作模式为:用户注册事件,而后Proactor框架监听,数据到达后,Proactor完成事件处理,而后返回给用户通知以及处理完成的数据。用户需向Proactor提供fd以及buf。

Reactor的buf由用户操控,因此可以复用,最多只需用户线程数*buf_size即可;而Proactor的buf由Proactor操控,因此需要请求数*buf_size,内存占用较大,同时提供给Proactor的buf用户在其返回前不能触碰,增大了编写代码难度。

二、Select和Poll

Select和Poll都是Posix规定的IO接口,这里主要介绍poll。

struct pollfd {  
 int fd;        //文件描述符  
 short events;  //要求查询的事件掩码  
 short revents; //返回的事件掩码  
};  
int poll(struct pollfd *ufds, unsigned int nfds, int timeout); 

poll是实现监听fd的工具。ufds数组是需要监听的fd,nfds是ufds的长度,timeout是超时时间,且poll是水平触发的。当用户调用poll时,内核会copy ufds(这样在poll监听的同时,用户仍然可以去修改本地的ufds),而后去监听这些fd,当有fd可读或可写,则poll返回触发的fd个数,同时用户可以去遍历ufds查找触发的fd。这样导致,如果注册的fd过多,poll的性能会下降,因为copy ufds是O(n),同时遍历ufds也是O(n)。

当使用poll来建立Reactor框架时,其结构为:poller线程池监控poll,当获取到fd时,将fd的任务分发给处理线程池worker。

三、EPoll、kQueue、iocp

由于poll性能下降的问题不能满足需求,同时posix并没有出台新的标准,因此各大server厂商实现了自己的高性能poll。

EPoll Linux 2002
kqueue FreeBsd(MAC) 2000
iocp Windows 1993

首先介绍EPoll。

epoll_create 创建一个epoll对象,一般epollfd = epoll_create()
epoll_ctl (epoll_add/epoll_del的合体),往epoll对象中增加/删除某一个流的某一个事件
比如
epoll_ctl(epollfd, EPOLL_CTL_ADD, socket, EPOLLIN);//有缓冲区内有数据时epoll_wait返回
epoll_ctl(epollfd, EPOLL_CTL_DEL, socket, EPOLLOUT);//缓冲区可写入时epoll_wait返回
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);等待直到注册的事件发生

epoll相比于poll来说,具有了自己的对象。poll上一次调用与下一次调用是毫无关系的,而epoll则是首先创建一个epoll对象,而后对这个对象进行注册、删除、修改,然后监听。当创建一个epoll对象时,其在内核中创建了一个红黑树和一个完成事件链表。红黑树存储注册事件,完成事件链表则保持返回的触发事件。这里使用红黑树,肯定需要一把锁,同时linux最大fd为65536,所以红黑树的性能在这个数量级很好。而由于其返回的是触发事件链表,因此用户只需遍历该链表即可获取到触发fd,不会有性能问题。

并且epoll可以使用ET(edge trigger边缘触发)和LT(level trigger水平触发)两种方式,使用ET内核会监控更少,但对编程要求更高。

LT模式:当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序可以不立即处理该事件。下次调用epoll_wait时,会再次响应应用程序并通知此事件。
ET模式:当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序必须立即处理该事件。如果不处理,下次调用epoll_wait时,不会再次响应应用程序并通知此事件。

epoll_wait调用后,会返回触发事件数int,以及事件链表events。这里maxevents参数代表该epoll_wait最多返回的enents数,如果返回fd过多,如10000,而有10个epoller线程监听同一个epfd,则设置maxevents为1000,每个线程最多获得1000个触发events,充分利用CPU。

而后介绍iocp(input output completion port)

高性能跨平台网络IO(Reactor、epoll、iocp)总结

 iocp在内核实现了poll、poller、task_queue,用户worker可以直接使用GetQueuesCompletionStatus获取到一个触发的事件任务, 而不需要管理poller以及fd封装任务分发等。同时,iocp只能监听windows的api触发事件。

其使用方法为:CreateIoCompletionPort函数创建一个新的IOCP;CreateIoCompletionPort把socket或文件句柄与一个已存在的IOCP关联起来;线程调用GetQueuedCompletionStatus函数等待放入IOCP的I/O完成包。

同时,线程可以用PostQueuedCompletionStatus函数在IOCP上投寄一个完成包。也有CancelEx可以杀掉某个监听事件,且操作为原子的,调用CancelEx后,只会出现监听成功的Task、监听失败Cancel成功的Task、监听失败Cancel失败的Task之一。

四、io框架

开源网络库:ace、boost.asio、libevent

五、监听事件队列超时处理方法:

不好的方法:

维护一个超时队列,epoll_wait超时时间最小的事件。

好的方法:

超时器数据结构:

  • 超时=0,不进入超时器
  • 超时!=0,进入超时器的超时队列
  • 超时队列支持增删改查,支持有序遍历

超时器工作模式:

  • 单独开一个LInux temerfd,挂在epoll上,监听读事件
  • 任何时刻提交的新IO请求,插入后如果位于队列头,重新设置timerfd
  • 当epoll告知timerfd可读时,从头遍历超时队列,对超时的socket
  1. 唤醒worker,告知用户超时了
  2. 调用epoll_ctrl、删除对应的sockerfd
  3. epoll删除完成时关闭socketfd
  • 如果要保证原子性,需要1把互斥锁、锁粒度略大