IO复用函数的使用
迪丽瓦拉
2025-05-31 00:33:06
0

IO复用

  • 理解IO复用
  • select
  • poll
  • epoll
    • LT和ET模式
      • ET
  • select,poll,epoll区别

理解IO复用

I/O 复用使得程序能同时监听多个文件描述符,这对于提高程序的性能至关重要。
在客户端连接服务器时会产生套接字,如果要进行并发运行,同时客户端的数量庞大,就会导致产生多个线程,当线程产生的足够多的时候,CPU占有率会非常高,导致资源占用极大,所以提出了IO复用模型,使用一个线程来监视管理跟踪.具体怎么理解呢?
我们可以举一个例子,比如开学班长通知班里学生领书,但是不知道谁是什么时间领书,正常情况班里的每一个同学都去领书的地点,但是没有到领书的时间,只能等着,但是当一个班的同学足够多的时候,就会导致大家浪费了时间.但是IO复用呢,就是让班长充当监听套接字的作用,班长在领书的地点等待着领书,当轮到哪个同学领书,便通知哪个同学来领书,这就大大减轻了我们的资源占用.
I/O 复用虽然能同时监听多个文件描述符,但它本身是阻塞的。并且当
多个文件描述符同时就绪时,如果不采取额外的措施,程序就只能按顺序依处理其中的每一个文件描述符,这使得服务器看起来好像是串行工作的。如果要提高并发处理的能力,可以配合使用多线程或多进程等编程方法。

select

在一段指定时间内,监听用户感兴趣的文件描述符的可读、可写和异常等事件.
和select函数一起配套使用的宏:

#include    
int FD_ZERO(int fd, fd_set *fdset);   //一个 fd_set类型变量的所有位都设为 0
int FD_CLR(int fd, fd_set *fdset);  //清除某个位时可以使用
int FD_SET(int fd, fd_set *fd_set);   //设置变量的某个位置位
int FD_ISSET(int fd, fd_set *fdset); //测试某个位是否被置位

select函数:返回就绪描述符总数,返回值为0为等待超时,返回值为-1,则select失败.

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include #define  MAXFD    10
int socket_init();
void fds_init(int fds[])
{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;break;}}
}
int main()
{int sockfd = socket_init();if( sockfd == -1 ){printf("sockfd err\n");exit(0);}int fds[MAXFD];//数组存放描述符fds_init(fds);fds_add(fds,sockfd);//将描述符添加到数组fds中fd_set fdset;//读事件的结合 while( 1 ){FD_ZERO(&fdset);int maxfd = -1;for( int i = 0; i < MAXFD; i++ ){if ( fds[i] == -1 ){continue;}FD_SET(fds[i],&fdset);if ( fds[i] > maxfd ){maxfd = fds[i];}}struct timeval tv = {5,0};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;//跳过无效描述符 -1}if ( FD_ISSET(fds[i],&fdset) )//是否事件就绪  (有没有数据,或者有人连接){if ( fds[i] == sockfd )//accept{struct sockaddr_in caddr;int len = sizeof(caddr);int c = accept(sockfd,(struct sockaddr*)&caddr,&len);if ( c < 0 ){continue;}printf("accept c=%d\n",c);fds_add(fds,c);}else//recv{char buff[128] = {0};int num = recv(fds[i],buff,1,0);//读到数据,-1, 0if ( num <= 0 ){close(fds[i]);fds_del(fds,fds[i]);printf("close\n");}else{printf("read(%d):%s\n",fds[i],buff);send(fds[i],"ok",2,0);}}}}}}
}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 ){return -1;}res = listen(sockfd,5);if ( res == -1 ){return -1;}return sockfd;
}

poll

