彻底搞懂 select/poll/epoll,就这篇了

3552 篇文章 111 订阅

之前已经把网络 I/O 相关要点都盘了,还剩 select/poll/epoll 这几个区别没说,这篇就来搞搞它们,并且是从完全理解原理的角度来区分它们。

本来是要上源码的,但是感觉没啥必要,身为应用开发者我觉得理解原理就行了,源码反正看了就忘了,理解才是最重要!所以我就尽量避免代码且用大白话来盘一盘这三个玩意。

话不多说,发车。

小思考

首先,我们知道 select/poll/epoll 是用来实现多路复用的,即一个线程利用它们即可 hold 诸多个 socket。

按照这个思路,线程不可能被任何一个被管理的 Socket 阻塞,且任一个 Socket 来数据之后都得告知 select/poll/epoll 线程。

想想看,这应该如何实现呢?

我们拿 select 的逻辑来分析下

按照我们的理解,select 管理多个 Socket 的模型如下图所示:

这里要注意一下内核态和用户态的交互,用户程序访问不了内核空间。

所以,我们调用 select 会把所有要管理的 socket 的 fd (文件描述符,Linux下皆为文件,简单理解就是通过 fd 能找到这个 socket)传到内核中。

此时,要遍历所有 socket,看看是否有感兴趣的事件发生。如果没有一个 socket 有事件发生,那么 select 一上线程就需要让出 cpu 阻塞等待,这个等待可以是不设置超时时间的死等,也可以是设置 timeout 的有超时时间的等待。

假设此时客户端发送了数据,网卡接收到的数据塞到对应的 socket 的接收队列中,此时 socket 知道来数据了,那如何唤醒 select 呢?

其实每个 socket 有个属于自己的睡眠队列,select 会安排一个内应,即在被管理的 socket 的睡眠队列里面塞入一个 entry。

当 socket 接收到网卡的数据后,就会去它的睡眠队列里遍历 entry,调用 entry 设置的 callback 方法,这个 callback 方法里就能唤醒 select !

所以 select 在每个被它管理的人 socket 每个睡眠队列里都塞入一个与它相关的 entry,这样不论哪个 socket 来数据了,它立马就能被唤醒然后干活!

但是,select 的实现不太好,因为唤醒的 select 此时只知道来活了,并不知道具体是哪个 socket 来数据了,所以只能傻傻地遍历所有 socket ,看看到底是哪个 scoket 来活了,然后把所有来活的 socket 封装成事件返回。

这样用户程序就能获得发生的事件,然后进行 I/O 和业务处理了。

这就是 select 的实现逻辑,理解起来应该不难。

这里再提一嘴 select 的限制,因为被管理的 socket fd 需要从用户空间拷贝到内核空间,为了控制拷贝的大小而做了限制,即每个 select 能拷贝的 fds 集合大小只有1024。

然后要改的话只能修改宏..再重新编译内核。网上很多文章都是这样说的,但是(没错有个但是)。

我看了一篇文章,确实有这个宏愿,值也是 1024,但内核根本没有限制 fds 集合的大小。然后托人问了个内核大佬,大佬说内核确实没做限制,glibc那层做了。

所以..重新编译内核?那篇文章放在文末。

poll

poll 这玩意相比于 select 主要就是优化了 fds 的结构,不再是 bit 数组了,而是一个叫 pollfd 的玩意,反正就是不用管啥 1024 的限制了。

不过现在也没人用 poll,我就不多说了。

epoll

这个就是重点了。

相信看了 select 的确,我们稍微思考下,就能想出几个可以优化的点。

比如,为什么每次 select 需要把监控的 fds 传输到内核里?不能在内核里维护么?

为什么 socket 只唤醒 select,不能告诉它是哪个 socket 来数据了?

epoll 主要就是基于上面两点做了优化。

首先,搞了个叫 epoll_ctl 的方法,这方法就是用来管理维护 epoll 所监控的那些 socket。

