【Linux】多路转接
📝前言:
这篇文章我们来讲讲Linux——多路转接:
- Select
- Poll
- Epoll
- Reactor反应堆模式
🎬个人简介:努力学习ing
📋个人专栏:Linux
🎀CSDN主页 愚润求学
🌄其他专栏:C++学习笔记,C语言入门基础,python入门基础,C++刷题专栏
目录
- 一,Select
- 1. 接口概述
- 2. fd_set位图操作
- 3. select的缺点
- 二,poll
- 1. 接口介绍
- 2. poll优缺点
- 三,epoll
- 1. epoll模型
- 2. epoll接口介绍
- epoll_create
- epoll_ctl
- epoll_wait
- 工作流程
- 3. LT 和 ET模式
- 4. 简单示例
- 5. epoll的写事件
- 四,Reactor反应堆模式
一,Select
1. 接口概述
select用于监视多个文件描述符的状态变化,程序会停在 select 这里等待,直到被监视的文件描述符有一个或多个发生了状态改变。
参数:
nfds
:要监听的最大文件符的值 + 1fd_set
:分别对应于需要检测的可读文件描述符的集合(本质是位图,一个fd对应一个bit位),可写文件描述符的集 合及异常文件描述符的集合。- 这个参数是一个输入输出型参数,函数返回后,
fd_set
会被修改:返回本次调用监听到的文件描述符 - 因为
fd_set
会被修改,所以我们一般需要一个辅助数组来保存我们需要监听的fd
,然后每次select
前把fd
集合重置【这也是select
的缺点之一】
- 这个参数是一个输入输出型参数,函数返回后,
timeout
:监听时间(是一个结构体timeval
)。在指定时间内如果没有监听到,则返回。如果监听到了,会返回剩余时间- NULL:则表示 select()没有 timeout,select 将一直被阻塞
0
:代表非阻塞监听- 具体的数值:监听时间具体时间
返回值:
- 成功:返回文件描述词状态已改变的个数
- 超时:没有一个fd变化,返回
0
- 错误:
-1
,错误原因存于 errno,此时参数 readfds,writefds,exceptfds 和 timeout 的值变成不可预测。
2. fd_set位图操作
我们需要使用指定的接口来操作fd_set的位图
void FD_CLR(int fd, fd_set *set);// 用来清除描述词组 set 中相关fd 的位
int FD_ISSET(int fd, fd_set *set); // 用来测试描述词组 set 中相关fd 的位是否为真
void FD_SET(int fd, fd_set *set);// 用来设置描述词组 set 中相关fd 的位
void FD_ZERO(fd_set *set);// 用来清除描述词组 set 的全部位
3. select的缺点
select
的fd
集合是位图,支持的文件描述符数量有限- 每次调用
select
,都需要重新手动设置fd集合,使用不方便 - 每次调用
select
,都需要把fd_set
从用户态拷贝到内核态,有开销 - 每次调用
select
,找就绪fd
的时候,需要遍历所有fd
,时间复杂度:O(n)
二,poll
1. 接口介绍
poll
的使用场景较少,因为老系统用select
,而poll
又不如epoll
好用
pollfd
结构:
struct pollfd {int fd; // 要监听的文件描述符/* file descriptor */short events; /* requested events */short revents; /* returned events */
};
poll接口把用户传入的需要监听的事件events
,和内核返回给用户的监听到的事件revents
做了区分
参数:
fds
:一个pollfd
数组nfds
:fds 数组的长度timeout
:单位毫秒,超时时间(和select
一样,不够更简单了)
2. poll优缺点
相比于select
的优点:
- 突破了文件描述符数量的限制
- 文件描述符集合不会被重置
任然存在的缺点:
- 每次调用 poll 时,仍需将整个pollfd数组从用户态拷贝到内核态
- 查找就绪文件描述符的时间复杂度仍为 O (n),仍需遍历整个pollfd数组检查revents字段才能找到就绪的文件描述符
三,epoll
1. epoll模型
epoll模型包含三个重要组成部分:红黑树(用来存储要监听的fd
,类似select
的辅助数组),就绪队列(用来存储事件就绪的fd
),回调方法(每个fd都注册了回调方法)
2. epoll接口介绍
epoll_create
size
:在老版本中,代表文件描述符的数量,新版本已经不用管了。【数量不限】- 返回值:返回一个
epfd
(文件描述符) - 注意:用完以后,需要
close()
关闭这个epfd
epoll模型存储在epfd
中,被一个private_data
指针指向。
epoll_ctl
- 用于管理要监听的
fd
,常见的op
(管理操作)有:EPOLL_CTL_ADD
:注册新的 fd 到 epfd 中EPOLL_CTL_MOD
:修改已经注册的 fd 的监听事件EPOLL_CTL_DEL
:从 epfd 中删除一个 fd
- 不同于
select()
在监听事件时告诉内核要监听什么类型的事件,epoll
要先注册要监听的事件类型。
struct epoll_event
结构如下:
events 可以是以下几个宏的集合:
EPOLLIN
: 表示对应的文件描述符可以读 (包括对端 SOCKET 正常关闭);EPOLLOUT
: 表示对应的文件描述符可以写;EPOLLPRI
: 表示对应的文件描述符有紧急的数据可读 (这里应该表示有带外
数据到来);EPOLLERR
: 表示对应的文件描述符发生错误;EPOLLHUP
: 表示对应的文件描述符被挂断;EPOLLET
: 将 EPOLL 设为边缘触发(Edge Triggered)模式, 这是相对于水平触发(Level Triggered)来说的.EPOLLONESHOT
:只监听一次事件, 当监听完这次事件之后, 如果还需要继续监听这个 socket 的话, 需要再次把这个 socket 加入到 EPOLL 队列里
epoll_data
OS不做修改,用户可以用来自行保存一些自定义数据。
epoll_wait
- 作用:epoll_wait 会等待内核就绪队列中有事件发生(即
fd
就绪),然后将就绪的事件(包含 fd 和事件类型)从内核态传递到用户态的 events 数组中。这个过程通过内存映射(mmap)优化,减少了传统拷贝的开销。 maxevents
:events
的大小,不能超过用户态 events 数组的实际大小- 参数 timeout 是超时时间 (毫秒,0 会立即返回,-1 是永久阻塞).
- 返回值:如果函数调用成功,返回对应 I/O 上已准备好的文件描述符数目,如返回 0 表示已超时, 返回小于 0 表示函数失败
工作流程
- 通过epoll_create创建epoll模型实例
- 通过epoll_ctl来管理要监听的fd,已经要等待的事件(所有事件被维护在一个红黑树中),每个fd会注册相应的回调方法。
- 当事件发生的时候,会触发回调方法,把就绪的事件加入到就绪队列中
- 当调用 epoll_wait 时,检查就绪队列中是否有已经就绪的事件
3. LT 和 ET模式
- LT模式(epoll的默认行为):当 fd 处于就绪状态(如缓冲区有未读数据)时,每次调用epoll_wait都会返回该事件,直到数据被完全读取(缓冲区为空)
read
的阻塞行为:没数据读 / 读不够。如:内核缓冲区只有512,但是read
要读1024个,所以会:先拷贝 512 字节到用户态,然后阻塞,等待更多数据凑够 1024 字节- 支持阻塞和非阻塞读,因为就算一次读到的数据不满足
read
的要求也不会阻塞(因为下一次epoll_wait
任然会通知)
- ET模式:仅在 fd 状态从 “未就绪” 变为 “就绪” 时触发一次事件(如数据首次到达),后续即使缓冲区仍有未读数据,也不会再次通知,直到有新数据写入才会再次触发。
- 只支持非阻塞读,因为ET模式只通知一次,如果当前数据不够读,则会阻塞,影响进程后续。
- 所以要求用
while
在一次通知时循环读完,直到返回EAGAIN或EWOULDBLOCK(表示当前无更多数据),确保不会遗漏数据。
ET对比LT的优势:
- ET通知更高效,
epoll_wait
的次数更少,有效通知次数更多 - ET要求非阻塞 + while循环读,规范程序员写的代码。
- 并且这种一次性读完的要求,可以尽快读取所有数据,腾出接受缓冲区空间,给对方提供一个更大的win窗口,加大对方滑动窗口的大小,提高网络发送的报文的并发度
4. 简单示例
epoll服务器(LT模式)→ 我的 Github
5. epoll的写事件
写就绪介绍:
- epoll读默认是不就绪的,写默认是就绪的(因为初始写入缓冲区为空)。
- 读事件检测可以常设,但是写事件要按需设置
- 正常情况下,若没有数据要写,无需将 “写事件” 添加到 epoll 监听中(因为初始就绪状态会导致 epoll 频繁唤醒,浪费资源)。
- 只有当主动写数据时发现写缓冲区已满(write() 返回 -1 且 errno=EAGAIN/EWOULDBLOCK),才需要将 “写事件” 添加到 epoll 中,等待内核缓冲区有空闲空间后再重试写入。
- 一旦数据写完(或缓冲区有足够空间写完剩余数据),必须立即删除 epoll 中的写事件监听,避免后续缓冲区空闲时 epoll 持续触发就绪通知(“写风暴”)。
处理写事件的标准流程(以非阻塞套接字为例):
- 初始不监听写事件:仅监听读事件(EPOLLIN),有数据要写时直接调用
write()
。 - 写操作判断:
- 若 write() 返回值 > 0:表示成功写入部分 / 全部数据,若数据已写完则结束,否则继续写。
- 若 write() 返回 -1,且 errno=EAGAIN/EWOULDBLOCK:表示缓冲区已满,立即将 EPOLLOUT 事件添加到 epoll 监听中,等待后续就绪通知。
- 写事件触发后的处理:
- 当 epoll 通知 EPOLLOUT 就绪时,再次调用 write() 写入剩余数据。
- 若数据全部写完:立即从 epoll 中删除 EPOLLOUT 事件(避免后续频繁触发)。
- 若仍返回 EAGAIN:说明缓冲区仍未足够,保留 EPOLLOUT 监听,等待下一次就绪。
- 错误处理:若 write() 返回其他错误(如 EPIPE、ECONNRESET),则关闭套接字并清理 epoll 监听。
当非阻塞IO的时候,遇到 error == EINTR 的错误,这是遇到了中断导致调用无法完成,这时候无序管它,continue
即可
四,Reactor反应堆模式
代码 → 我的Github
🌈我的分享也就到此结束啦🌈
要是我的分享也能对你的学习起到帮助,那简直是太酷啦!
若有不足,还请大家多多指正,我们一起学习交流!
📢公主,王子:点赞👍→收藏⭐→关注🔍
感谢大家的观看和支持!祝大家都能得偿所愿,天天开心!!!