多路复用之select,poll

本文详细介绍了五种I/O模型,重点讲解了I/O多路转接中的select和poll机制。select允许同时等待多个文件描述符的就绪状态,但存在效率问题和最大文件描述符限制。poll与select类似,但没有最大值限制,事件通过pollfd结构体管理,减少了重复设置的步骤,但也需要手动维护文件描述符集。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

五种I/O模型

程序数据的输入,输出称之为I/O。我们在前面学习的socket套接字阶段,从网络中读取和发送数据,就属于I/O操作。当网络中没有数据可读的时候,进程就会阻塞式的等待,直到有数据准备就绪。socket套接字默认的都是阻塞的方式。


阻塞式I/O
这里写图片描述
I/O操作分为两部,第一部分是等待,第二部分是进行数据的拷贝。阻塞式I/O的特点就是当没有是数据就绪的时候,程序一直等待底层有数据准备好,然后再进行拷贝。


非阻塞式I/O
这里写图片描述
阻塞式I/O的特点就是等待效率低下,程序无法进行推进,非阻塞式I/O可以在系统调用的时候,就算底层数据没有准备好,也会进行返回,并返回错误码EWOULDBLOCK,但是非阻塞式I/O需要程序员自己进行不断的轮询调用,对CPU资源来说是一种很大的浪费。


信号驱动I/O
这里写图片描述
信号驱动I/O的特点是注册SIGIO的处理函数,在底层有数据准备好的时候,内核就给进程发送信号,在信号处理函数中调用recvfrom区拷贝底层数据。


I/O多路转接
这里写图片描述
前面的I/O操作都是等待一个文件描述符的就绪状态,而I/O多路转接是可以同时等待多个文件描述符的就绪状态,只要一个就绪,那么就返回刻度条件,并且拷贝底层数据。


异步I/O
这里写图片描述
前面的四种I/O模型都是同步方式,异步I/O是拷贝工作由内核完成,拷贝完成再通知应用程序。
再谈阻塞与非阻塞,当一个文件描述符被打开的时候,它默认是阻塞的I/O,但是我们也可以设置文件描述符的属性,设置为非阻塞式I/O。
这里写图片描述这里的cmd操作有5种功能,我们一般使用F_GETFL来获取文件描述符的标记,再使用F_SETFL设置文件描述符,设置的时候加上一个ONONBLOCK就可以把文件描述符设置为非阻塞式。


I/O多路转接之select

这里写图片描述
前面说过,select是I/O多路转接的接口,它可以同时等待多个文件述,只要有一个或者多个文件描述符的状态发生改变,就会返回,它只完成一件事情那就是等。
参数及返回值:
nfds:表示想要等待的最大文件描述符+1.
readfds,writefds,exceptfds:关心读的文件描述符集,关心写的文件描述符集,关心异常的文件描述符集。这里的fd_set类型表示文件描述符集。它可以理解为一张位图,每一个比特位的下标就表示那一个文件描述符,比特位的内容,1表示关心这个位置上的文件描述符事件。0表示不关心。同时,这三个参数都是输入输出型参数,输入表示关心那些文件描述符上上的那些事件,输出表示关心的文件描述符集上那些事件已经就绪。因此,在每一次调用select之前,都需要重新设置,我们要自己维护一个数组来保存关心的事件。fd_set类型必须用以下函数进行操作。
这里写图片描述
用来将文件描述符集的指定位清空,判断一个文件描述符是否在这个集合当中,设置一个文件描述符到文件描述符集中,把文件描述符集所有位清空。
timeval结构体:timeval用于描述一段事件,如果等待的文件描述符集在这一段时间之内没有事件发生,那么将超时,select将返回0。
select成功返回表示返回文件描述符改变的个数,返回0表示超时,返回负数代表出错。
一般情况下,我们关心文件描述符的读写情况,那么什么情况下读写条件就绪。
1.读就绪:
(1)在socket内核当中,当接受缓冲区的字节数大于等于低水位时候,这时候就可以非阻塞式的读。
(2)在TCP通信时候,对端关闭连接,此时对该socket读,返回0;
(3)监听套接字上有新的连接请求的时候。
(4)socket上有未处理的错误。
2.写就绪
(1)socket写操作被关闭,那么此时对该socket进行写,就会触发SIGPIPE信号。
(2)在socket内核中,当发送缓冲区字节数大小大于等于低水位的时候,此时可以无阻塞式的写,并且返回值大于0;
(3)socket使用非阻塞的connect连接成功的时候;
(4)socket上有未读取的错误。
最后还有一种方式就是异常就绪:比如在TCP报头中,就有一个紧急指针位,如果触发了紧急指针位,事件也会就绪。


