epoll是在2.6内核中提出的,是之前的select和poll的增强版本。相对于select和poll来说,epoll更加灵活,没有描述符限制。epoll使用一个文件描述符管理多个描述符,将用户关系的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的copy只需一次

基本原理

epoll支持水平触发和边缘触发,最大的特点在于边缘触发,它只告诉进程哪些fd刚刚变为就绪态,并且只会通知一次。还有一个特点是,epoll使用“事件”的就绪通知方式,通过epoll_ctl注册fd,一旦该fd就绪,内核就会采用类似callback的回调机制来激活该fd,epoll_wait便可以收到通知。

epoll 优点

select缺点

  1. 单个进程可监视的fd数量被限制。 需要维护一个用来存放大量fd的数据结构,这样会使得用户空间和内核空间在传递该结构时复制开销大。 3) 对fd进行扫描时是线性扫描。fd剧增后,IO效率较低,因为每次调用都对fd进行线性扫描遍历,所以随着fd的增加会造成遍历速度慢的性能问题 4)select() 函数的超时参数在返回时也是未定义的,考虑到可移植性,每次在超时之后在下一次进入到select之前都需要重新设置超时参数。

  2. 没有最大并发连接的限制,能打开的FD的上限远大于1024(1G的内存上能监听约10万个端口)。而select的上限只有1024,

  3. 效率提升,不是轮询的方式,不会随着FD数目的增加效率下降。只有活跃可用的FD才会调用callback函数;即Epoll最大的优点就在于它只管你“活跃”的连接,而跟连接总数无关,因此在实际的网络环境中,Epoll的效率就会远远高于select和poll。

  4. 内存拷贝,利用mmap()文件映射内存加速与内核空间的消息传递;即epoll使用mmap减少复制开销。而 selectpoll 在遍历fd集合都需要将集合从用户态拷贝到内核态,

水平触发和边缘触发

水平触发

对于读操作 : 只要缓冲内容不为空,LT模式返回读就绪。

对于写操作 : 只要缓冲区还不满,LT模式会返回写就绪。

边缘触发

对于读操作 :

  • 当缓冲区由不可读变为可读的时候,即缓冲区由空变为不空的时候。
  • 当有新数据到达时,即缓冲区中的待读数据变多的时候。
  • 当缓冲区有数据可读,且应用进程对相应的描述符进行EPOLL_CTL_MOD 修改EPOLLIN事件时。

对于写操作

  • 当缓冲区由不可写变为可写时
  • 当有旧数据被发送走,即缓冲区中的内容变少时
  • 当缓冲区有空间可写,且应用进程对相应的描述符进行EPOLL_CTL_MOD 修改EPOLLOUT事件时。

epoll的惊群效应

惊群现象就是多进程(多线程)在同时阻塞等待同一个事件的时候(休眠状态),如果等待的这个事件发生,那么他就会唤醒等待的所有进程(或者线程),但是最终却只可能有一个进程(线程)获得这个时间的“控制权”,对该事件进行处理,而其他进程(线程)获取“控制权”失败,只能重新进入休眠状态,这种现象和性能浪费就叫做惊群。

惊群效应到底消耗了什么 ?

  1. 系统对用户进程/线程频繁地做无效的调度,上下文切换系统性能大打折扣。
  2. 为了确保只有一个线程得到资源,用户必须对资源操作进行加锁保护,进一步加大了系统开销。

Golang 中epoll的应用

go 语言实现的网络库中就是通过epoll 模式实现的,在src/runtime/netpoll_*.go 可以看到其源码。网络上很多对这些源码的分析,不是重点。就知道为什么Golang网络并发为何如此优秀即可。这也是为何当您在 Go 中发送 HTTP 请求时,Go 运行时通常会创建两个 goroutine 来处理请求和响应。第一个 goroutine负责发送请求并等待响应。这个 goroutine 通常是在你发起 HTTP 请求时通过 http.Client.Do() 方法或者 http.NewRequest() 方法创建的。

参考

  1. Go netpoller 原生网络模型之源码全面揭秘
  2. 网络轮询器