关于linux epoll的全部真相

好吧,或者几乎所有...



我相信现代互联网上的问题是过多的不同质量的信息。 在感兴趣的主题上查找材料不是问题;问题是,如果您在该领域经验不足,则可以将好材料与坏材料区分开。 当“概述”(几乎在一个简单列表的级别)上有很多概述信息,很少有深入的文章并且没有从简单到复杂的过渡性文章时,我会看到一张图片。 尽管如此,正是对特定机制的特征的了解使我们能够在开发过程中做出明智的选择。


在本文中,我将尝试揭示epoll与其他机制之间的根本区别是什么,什么使epoll与众不同,并列举了仅需阅读才能进一步了解epoll的可能性和问题的文章。


任何人都可以挥动斧头,但是要让它唱出近战旋律需要一个真正的战士。

我假设读者熟悉epoll ,至少阅读了手册页。 关于epollpollselect的文章已经足够写了,所以在Linux下开发的每个人都至少听说过一次。


很多fd


当人们基本上谈论epoll时,我听到一个论点,即“在有许多文件描述符的情况下其性能会更好”。


只想问一个问题-多少是多少? 需要多少个连接,最重要的是, epoll将在什么条件下开始产生明显的性能提升?


对于那些研究过epoll的人来说 (答案很多,包括科学论文在内),答案是显而易见的-仅当“等待事件”化合物的数量大大超过“准备处理”的数量时,答案才会更好。 数量的标记,当增益变得如此重要以至于没有尿液可以忽略这一事实时,可以考虑使用10k化合物[4]。


大多数连接即将挂起的假设来自合理的逻辑和对正在使用的服务器的负载监视。


如果活性化合物的数量争取总数, 没有收获 不会有明显的收益,这是由于并且仅因为epoll仅返回需要注意的描述符,而poll返回所有为观察而添加的描述符,才有了明显的收益。


显然,在后一种情况下,我们花时间遍历所有描述符并从内核复制事件数组的开销。


的确,在附加到补丁[9]的初始性能测量中,没有强调这一点,并且只能通过本文中提到的deadcon实用程序的存在来猜测(不幸的是,pipetest.c实用程序代码丢失了)。 另一方面,在其他来源[6,8]中,很难注意到这一点,因为实际上这一点很明显。


问题立刻出现了,但是如果不打算像过去那样并且不需要这样的epoll文件描述符,那么现在该怎么办呢?


尽管epoll最初是专门为这种情况而创建的[ 5,8,9 ],但这远不是epoll之间的唯一区别。


EPOLLET


首先,我们将探讨边缘触发和电平触发的区别。在文章《 边缘触发与电平触发的中断-Venkatesh Yadav》中有一个很好的陈述:


在水平上打断,就像一个孩子。 如果婴儿在哭,您必须放弃所做的一切,跑去给婴儿喂奶。 然后,您将婴儿放回婴儿床。 如果他再次哭泣,您将不会把他留在任何地方,但是您将尝试使他平静下来。 而且,在孩子哭泣的同时,您不会离开他一会儿,只有在他冷静下来之后才能恢复工作。 但是,可以说当孩子开始哭泣时,我们进入花园(中断中断),然后当您回家(中断中断)时,您要做的第一件事就是去检查孩子。 但是你永远不会知道你在花园里的时候他在哭。

前面的中断就像是聋人父母的电子保姆。 当孩子开始在设备上哭泣时,红灯会亮起并点亮,直到您按下按钮为止。 即使孩子开始哭泣,但很快停下来入睡,您仍然会知道孩子在哭泣。 但是,如果他开始哭泣,并且您按了按钮(确认中断),即使他继续哭泣,指示灯也不会点亮。 房间内的声音水平应先下降然后再上升,以使灯亮起。

如果描述符在指定状态下以级别触发的行为解锁了epoll (以及poll / select ),并且在清除该状态之前将其视为活动状态,则仅通过更改当前给定的有序状态来解锁沿触发的状态。


这使您可以稍后处理事件,而不必在收到消息后立即处理(几乎直接与中断处理程序的上半部分和下半部分类似)。


epoll的具体示例:


触发水平


  • 使用标志EPOLLIN添加到epoll的 句柄
  • epoll_wait()在等待事件时阻塞
  • 写入文件描述符19个字节
  • epoll_wait()通过EPOLLIN事件解锁
  • 我们不会处理收到的数据
  • epoll_wait()通过EPOLLIN事件再次解锁

这将继续,直到我们完全计算或重置描述符中的数据为止。


边缘触发


  • EPOLLIN标志将句柄添加到epoll中 | EPOLLET
  • epoll_wait()在等待事件时阻塞
  • 写入文件描述符19个字节
  • epoll_wait()通过EPOLLIN事件解锁
  • 我们不会处理收到的数据
  • epoll_wait()被阻止等待新事件
  • 向文件描述符再写入19个字节
  • epoll_wait()通过新的EPOLLIN事件解锁
  • epoll_wait()被阻止等待新事件