poll 系统调用和 select 类似,也是在指定时间内轮询一定数量的文件描述符,以测试其
中是否有就绪者。

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include #define  MAXFD  10int 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;}}
}int accept_client(int sockfd)
{struct sockaddr_in caddr;int len = sizeof(caddr);int c = accept(sockfd,(struct sockaddr*)&caddr,&len);return c;
}int recv_data(int c )
{char buff[128] = {0};int n = recv(c,buff,127,0);// 0 对方关闭连接, -1 recv失败,if ( n > 0 ){printf("read:%s\n",buff);send(c,"ok",2,0);}return n;
}
int main()
{int sockfd = socket_init();if ( sockfd == -1 ){exit(1);}struct pollfd fds[MAXFD];fds_init(fds);//清空数组fds_add(fds,sockfd);//添加sockfd监听套接字到fdswhile( 1 ){int n = poll(fds,MAXFD,5000);//阻塞 if ( n == -1 ){printf("poll err\n");}else if ( n == 0 ){printf("time out\n");}else{//处理就绪的描述符//首先找到就绪的描述符-- sockfd  acce close(c);pt , c  recv for( int i = 0; i < MAXFD; i++ ){if ( fds[i].fd == -1 )//跳过无效描述符{continue;}if ( fds[i].revents & POLLIN ) //检查有没有读事件就绪{//sockfd, c if ( fds[i].fd == sockfd )//监听套接字  accept{int c = accept_client(sockfd);if ( c >= 0 ){fds_add(fds,c);printf("accept c=%d\n",c);}}else//recv{if( recv_data(fds[i].fd) <= 0 )//连接套接字 recv  {close(fds[i].fd);fds_del(fds,fds[i].fd);//移除描述符printf("client close\n");}}}}}}}int socket_init()
{int sockfd = socket(AF_INET,SOCK_STREAM,0);if ( sockfd == -1 ){printf("socket err\n");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 ){printf("listen err\n");return -1;}return sockfd;
}

epoll

epoll 是 Linux 特有的 I/O 复用函数。它在实现和使用上与 select、poll 有很大差异。首先,epoll 使用一组函数来完成任务,而不是单个函数。其次,epoll 把用户关心的文件描述符上的事件放在内核里的一个事件表中。从而无需像 select 和 poll 那样每次调用都要重复传入文件描述符或事件集。但 epoll 需要使用一个额外的文件描述符,来唯一标识内核中的这个事件表。

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include #define  MAXFD    10
int socket_init();
void fds_init(int fds[])
{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;break;}}
}
int main()
{int sockfd = socket_init();if( sockfd == -1 ){printf("sockfd err\n");exit(0);}int fds[MAXFD];//数组存放描述符fds_init(fds);fds_add(fds,sockfd);//将描述符添加到数组fds中fd_set fdset;//读事件的结合 while( 1 ){FD_ZERO(&fdset);int maxfd = -1;for( int i = 0; i < MAXFD; i++ ){if ( fds[i] == -1 ){continue;}FD_SET(fds[i],&fdset);if ( fds[i] > maxfd ){maxfd = fds[i];}}struct timeval tv = {5,0};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;//跳过无效描述符 -1}if ( FD_ISSET(fds[i],&fdset) )//是否事件就绪  (有没有数据,或者有人连接){if ( fds[i] == sockfd )//accept{struct sockaddr_in caddr;int len = sizeof(caddr);int c = accept(sockfd,(struct sockaddr*)&caddr,&len);if ( c < 0 ){continue;}printf("accept c=%d\n",c);fds_add(fds,c);}else//recv{char buff[128] = {0};int num = recv(fds[i],buff,1,0);//读到数据,-1, 0if ( num <= 0 ){close(fds[i]);fds_del(fds,fds[i]);printf("close\n");}else{printf("read(%d):%s\n",fds[i],buff);send(fds[i],"ok",2,0);}}}}}}
}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 ){return -1;}res = listen(sockfd,5);if ( res == -1 ){return -1;}return sockfd;
}

LT和ET模式