如果你的 epoll 要新加一个 socket 来管理,那就调用 epoll_ctl,要删除一个 socket 也调用 epoll_ctl,通过不同的入参来控制增删改。

这样,在内核里面就维护了此 epoll 管理的 socket 集合,这样就不用每次调用的时候都得把所有管理的 fds 拷贝到内核了。

对了,这个 socket 集合是用红黑树实现的。

然后和 select 类似,每个 socket 的睡眠队列里都会加个 entry,当每个 socket 来数据之后,同样也会调用 entry 对应的 callback。

与 select 不同的是,引入了一个 ready_list 双向链表,callback 里面会把当前的 socket 加入到 ready_list 然后唤醒 epoll。

这样被唤醒的 epoll 只需要遍历 ready_list 即可,这个链表里一定是有数据可读的 socket,相比于 select 就不会做无用的遍历了。

同时收集到的可读的 fd 按理是要拷贝到用户空间的,这里又做了个优化,利用了 mmp,让用户空间和内核空间映射到同一块内存中,这样就避免了拷贝。

完美啊~

这就是 epoll 基于 select 所作的优化,还有一些差别没细说,比如 epoll 是阻塞睡眠在一个 single_epoll_wait_list 而不是 socket 的睡眠队列等等,我就不提了,理解上面的这些已经够了。

ET<

都谈到 epoll 了,避免不了要扯扯 ET 和 LT 两个模式。

ET,边沿触发。

按照上面的逻辑就是 epoll 遍历 ready_list 的时候,会把 socket 从 ready_list 里面移除,然后读取这个 scoket 的事件。

而 LT,水平触发,有点不一样。

在这个模式下 epoll 遍历 ready_list 的时候,会把 socket 从 ready_list 里面移除,然后读取这个 scoket 的事件,如果这个 socket 返回了感兴趣的事件,那么当前这个 socket 会再被加入到 ready_list 中,这样下次调用 epoll_wait 的时候,还能拿到这个 socket。

这就是这两者最本质的区别了。

看到这有人会问,这两种模式的使用会造成哪种不一样的结果?

如果此时一个客户端同时发来了 5 个数据包,按正常的逻辑,只需要唤醒一次 epoll ,把当前 socket 加一次到 ready_list 就行了,不需要加 5 次。然后用户程序可以把 socket 接收队列的所有数据包都读完。

但假设用户程序就读了一个包,然后处理报错了,后面不读了,那后面的 4 个包咋办?

如果是 ET 模式,就读不了了,因为没有把 socket 加入到 ready_list 的触发条件了。除非这个客户端发了新的数据包过来,这样才会再把当前 socket 加入到 ready_list,在新包过来之前,这 4 个数据包都不会被读到。

而 LT 模式不一样,因为每次读完有感兴趣的事件发生之后,会把当前 socket 再加入到 ready_list,所以下次肯定能读到这个 socket,所以后面的 4 个数据包会被访问到,不论客户端是否发送新包。

至此,我想你应该理解什么是 ET ,什么是 LT 了,而不用对着一些什么状态变更触发这些不易理解的名词而发晕。

最后

好了,今天的分析到此完毕,我个人觉得对 select/poll/epoll 的理解到这个程度就差不多了,当然还有很多细节,需要自行去看源码探究,问我我也不懂,这些都是阅读网上的源码分析文章得出的结论。