简单示例: epollet_socket.c


该机制旨在防止由于已经处理过的事件而返回epoll_wait()


如果在级别的情况下,当调用epoll_wait()时,内核检查以查看fd是否处于此状态,则edge跳过此检查,并立即将调用进程置于睡眠状态。


EPOLLET本身就是使epoll O(1)成为事件多路复用器的原因。


有必要对EAGAINEPOLLET进行解释-EAGAIN的建议不要处理字节流,只有在您没有读完描述符并且没有新数据的情况下,后一种情况下的危险才会出现。 然后尾巴将挂在描述符中,但是您将不会收到新的通知。 使用accept(),情况就不同了,您必须继续执行直到accept()返回EAGAIN ,只有在这种情况下,才能保证正确的操作。


// TCP socket (byte stream) //  fd    EPOLLIN      int len = read(fd, buffer, BUFFER_LEN); if(len < BUFFER_LEN) { //   } else { //         //  -       epoll_wait, //      } 

  // accept //  listenfd    EPOLLIN      event.events = EPOLLIN | EPOLLERR; epoll_ctl(epoll_fd, EPOLL_CTL_ADD, server_fd, &event); sleep(5); //       >1  //   while(epoll_wait()) { newfd = accept(listenfd, ...); //      //        //  epoll_wait    listenfd    } //   while(epoll_wait()) { while((newfd = accept(...)) > 0) { //  -  } if(newfd == -1 && errno = EAGAIN) { //       //       } } 

有了这个属性,仅仅饥饿就足够了:


  • 数据包到达描述符
  • 将数据包读取到缓冲区
  • 另一个小包来了
  • 将数据包读取到缓冲区
  • 一小部分
  • ...

因此我们不会很快收到EAGAIN ,但可能根本不会收到。


因此,其他文件描述符没有时间来处理,我们正忙于读取不断到达的一小部分数据。


雷声 书呆子 牛群


为了到达最后一个标记,您需要了解为什么它实际上是创建的,以及随着技术和软件的发展对开发人员造成的问题之一。


雷电群问题


雷群问题

想象大量等待事件的进程。 如果发生事件,则将它们唤醒,并且将开始争夺资源,尽管仅需要一个过程来处理事件的进一步处理。 其余过程将再次休眠。

IT术语-Vasily Alekseenko

在这种情况下,我们对与epoll一起分布在流上的accept()read()问题感兴趣。


接受


实际上,通过阻塞调用accept(),很长时间以来就没有问题。 内核将确保只有一个进程为此事件被解锁,并且所有传入的连接都将被序列化。


但是使用epoll,这种技巧将行不通。 如果我们在非阻塞套接字上创建了listen() ,则在建立连接时,所有epoll_wait()都会等待此描述符中的事件。


当然, accept()将只能执行一个线程,其余的将接收EAGAIN ,但这是浪费资源。


而且, EPOLLET也无济于事,因为我们不确切知道连接队列中有多少个连接( 积压 )。 我们记得,当使用EPOLLET时 ,套接字处理应该继续进行,直到返回带有EAGAIN错误代码的消息为止,因此所有的accept()都有可能被一个线程处理,而其余的则无法工作。


这又将我们引向了白白唤醒附近小溪的情况。


我们还可以得到另一种类型的饥饿-我们将仅加载一个线程,其余线程将不接收用于处理的连接。


EPOLLONESHOT


在版本4.5之前的版本中,使用下一个accept()调用将分布式epoll处理为非阻塞listen()描述符的唯一正确方法是设置EPOLLONESHOT标志,这再次导致我们一次只在一个线程中处理accept()


总之-使用epoll_ctl与特定的描述符相关EPOLLONESHOT事件的情况下,只能使用一次,那么你就需要重新公鸡标志()。


弹性


在这里, EPOLLEXCLUSIVE和级别触发可为我们提供帮助。


EPOLLEXCLUSIVE一次为一个事件解锁一个未决的epoll_wait()


该方案非常简单(实际上不是):


  • 我们有N个线程在等待连接事件
  • 第一位客户与我们联系
  • 线程0将被分散并开始处理,其他线​​程将保持阻塞
  • 第二个客户端连接到我们,如果线程0仍在忙于处理,则线程1被解锁
  • 我们继续进行下去,直到线程池耗尽为止(没人期待epoll_wait()上的事件)
  • 另一个客户正在与我们联系
  • 并且其处理将收到第一个线程,它将调用epoll_wait()
  • 第二个线程将收到第二个客户端,它将调用epoll_wait()

因此,所有维护均匀地分布在流中。


 $ ./epollexclusive --help -i, --ip=ADDR specify ip address -p, --port=PORT specify port -n, --threads=NUM specify number of threads to use #    -  n*8 -t, --thunder not adding EPOLLEXCLUSIVE #     thunder herd -h, --help prints this message $ sudo taskset -c 0-7 ./epollexclusive -i 10.56.75.201 -p 40000 -n 8 2>&1 

