当前位置: 首页 > ds >正文

Linux——I/O复用函数

一、I/O复用技术

1.定义

I/O复用技术是一种在单个线程或进程中同时管理多个输入输出(I/O)操作的方法,主要用于提高程序的并发性能。允许程序在一个线程或进程中同时监视多个I/O操作是否就绪(如数据到达、可以写入),一旦某个或多个I/O事件发生,就进行处理。

2.本质

I/O复用使得程序能够同时监听多个文件描述符,但本身也是阻塞的。如果多个描述符同时就绪,则只能按顺序处理其中的每一个文件描述符。

3.常见的I/O复用技术

(1)select

通过检测多个文件描述符的状态(可读、可写、异常)来进行I/O操作。

但在处理的文件描述符较多时效率较低。

(2)poll

类似于select,也是在指定时间内轮询一定数量的文件描述符,测试是否有就绪者。

但没有描述符数量的限制,支持较大规模的文件描述符集,更加灵活。

(3)epoll(Linux特有)

使用一组函数来完成任务,而不是单个函数。

比select和poll更高效,采用事件驱动机制,适合大量连接的服务器应用。

二、select()

1.工作原理

select函数监视最多FD_SETSIZE(通常是1024)个文件描述符,传入三个集合:可读、可写、异常。当调用select时,它会阻塞直到某些文件描述符就绪。

2.select实现TCP服务器端

(1)服务器端代码

①首先,创建一个数组,该数组用来存放程序中的文件描述符。

②然后,创建一个集合,并调用FD_ZERO 方法,将集合中的每一个位清空。

③接着,使用FD_SET方法,将数组中的每个文件描述符传入到fd_set的集合中。

④调用select()方法,返回就绪文件描述符的总数。

