场景:
100w用户同时与服务器(一个进程)保持TCP连接,但每一时刻只有几十或几百个TCP连接是活跃的(收发TCP包),如何实现?
select/poll实现:用户发送100w个fd给内核去查找活跃的TCP(有事件发生)——用户态到内核态内存拷贝——最多只能处理几千(一般1024)并发连接
epoll实现:在内核申请简易文件系统。在启动时创建epoll对象,需要时添加删除fd(100w),就绪fd存队列中,epoll_wait时无需传100wfd,也无需遍历这100wfd,返回队列即可

概念
基于内核2.6,相比select、poll,无描述符数量限制。epoll使用一个文件描述符管理多个文件描述符,将相应事件存储在内核的事件表中,在用户空间和内核空间的copy只需一次???

Linux API(system call)

  1. #include <sys/epoll.h>
  2. int epoll_create(int size);
  3. int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
  4. int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);

epoll_create

原型

int epoll_create(int size);

功能

在内核创建epoll实例,并返回相应epfd给用户态

参数

  • size linux2.6后已忽略,默认填1

返回值

  • 0 epfd

  • -1 errno

备注

close(epfd)或内核自动释放

epoll_ctl

原型

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

功能

为每个fd单独注册事件(不同于select、poll为fd_set注册事件)

参数

  • epfd 相当于红黑树根节点
  • op 控制操作
    • EPOLL_CTL_ADD 注册事件,即添加节点
    • EPOLL_CTL_DEL 移除事件,即删除节点
    • EPOLL_CTL_MOD 修改事件,即变更节点状态
  • fd 目标文件描述符(connfd、listenfd、pipefd)
  • event 事件(EPOLLIN、EPOLLOUT、EPOLLET等)

返回值

成功返回0,失败返回-1并设置errno

备注

epoll_wait

原型

int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);

功能

获取就绪事件

参数

  • epfd
  • events 保存就绪事件数组(用户态)
  • maxevents 数组最大空间
  • timeout 超时时间,超时后将阻塞,单位毫秒
    • -1 无超时限制
    • 0 非阻塞,调用后立即返回
    • 0

返回值

成功返回就绪事件fd数量(可用于遍历数组),失败返回-1并设置errno

备注

struct epoll_event

  1. typedef union epoll_data
  2. {
  3. void *ptr; // 自定义数据结构存储
  4. int fd; // 常用
  5. uint32_t u32;
  6. uint64_t u64;
  7. } epoll_data_t;
  8. struct epoll_event
  9. {
  10. uint32_t events; /* Epoll events */
  11. epoll_data_t data; /* User data variable */
  12. } __EPOLL_PACKED;

EPOLL事件类型

EPOLLIN 可读事件 fd缓冲区有数据;sockfd正常关闭
EPOLLOUT 可写事件 fd缓冲区空
EPOLLET 边沿触发模式
EPOLLERR fd发送错误
EPOLLHUP fd被挂断
EPOLLONESHOT 仅监听一次,如需继续监听该sockfd需再次添加(保证一个socket连接在任一时刻只被一个线程处理)
EPOLLPRI fd有紧急数据可读(带外数据)

EPOLL工作模式