示例代码: epollexclusive.c (仅适用于4.5或更高版本的内核)


我们在epoll上获得了一个前叉模型。 此方案非常适用于短时 TCP连接。


已读


但是对于字节流而言,使用read()时,像EPOLLET一样, EPOLLEXCLUSIVE不会帮助我们。


出于明显的原因,没有EPOLLEXCLUSIVE,我们根本无法使用电平触发。 与所有EPOLLEXCLUSIVE不是更好,因为我们可以得到的包,分布在溪流中,除了一个未知的秩序便字节。


使用EPOLLET,情况是一样的。


在这里, EPOLLONESHOT会在工作完成后重新初始化。 因此,只要有一个线程将与此文件描述符和缓冲区一起使用:


  • EPOLLONESHOT标志将句柄添加到epoll中 | EPOLLET
  • 等待epoll_wait()
  • 从套接字读取到缓冲区,直到read()返回EAGAIN
  • EPOLLONESHOT标志重新初始化| EPOLLET

struct epoll_event


 typedef union epoll_data { void *ptr; int fd; uint32_t u32; uint64_t u64; } epoll_data_t; struct epoll_event { uint32_t events; /* Epoll events */ epoll_data_t data; /* User data variable */ }; 

此项可能是我个人恕我直言在我的文章中唯一的一项。 使用指针或数字的功能很有帮助。 例如,在使用epoll时使用指针可以使您完成以下操作:


 #define container_of(ptr, type, member) ({ \ const typeof( ((type *)0)->member ) *__mptr = (ptr); \ (type *)( (char *)__mptr - offsetof(type,member) );}) struct epoll_client { /** some usefull associated data...*/ struct epoll_event event; }; struct epoll_client* to_epoll_client(struct epoll_event* event) { return container_of(event, struct epoll_client, event); } struct epoll_client ec; ... epoll_ctl(efd, EPOLL_CTL_ADD, fd, &ec.e); ... epoll_wait (efd, events, 1, -1); struct epoll_client* ec_ = to_epoll_client(events[0].data.ptr); 

我想每个人都知道这项技术的来历。


结论


我希望我们能够打开epoll的话题。 那些有意识地使用此机制的人,只需阅读参考文献列表中的文章[1、2、3、5]。


基于此材料(或者甚至更好地从参考文献中仔细地阅读材料),您可以制作多线程的前叉(进程的高级生成)无锁(无阻塞)服务器,或者根据epoll()的特殊属性修改现有策略。


epoll是选择Linux编程路径的人们需要知道的独特机制之一,因为它们比其他操作系统具有更大的优势),并且也许会针对这种特殊情况放弃跨平台(让它起作用)仅在Linux上使用,但效果会很好)。


关于问题的“特殊性”的推理


在有人谈论这些标志和使用模式的特殊性之前,我想问一个问题:


“但是,我们没有试图讨论最初为特定任务创建的机制的特殊性[9,11]?还是我们甚至不愿意为程序员提供1k连接服务?”


我不理解“任务特殊性”的概念;它使我想起对所教授的各个学科的有用性和无用性的种种呼声。 为了让自己以这种方式进行推理,我们谨请自己决定为他人决定哪些信息对他们有用而哪些信息无用的权利,同时请注意,您没有参与整个教育过程。


对于怀疑者,有两个链接:


在NGINX 1.9.1中使用SO_REUSEPORT提高性能-VBart
向独角兽学习:雷电追捕非问题-克里斯·西本曼(Chris Siebenmann)
序列化accept(),又名雷电群,又称Zeeg问题-Roberto De Ioris
epoll的EPOLLEXCLUSIVE模式如何与电平触发相互作用?


参考文献


  1. 选择从根本上被打破-Marek
  2. Epoll从根本上打破了1/2-Marek
  3. Epoll从根本上打破了2/2-Marek
  4. C10K问题-Dan Kegel
  5. 投票与Epoll,再一次-雅克·马修
  6. epoll-I / O事件通知工具-The Mann
  7. epoll疯狂的方法-Cindy Sridharan

基准测试


  1. https://www.kernel.org/doc/ols/2004/ols2004v1-pages-215-226.pdf
  2. http://lse.sourceforge.net/epoll/index.html
  3. https://mvitolin.wordpress.com/2015/12/05/endurox-testing-epollexclusive-flag/

epoll的演变


  1. https://lwn.net/Articles/13918/
  2. https://lwn.net/Articles/520012/
  3. https://lwn.net/Articles/520198/
  4. https://lwn.net/Articles/542629/
  5. https://lwn.net/Articles/633422/
  6. https://lwn.net/Articles/637435/

后记


非常感谢谢尔盖( dlinyj )和Peter Ovchenkovu有价值铁饼,意见和支持!

Source: https://habr.com/ru/post/zh-CN416669/


All Articles