epoll是在2.6内核中提出的,是之前的select和poll的增强版本。相对于select和poll来说,epoll更加灵活,没有描述符限制。epoll使用一个文件描述符管理多个描述符,将用户关系的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的copy只需一次
。
基本原理
epoll支持水平触发和边缘触发,最大的特点在于边缘触发,它只告诉进程哪些fd刚刚变为就绪态,并且只会通知一次
。还有一个特点是,epoll使用“事件”的就绪通知方式
,通过epoll_ctl注册fd,一旦该fd就绪,内核就会采用类似callback的回调机制来激活该fd
,epoll_wait便可以收到通知。
epoll 优点
select缺点
单个进程可监视的fd数量被限制。 需要维护一个用来存放大量fd的数据结构,这样会使得用户空间和内核空间在传递该结构时复制开销大。 3) 对fd进行扫描时是线性扫描。fd剧增后,IO效率较低,因为每次调用都对fd进行线性扫描遍历,所以随着fd的增加会造成遍历速度慢的性能问题 4)select() 函数的超时参数在返回时也是未定义的,考虑到可移植性,每次在超时之后在下一次进入到select之前都需要重新设置超时参数。
没有最大并发连接的限制
,能打开的FD的上限远大于1024(1G的内存上能监听约10万个端口)。而select
的上限只有1024,效率提升,不是轮询的方式,不会随着FD数目的增加效率下降
。只有活跃可用的FD才会调用callback函数;即Epoll最大的优点就在于它只管你“活跃”的连接,而跟连接总数无关
,因此在实际的网络环境中,Epoll的效率就会远远高于select和poll。内存拷贝
,利用mmap()文件映射内存加速与内核空间的消息传递;即epoll使用mmap减少复制开销
。而select
和poll
在遍历fd集合都需要将集合从用户态拷贝到内核态,
水平触发和边缘触发
水平触发
对于读操作 : 只要缓冲内容不为空,LT模式返回读就绪。
对于写操作 : 只要缓冲区还不满,LT模式会返回写就绪。
边缘触发
对于读操作 :
- 当缓冲区由不可读变为可读的时候,即缓冲区由空变为不空的时候。
- 当有新数据到达时,即缓冲区中的待读数据变多的时候。
- 当缓冲区有数据可读,且应用进程对相应的描述符进行
EPOLL_CTL_MOD
修改EPOLLIN
事件时。
对于写操作
- 当缓冲区由不可写变为可写时
- 当有旧数据被发送走,即缓冲区中的内容变少时
- 当缓冲区有空间可写,且应用进程对相应的描述符进行
EPOLL_CTL_MOD
修改EPOLLOUT
事件时。
epoll的惊群效应
惊群现象就是多进程(多线程)在同时阻塞等待同一个事件的时候(休眠状态),如果等待的这个事件发生,那么他就会唤醒等待的所有进程(或者线程),但是最终却只可能有一个进程(线程)获得这个时间的“控制权”,对该事件进行处理,而其他进程(线程)获取“控制权”失败,只能重新进入休眠状态,这种现象和性能浪费就叫做惊群。
惊群效应到底消耗了什么 ?
- 系统对用户进程/线程频繁地做无效的调度,上下文切换系统性能大打折扣。
- 为了确保只有一个线程得到资源,用户必须对资源操作进行加锁保护,进一步加大了系统开销。
Golang 中epoll的应用
go 语言实现的网络库中就是通过epoll 模式实现的,在src/runtime/netpoll_*.go
可以看到其源码。网络上很多对这些源码的分析,不是重点。就知道为什么Golang网络并发为何如此优秀即可。这也是为何当您在 Go 中发送 HTTP 请求时,Go 运行时通常会创建两个 goroutine 来处理请求和响应。第一个 goroutine
负责发送请求并等待响应。这个 goroutine
通常是在你发起 HTTP 请求时通过 http.Client.Do()
方法或者 http.NewRequest()
方法创建的。