⑤最后,使用FD_ISSET方法,检测哪些位被设置。

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<assert.h>
#include<sys/socket.h>
#include<sys/types.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include<sys/select.h>
#include<sys/time.h>
//TCP服务器端使用select,同时处理监听套接字和连接套接字#define MAXFD 10  //定义最大的文件描述符
int socket_init();//初始化套接字void fds_init(int fds[]) //清空数组,每个元素是-1
{for(int i=0;i<MAXFD;i++){fds[i]=-1;}
}void fds_add(int fds[],int fd)//添加一个描述符
{for(int i=0;i<MAXFD;i++){if(fds[i]==-1){fds[i]=fd;break;}}
}void fds_del(int fds[],int fd)//删除一个描述符
{for(int i=0;i<MAXFD;i++){if(fds[i]==fd){fds[i]=-1;}}
}void accept_client(int sockfd,int fds[]) //接受连接
{int c=accept(sockfd,NULL,NULL);//得到连接套接字if(c<0){return ;}printf("accept c=%d\n",c);fds_add(fds,c);//将连接套接子加入数组
}void recv_data(int c,int fds[]) //接受数据
{char buff[128]={0};int n=recv(c,buff,127,0);//只要接收缓冲区有数据,select会继续执行(未读完继续读)if(n<=0)   //如果对方关闭或者接受失败,则删除数组中的套接字{close(c);fds_del(fds,c);printf("client close\n");return  ;}printf("bufff=%s\n",buff);send(c,"ok",2,0);
}int main()
{//创建套接字int sockfd=socket_init(); if(sockfd==-1){exit(1);}//定义一个数组,收集描述符//原因:需要数组来保存所有的文件描述符,将数组中的元素再存放到集合中,执行select后,select会修改集合中的文件描述符int fds[MAXFD];fds_init(fds);//初始化数组fds_add(fds,sockfd);//向数组中添加监听套接字fd_set fdset;//定义一个集合while(1){FD_ZERO(&fdset);//清除fdset的所有位int maxfd=-1;//定义文件描述符最大值for(int i=0;i<MAXFD;i++) //循环遍历数组,将数组中的套接字放入到集合,并置为1{if(fds[i]==-1){continue;}FD_SET(fds[i],&fdset);//将数组中的元素添加到集合中,将对应的文件标识符位置置为1if(maxfd<fds[i]) //找出最大的文件描述符{maxfd=fds[i];}}//定义时间struct timeval tv={5,0};//使用select方法,会修改fdset集合int n=select(maxfd+1,&fdset,NULL,NULL,&tv);//失败if(n==-1){printf("select err\n");}else if(n==0) //超时{printf("time out\n");}else{for(int i=0;i<MAXFD;i++){if(fds[i]==-1){continue;}if(FD_ISSET(fds[i],&fdset))//测试发现该描述符fds[i]有事件{if(fds[i]==sockfd)//accept{accept_client(sockfd,fds);}else//recv{recv_data(fds[i],fds);}}}}}}
int socket_init()
{int sockfd=socket(AF_INET,SOCK_STREAM,0);//创建套接字if(sockfd==-1){return -1;}struct sockaddr_in saddr;memset(&saddr,0,sizeof(saddr));saddr.sin_family=AF_INET; //使用ipv4协议saddr.sin_port=htons(6000);//端口号saddr.sin_addr.s_addr=inet_addr("127.0.0.1");//ip地址int res=bind(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));//绑定端口和ip地址if(res==-1){printf("bind err\n");return -1;}res=listen(sockfd,5); //创建监听队列if(res==-1){return -1;}return sockfd;
}

(2)注意点

使用数组,而不直接集合是因为select方法会对集合进行修改,不能保证下一次调用的正确性。

3.select的缺点

(1)文件描述符数量有限(受限于FD_SETSIZE)

(2)每次调用时都需要重新设置文件描述符集合,效率较低

(3)在大量连接时性能下降(因为要线性扫描所有描述符)

三、poll()

1.工作原理

poll的核心类似于select,但使用一个pollfd 数组,指定每个文件描述符和监控事件。当调用 poll时,返回准备就绪的描述符。

2.poll实现TCP服务器端

①首先,创建一个pollfd数组,该数组用来存放程序中的文件描述符。

②为该数组的数据成员(fd,events,revents)赋值

③使用&检测事件

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<assert.h>
#include<sys/socket.h>
#include<sys/types.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include<sys/select.h>
#include<sys/time.h>
#include<poll.h>
//TCP服务器端使用poll
//poll传递的参数是结构体数组#define MAXFD 10  //定义最大的文件描述符
int socket_init();//初始化套接字void fds_init(struct pollfd fds[])//初始化结构体数组
{for(int i=0;i<MAXFD;i++){fds[i].fd=-1;fds[i].events=0;fds[i].revents=0;}
}void fds_add(struct pollfd fds[],int fd) //向结构体数组中添加描述符
{for(int i=0;i<MAXFD;i++){if(fds[i].fd==-1){fds[i].fd=fd;fds[i].events=POLLIN;//设置事件为可读fds[i].revents=0;break;}}
}void fds_del(struct pollfd fds[],int fd) //向结构体数组中删除描述符
{for(int i=0;i<MAXFD;i++){if(fds[i].fd==fd){fds[i].fd=-1;fds[i].events=0;fds[i].revents=0;break;}}
}void accept_client(int sockfd,struct pollfd fds[])//接受客户端的连接
{int c=accept(sockfd,NULL,NULL);if(c<0){return ;}printf("accept c=%d\n",c);fds_add(fds,c);//向结构体数组中添加文件描述符
}void recv_data(int c,struct pollfd fds[]) //接受客户端的数据
{char buff[128]={0};int num=recv(c,buff,127,0);//只要接收缓冲区有数据,poll会继续执行(未读完继续读)if(num<=0) //对方关闭缓冲区或者缓冲区五无数据则关闭连接套接字{close(c);fds_del(fds,c);//从结构体数组中删除描述符printf("client close\n");return;}printf("buff=%s\n",buff);send(c,"ok",2,0);}
int main()
{int sockfd=socket_init(); //创建套接字if(sockfd==-1){exit(1);}struct pollfd fds[MAXFD]; //定义结构体数组fds_init(fds);//初始化结构体数组fds_add(fds,sockfd);//向结构体数组中添加数据while(1){int n=poll(fds,MAXFD,5000);//执行pollif(n==-1) //失败{printf("poll err\n");}else if(n==0)//超时{printf("time out \n");}else{for(int i=0;i<MAXFD;i++){if(fds[i].fd==-1)//无效{continue;}if(fds[i].revents&POLLIN) //检测是否有读数据的描述符{if(fds[i].fd==sockfd) //如果是监听套接字,则建立连接{accept_client(sockfd,fds);}else  //接受客户端连接{recv_data(fds[i].fd,fds);}}}}}}int socket_init()
{int sockfd=socket(AF_INET,SOCK_STREAM,0);//创建套接字if(sockfd==-1){return -1;}struct sockaddr_in saddr;memset(&saddr,0,sizeof(saddr));saddr.sin_family=AF_INET; //使用ipv4协议saddr.sin_port=htons(6000);//端口号saddr.sin_addr.s_addr=inet_addr("127.0.0.1");//ip地址int res=bind(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));//绑定端口和ip地址if(res==-1){printf("bind err\n");return -1;}res=listen(sockfd,5); //创建监听队列if(res==-1){return -1;}return sockfd;
}

3.poll的优点和缺点

(1)优点

①支持超过FD_SETSIZE的文件描述符

②不受限制的数组大小

(2)缺点

和select一样,每次调用都要重新设置数组,仍有线性扫描的问题

四、epoll()

1.工作原理

epoll采用事件驱动机制,epoll将文件描述符上的事件放在内核的事件表(红黑树实现)中,需要一个额外的文件描述符,来唯一标识内核中的这个事件表。

2.epoll实现TCP服务器端

①首先,使用epoll_create()创建epoll对象 。

②然后,使用epoll_ctl()操作内核事件表。

③接着,使用epoll_wait()等待事件发生,获取就绪队列。

事件发生后,立即获得就绪的描述符集合(无须线性扫描全部描述符)

#include<sys/epoll.h>
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<assert.h>
#include<sys/socket.h>
#include<sys/types.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include<sys/select.h>
#include<sys/time.h>#define MAXFD 10
//创建套接字
int socket_init() 
{int sockfd=socket(AF_INET,SOCK_STREAM,0);if(sockfd==-1){return -1;}struct sockaddr_in saddr;memset(&saddr,0,sizeof(saddr));saddr.sin_family=AF_INET;saddr.sin_port=htons(6000);saddr.sin_addr.s_addr=inet_addr("127.0.0.1");int res=bind(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));if(res==-1){printf("bind err\n");return -1;}res=listen(sockfd,5);if(res==-1){return -1;}return sockfd;}//添加数据到内核事件表
void epoll_add(int epfd,int fd)
{struct epoll_event ev; //定义数组结构体ev.data.fd=fd;  //将数据添加到表中ev.events=EPOLLIN;//设置事件为读事件if(epoll_ctl(epfd,EPOLL_CTL_ADD,fd,&ev)==-1){printf("epoll_ctl err\n");}                                                                                          
}
//从内核表中删除数据
void epoll_del(int epfd,int fd)
{if(epoll_ctl(epfd,EPOLL_CTL_DEL,fd,NULL)==-1){printf("epoll ctl del err\n") ;}
}
//接受客户端的连接
void accept_client(int sockfd,int epfd)
{int c=accept(sockfd,NULL,NULL);if(c<=0){printf("accept err\n");return ;}printf("accept c=%d\n",c);epoll_add(epfd,c);//将c添加到内核事件表
} 
//接受客户端的数据
void recv_data(int c,int epfd)
{char buff[128]={0};int n=recv(c,buff,1,0);if(n<=0){epoll_del(epfd,c); //将c从内核事件表删除close(c);printf("client close\n");return;}printf("buff=%s\n",buff);send(c,"ok",2,0);
}int main()
{int sockfd=socket_init();//创建套接字if(sockfd==-1){exit(1);}//创建内核事件表(红黑树) 就绪队列(链表)int epfd=epoll_create(MAXFD);//将监听套接字添加到内核事件表epoll_add(epfd,sockfd);struct epoll_event evs[MAXFD];//用户数组,收集就绪描述符while(1){//获取就绪描述符,会阻塞int n=epoll_wait(epfd,evs,MAXFD,5000);//从内核事件表中获取就绪描述符存放到evs中if(n==-1)  //失败{printf("epoll wait err\n");exit(1);}else if(n==0) //超时{printf("time out\n");}else{for(int i=0;i<n;i++)//就绪描述符的个数=n{int fd=evs[i].data.fd;//从就绪数组中取出描述符if(fd==-1){continue; //无效}if(evs[i].events&EPOLLIN) //判断读事件是否发生               {if(fd==sockfd) //accept{accept_client(fd,epfd); }else   {recv_data(fd,epfd);}}    }}}
}

3.ET模式和LT模式

(1)LT模式(水平触发模式)

当检测到有事件发生但用户未处理完成时,会将此事件通知应用程序,只要缓冲区非空可以读/空间可写就会一直通知,直到应用程序读取或写入完毕。

(2)ET模式(边缘触发模式)

当检测到有事件发生但用户未处理完成时,只提醒一次,无论用户是否将事件处理完毕,都不再提醒。直到缓冲区状态再次发生变化,会提醒。

4.epoll()的优点和缺点

(1)优点

①高效处理大量连接。

②内核管理就绪事件,减少复制开销。

③支持边缘触发(edge-triggered)模式,提高性能

(2)缺点

仅在Linux系统上支持

http://www.xdnf.cn/news/13277.html

相关文章:

  • 245. 2019年蓝桥杯国赛 - 数正方形(困难)- 递推
  • RocketMQ基础命令
  • 【Linux】使用1Panel 面板让服务器定时自动执行任务
  • 小木的算法日记-二叉堆
  • 代码随想录算法训练营第60期第六十二天打卡
  • 全面掌握Pandas时间序列处理:从基础到实战
  • 多面体模型-学习笔记2
  • 管理学院权限管理系统开发总结
  • Blazor-Ant Design of Blazor快速开始
  • 蓝桥杯 回文日期
  • uniapp 字符包含的相关方法
  • RAG 文档解析难点1:多栏布局的 PDF 如何解析
  • 【渲染】Unity-分析URP的延迟渲染-DeferredShading
  • ZeenWoman 公司数据结构文档
  • window 显示驱动开发-如何查询视频处理功能(三)
  • Windows电脑能装鸿蒙吗_Windows电脑体验鸿蒙电脑操作系统教程
  • 算法岗面试经验分享-大模型篇
  • MODBUS TCP转CANopen 技术赋能高效协同作业
  • 华为网路设备学习-24(路由器OSPF - 特性专题)
  • Linux文件管理和输入输出重定向
  • VS创建Qt项目,Qt的关键字显示红色波浪线解决方法
  • 未授权访问事件频发,我们应当如何应对?
  • 求解Ax=b
  • Sonic EVM L1:沉睡的雄狮已苏醒
  • Coze工作流-故事语音转文本-语音转文本的应用
  • 从“安全密码”到测试体系:Gitee Test 赋能关键领域软件质量保障
  • LNG 应急储配站液氮利用率的调研
  • IDEA运行VUE项目报错相关
  • 线程同步:确保多线程程序的安全与高效!
  • python Day46 学习(日志Day15复习)