当前位置: 首页 > 科技观察

吃透Select-Poll-Epoll,就是这个!

时间:2023-03-15 22:08:14 科技观察

之前已经把网络I/O相关的重点都讲完了,select/poll/epoll还是有一些区别的。本文将对它们进行讨论,从充分理解原理的角度对它们进行区分。本来想上传源码的,但是觉得没必要。作为一个应用开发者,我觉得理解原理就够了。反正源码看了就忘了。理解最重要!所以我尽量避免代码,用通俗易懂的语言说话。一盘这三样东西。话不多说,让我们开始吧。小思考首先,我们知道select/poll/epoll是用来实现多路复用的,即一个线程通过使用可以持有多个socket。按照这个思路,线程不能被任何托管的Socket阻塞,任何Socket收到数据后必须通知select/poll/epoll线程。想一想,这应该如何实现?下面分析一下select的逻辑。按照我们的理解,select管理多个Socket的模型如下图所示:这里要注意内核态和用户态的交互,用户程序访问没有内核空间。因此,当我们调用select时,会将所有要管理的socket的fd(文件描述符,linux下都是文件,简单理解就是通过fd可以找到socket)传递给内核。此时,需要遍历所有socket,看看是否有感兴趣的事件发生。如果任何一个socket上都没有事件发生,那么select线程就需要让出cpu阻塞等待。这个等待可以是不设置超时的死等待,也可以是设置了超时的等待。假设此时客户端发送数据,网卡接收到的数据被塞入对应socket的接收队列。这时候socket就知道有数据来了,那怎么唤醒select呢?事实上,每个套接字都有自己的睡眠队列。select将安排一个内部响应,即在托管套接字的睡眠队列中插入一个条目。当socket从网卡接收到数据时,会遍历自己睡眠队列中的entry,调用entry设置的回调方法。这个回调方法可以唤醒select!所以select是在它管理的每个socket的睡眠队列中插入一个与之相关的entry,这样无论哪个socket收到数据,都可以立即被唤醒,然后工作!但是select的实现并不是很好,因为被唤醒的select这个时候只知道自己来干活了,并不知道是哪个socket在接收数据,所以只能傻傻的遍历所有的socket来查看哪个套接字处于活动状态,然后将所有活动的套接字封装到事件中并返回。这样用户程序就可以获得发生的事件,然后进行I/O和业务处理。这就是select的实现逻辑,应该不难理解。这里再提一下select的局限性,因为managedsocketfd需要从用户空间拷贝到内核空间,控制拷贝的大小是有限制的,也就是fds集合的大小可以每次select被copy的只有1024个,那么要改的话就只能修改宏了。.再次重新编译内核。网上很多文章都是这么说的,但是(是的,有一个但是)。看了一篇文章,确实有这个宏,值也是1024,但是内核根本没有限制fds集合的大小。然后托人问了一个kernel老大,老大说kernel没有限制,glibc层有。所以..重新编译内核?那篇文章在文章的最后。pollpoll相对于select,主要优化了fds的结构。它不再是一个位数组,而是一个叫做pollfd的东西。无论如何,您不必担心1024的限制。但是现在没有人用poll,就不多说了。epoll是关键点。相信看到select的实现后,稍微想一想就可以得出几点可以优化的地方。比如为什么每次select的时候都需要把监控的fds传给kernel?你不能在内核中维护一个吗?为什么socket只唤醒select,却不能告诉它是哪个socket在接收数据?epoll主要是基于以上两点进行优化。首先,有一个方法叫epoll_ctl,用来管理和维护epoll监听的socket。如果你的epoll需要增加一个新的socket来管理,那么就调用epoll_ctl,同时也调用epoll_ctl删除一个socket,通过不同的入参来控制增删改查。这样epoll管理的socket集合就维护在内核中了,这样就不用每次调用都把所有管理的fds拷贝到内核中。对了,这个socket集合是用红黑树实现的。然后和select类似,会在每个socket的sleep队列中加入一个entry,当每个socket接收到数据时,也会调用这个entry对应的callback。与select不同的是引入了一个ready_list双向链表,callback会将当前socket添加到ready_list中并唤醒epoll。这样被唤醒的epoll只需要遍历ready_list即可。该链表中必须存在数据可读的套接字。和select相比,不会做无用的遍历。同时将同时收集的可读fd复制到用户空间。这里又做了一个优化,使用mmp,让用户空间和内核空间映射到同一块内存,从而避免了拷贝。完美~这是epoll在select的基础上做的优化。还有一些差异我没有详细说明。比如epoll阻塞sleep在single_epoll_wait_list而不是socket的sleep队列等,我就不提了。了解以上内容就够了。.ET<都谈到了epoll,就免不了要说说ET和LT这两种模式。ET,边沿触发。按照上面的逻辑,epoll在遍历ready_list时,会将socket从ready_list中移除,然后读取这个socket的事件。而LT,leveltriggered,有点不同。在这种模式下,epoll在遍历ready_list时,会将socket从ready_list中移除,然后读取这个scoket的事件。如果这个socket返回了一个感兴趣的事件,那么当前socket会被重新加入到ready_list中,所以下次调用epoll_wait时,仍然可以得到这个socket。这是两者最本质的区别。看到这里,可能有人会问,这两种模式的使用会造成什么样的不同结果呢?如果一个client同时发送5个数据包,按照正常的逻辑,只需要唤醒epoll一次,当前把socket加入ready_list一次就够了,没必要再加入5次。然后用户程序就可以读取socket接收队列中的所有数据包了。但是假设用户程序读取了一个包,然后在处理中报错,之后又没有读取,那么接下来的四个包呢?如果是ET模式,则无法读取,因为没有将socket加入ready_list的触发条件。除非客户端发送新的数据包,否则会将当前socket加入ready_list,直到新数据包到来时才会读取这4个数据包。LT模式则不同,因为每次读到一个感兴趣的事件后,会将当前socket加入到ready_list中,所以下一次肯定会读到socket,所以会访问接下来的4个数据包。不管客户端是否发送新包。到这里,我想你应该明白什么是ET,什么是LT了,不需要被状态变化引发的一些看不懂的名词搞得头晕目眩。终于,今天的分析结束了。个人觉得select/poll/epoll的理解差不多。当然,还有很多细节。都是看网上源码分析文章得出的结论。不建议读这么深。毕竟人的精力是有限的对吧。涉及到相关的底层优化,再研究也不迟。我是的,从一点点到十亿点,下篇文章见。参考:https://blog.csdn.net/dog250/article/details/105896693(select真的被1024限制了吗?)https://blog.csdn.net/dog250/article/details/50528373