在epoll该方法下存在两种模式分别是LT和ET,这两种模式怎么来理解呢?
LT是epoll的默认操作模式,当epoll_wait函数检测到有事件发生并将通知应用程序,而应用程序不一定必须立即进行处理,这样epoll_wait函数再次检测到此事件的时候还会通知应用程序,直到事件被处理。
而ET模式,只要epoll_wait函数检测到事件发生,通知应用程序立即进行处理,后续的epoll_wait函数将不再检测此事件。因此ET模式在很大程度上降低了同一个事件被epoll触发的次数,因此效率比LT模式高。
简单来讲,LT就是客户端在给服务器发送数据或者服务器向客户端发送数据的时候,recv函数每次只能读取有限个数据,一次将数据读取不完,这样就会导致数据放在了服务器的接收缓存区中,LT模式就是epoll函数会知道该缓存区中有数据,会直接进行读取,而ET呢就是只读取一次,没有将数据读取完成便会在下一次接收数据时继续读取,如果没有接收数据就会一直停留在缓存区中。

ET

ET模式解决的方法:
将套接字设置为非阻塞模式,这样就会使得读取信息时缓冲区没有数据就会返回-1,但是这样我们会发现读取失败和读完都会返回-1,这样我们就用到了errno,类似于标记的一个全局变量,如果缓冲区中没有数据返回-1就会看到errno为 EAGAIN 或者EWOULDBLOCK,情况不同返回的errno不同,这样我们就可以区分出是那种情况返回-1了。代码如下

 #include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include   
#include #define  MAXFD   10//SIGPIPE
//
int socket_init();
void set_nonblock(int fd)
{int oldfl = fcntl(fd,F_GETFL);int newfl = oldfl | O_NONBLOCK;//非阻塞if ( fcntl(fd,F_SETFL,newfl) == -1 ){printf("fcntl set err\n");}
}
void epoll_add(int epfd, int fd)
{struct epoll_event ev;ev.events = EPOLLIN|EPOLLET;ev.data.fd = fd;set_nonblock(fd);//设置非阻塞标志if ( epoll_ctl(epfd,EPOLL_CTL_ADD,fd,&ev) == -1 ){printf("epoll add err\n");}
}
void epoll_del(int epfd, int fd)
{if( epoll_ctl(epfd,EPOLL_CTL_DEL,fd,NULL) == -1 ){printf("epoll del err\n");}
}int accept_client(int sockfd)
{struct sockaddr_in caddr;int len = sizeof(caddr);int c = accept(sockfd,(struct sockaddr*)&caddr,&len);return c;
}int recv_data(int fd)
{int res = -1;while( 1 ){char buff[128] = {0};int n = recv(fd,buff,1,0);if ( n == -1 ){if ( errno == EAGAIN || errno == EWOULDBLOCK ){res = 1;send(fd,"ok",2,0);}else{printf("recv err\n");res = -1;} break;}else if ( n == 0 ){res = 0;break;}else{printf("read:%s\n",buff);}}return res;
}
int main()
{int sockfd = socket_init();if ( sockfd == -1 ){printf("sockfd err\n");exit(1);}int epfd = epoll_create(MAXFD);//创建内核事件表if ( epfd == -1 ){printf("epoll create err\n");exit(1);}epoll_add(epfd,sockfd);//向内核事件表中添加监听套接字struct epoll_event evs[MAXFD];//10, 存放就绪描述符while( 1 ){int n = epoll_wait(epfd,evs,MAXFD,5000);//获取就绪描述符   -- if( n == -1 ){printf("epoll wait err\n");}else if ( n == 0 ){printf("time out\n");}else{for( int i = 0; i < n; i++ ){int fd = evs[i].data.fd;if ( evs[i].events & EPOLLIN ){if ( fd == sockfd)//accept(){int c = accept_client(fd);if ( c >= 0 ){printf("accept c = %d\n",c);epoll_add(epfd,c);//将连接套接字c添加到内核事件表}}else//recv(){int num = recv_data(fd);if ( num == 0 ){epoll_del(epfd,fd);//先移除close(fd);//后关闭printf("client close\n");}}}}}}}
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 ){return -1;}res = listen(sockfd,5);if ( res == -1 ){return -1;}return sockfd;
}

select,poll,epoll区别

