为什么是EPOLL ? LT还是ET?

为什么在linux下选择epoll?

  • select

    本质上是通过设置或者检查存放fd标志位的数据结构来进行下一步处理。这样所带来的缺点是:
    1 单个进程可监视的fd数量被限制
    2 需要维护一个用来存放大量fd的数据结构,这样会使得用户空间和内核空间在传递该结构时复制开销大
    3 对socket进行扫描时是线性扫描
    单个进程所能打开的最大连接数有FD_SETSIZE宏定义,其大小是32个整数的大小(在32位的机器上,大小就是3232,同理64位机器上FD_SETSIZE为3264),当然我们可以对进行修改,然后重新编译内核,但是性能可能会受到影响,这需要进一步的测试。因为每次调用时都会对连接进行线性遍历,所以随着FD的增加会造成遍历速度慢的“线性下降性能问题”。内核需要将消息传递到用户空间,都需要内核拷贝动作

  • poll

    本质上和select没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态,如果设备就绪则在设备等待队列中加入一项并继续遍历,如果遍历完所有fd后没有发现就绪设备,则挂起当前进程,直到设备就绪或者主动超时,被唤醒后它又要再次遍历fd。这个过程经历了多次无谓的遍历。
    它没有最大连接数的限制,原因是它是基于链表来存储的,但是同样有一个缺点:大量的fd的数组被整体复制于用户态和内核地址空间之间,而不管这样的复制是不是有意义。poll还有一个特点是“水平触发”,如果报告了fd后,没有被处理,那么下次poll时会再次报告该fd。poll本质上和select没有区别,但是它没有最大连接数的限制,原因是它是基于链表来存储的
    因为每次调用时都会对连接进行线性遍历,所以随着FD的增加会造成遍历速度慢的“线性下降性能问题”。内核需要将消息传递到用户空间,都需要内核拷贝动作

  • epoll

    在前面说到的复制问题上,epoll使用mmap减少复制开销。还有一个特点是,epoll使用“事件”的就绪通知方式,通过epoll_ctl注册fd,一旦该fd就绪,内核就会采用类似callback的回调机制来激活该fd,epoll_wait便可以收到通知 虽然连接数有上限,但是很大,1G内存的机器上可以打开10万左右的连接,2G内存的机器可以打开20万左右的连接 因为epoll内核中实现是根据每个fd上的callback函数来实现的,只有活跃的socket才会主动调用callback,所以在活跃socket较少的情况下,使用epoll没有前面两者的线性下降的性能问题,但是所有socket都很活跃的情况下,可能会有性能问题。epoll通过内核和用户空间共享一块内存来实现的。

ET还是LT?

什么是epol的LT与ET

电子通信领域的名词,文字描述有点儿困难,用两个图来表示。

在图中

  • x 轴表示时间。
  • high 表示 fd 不可读或不可写。
  • low 表示 fd 可读或可写。
  • 0 表示 IO 多路复用器不返回事件。
  • 1 表示 IO 多路复用器返回事件。

Edge Trigger 边缘触发

high           00000000      00000000
               0      0      0      0
               0      0      0      0
low     00000001      00000001      00000000

Level Trigger 水平触发

high           11111111      11111111
               1      1      1      1
               1      1      1      1
low     00000001      10000001      10000000

一图胜千言,简单来说就是当边缘触发时,只有 fd 变成可读或可写的那一瞬间才会返回事件。当水平触发时,只要 fd 可读或可写,一直都会返回事件。

我们来举个例子,当一个 fd 收到 1000 字节的数据,这时无论 边缘触发 还是 水平触发 都会返回可读事件。如果我们调用 recv 读了 500 字节并返回 IO多路复用器 。在 边缘触发 的IO多路复用器中,因为可读的状态并没有回到 low 位,我们以后永远都不会再收 fd 的读事件了,除非我们把所有 buffer 中的数据全部读完。在条件触发的IO多路复用器中,我们会立即再次收到可读事件,直到我们把剩下的 500 字节 recv 完。

通常来讲,我们倾向于使用 Level Trigger 处理读事件,以防代码有 Bug 导致接收不到可读事件

文字描述:

  • LT: 水平触发 一直触发

·内核中的socket接受缓冲区不为空 有数据可读 读事件一直触发
·内核中的socket发送缓冲区不满 可以继续写入数据 写事件一直触发
·因此在一开始的时候不能马上关注EPOLLOUT 因为这个时候内核socket发送缓冲区中没有数据一直可写 所以会一直触发EPOLLOUT 导致busy_loop 应该在写数据事件返回EAGAIN(说明内核中socekt的发送缓冲区满了)的时候 才开始关注EPOLLOUT 即关注内核socket缓冲区的可写事件 把还没写完的数据存至应用缓冲区 等内核缓冲区空出来,可写事件就触发,就从应用缓冲区取出数据拷贝到内核缓冲区,如果应用缓冲区中的数据已经完倒拷到了内核缓冲区,就取消可写事件,如果不能完全拷贝完,就不取消

  • ET: 边沿触发 从低到高这种变化才会通知

·由低到高 由高到低出发
·一开始的时候就关注EPOLLIN EPOLLOUT不会产生busy_loop
·这里读要一次行全部读完到产生EAGAIN为止 才会处于低电平模式 否则一直处于高电平没有高低变化再有数据到来的时候 不再出发EPOLLIN事件 容易落掉数据
·将未发送的数据添加到应用层缓冲区 connfd EPOLLOUT事件到来内核缓冲区可写 发送数据
·发送数据直到发送完或者产生EAGAIN(内核缓冲区满了)
·如果是epoll模型,可以改用edge trigger(ET)。问题是如果漏掉了一次accpet(2) 程序再也不会受到新的连接

  • ET还是LT?

相对于ET LT更容易处理些 理论上ET更高效些 但是实际的使用中处理不好容易漏掉事件 并且在实际的使用中ET未必比LT效率高
ngx使用了ET muduo skynet使用了LT

参考
wiki/epoll
大并发服务器开发
Linux epoll介绍和程序实例
linux下epoll ET模型accept并发问题
用 Python 理解服务器模型

标签: none

添加新评论