LT:Level trigger,水平触发,默认
ET:Edge trigger,边沿触发
区别:为什么都是针对读而不是写
LT:当检测事件就绪并通知应用程序后,APP可不立即处理而支持再次通知(相当于未处理会一直通知)
只要缓冲区有剩余未读数据都会通知即epoll_wait均会返回
读写:read 操作比较简单,有 read 事件就读,读多读少都没有问题,但是 write 就不那么容易了,一般来说 socket 在空闲状态时发送缓冲区一定是不满的,假如 fd 一直在监控中,那么会一直通知写事件,不胜其烦。
所以必须保证没有数据要发送的时候,要把 fd 的写事件监控从 epoll 列表中删除,需要的时候再加入回去,如此反复
支持阻塞和非阻塞套接字
ET:当检测事件就绪并通知应用程序后,APP需立即处理,若不处理不会被再次通知(相当于未处理会被丢弃)
只有数据到来才会触发
如该fd可读,则必须一直读空,让errno返回EAGAIN为止,否则下次会丢掉。若ET未设置成非阻塞,则会一直读/写直到阻塞
读写:fd 可读则返回可读事件,若开发者没有把所有数据读取完毕,epoll 不会再次通知 read 事件,也就是说如果没有全部读取所有数据,那么导致 epoll 不会再通知该 socket 的 read 事件,事实上一直读完很容易做到。
若发送缓冲区未满,epoll 通知 write 事件,直到开发者填满发送缓冲区,epoll 才会在下次发送缓冲区由满变成未满时通知 write 事件。
ET模式下只有 socket 的状态发生变化时才会通知,也就是读取缓冲区由无数据到有数据时通知 read 事件,发送缓冲区由满变成未满通知 write 事件。
线程饥饿:如果某个 socket 源源不断地收到非常多的数据,在试图读取完所有数据的过程中,有可能会造成其他的 socket 得不到处理,从而造成饥饿问题。
解决办法:
为每个已经准备好的描述符维护一个队列,这样程序就可以知道哪些描述符已经准备好了但是并没有被读取完,然后程序定时或定量的读取,如果读完则移除,直到队列为空,这样就保证了每个 fd 都被读到并且不会丢失数据。
流程如图:
最好可以有案例跑一遍

如果采用EPOLLLT模式的话,系统中一旦有大量你不需要读写的就绪文件描述符(什么是不需要读写的fd,有事件的不应该都是需要读写的吗???)**,它们每次调用epoll_wait都会返回,这样会大大降低处理程序检索自己关心的就绪文件描述符的效率
ET减少了epoll事件被重复触发的次数,故效率更高。(必须
使用非阻塞套接口,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死???**)

惊群效应
你在广场喂鸽子,你只投喂了一份食物,却引来一群鸽子争抢,最终还是只有一只鸽子抢到了食物,对于其他鸽子来说是徒劳的。
2016 年 Linux 4.5 内核新添加的一个 epoll 的标识 EPOLLEXCLUSIVE,保证一个事件发生时候只有一个线程会被唤醒,以避免多侦听下的惊群问题

对比select

  • 对 fd 数量没有限制(当然这个在 poll 也被解决了)
  • 抛弃了 bitmap 数组实现了新的结构来存储多种事件类型
  • 无需重复拷贝 fd 随用随加 随弃随删
  • 采用事件驱动避免轮询查看可读写事件

原理
调用epoll_create后,内核创建 eventpoll,着重两个成员:rdllist、rbr

  1. 在epoll文件系统中创建file结点——对应 epfd
  2. 内核cache中创建红黑树——存注册的sockfd(epoll_ctl的参数)
  3. 内核cache中创建双向链表——存准备就绪事件

调用epoll_ctl,即红黑树中插入删除修改sockfd(key)

  1. 所有添加到epoll中的事件都会与设备(如网卡)驱动程序建立回调关系,也就是说相应事件的发生时会调用这里的回调方法。这个回调方法在内核中叫做ep_poll_callback,它会把这样的事件放到上面的rdllist双向链表中
  2. 当添加sockfd(注册)时,会检查是否在红黑树中存在,存在立即返回,不存在则添加并向内核注册回调函数——用于当中断事件发生时向就绪链表中插入数据

调用epoll_wait,即判断就绪链表中是否有数据,有则返回,没有则等timeout后返回——高效原因

  1. 即检查rdllist中是否含有 epitem 元素
  2. 若不为空,会将事件(思考是否就是epitem)复制到用户态内存,并返回事件数量——使用共享内存提升效率???