socket的特点以及缺点:
socket可以同时等待多个文件描述符上的一个或者多个文件描述符事件就绪,并返回就绪条件,极大的提高了程序I/O的效率,一个进程能最大等待的文件描述符个数取决于fd_set的大小,不同平台是不一样的。它的缺点是我们要自己维护一个数组来保存想要等待的文件描述符集,并且每次都需要重新设置,而且每次调用select都需要把用户态的fds拷贝到内核,一旦fds大效率就很低,而且我们每次需要遍历的方式去查看寻找fds的最大值。而且select能支持的文件描述符有限制。


select服务器。

  1 #include<stdio.h>
  2 #include<sys/types.h>
  3 #include<sys/socket.h>
  4 #include<sys/select.h>
  5 #include<arpa/inet.h>
  6 #include<netinet/in.h>
  7 #include<stdlib.h>
  8 #include<string.h>
  9 int StartUp(int port)
 10 {   
 11 struct sockaddr_in local;
 12 local.sin_family=AF_INET;
 13 local.sin_port=htons(port);
 14 local.sin_addr.s_addr=INADDR_ANY;
 15 int ret =socket(AF_INET,SOCK_STREAM,0);
 16 if(ret<0)
 17 {   
 18     perror("socket");
 19     exit(2);
 20 }  
 21 if(bind(ret,(struct sockaddr*)&local,sizeof(local))<0)
 22 {  
 23     perror("bind");
 24     exit(3);
 25 }   
 26 int listen_sock=listen(ret,5);
 27 if(listen_sock<0)
 28 {  
 29 perror("listen");
 30 exit(4);
 31 }   
 32 return listen_sock;
 33 }   
 34 void Init(int *array,int size)//全部填充为-1表示不关心
 35 {  
 36 int i=0;
 37     for( i=0;i<size;i++)
 38     {
 39    
 40         array[i]=-1;
 41     }
 42 }   
 43     
 44 void Reload(int listen_sock,int *array,int size,fd_set*r_fds,int *max_fd)
 45 {   
 46     FD_ZERO(r_fds);
 47     FD_SET(listen_sock,r_fds);
 48     int max=listen_sock;
 49     int i=0;
 50     for( i=0;i<size;++i)
 51     {
 52         if(array[i]!=-1)
 53         FD_SET(array[i],r_fds);
 54         if(array[i]>max)
 55             max=array[i];
 56     }
 57   *max_fd=max;
 58    
 59 }   
 60 void ADD(int connect_sock,int *array,int size)
 61 {   
 62     int i=0;
 63     for( i=0;i<size;i++)
 64     {
 65     
 66         if(array[i]==-1)
 67         {
 68             array[i]=connect_sock;
 69             break;
 70         }
 71     }
 72 }   
 73 void ServerIo(int listen_sock,int* array,int size,fd_set*r_fds,int *max_fd)
 74 {   
 75 if(FD_ISSET(listen_sock,r_fds))//监听套接字准备就绪,获取新连接
 76 {   
 77     struct sockaddr_in client;
 78     socklen_t len;
 79     int connect_sock=accept(listen_sock,(struct sockaddr*)&client,&len);
 80     if(connect_sock<0)
 81     {
 82         perror("accept");
 83         return ;
 84     }
 85     printf("Get new connect,[%s]:[%d]\n",inet_ntoa(client.sin_addr),ntohs(client.sin_port));
 86     ADD(connect_sock,array,size);
 87 }   
 88 //处理新连接
 89 int i=0;
 90 for( i=0;i<size;i++)
 91 {   
 92     
 93     if(array[i]=-1)
 94     {
 95        return ;    
 96     }
 97     if(!FD_ISSET(array[i],r_fds))
 98         return;
 99     char buf[1024];
100     ssize_t s=read(array[i],buf,sizeof(buf)-1);
101     if(s<0)
102     {
103         perror("read");
104         return;
105     }
106     if(s==0)
107     {
108         printf("client say good bye\n");
109         close(array[i]);
110         array[i]=-1;
111     }
112     buf[s]=0;
113     printf("client say :%s\n",buf);
114     write(array[i],buf,strlen(buf));
115 }  
116 }  
117 int main(int argc, char*argv[])
118 {  
119     if(argc!=2)
120     {
121    
122         printf("Usage:%s:[port]\n",argv[0]);
123         return 1;
124     }
125     int listen_sock=StartUp(atoi(argv[1]));//创建监听套接字
126     fd_set r_fds;
127     int array[1024];//用数组保存我们关系的那些文件描述符中的那些事件。
128     Init(array,sizeof(array)/sizeof(array[0]));
129     for(;;)
130     {
131         int max_fd=listen_sock;
132         Reload(listen_sock,array,sizeof(array)/sizeof(array[0]),&r_fds,&max_fd);//每次将重置
133         int ret=select(max_fd+1,&r_fds,NULL,NULL,0);//r_fds输入输出型参数,输入表示关系那些文件描述符上的事件,输出表示有哪些文件描述符上面的事件以及就绪
134         switch(ret)
135         {
136         case -1:
137             perror("select");
138             exit(5);
139             break;
140             case 0:
141             printf("timeout...\n");
142             break;
143             default:
144             ServerIo(listen_sock,array,sizeof(array)/sizeof(array[0]),&r_fds,&max_fd);  
145    
146             }
147     }
148  
149 return 0;
150 }


