Linux I/O 多路复用实战:Select/Poll 编程指南

Linux I/O 多路复用实战:Select/Poll 编程指南
前言:本文将详细解析 select 和 poll 系统调用的工作原理与性能瓶颈。由于 epoll 内核机制比较复杂(包含红黑树、就绪队列、回调机制及 LT/ET 模式等),内容量大,将为其单独撰写一篇文章,敬请关注后续更新!

文章目录

一、什么是IO多路复用?

IO多路复用的本质是使用一个执行流同时等待多个文件描述符就绪。它解决了阻塞IO中“一个连接需要一个线程”导致的资源消耗过大问题,也解决了非阻塞IO需要不断轮询导致的CPU利用率低的问题。

实现IO多路复用的常用三种方法:select/poll/epoll,接下来我们一一进行学习:

二、select

我们知道IO = 等+拷贝,而select只负责‘’这个步骤,一次可以等待多个fd,有任意一个或多个fd就绪了告诉用户可以IO了。

select的本质:通过等待多个fd的一种就绪事件通知机制。

  • 什么是可读?底层(比如接收缓冲区)有数据,读事件就绪。
  • 什么是可写?底层(比如发送缓冲区)有空间,写事件就绪。

在默认情况下,接收缓冲区和发送缓冲区都是空的,因此默认情况下,读事件通常不就绪,而写事件通常就绪(因为发送缓冲区有空间)。接下来我们以等待读事件就绪为例子讲解select:

1. select参数介绍

输入以下指令可查看select使用手册:

manselect

头文件:#include <sys/select.h>(该头文件声明了select系统调用,表明其为内核提供的系统级接口)
select接口:

intselect(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds,structtimeval*timeout);

select参数:

  • nfds:传入所有需要等待的文件描述符中的最大文件描述符加1(内核通过该值确定需遍历的fd范围,避免无效遍历)
    • tv_sec:表示阻塞等待的秒数
    • tv_usec:表示阻塞等待的微妙数。
    • 最终等待阻塞时间为tv_sec+tv_usec1
    • timeout作为输出型参数时表示的是剩余的时间。比如timeout传入的是5秒,而只等了2秒就有文件就绪并进行返回,那么返回的剩余的时间就是3秒。
  • readfds/writefds/exceptfds
    这三个参数都是fd_set类型的输入输出型参数,用法是一样的,这里就以readfds为例进行讲解
    • readfds:只关心读事件。
    • writefds:只关心写事件。
    • exceptfds:只关心异常事件。

timeout:这是一个输入输出型参数,struct timeval类型成员如下:

structtimeval{int tv_sec;/* seconds */int tv_usec;/* microseconds */};

select是管理多个描述符的,怎么传入多个描述符?
首先我们需要清楚fd_set类型,这是一个文件描述符集合,是内核提供给用户的数据结构,我们需要向fd_set里添加需要监控的fd,而fd本质是数组下标(即0,1,2,3…),什么结数据结构可以表示这些信息呢?所以fd_set是位图结构,内存紧凑、操作高效。

fd_set位图是怎么表示某个描述符是否被关心呢?

  • 比特位的编号:从右到左分别表示文件描述符0,1,2,3…
  • 比特位的内容
    • 作为输入型参数:表示该文件描述符是否被关心。(0不关心,1关心)
    • 作为输出型参数:表示该文件描述符是否已就绪。(0不就绪,1就绪)

比如这样一段比特位:0000 1000,作为输入型参数表示3号文件描述符被关心;作为输出型参数表示3号文件描述符已就绪。

细节:

  1. 位图是输入输出型参数,所以位图一定会频繁变更。如果下次还需要关心该描述符,需要我们频繁去修改位图。
  2. fd_set是数据类型,那么它就有固定的大小,也就是可关心的文件描述符是有上限的,上限是多少呢?每个系统内核的值不同,我们使用sizeof(fd_set)*8可以查看,通常是1024。虽然select可关心的描述符有上限,但有的老内核只支持select,select有很好的跨平台性。

select返回值:

  • 大于0:这个值是多少就表示有多少个描述符就绪。
  • 等于0:表示超时,只有当timeout设为非nullptr才会有该情况。
  • 小于0:表示select执行出错,比如有非法描述符等。

2. select程序编写