每个事件都建立一个epitem结构,不是每个fd只建立一个epitem,一个fd可以同时反生EPOLLIN和EPOLLOUT吗?

  1. // linux-5.9.8/fs/eventpoll.c
  2. struct eventpoll {
  3. ...
  4. /* List of ready file descriptors */
  5. struct list_head rdllist; // 双向链表,存就绪fd
  6. /* RB tree root used to store monitored fd structs */
  7. struct rb_root_cached rbr; // 存注册的fd 以前版本:struct rb_root rbr;
  8. ...
  9. };
  10. struct rb_root_cached {
  11. struct rb_root rb_root;
  12. struct rb_node *rb_leftmost; // 新加的
  13. };
  14. // linux-5.9.8/fs/eventpoll.c
  15. struct epitem { // 每个事件都建立一个epitem结构
  16. union {
  17. /* RB tree node links this structure to the eventpoll RB tree */
  18. struct rb_node rbn; // 红黑树中的每个结点
  19. /* Used to free the struct epitem */
  20. struct rcu_head rcu;
  21. };
  22. /* List header used to link this structure to the eventpoll ready list */
  23. struct list_head rdllink; // 就绪双向链表中的每个结点
  24. /*
  25. * Works together "struct eventpoll"->ovflist in keeping the
  26. * single linked chain of items.
  27. */
  28. struct epitem *next;
  29. /* The file descriptor information this item refers to */
  30. struct epoll_filefd ffd; // 事件句柄信息
  31. /* Number of active wait queue attached to poll operations */
  32. int nwait;
  33. /* List containing poll wait queues */
  34. struct list_head pwqlist;
  35. /* The "container" of this item */
  36. struct eventpoll *ep; // 对应的eventpoll——epfd
  37. /* List header used to link this item to the "struct file" items list */
  38. struct list_head fllink;
  39. /* wakeup_source used when EPOLLWAKEUP is set */
  40. struct wakeup_source __rcu *ws;
  41. /* The structure that describe the interested events and the source fd */
  42. struct epoll_event event; // 期待事件
  43. };
  44. struct epoll_filefd {
  45. struct file *file;
  46. int fd;
  47. } __packed;
  48. static int ep_poll_callback(wait_queue_entry_t *wait, unsigned mode, int sync, void *key)
  49. {
  50. int pwake = 0;
  51. struct epitem *epi = ep_item_from_wait(wait);
  52. struct eventpoll *ep = epi->ep;
  53. __poll_t pollflags = key_to_poll(key);
  54. unsigned long flags;
  55. int ewake = 0;
  56. read_lock_irqsave(&ep->lock, flags);
  57. ep_set_busy_poll_napi_id(epi);
  58. /*
  59. * If the event mask does not contain any poll(2) event, we consider the
  60. * descriptor to be disabled. This condition is likely the effect of the
  61. * EPOLLONESHOT bit that disables the descriptor when an event is received,
  62. * until the next EPOLL_CTL_MOD will be issued.
  63. */
  64. if (!(epi->event.events & ~EP_PRIVATE_BITS))
  65. goto out_unlock;
  66. /*
  67. * Check the events coming with the callback. At this stage, not
  68. * every device reports the events in the "key" parameter of the
  69. * callback. We need to be able to handle both cases here, hence the
  70. * test for "key" != NULL before the event match test.
  71. */
  72. if (pollflags && !(pollflags & epi->event.events))
  73. goto out_unlock;
  74. /*
  75. * If we are transferring events to userspace, we can hold no locks
  76. * (because we're accessing user memory, and because of linux f_op->poll()
  77. * semantics). All the events that happen during that period of time are
  78. * chained in ep->ovflist and requeued later on.
  79. */
  80. if (READ_ONCE(ep->ovflist) != EP_UNACTIVE_PTR) {
  81. if (chain_epi_lockless(epi))
  82. ep_pm_stay_awake_rcu(epi);
  83. } else if (!ep_is_linked(epi)) {
  84. /* In the usual case, add event to ready list. */
  85. if (list_add_tail_lockless(&epi->rdllink, &ep->rdllist))
  86. ep_pm_stay_awake_rcu(epi);
  87. }
  88. /*
  89. * Wake up ( if active ) both the eventpoll wait list and the ->poll()
  90. * wait list.
  91. */
  92. if (waitqueue_active(&ep->wq)) {
  93. if ((epi->event.events & EPOLLEXCLUSIVE) &&
  94. !(pollflags & POLLFREE)) {
  95. switch (pollflags & EPOLLINOUT_BITS) {
  96. case EPOLLIN:
  97. if (epi->event.events & EPOLLIN)
  98. ewake = 1;
  99. break;
  100. case EPOLLOUT:
  101. if (epi->event.events & EPOLLOUT)
  102. ewake = 1;
  103. break;
  104. case 0:
  105. ewake = 1;
  106. break;
  107. }
  108. }
  109. wake_up(&ep->wq);
  110. }
  111. if (waitqueue_active(&ep->poll_wait))
  112. pwake++;
  113. out_unlock:
  114. read_unlock_irqrestore(&ep->lock, flags);
  115. /* We have to call this outside the lock */
  116. if (pwake)
  117. ep_poll_safewake(ep, epi);
  118. if (!(epi->event.events & EPOLLEXCLUSIVE))
  119. ewake = 1;
  120. if (pollflags & POLLFREE) {
  121. /*
  122. * If we race with ep_remove_wait_queue() it can miss
  123. * ->whead = NULL and do another remove_wait_queue() after
  124. * us, so we can't use __remove_wait_queue().
  125. */
  126. list_del_init(&wait->entry);
  127. /*
  128. * ->whead != NULL protects us from the race with ep_free()
  129. * or ep_remove(), ep_remove_wait_queue() takes whead->lock
  130. * held by the caller. Once we nullify it, nothing protects
  131. * ep/epi or even wait.
  132. */
  133. smp_store_release(&ep_pwq_from_wait(wait)->whead, NULL);
  134. }
  135. return ewake;
  136. }

