ET和LT:
LT一般用在单线程。
ET和EPOLLONESHOT配合用在多线程共享一个epoll环境下,EPOLLONESHOT标记触发过的事件从epoll中移除,下次必须重新注册,用来防止多线程同时取到同一个socket的事件产生冲突。
epoll_wait 第三个参数 取事件数量:
单线程模型当然尽可能一次多取一些效率高,多线程为了防止一个线程把所有事件取完其他线程饥饿,ACE实现是只取1个。
错误处理:
EAGIN | EINTR | EWOULDBLOCK 重试。
EPOLLERR | EPOLLHUP | EPOLLRDHUP 断开连接。
惊群:
默认系统都会有这问题,据说新系统有修复不过还是处理一下比较好,一般解决方案是同时只有一个线程等待accept,可以单独线程accept,将连接在分给其他工作线程。nginx是多进程模型,使用了基于共享内存的互斥锁,使得同时只有一个工作进程的epoll含有accept的socket,通过这种方式实现连接数上的负载均衡(连接数少的工作进程得到accept锁的概率高)。
为了避免大数据量io时,et模式下只处理一个fd,其他fd被饿死的情况发生。linux建议可以在fd联系到的结构(一般都会自己封装一个包含fd和读写缓冲的结构体)中增加ready位,然后epoll_wait触发事件之后仅将其置位为ready模式,然后在下边轮询ready fd列表。
epoll实现:
epoll内部用了一个红黑树记录添加的socket,用了一个双向链表接收内核触发的事件。
注册的事件挂载在红黑树中(红黑树的插入时间效率是logN,其中n为树的高度)。
挂载的事件会与设备(网卡)驱动建立回调关系,也就是说,当相应的事件发生时会调用这个回调方法。这个回调方法在内核中叫ep_poll_callback,它会将发生的事件添加到rdlist双链表中。
使用mmap映射内存,减少内核态和用户态的不同内存地址空间拷贝开销。每次注册新的事件到epoll中时,会把fd拷贝进内核,通过内核于用户空间mmap同一块内存保证了只会拷贝一次。(返回的时候不需要拷贝,select要)
执行epoll_ctl时,除了把socket放到红黑树上,还会给内核中断处理程序注册一个回调函数,告诉内核,如果这个句柄的中断到了,就把它放到准备就绪list链表里。所以,当一个socket上有数据到了,内核在把网卡上的数据copy到内核中后就把socket插入到准备就绪链表里了。链表又是通过mmap映射的空间,所以在传递给用户程序的时候不需要复制(这也是为什么比select效率高的原因,epoll_wait返回的只是就绪队列,不需要轮询 不需要复制完成的事件列表,select,poll实现需要自己不断轮询所有fd集合,直到设备就绪)。
epoll_wait最后会检查socket,如果是 LT,并且这些socket上确实有未处理的事件时,又把该句柄放回到刚刚清空的准备就绪链表了(LT比ET低效的原因)。
可见,如果没有大量的空闲,无效连接,epoll效率不比select高。
测试数据(仅是刚接触go的时候好奇做的参考意义的测试):
同样的环境,echo服务器测并发io,单线程epoll qps:45000左右,每连接/协程 go: 50000多,多线程epoll(开6个epoll,每个epoll开8线程,一共48线程):qps 70000多。