文章目录
- 概要&序論
- 一、从 select 的痛点走向 poll:接口设计、结构体与事件机制
- 1.1 poll 的函数原型
- 1.2 深入理解 struct pollfd 结构体
- 1.3 常用的事件类型
- 1.4 poll 是如何解决 select 的痛点的?
- 1.4.1 问题一:输入输出参数分离,无需重复重置
- 1.4.2 问题二:彻底打破数量上限
- 1.4.3 实用小技巧:忽略某个 fd
- 1.5 实战插曲:如何查看一个进程的文件描述符?
- 二、poll 的优缺点分析与性能瓶颈
- 2.1 poll 相比 select 的核心优势
- 2.2 无法逃避的性能瓶颈:poll 的缺点
- 2.3 总结与过渡:为什么需要 epoll?
概要&序論
Hello大家好,我是此方。上一篇我们讲解了Select,本文我们来讲解Poll,看看Poll在Select的基础上又有哪些优化。
一、从 select 的痛点走向 poll:接口设计、结构体与事件机制
在上一节中,我们深度剖析了 select 的机制。虽然它是 IO 多路复用的老大哥,但它的痛点也极其明显:每次调用都要手动重置 fd_set 集合,而且还受限于 fd_set 的位图大小,无法支撑海量并发。为了解决这些问题,poll横空出世了。
poll的作用和定位与select一致,它依然只负责等,一次可以等待多个 fd,事件就绪后,就可以对上层进行事件通知。但它在接口和底层数据结构的设计上,做了一次极其优雅的升级。
1.1 poll 的函数原型
我们来看看poll的函数原型:
#include<poll.h>intpoll(structpollfd*fds,nfds_tnfds,inttimeout);这个接口比select清爽太多了,只有三个参数:
fds是数组的起始地址。nfds是数组元素的个数。timeout直接以毫秒为单位。如果传入 0 表示非阻塞,传入 -1 表示阻塞等待。
1.2 深入理解 struct pollfd 结构体
poll抛弃了select那种反人类的位图(bitmap)操作,转而使用了一个结构体数组。我们来看看这个结构体:
structpollfd{intfd;/* file descriptor */shortevents;/* requested events */shortrevents;/* returned events */};这个设计极其绝妙,把输入和输出完美分离了:
events是事件位图,代表用户告诉内核你要帮我关心fd上面的events事件。revents也是事件位图,代表poll成功返回时,内核告诉用户,你让我关心的fd上面的事件,哪些已经就绪了!
调用的时候,fd和events有效;成功返回时,fd和revents有效。
1.3 常用的事件类型
在poll的世界里,事件类型非常丰富,但作为初学者,别的我们先不要管,我们就记住这两个核心事件就够了:
POLLIN:数据(包括普通数据和优先数据)可读。POLLOUT:数据(包括普通数据和优先数据)可写。
那要怎么设置和检查呢?
- 设置监听读事件:
events |= POLLIN - 检查是否就绪:
revents & POLLIN
1.4 poll 是如何解决 select 的痛点的?
对比select,poll到底帮我们解决了哪些恶心的问题?
1.4.1 问题一:输入输出参数分离,无需重复重置
poll的输入输出参数彻底分离了,所以你不用再在 poll 之前进行参数重置了!你只需要初始化一次events,如果下次还要复用这个数组,直接再次调用poll即可。revents每次会被内核重新覆盖,不会影响下一次的监听逻辑。
1.4.2 问题二:彻底打破数量上限
poll 等待的 fd 个数,没有上限!
为什么呢?因为上限问题与poll这个系统调用本身无关了。原本select是直接在内核里写死了fd_set大小,限制了上限。但是现在,poll的上限直接由用户自己决定,用户甚至可以直接 malloc 开辟出来的动态数组传递进去。你想监听一万个还是十万个 fd,只需要把数组开得足够大即可。
1.4.3 实用小技巧:忽略某个 fd
如果某天你不想关心数组中的某个 fd 了,但又不想去移动数组元素,有个非常简单的办法:把该结构体里的fd设置为-1。在内核中,如果遇到fd == -1,内核就不关心这类 fd 的 events。
1.5 实战插曲:如何查看一个进程的文件描述符?
在编写poll服务器的代码时,我们经常需要查看底层到底打开了哪些 fd。在 Linux 终端下,你可以通过进程号来直接查看:
# 假设你的进程号是 235849ls/proc/235849/fd-l执行后,你能清晰地看到该进程打开的socket文件描述符、/dev/pts终端描述符等。这对于排查网络 IO 程序的资源泄漏有着极大的帮助。
既然poll已经解决了select的两大痛点,那它是不是就是完美的了?下一节,我们将把select改成poll服务器,并看看它依然存在的隐患。
我写了一个小Demo可供参考:Select,epoll,poll三大多路转接工具进行测试的demo代码
二、poll 的优缺点分析与性能瓶颈
我们在上一节中了解了poll的基本用法,也知道了它是为了解决select的痛点而诞生的。但在实际工程中,没有任何一种模型是完美的。我们来系统性地梳理一下poll的优势与它无法逃避的物理限制。
2.1 poll 相比 select 的核心优势
不同于 select 使用三个位图来表示三个 fdset 的方式,poll 使用一个pollfd指针实现。这带来了几个非常直观的好处:
- pollfd 结构包含了要监视的 event 和发生的 event,不再使用 select “参数-值”传递的方式。接口使用比 select 更方便。
- poll 并没有最大数量限制(但是数量过大后性能也是会下降)。
2.2 无法逃避的性能瓶颈:poll 的缺点
虽然poll摆脱了位图,也不用每次重置参数了,但当并发量真正上来的时候,它的致命缺陷就暴露了。
每次调用 poll 都需要把大量的pollfd结构从用户态拷贝到内核中。这算是一个特点,因为当你传的数据越多,这次搬运的成本就越高,这个开销是无法省去的。
更严重的是,poll 中监听的文件描述符数目增多时,它和 select 函数一样,poll 返回后,需要轮询pollfd来获取就绪的描述符。同时连接的大量客户端在一时刻可能只有很少的处于就绪状态,因此随着监视的描述符数量的增长,其效率也会线性下降。
这就是经典的O(n) 复杂度问题。假设你监听了 10 万个连接,但这一刻只有 1 个连接有数据到达,内核依然会遍历这 10 万个 fd,白白浪费 CPU 算力。
2.3 总结与过渡:为什么需要 epoll?
select和poll共同的硬伤在于:内核不保存你要监听的文件描述符集合,每次调用都要重新传入;而且内核是“盲找”的,每次都要遍历所有 fd 才能知道谁就绪了。
为了提升效率 —— 于是我们引入了epoll。它不仅能突破数据拷贝的瓶颈,还能做到只返回就绪的 fd,实现真正的 O(1) 事件通知!