此图需要去理清,且需要自己重新画一幅关于epoll图
image.png

epoll反应堆模型
与epoll接口最本质区别在于:

  1. 传入联合体的是自定义结构指针 struct myevent_t
  2. 直接调用事件对应回调函数而非事件分类处理
  1. struct myevent {
  2. int fd; // 需添加到epfd中的fd
  3. void *args; // 回调函数参数
  4. void (*callback)(void *args); // 回调函数
  5. ...
  6. // 其他非必需参数:用户缓冲区、节点状态、节点创建时间等
  7. } myevent_t;

epoll原生模型

  1. epoll_create,创建epoll
  2. epoll_ctl,添加listenfd
  3. epoll_wait,检车listenfd是否有事件
    1. 返回数组并遍历
    2. listenfd事件,则接受连接即accept,成功并将connfd注册到epoll中
    3. connfd事件,则进行read/write

epoll反应堆模型

  1. epoll_create,创建epoll
  2. epoll_ctl,添加listenfd
  3. epoll_wait,检车listenfd是否有事件

image.png

错误观点:epoll_wait 返回时,对于就绪的事件,epoll 使用的是共享内存的方式,即用户态和内核态都指向了就绪链表,所以就避免了内存拷贝消耗
实际拷贝实现:
revents = ep_item_poll(epi, &pt);//获取就绪事件
if (revents) {
if (put_user(revents, &uevent->events) ||
put_user(epi->event.data, &uevent->data)) {
list_add(&epi->rdllink, head);//处理失败则重新加入链表
ep_pm_stay_awake(epi);
return eventcnt ? eventcnt : -EFAULT;
}
eventcnt++;
uevent++;
if (epi->event.events & EPOLLONESHOT)
epi->event.events &= EP_PRIVATE_BITS;//EPOLLONESHOT标记的处理
else if (!(epi->event.events & EPOLLET)) {
list_add_tail(&epi->rdllink, &ep->rdllist);//LT模式处理
ep_pm_stay_awake(epi);
}
}

面试
问题:使用 Linux epoll 模型的 LT 水平触发模式,当 socket 可写时,会不停的触发 socket 可写的事件,如何处理?

普通做法:
当需要向 socket 写数据时,将该 socket 加入到 epoll 等待可写事件。接收到 socket 可写事件后,调用 write 或 send 发送数据,当数据全部写完后, 将 socket 描述符移出 epoll 列表,这种做法需要反复添加和删除。
改进做法:
向 socket 写数据时直接调用 send 发送,当 send 返回错误码 EAGAIN,才将 socket 加入到 epoll,等待可写事件后再发送数据,全部数据发送完毕,再移出 epoll 模型,改进的做法相当于认为 socket 在大部分时候是可写的,不能写了再让 epoll 帮忙监控。
上面两种做法是对 LT 模式下 write 事件频繁通知的修复,本质上 ET 模式就可以直接搞定,并不需要用户层程序的补丁操作

参考
epoll原理详解及epoll反应堆模型_青萍之末的博客-CSDN博客_epoll
徒手造了个轮子 — 实现epoll - 知乎
如果这篇文章说不清epoll的本质,那就过来掐死我吧! (1) - 知乎
如果这篇文章说不清epoll的本质,那就过来掐死我吧! (2) - 知乎
如果这篇文章说不清epoll的本质,那就过来掐死我吧! (3) - 知乎