epoll

epoll和select一样,都是完成多路复用的一种方式,同时两者也很相似

这里写图片描述
fds是poll监听的一个列表,其中每一个元素是一个文件描述符信息。这里i传一个结构体数组,第二个参数nfds表示数组的长度,timeout位毫秒。函数返回值和selec一样。
下面来看看pollfd这个结构体:
这里写图片描述
结构体第一个fd表示关心那一个文件描述符,events表示关心这个文件描述符的什么事件,revents表示这个事情返回情况。
这里写图片描述
这是events和revents取值。


poll的优点:
poll相对于select最大的特点就是将关心的事件和返回的事件封装到一个结构体当中,不需要像select那样每次都要重新进行设置。并且poll并没有最大值的限制。
poll的缺点:
和select一样,每一次poll返回,都需要轮询的方式获取就绪事件的文件描述符。
并且pollfd这个结构体数组也需要我们手动维护,每一次都需要从用户态拷贝到内核态。
随着监视文件描述符的增长,效率还是会下降。

### 异步IO多路复用selectpoll和epoll的区别 #### Select机制分析 Select采用的是轮询的方式去检测每一个文件描述符的状态变化,当调用`select()`时,它会阻塞直到有一个或多个文件描述符准备好进行取或入操作,或者是超时发生。然而,在每次调用之前都需要重新设置感兴趣的文件描述符集合,并且对于大量连接来说效率较低,因为其内部实现是对所有传入的文件描述符依次做线性扫描。 #### Poll机制解析 PollSelect相似之处在于两者都基于水平触发模式工作,这意味着只要某个文件描述符处于可状态,则一直保持激活直至被消费掉为止。但是poll解决了select的最大文件描述符数量受限于FD_SETSIZE的问题,理论上支持更大的并发量。不过poll同样存在性能瓶颈—随着监控列表的增长,遍历整个数组查找已准备好的事件仍然是一项耗时的工作[^4]。 #### Epoll特性介绍 Epoll则采用了完全不同的设计理念来优化上述两种方式存在的缺陷: - **边缘触发(Edge Triggered, ET) vs 水平触发(Level Triggered, LT)**: 支持这两种模式,默认情况下为LT模式下运作。ET模式仅在监测到新的数据到来瞬间触发一次回调通知给应用层;而在LT模式里只要有未处理的数据就会持续报告。 - **高效的通知机制**: 不同于前两者的被动查询模型,epoll主动告知哪些具体的socket已经发生了特定类型的活动,从而使得开发者能够精准定位并响应感兴趣的变化源而不必逐一排查全部可能的对象。 - **高性能扩展能力**: 利用了Linux内核提供的红黑树结构以及hash表技术实现了高效的动态增删改查功能,这不仅提高了系统的吞吐率还降低了CPU占用率。此外,由于只关心活跃链接所以即使面对海量级的同时在线用户也能维持良好的表现力[^1]。 ```c int epoll_fd = epoll_create(EPOLL_SIZE); struct epoll_event event; event.events = EPOLLIN | EPOLLET; // 设置为边沿触发 event.data.fd = listen_sock; // 注册监听套接字至epoll实例中 if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, listen_sock, &event) == -1){ perror("epoll set insertion error"); } ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值