epool
最近看高并发websocket服务器的代码,其中设计到epool的使用,参考了这篇文章:
https://segmentfault.com/a/1190000003063859
有一些心得:
- IO 为什么是阻塞的? 首先,当用户程序读写IO设备时,一定会进行系统调用,从而发生内核切换,内核调用硬件读写数据,并将数据通过
内核的缓存空间拷贝到用户的缓存空间,再切换回用户的内核进程。这中间过程,用户进程是不会进行读写的,因此IO是阻塞的。 - 非阻塞IO呢? 所谓的异步IO是指,用户态程序读取IO时,如果读写不到数据,则立刻返回。过一会,用户进程再次去读取IO,以此来轮询。
- IO 多路复用。select/pool/epool 属于单个process就可以处理多个IO,他们会轮询所负责的所有socket,当有数据到达的时候,就通知用户进程。
就是通过一个机制同时等待多个文件描述符,任何一个感兴趣的时间就绪的时候,就能通知用户进程。 - epool跟前面两者的好处在于:1, 没有文件描述符个数的限制;2, 即使文件描述符数目增大,也不会影响效率。
了解下epool就三个函数 epool_create, epool_ctl, epoll_wait,分别用于创建epool,将文件描述符-事件放入epool,等到事件就绪。epool的wait能返回
就绪的所有文件描述符,而select必须用户去轮询所有的文件描述符。
两种工作模式:
- 水平触发(默认)
告诉你某个文件描述符就绪了,如果不做任何处理,下次还会触发。 - 边缘触发
内核第一次通知你,认为你会去对文件做相应的处理,之后状态再次变化的时候才会通知你,效率更高。
我们怎么使用epool实现高并发的:
首先服务器listen在tcp端口上,把这个文件描述符放入pool,事件是 read|oneshot。(就是只触发一次)
挂载的回调函数:
对listener accept,得到的connection的,首先进行websocket upgrade。之后文件描述符号,放入pool,事件是 read| EdgeTriger.
挂载的回调函数是: 判断event如果是Hup,则close连接,文件描述符移除出pool。否则读取完整message列表,并处理。
最后resume重新把listen的文件描述符继续(不然oneshot只触发一次)。
一些有趣的bug:
- 测试高并发大量连接之后,一段时间后,新进来的连接无法建立。原因是这些大量测试连接不会去读取数据,协程池被耗尽,导致write阻塞,但是connection write之前
是有设置deadline,但是deadline不生效,是由于netpool将偷偷将连接设置成block ip。 - 某一些websocket连接会断掉,客户端是发送了pong响应的,但是服务端认为没有读到,认为客户端超时。原因是每次触发读时间的时候,发现有时一个tcp package会有多个message,
但是原先的reader只读取一个message就去做处理,而netpool是edgeTriger的形式,没有新的数据进来的时候,不会再次触发读取,导致漏了pong数据,修改成读尽多个message再返回处理。