类型selectpolleopll
操作方式遍历遍历回调
底层实现数组链表RBTree
IO效率O(n)O(n)O(1)
最大连接数1024无上限无上限
fd拷贝每次调用select都需要把fd集合从用户态拷贝到内核态每次调用poll,都需要把fd集合从用户态拷贝到内核态调用epoll_ctl时拷贝进内核并且保存,之后每一次epoll_wait不拷贝
模式LTLTET或者LT

上表中阐述了三者的区别,select和poll的操作方式是遍历,所以IO效率是O(n),相信大家都可以从代码中看出来,但是回调是什么呢?为什么IO效率是O(1)呢?回调可以这么理解,老师要检查一个班里学生的作业,老师一个一个人检查,这就是遍历,而回调就是提前发布了命令,写了作业的把作业交上来,这样便不需要进行遍历,直接就知道谁写了作业。
fd拷贝呢,select和poll是每次调用函数时,将整个集合的数据拷贝一份到内核中,内核进行运算得到有没有就绪的描述符,存在就绪描述符我们将进行下一步重新遍历寻找就绪的描述符并且做处理,而epoll呢在eoill_ctl函数时就将数据从拷贝进了内核,之后运行epoll函数时就不需要进行拷贝,内核会直接将就绪描述符存放进evs数组中,后面也不需要进行整体遍历,直接进行处理该数组即可。

相关内容

热门资讯

linux入门---制作进度条 了解缓冲区 我们首先来看看下面的操作: 我们首先创建了一个文件并在这个文件里面添加了...
C++ 机房预约系统(六):学... 8、 学生模块 8.1 学生子菜单、登录和注销 实现步骤: 在Student.cpp的...
A.机器学习入门算法(三):基... 机器学习算法(三):K近邻(k-nearest neigh...
数字温湿度传感器DHT11模块... 模块实例https://blog.csdn.net/qq_38393591/article/deta...
有限元三角形单元的等效节点力 文章目录前言一、重新复习一下有限元三角形单元的理论1、三角形单元的形函数(Nÿ...
Redis 所有支持的数据结构... Redis 是一种开源的基于键值对存储的 NoSQL 数据库,支持多种数据结构。以下是...
win下pytorch安装—c... 安装目录一、cuda安装1.1、cuda版本选择1.2、下载安装二、cudnn安装三、pytorch...
MySQL基础-多表查询 文章目录MySQL基础-多表查询一、案例及引入1、基础概念2、笛卡尔积的理解二、多表查询的分类1、等...
keil调试专题篇 调试的前提是需要连接调试器比如STLINK。 然后点击菜单或者快捷图标均可进入调试模式。 如果前面...
MATLAB | 全网最详细网... 一篇超超超长,超超超全面网络图绘制教程,本篇基本能讲清楚所有绘制要点&#...
IHome主页 - 让你的浏览... 随着互联网的发展,人们越来越离不开浏览器了。每天上班、学习、娱乐,浏览器...
TCP 协议 一、TCP 协议概念 TCP即传输控制协议(Transmission Control ...
营业执照的经营范围有哪些 营业执照的经营范围有哪些 经营范围是指企业可以从事的生产经营与服务项目,是进行公司注册...
C++ 可变体(variant... 一、可变体(variant) 基础用法 Union的问题: 无法知道当前使用的类型是什...
血压计语音芯片,电子医疗设备声... 语音电子血压计是带有语音提示功能的电子血压计,测量前至测量结果全程语音播报࿰...
MySQL OCP888题解0... 文章目录1、原题1.1、英文原题1.2、答案2、题目解析2.1、题干解析2.2、选项解析3、知识点3...
【2023-Pytorch-检... (肆十二想说的一些话)Yolo这个系列我们已经更新了大概一年的时间,现在基本的流程也走走通了,包含数...
实战项目:保险行业用户分类 这里写目录标题1、项目介绍1.1 行业背景1.2 数据介绍2、代码实现导入数据探索数据处理列标签名异...
记录--我在前端干工地(thr... 这里给大家分享我在网上总结出来的一些知识,希望对大家有所帮助 前段时间接触了Th...
43 openEuler搭建A... 文章目录43 openEuler搭建Apache服务器-配置文件说明和管理模块43.1 配置文件说明...