这里我们仅仅讲解核心代码部分,突出重点,如下:

class SelectServer { public://完成初始化,打开套接字,端口绑定,打开监听...voidStart(){while(true){//是否进行accept?}}//...... private:int _listenfd;//......};

注意这里监听描述符_listenfd也是文件描述符,需要我们用select进行管理,而不是直接accept
服务器在刚启动时,默认只有一个fd,accept本质是阻塞IO。accept是一个IO,只不过不是用来传输数据的,而它关心的是_listenfd的读事件。我们需要将_listenfd添加到select函数中,让select帮我关心读事件就绪。

  • 结论:新连接到来,读事件就绪。

首先定义一个fd_set位图,把_listenfd添加到位图里,然后把该位图作为readfds参数传入select中。注意:我们不能自己使用位操作把_listenfd添加到fd_set位图,而是使用OS提供的相应的接口,如下:

  • void FD_CLR(int fd, fd_set* set):清除指定描述符。
  • int FD_ISSET(int fd, fd_set* set):判断fd是否在fd_set集合里。
  • void FD_SET(int fd, fd_set* set):设置fd到fd_set集合里
  • void FD_ZERO(fd_set* set):清空fd_set集合。

即:

fd_set rfds;FD_ZERO(&rfds);FD_SET(_listenfd,&rfds);

注意:这里没有设置到内核里,只是在用户栈上。
因为在此时只关心_listenfd的读事件,所以select的第一个参数只用填_listenfd+1,writefdsexceptfds部分填nullptr即可。这里我们使用非阻塞模式,即timeoutnullptr
示例:

class SelectServer { public://完成初始化(创建套接字、绑定端口、开启监听)...//......voidStart(){while(true){//如果直接用accept会直接阻塞,我们使用select检测_listenfd读事件是否就绪//1.定义rfds文件描述符集。 fd_set rfds;FD_ZERO(&rfds);FD_SET(_listenfd,&rfds);//2.执行select,把rfds设置到内核。int n =select(_listenfd+1,&rfds, nullptr, nullptr, nullptr);//3.处理select返回值switch(n){case-1: std::cout<<"select fail"<<std::endl;break;case0: std::cout<<"time out..."<<std::endl;break;default: std::cout<<"事件就绪..."<<std::endl;//处理事件//......break;}}}//...... private:int _listenfd;//......};

如上代码如果事件就绪后不进行处理会出现死循环打印 “事件就绪…”
当有事件就绪,需要处理就绪事件,通常调用事件处理函数。比如以上场景我们需要做的就是进行accept,示例:

//调用事件处理函数:HandlerEvent(){//调用accpet获取用户fd}
  • 问题1:这里accept会不会阻塞?不会,因为上层已经告诉我有连接就绪了。
  • 问题2:获取到用户fd能直接读吗?不能,因为如果用户没有发数据,那么程序就会被阻塞在这里,其他用户来访问了也不会去处理。
  • 问题3:用户fd不能直接读,那什么时候读?有数据就绪时再读就不会阻塞。怎么知道它有没有数据就绪?可以通过select。总结:accept获取到的fd需要进行select管理。select管理的fd多起来的原因就是通过拿到新的用户fd。

当select管理的fd越来越多,有会带来新的问题。因为select返回时rfds已经被内核修改,那么下次再设置rfds时怎么历史管理过那些fd呢?所以需要我们把受到管理的fd记录下来,这里就要用到一个辅助数组(其他数据结构也可以),辅助下一次设置rfds。

  • 添加成员int _fd_array[FDSIZE],这里把FDSIZE设为1024
  • 初始化_fd_array:将数组初始化为全-1,然后把_fd_array[0]设置为_listenfd

注意:select第一个参数是被管理的文件描述符中最大值加1,所以需要从_fd_array中取到最大fd。
文件描述符集rfds的填写示例:

fd_set rfds;FD_ZERO(&rfds);int maxfd =-1;//存取最大fdfor(int i =0; i < FDSIZE; i++)//遍历辅助数组{if(_fd_array[i]!=-1)FD_SET(_fd_array[i],&rfds); maxfd =max(maxfd, _fd_array[i]);//找到最大fd}

那么我们怎么把新获取到的userfd(accept获取到的用户fd)托管给select呢?
只需要把userfd给辅助数组即可。如下:

