背景
在阻塞I/O下,一个线程只能处理一个流的I/O事件,这个流的处理卡住会阻塞整个线程。如果想同时处理多个流,要么多进程fork,要么多线程。很不巧两种方法效率都不高,这时I/O复用就诞生了。
首先定义流的概念,一个流可以是文件、socket、pipe等可以进行I/O操作的内核对象。所以不管你是文件,套接字还是管道我们都可以把他们看做流
要完成I/O复用要完成以下几件事情
1.用户态怎么将fd传递到内核态
2.内核态如何判断fd可读可写
3.内核怎么通知监控者fd可读可写
4.监控者这如何找到可读可写的fd并传递给用户态程序
5.继续循环时监控者这怎么重复上述步骤?
下面以fd来代表表示所有流
工作流程
一棵红黑树,一张准备就绪链表,少量内核cache就帮我们解决了大并发下socket处理问题。
- 执行epoll_create时,内核在高速cache中创建了红黑树和就绪链表。
- 执行epoll_ctl时,如果增加fd,则检查在红黑树中是否存在,存在立即返回,不存在则添加到树干上(用户态到内核态内存拷贝),然后向内核的中断处理程序注册回调,当红黑树中某个fd的中断事件来临时向就绪链表中插入数据(比如当一个socket上有数据到了,内核就把网卡上的数据copy到内核中然后把socket插入到就绪链表中)
- 执行epoll_wait时只监控就绪链表。有数据立即返回(少量拷贝),没有数据就sleep等到timeout事件后即使没数据也返回。
伪代码
|
|
红黑树查询复杂度,插入复杂度都是O(logN)
Epoll_wait行为见文档 linux-epoll_wait
水平触发(LT)和边缘触发(ET)
当一个socket句柄上有事件时,内核会把该句柄插入上面所说的准备就绪链表,这是我们调用epoll_wait,会把准备就绪的socket拷贝到用户态内存,然后清空准备就绪list链表,最后epoll_wait做了件事,就是检查这些socket,如果是LT模式,并且这些socket上有未处理的事件,就会把这些socket再次放回刚刚清空的就绪链表。所以,非ET的句柄,只要它上面还有事件,epoll_wait每次都会返回,而ET模式的句柄,除非有新中断到,即使socket上的事件没有处理完,也不会次次从epoll_wait返回。
php使用epoll
php本身无法直接使用epoll,不过我们可以借助libevent库来间接达到使用epoll的目的。libevent是一个高性能的网络事件库,内部实现了对select、poll、epoll、kqueue等的封装。
php想要使用libevent需要借助Event
扩展
基于libevent实现定时器
|
|
基于libevent实现的聊天室服务端,通过telnet 127.0.0.1:9999
直接连接
|
|
总结
本质上epoll提升性能的点在于
- 减少了文件描述符的拷贝次数
- 减少遍历文件描述符时检查的次数
参考
epoll原理
epoll相关基础知识
Linux内核epoll源码剖析
PHP socket初探 —- 一些零碎细节的拾漏补缺