我也不建议读的那么深,毕竟人的精力有限对吧,有涉及到相关底层优化的时候,再去研究也不迟。

 

  • 5
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: selectpollepoll都是用来处理多路复用IO的方法。 - select: 是最早出现的多路复用IO方泏,它能同时处理多个文件描述符,但是它存在最大文件描述符数量和每次调用时间复杂度的限制。 - poll: 与select相比,它能同时处理更多的文件描述符,并且每次调用的时间复杂度也更低。 - epoll: 是Linux下特有的多路复用IO方法,它的性能比selectpoll更优,能同时处理更多的文件描述符。 总结来说,epoll相比selectpoll性能更优,能处理更多的文件描述符。 ### 回答2: selectpollepoll都是Linux中用来实现I/O多路复用的函数。I/O多路复用的主要作用是实现在一个进程中同时监听多个文件描述符的可读、可写和异常事件,以减少进程的系统调用数量,提高程序的并发性能。 select是最早出现的I/O多路复用函数,其调用方式比较简单,只需要用一个fd_set集合存储要监听的文件描述符,然后调用select函数。但是select有一些缺点,因为它使用位图来存储文件描述符,当监听的文件描述符数量增加时,位图的大小也会增加,导致性能下降。 poll是一种改进的I/O多路复用函数,它使用一个pollfd结构来存储文件描述符和事件,并在同一个结构体数组中存储所有需要监听的文件描述符,这样的话,无论监听的文件描述符数量增加,只需要重新分配一个更大的数组即可,可以提高性能。 epollLinux中最新、最高效的I/O多路复用函数,它使用事件驱动模型实现,可以处理大量的文件描述符。epoll用一个epoll_create函数创建一个epoll文件描述符,然后使用epoll_ctl函数向内核注册需要监听的文件描述符和事件类型,最后使用epoll_wait函数等待文件描述符上的事件。epoll最大的优点是可以支持水平触发和边缘触发两种模式,水平触发模式只要有数据可读或可写就会返回一个事件,而边缘触发模式只有当描述符上数据有变化时才会返回一个事件。 综上所述,selectpollepoll都是Linux中用来实现I/O多路复用的函数,它们的主要区别在于使用的数据结构不同,以及性能方面的优化不同。epoll是最高效的I/O多路复用函数,性能比selectpoll要高,并且支持水平触发和边缘触发两种模式。 ### 回答3: selectpollepoll均是Linux下常见的网络编程I/O多路复用技术。这些技术的核心是将多个I/O操作以非阻塞的方式同时处理,从而提高程序的性能。虽然三者都实现了I/O多路复用,但是实现方式却有所区别select是最古老的I/O多路复用技术,在大多数操作系统上都有实现。它的操作过程是:将要监视的文件描述符(包括输入和输出)分别存入一个数组中,并调用select函数开始监听,在有文件描述符就绪(有数据可读或者数据可写)的时候,返回一个事件的集合,可以通过遍历fd_set来得到哪些文件可以读/写。缺点是select函数的时间复杂度是O(n),随着监视的文件描述符数量增加,时间复杂度会越来越高,同时select也有FD_SETSIZE的限制(默认是1024)。 poll是在select的基础上进行了改进,可以监视的文件描述符数目不受限制。与select相同的是,都需要调用轮询函数来等待事件,当某个文件描述符就绪时,内核会将就绪的文件存在一个链表中并返回给应用程序。每次都需要遍历整个被监视的描述符集合,找到哪些描述符有数据可读,效率也不是非常理想。 而epoll是为了解决selectpoll的效率低下、不易扩展的问题而设计的,它利用了操作系统内核的支持,并支持边缘触发和水平触发两种工作模式。epoll会将每个文件描述符对应的文件表项都放在一个红黑树中,然后在树中搜索已经就绪的文件来获取就绪的文件列表。与selectpoll的轮询方式不同,epoll是基于事件驱动方式的,当注册的文件描述符已经准备好时,内核会通过事件通知方式来通知应用程序。相比之前的两种方法,epoll的效率更高,也更容易扩展。但是它的实现比较复杂,需要大量调用系统API函数。 总之,selectpollepoll这三种I/O多路复用方式各有优点缺点,不同场景选择不同的方式来处理I/O更加合适。在所监视的文件描述符数量较少时,selectpoll比较合适;而对于应用程序并发性能要求较高的,epoll方式可以获得更好的性能表现。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值