  1. 找到_fd_array中的空位置。
  2. 如果没有空位置了(服务器被打满),则关闭userfd;如果有则将空位置设置为userfd

示例:

for(int i =0; i < FDSIZE; i++)//遍历辅助数组{if(_fd_array[i]==-1)//当有空位置时,把userfd添加上{ _fd_array[i]= userfd; std::cout<<"accept success fd = "<<userfd<<std::endl;break;}elseif(i == FDSIZE -1)//如果不是空位置,而且遍历到底了,则关闭userfd,退出循环{ std::cout<<"服务器繁忙..."<<std::endl;close(userfd);break;}}

当select管理的fd变多,我们可以通过返回值知道有多少个fd就绪,但并不知道是那个fd就绪,是读就绪还是写就绪。所以在事件处理函数HandlerEvent中我们还要判断,那些fd就绪?读就绪还是写就绪或者是异常?(这里只考虑读就绪)。
其次不同文件描述符就绪的处理方式不同,比如listenfd读就绪就要进行accept获取userfd,如果是userfd读就绪则需要读取接收缓冲区数据。需要针对不同描述符就绪做不同处理,所以需要我们重新设计HandlerEvent,示例:

voidHandlerEvent(fd_set& rfds/*, fd_set& wfds*/){for(int i=0; i<FDSIZE; i++){//如果_fd_array[i]不合法则continueif(_fd_array[i]==-1)continue;//接下来判断是否读就绪if(FD_ISSET(_fd_array[i],&rfds)){//能确定读就绪,接下来根据不同的描述符做不同处理。if(_fd_array[i]== _listenfd){//调用自定义Accept()......}else{//调用Read()......}}}}

注意:

  • Accept中需要完成把新的userfd托管给select的操作。
  • Read中当判断用户把连接断开后要把对应的userfd从_fd_array中移除(即将_fd_array中值为userfd的位置改为值-1),然后再关闭userfd

注意:调用Read时就证明读就绪了,不会阻塞。但不能在Read循环读,而是只读一次。数据没读完还会触发就绪,会再次调用Read。

到这里程序的核心逻辑就完成了,没有多进程,没有多线程,却能同时处理多个IO请求,做出了多执行流的效果。没有进程/线程切换成本,也没有内核调度成本。

3. select性能总结

特点:

  • 可监控描述符有上限。
  • 需要辅助数组保存文件描述符,两个作用:
    • 在select返回后readfds/writefds/exceptfds作为源借助辅助数组判定fd是否就绪
    • select调用后内核会把原文件描述符集更改为以就绪的文件描述符集,需要借助辅助数组重置文件描述符集。

缺点:

  • 需要各种遍历,select本身也遍历文件描述符表(select第一个参数就是用来确定遍历到那个文件描述符的),所以比较慢。
  • 每次都要对文件描述符集重置,很繁琐。
  • select支持的文件描述符数量有限。

三、poll

poll的作用和效果与select类似,但其接口设计更简单,在某些场景下也更高效。

1. poll参数介绍

输入以下指令可查看poll使用手册:

man poll 

头文件:#include <poll.h>
poll接口:

intpoll(structpollfd*fds,nfds_t nfds,int timeout);

poll参数:

  • timeout:和select中的timeout参数的作用相同,这里的timeout做了简化,是int类型,单位是毫秒。
    • -1(小于0):阻塞
    • 等于0:非阻塞
    • 大于0:阻塞timeout毫秒后返回
  • fds一个struct pollfd类型数组的起始地址
  • nfds数组元素个数

poll返回值(同select):

  • 大于0:这个值是多少就表示有多少个描述符就绪。
  • 等于0:表示“超时”或“非阻塞时无就绪事件。
  • 小于0:表示poll执行出错,比如有非法描述符等。

关于struct pollfd类型,成员如下:

structpollfd{int fd;short events;short revents;}
  • fd:文件描述符
  • events:输入型参数。用位图的思想标记需要关心的该fd的什么事件。
  • revents:输出型参数。内核给用户返回已经就绪的事件。

poll与select最大的区别就是把输入型参数和输出型参数分开了,不用繁琐的重置文件描述符集。
可关心的事件:

事件描述作为输入作为输出
POLLIN数据(包括普通数据和优先数据)可读
POLLRDNORM普通数据可读
POLLRDBAND优先级带数据可读(Linux 不支持)
POLLPRI高优先级数据可读,比如 TCP 带外数据
POLLOUT数据(包括普通数据和优先数据)可写
POLLWRNORM普通数据可写
POLLWRBAND优先级带数据可写
POLLRDHUPTCP 连接被对方关闭,或者对方关闭了写操作,它由 GNU 引入
POLLERR错误
POLLHUP挂起。比如普通的写端被关闭后,该端描述符上将收到 POLLHUP 事件
POLLNVAL文件描述符没有打开

如上事件的本质是比特位为1的宏(即都是2的次方数),所以可以通过位操作设置到events中。这里我们只用重点关注POLLIN(读事件)POLLOUT(写事件)即可。

在这里插入图片描述


poll调用时,fd和events为有效输入,用户通过这两个字段告诉内核需关心该fd上的events事件(使用"|"运算符把事件添加到events中即可)。poll成功返回时fd和revents有效,内核告诉用户哪些fd上面的revents事件就绪(拿着revents使用"&"运算符去匹配事件即可)。

2. poll程序编写

  1. 加头文件#include<poll.h>
  2. 定义数组,这里就用固定大小,即struct pollfd _fds[FDSIZE]FDSIZE设为4096。(也可以使用数组指针动态开辟内存大小)。

初始化数组(注意fd为-1时内核并不会关心该文件描述符,所以把数组fd字段全初始化为-1),如下:

for(int i=0; i<FDSIZE; i++){ _fds[i].fd =-1; _fds[i].events =0; _fds[i].revents =0;} _fds[0].fd = _listenfd; _fds[0].events = POLLIN;

Start函数:

voidStart(){while(true){int n =poll(&_fds, FDSIZE,0);//处理poll返回值switch(n){case-1: std::cout<<"select fail"<<std::endl;break;case0: std::cout<<"time out..."<<std::endl;break;default: std::cout<<"事件就绪..."<<std::endl;//处理事件HandlerEvent();//......break;}}}

事件处理(可在select基础上修改):

voidHandlerEvent(){for(int i=0; i<FDSIZE; i++){//如果_fds[i].fd不合法则continueif(_fds[i].fd ==-1)continue;//接下来判断是否读就绪if(_fds[i].revents&POLLIN){//能确定读就绪,接下来根据不同的描述符做不同处理。if(_fds[i].fd == _listenfd){//调用Accept()......}else{//调用Read()......}}}}

在Accept中要把新连接userfd托管给poll,只需要把userfd给_fds数组即可。如下:

  1. 找到_fds中的空位置。(即fd为-1的位置)
  2. 如果没有空位置了(服务器被打满),则关闭userfd或给数组扩容;如果有则将空位置fd设置为userfd并设置events。

示例:

for(int i =0; i < FDSIZE; i++){if(_fds[i].fd ==-1){ _fds[i].fd = userfd; _fds[i].events = POLLIN; std::cout<<"accept success fd = "<<userfd<<std::endl;break;}elseif(i == FDSIZE -1){ std::cout<<"服务器繁忙..."<<std::endl;close(userfd);break;}}

在Read()中如果用户断开连接需要把userfd关闭,然后从_fds中移除(即把fd设为-1,events和revents设为0)。

3. poll性能总结

解决了select什么问题:

  1. 将输入和输出参数分离,不用在每次poll之前进行文件描述符集重置。
  2. 可管理的fd没有上限(由数组大小决定,无限制)。

缺点:

  1. 和select一样,poll返回后,需要轮询fd来获取就绪的描述符。
  2. 同时连接的大量客户端在一段时间可能很少处于就绪状态(即大量用户活跃度低),因此随着监视描述符数量增长,其效率也会线性下降。
非常感谢您能耐心读完这篇文章。倘若您从中有所收获,还望多多支持呀!

  1. 1 秒 = 10 3 毫秒 1秒=10^3毫秒 1秒=103毫秒, 1 秒 = 10 6 微妙 1秒=10^6微妙 1秒=106微妙↩︎

Read more

Flutter 三方库 a2a 的鸿蒙化适配指南 - 实现高效的 Array-to-Array 结构转换、支持跨维度数据映射与集合内容深度克隆

Flutter 三方库 a2a 的鸿蒙化适配指南 - 实现高效的 Array-to-Array 结构转换、支持跨维度数据映射与集合内容深度克隆

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net Flutter 三方库 a2a 的鸿蒙化适配指南 - 实现高效的 Array-to-Array 结构转换、支持跨维度数据映射与集合内容深度克隆 前言 在进行 Flutter for OpenHarmony 的大规模数据处理或图形计算开发时,经常需要对多维数组(嵌套列表)进行结构化调整。例如,将一个扁平化的传感器采样序列转换为 UI 渲染所需的网格坐标点集。a2a 是一个专门为 Array-to-Array 转换设计的极简工具库。它致力于通过声明式的 API 解决集合变换过程中的逻辑繁琐问题。本文将探讨如何在鸿蒙端利用该库提升集合操作的优雅度。 一、原原理性解析 / 概念介绍 1.1 基础原理 a2a 建立在一套强大的“映射算子(Mapping Operators)”之上。它获取输入数组,通过定义的投影(Project)

By Ne0inhk
鸿蒙金融理财全栈项目——运维监控、性能优化、安全加固

鸿蒙金融理财全栈项目——运维监控、性能优化、安全加固

《鸿蒙APP开发从入门到精通》第20篇:鸿蒙金融理财全栈项目——运维监控、性能优化、安全加固 📊🔧🛡️ 内容承接与核心价值 这是《鸿蒙APP开发从入门到精通》的第20篇——运维监控、性能优化、安全加固篇,100%承接第19篇的生态合作、用户运营、数据变现架构,并基于金融场景的运维监控、性能优化、安全加固要求,设计并实现鸿蒙金融理财全栈项目的运维监控、性能优化、安全加固功能。 学习目标: * 掌握鸿蒙金融理财项目的运维监控设计与实现; * 实现应用监控、服务器监控、数据库监控; * 理解性能优化在金融场景的核心设计与实现; * 实现前端优化、后端优化、数据库优化; * 掌握安全加固在金融场景的设计与实现; * 实现代码加固、数据加密、安全审计; * 优化金融理财项目的用户体验(运维监控、性能优化、安全加固)。 学习重点: * 鸿蒙金融理财项目的运维监控设计原则; * 性能优化在金融场景的应用; * 安全加固在金融场景的设计要点。 一、 运维监控基础 🎯 1.1 运维监控定义 运维监控是指对金融理财项目的应用、

By Ne0inhk

Mac mini 4 docker 安装openclaw

mac 通过docker 本地安装openclaw 教程 OpenClaw 不仅仅是一个聊天机器人,而是一个功能强大的 AI 智能体执行框架。你可以把它想象成一个能自主思考、调用工具、并替你完成复杂任务的数字员工。 1.环境准备 1.1安装Docker Desktop for mac 官网 下载安装即可 docker 中设置加速地址 "registry-mirrors": [ "https://docker.m.daocloud.io", "http://hub-mirror.c.163.com", "https://mirror.baidubce.com", "https://docker.mirrors.

By Ne0inhk
Flutter 三方库 media_kit 极致视听的全能播放器内核(音视频旗舰引擎,深度适配鸿蒙 HarmonyOS Next ohos)

Flutter 三方库 media_kit 极致视听的全能播放器内核(音视频旗舰引擎,深度适配鸿蒙 HarmonyOS Next ohos)

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net。 欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net。 前言 在鸿蒙(OpenHarmony)应用中实现极致的音视频播放体验,media_kit 是理想的旗舰级引擎。基于强大的 libmpv 核心,它提供了硬件加速、全格式支持以及灵活的渲染接口。 ⚠️ 重要说明:media_kit 官方版本(pub.dev)尚未原生支持鸿蒙系统。AtomGit 上的 OpenHarmony-SIG 社区已开始对该插件进行鸿蒙适配,但在实际部署到鸿蒙真机时,我们发现仍存在两个关键阻塞问题需要手动修复。 本文将详细记录: 1. 适配过程中遇到的两个核心问题及其修复方案。 2. media_kit 在鸿蒙平台的 API 使用方法。 3. 当前适配的完成度与后续展望。 一、核心价值 1.1

By Ne0inhk