高级IO:五种高阶IO模型、多路转接(select、poll、epoll)

文章目录:

  • 前言
  • 1.五种高阶IO模型
    • 1.1 阻塞IO
    • 1.2 非阻塞IO
    • 1.3 信号驱动IO
    • 1.4 多路转接IO(可监控多个文件描述符)
    • 1.5 异步IO
  • 2.多路转接技术(select、poll、epoll)
    • 2.1 select
      • 2.1.1 函数原型及参数解释
      • 2.1.2 fd_set解释
      • 2.1.3 返回值
      • 2.1.4 select的优缺点
      • 2.1.5 selset代码验证
        • (1) select简单验证
    • 2.2 poll
      • 2.2.1 函数原型及参数解释
      • 2.2.2 poll的优缺点
      • 2.2.3 poll代码验证
    • 2.3 epoll
      • 2.3.1 epoll相关函数
        • (1)创建epoll操作句柄 epoll_create
        • (2)epoll_ctl
        • (3)epoll等待接口epoll_wait
      • 2.3.2 epoll原理
      • 2.3.3 epoll的优点
      • 2.3.4 epoll代码验证
        • (1)epoll简单验证
        • (2)利用epoll解决TCP单进程阻塞问题


前言

任何IO操作都需要经过 等待拷贝数据 这两个过程,而这两个过程是在内核当中的过程

1.五种高阶IO模型

1.1 阻塞IO

在内核将数据准备好之前, 系统调用接口会一直等待,不会返回,所有的套接字, 默认都是阻塞方式.

图解如下:
高级IO:五种高阶IO模型、多路转接(select、poll、epoll)_第1张图片

1.2 非阻塞IO

应用进程发起系统调用,在内核中判断数据没准备好,直接返回,并且返回EWOULDBLOCK错误码
非阻塞IO一般需要搭配循环使用,反复尝试读写文件描述符, 这个过程称为轮询. 这对CPU来说是较大的浪费, 一般只有特定场景下才使用

图解如下:
高级IO:五种高阶IO模型、多路转接(select、poll、epoll)_第2张图片

1.3 信号驱动IO

内核将数据准备好的时候, 会向应用进程发送SIGIO信号告知应用进程可以进行IO操作

图解如下:
高级IO:五种高阶IO模型、多路转接(select、poll、epoll)_第3张图片

1.4 多路转接IO(可监控多个文件描述符)

内核帮我们监控了多个文件描述符,当某一个或者若干个文件描述符就绪的时候,就会通知调用者,调用者调用系统调用函数针对就绪的文件描述符进行操作

图解如下:
高级IO:五种高阶IO模型、多路转接(select、poll、epoll)_第4张图片

1.5 异步IO

由内核在数据拷贝完成时, 通知应用程序进行相关操作
(而信号驱动是当内核中数据准备好了就通知应用程序).

图解如下:
高级IO:五种高阶IO模型、多路转接(select、poll、epoll)_第5张图片

2.多路转接技术(select、poll、epoll)

多路转接本质上是让内核帮助程序员监控多个文件描述符的IO事件,一旦监控的某个文件描述符对应的事件产生(IO就绪),就会通知调用者

2.1 select

作用:监控多个文件描述符,就绪之后通知调用者

2.1.1 函数原型及参数解释

函数介绍
在这里插入图片描述

  • nfds:select监控事件集合(fd_set)的范围,范围是从[0,1023]之间去选择范围;
  • nfds的取值为:监控的最大文件描述符数值+1
  • fd_set:事件集合类型
  • readfds:可读事件集合
  • writefds:可写事件集合
  • exceptfds:异常事件集合
  • timeout
        阻塞方式:传递NULL
        非阻塞方式:传递0
        带有超时时间的方式:
    高级IO:五种高阶IO模型、多路转接(select、poll、epoll)_第6张图片

2.1.2 fd_set解释

使用vim打开/usr/include/sys/select.h路径下查看源码
高级IO:五种高阶IO模型、多路转接(select、poll、epoll)_第7张图片
高级IO:五种高阶IO模型、多路转接(select、poll、epoll)_第8张图片
在这里插入图片描述

内核在使用该数组的时候采用的是位图的方式,一共有16*8*8=1024个比特位
高级IO:五种高阶IO模型、多路转接(select、poll、epoll)_第9张图片
fd_set事件集合占用比特位的个数和宏_FD_SETSIZE强相关,即,_FDSETSIZE多大,fd_set事件集合就有多少个比特位

其实fd_set结构就是一个整数数组,更严格的是,是一个“位图”使用位图中对应的位来表示要监控的文件描述符,如下图所示:
高级IO:五种高阶IO模型、多路转接(select、poll、epoll)_第10张图片

如上图所示

  • 如果关心某个文件描述符对应的某个事件,则将文件描述符添加到对应的事件集合当中。eg:关心3号文件描述符的可读事件,则将3号文件描述符添加到read_fds当中
  • 添加文件描述符到事件集合的时候,是将文件描述符对应的比特位设置为1
  • 如果一个文件描述符关心多种事件(可读,可写,异常),则将文件描述符添加到不同的事件集合当中
  • select的监控效率会随着监控文件描述符的增多而下降,本质原因是由于监控轮询的范围变大了

为了方便操的作位图,可使用如下接口来操作fd_set:

从事件集合当中删除一个文件描述符

void FD_CLR(int fd, fd_set *set);

判断文件描述符是否在某一个事件集合当中

int FD_ISSET(int fd, fd_set *set);

设置文件描述符到事件集合当中

void FD_SET(int fd, fd_set *set);

清空事件集合,将所有的比特位全部置为0

void FD_ZERO(fd_set *set);

2.1.3 返回值

  • 监控成功:返回就绪的文件描述符个数,会将事件集合当中未就绪的文件描述符去掉
  • 监控失败:返回-1

【注意】:select返回之后,需要重新添加文件描述符

2.1.4 select的优缺点

优点:

  • select遵循的是POSIX标准,说明select函数是一个跨平台的函数,既可以在Windows当中运行,也可以在Linux当中运行
  • select在带有超时时间的监控的时候,超时时间的单位可以为微妙

缺点:

  • 监控文件描述符个数的上限为1024
  • 随着文件描述符的增多,select监控效率在下降(本质是select在轮询进行监控)
  • 可读、可写、异常这些事件需要单独的添加到不同的事件集合当中
  • 当select监控成功之后,会从事件集合当中去除掉未就绪的文件描述符,这使程序下一次调用select时,还需要重新添加文件描述符
  • 在每次select进行监控的时候,都会将准备好的事件集合拷贝到内核空间,select返回的时候都会将内核空间拷贝给用户空间

2.1.5 selset代码验证

(1) select简单验证

我们使用select对0号文件描述符进行监控,如果监控到了我们从0号文件描述符中去读取内容并将读取到的内容打印出来

代码如下:

  1 #include<stdio.h>                                                                        
  2 #include<unistd.h>
  3 #include<sys/select.h>
  4                                                     
  5 int main()         
  6 {        
  7     fd_set readfds;          
  8     FD_ZERO(&readfds);
  9     FD_SET(0,&readfds);
 10 
 11     while(1)                 
 12     {                             
 13         int ret = select(1,&readfds,NULL,NULL,NULL);
 14         if(ret < 0)        
 15         {
 16             perror("select");
 17             return 0;
 18         }
 19 
 20         char buf[1024] = {0};
 21         read(0,buf,sizeof(buf)-1);
 22                                                     
 23         printf("%s\n",buf);
 24     }    
 25                              
 26     return 0;        
 27 }                              

验证结果如下:
高级IO:五种高阶IO模型、多路转接(select、poll、epoll)_第11张图片

2.2 poll

前提:与select相比并不支持跨平台,与epoll相比,没有epoll的效率高

2.2.1 函数原型及参数解释

在这里插入图片描述

  • struct pollfd:事件结构
    在这里插入图片描述
  • 想让poll监控多个文件描述符,只需要在定义事件结构数组的时候,多传递几个元素
    eg:struct pollfd arr[10];
      arr[0].fd = 0;
      arr[0].events = POLLIN;
  • nfds:事件结构数组中有效元素的个数
  • timeout:
      >0:带有超时时间,单位:秒
      ==0:非阻塞
      <0:阻塞
  • 返回值:就绪文件描述符的个数

2.2.2 poll的优缺点

优点:

  • 提出了事件结构的方式,在给poll函数传递参数的时候,不需要分别添加到“事件集合”中
  • 事件结构数组的大小可以根据程序员自己进行定义,并没有上限要求
  • 不用在监控到就绪文件描述符之后,重新添加文件描述符

缺点:

  • 不支持跨平台
  • 内核对事件结构数组的监控也是采用轮询遍历的方式,即随着监控文件描述符的增多,监控效率会下降
  • 每次调用poll都需要把大量的pollf结构从用户态拷贝到内核态,poll返回的时候,会将内核空间拷贝给用户空间(从内核态到用户态会调用do_signal会有开销)

2.2.3 poll代码验证

  1 #include<stdio.h>
  2 #include<poll.h>
  3 #include<unistd.h>
  4 
  5 int main()
  6 {
  7     struct pollfd arr[10];                                                               
  8 
  9     arr[0].fd = 0;
 10     arr[0].events = POLLIN;
 11 
 12     while(1)
 13     {
 14         int ret = poll(arr,1,-1);
 15         if(ret < 0)
 16         {
 17             perror("poll");
 18             return 0;
 19         }
 20         for(int i = 0;i < 10;++i)
 21         {
 22             if(arr[i].revents == POLLIN)
 23             {              
 24                 char buf[1024] = {0};
 25                 read(arr[i].fd,buf,sizeof(buf)-1);
 26                 printf("%s\n",buf);
 27             }                    
 28         }          
 29     }    
 30     return 0;              
 31 }                                          

验证结果如下:
高级IO:五种高阶IO模型、多路转接(select、poll、epoll)_第12张图片

2.3 epoll

epoll是目前世界公认的在Linux下,多路转接监控效率最高的模型

2.3.1 epoll相关函数

(1)创建epoll操作句柄 epoll_create

在这里插入图片描述

  • size:自从Linux2.6.8之后,size参数是被忽略的;但不要传递小于0的数字
  • 返回值:返回epoll的操作句柄

(2)epoll_ctl

在这里插入图片描述

  • epfd:epoll操作句柄
  • op:告诉epoll要做什么是事
     ① EPOLL_CTL_ADD:添加一个文件描述符对应的事件结构到epoll当中
     ② EPOLL_CTL_MOD:修改一个文件描述符的事件结构
     ③ EPOLL_CTL_DEL:从epoll当中删除一个文件描述符对应的事件结构
  • fd:待处理(添加、修改、删除)的文件描述符
  • event:文件描述符对应的事件结构
  • epoll_event结构体高级IO:五种高阶IO模型、多路转接(select、poll、epoll)_第13张图片

(3)epoll等待接口epoll_wait

在这里插入图片描述

  • epfd:epoll的操作句柄
  • events:时间结构数组(集合),从epoll当中获取就绪的事件结构
  • maxevents:最多一次获取多少个事件结构
    timeout
      0:带有超时事件
      ==0:非阻塞
      <0:阻塞
  • 返回值:就绪的文件描述符个数

2.3.2 epoll原理

高级IO:五种高阶IO模型、多路转接(select、poll、epoll)_第14张图片
当调用epoll_create函数时,会在内核创建一个eventpoll结构体,在该结构体中有一个rdlist成员和rbr成员,它两分别是一个双向链表和红黑树,而调用epoll_ctl函数添加、修改、删除文件描述符对应的事件集合其实是对红黑树中的节点进行相应的添加、修改、删除操作,而所有添加到epoll的事件都会与设备(网卡)驱动程序建立回调关系,也就是说,当文件描述符准备就绪后,内核会回调ep_poll_callback函数,将准备就绪的事件集合添加到rdlist双向链表中,而当调用epoll_wait进行监控的时候,如果双向链表为空,则表明当前没有就绪的事件发生,如果不为空,则将双向链表中的内容复制到用户态,并返回将事件数量返回给用户。

【注意】每一个epoll对象都有一个独立的eventpoll结构体,用于存放通过epoll_ctl方法向epoll对象中添加进来的事件

2.3.3 epoll的优点

  • 事件回调机制,当文件描述符就绪之后,会调用回调函数将事件结构复制到双向链表中,epoll_wait 返回直接访问就绪队列就知道哪些文件描述符就绪. 这个操作时间复杂度O(1). 即使文件描述符数目很多, 效率也不会受到影响.
  • 数据拷贝轻量: 只在合适的时候调用 EPOLL_CTL_ADD 将文件描述符结构拷贝到内核中, 这个操作并不频繁(而select/poll都是每次循环都要进行拷贝)
  • 没有数量限制: 文件描述符数目无上限.

2.3.4 epoll代码验证

(1)epoll简单验证

我们使用epoll对0号文件描述符进行监控,如果监控到了我们从0号文件描述符中去读取内容并将读取到的内容打印出来

  1 #include<stdio.h>
  2 #include<unistd.h>
  3 #include<sys/epoll.h>                                                                         
  4 
  5 int main()
  6 {
  7     int epfd = epoll_create(3);
  8     if(epfd < 0)
  9     {
 10         perror("epoll_create");
 11         return 0;
 12     }
 13 
 14     struct epoll_event ee;
 15     ee.events = EPOLLIN;
 16     ee.data.fd = 0;
 17     epoll_ctl(epfd,EPOLL_CTL_ADD,0,&ee);
 18 
 19     while(1)
 20     {
 21         struct epoll_event arr[2];
 22         int ret = epoll_wait(epfd,arr,sizeof(arr)/sizeof(arr[0]),-1);
 23         if(ret < 0)
 24         {
 25             perror("epoll_wait");
 26             continue;
 27         }
 28 
 29         for(int i = 0;i < ret;++i)
 30         {
 31             if(arr[i].events == EPOLLIN)
 32             {
 33                 char buf[1024] = {0};                                                         
 34                 read(arr[i].data.fd,buf,sizeof(buf) - 1);
 35 
 36                 printf("%s\n",buf);
 37             }
 38         }
 39     }
 40     return 0;
 41 }                   

验证结果如下:
高级IO:五种高阶IO模型、多路转接(select、poll、epoll)_第15张图片

(2)利用epoll解决TCP单进程阻塞问题

在TCP单进程中存在一个问题:

  • 将accept放在while循环外部,只能接收一个客户端的连接(客户端recv函数阻塞)
  • 将accept放到while循环内部,和每个客户端只能通信一次(服务端accept阻塞)

具体分析可查看【TCP单进程版本问题】

而多路转接正好可以解决这个问题,下面是使用epoll解决的代码

客户端1

  1 #include<stdio.h>                                                                             
  2 #include<string.h>
  3 #include<unistd.h>
  4 #include<sys/socket.h>
  5 #include<arpa/inet.h>
  6 
  7 int main()
  8 {
  9     int sockfd = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
 10     if(sockfd < 0)
 11     {
 12         perror("socket");
 13         return 0;
 14     }
 15 
 16     struct sockaddr_in addr;
 17     addr.sin_family = AF_INET;
 18     addr.sin_port = htons(18989);
 19     addr.sin_addr.s_addr = inet_addr("0.0.0.0");
 20     int ret = connect(sockfd,(struct sockaddr*)&addr,sizeof(addr));
 21     if(ret < 0)
 22     {
 23         perror("connect");
 24         return 0;
 25     }
 26 
 27 
 28     while(1)
 29     {
 30 
 31         sleep(1);
 32 
 33        char buf[1024] = {0};                                                                  
 34        sprintf(buf,"hello server,i am client xxxxx");
 35        ssize_t send_ret = send(sockfd,buf,strlen(buf),0);
 36        if(send_ret < 0)
 37        {
 38            perror("send");
 39            continue;
 40        }
 41 
 42 
 43        memset(buf,'\0',sizeof(buf));
 44        ssize_t recv_ret = recv(sockfd,buf,sizeof(buf)-1,0);
 45        if(recv_ret < 0)
 46        {
 47            perror("recv");
 48            continue;
 49        }
 50        else if(recv_ret == 0)
 51        {
 52            printf("server close");                                                            
 53 
 54            close(sockfd);
 55 
 56            return 0;
 57        }
 58 
 59        printf("server say: %s\n",buf);
 60     }
 61 
 62     close(sockfd);
 63     return 0;
 64 }                               

客户端2

  1 #include<stdio.h>                                                                             
  2 #include<string.h>
  3 #include<unistd.h>
  4 #include<sys/socket.h>
  5 #include<arpa/inet.h>
  6 
  7 int main()
  8 {
  9     int sockfd = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
 10     if(sockfd < 0)
 11     {
 12         perror("socket");
 13         return 0;
 14     }
 15 
 16     struct sockaddr_in addr;
 17     addr.sin_family = AF_INET;
 18     addr.sin_port = htons(18989);
 19     addr.sin_addr.s_addr = inet_addr("0.0.0.0");
 20     int ret = connect(sockfd,(struct sockaddr*)&addr,sizeof(addr));
 21     if(ret < 0)
 22     {
 23         perror("connect");
 24         return 0;
 25     }
 26 
 27 
 28     while(1)
 29     {
 30 
 31         sleep(1);
 32 
 33        char buf[1024] = {0};                                                                  
 34        sprintf(buf,"hello server,i am client PPPPPPPPP");
 35        ssize_t send_ret = send(sockfd,buf,strlen(buf),0);
 36        if(send_ret < 0)
 37        {
 38            perror("send");
 39            continue;
 40        }
 41 
 42 
 43        memset(buf,'\0',sizeof(buf));
 44        ssize_t recv_ret = recv(sockfd,buf,sizeof(buf)-1,0);
 45        if(recv_ret < 0)
 46        {
 47            perror("recv");
 48            continue;
 49        }
 50        else if(recv_ret == 0)
 51        {
 52            printf("server close");                                                            
 53 
 54            close(sockfd);
 55 
 56            return 0;
 57        }
 58 
 59        printf("server say: %s\n",buf);
 60     }
 61 
 62     close(sockfd);
 63     return 0;
 64 }                               

服务端:

  1 #include<stdio.h>                                                                                    
  2 #include<string.h>
  3 #include<unistd.h>                                                              
  4 #include<sys/epoll.h>
  5 #include<sys/socket.h>                       
  6 #include<arpa/inet.h>                                                           
  7                                                                           
  8 int main()                      
  9 {                
 10     int listen_sockfd = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
 11     if(listen_sockfd < 0)    
 12     {            
 13         perror("socket");
 14         return 0;
 15     }    
 16 
 17     struct sockaddr_in addr;
 18     addr.sin_family = AF_INET;
 19     addr.sin_port = htons(18989);
 20     addr.sin_addr.s_addr = inet_addr("0.0.0.0");
 21     int ret = bind(listen_sockfd,(struct sockaddr*)&addr,sizeof(addr));
 22     if(ret < 0)
 23     {
 24         perror("bind");
 25         return 0;
 26     }
 27 
 28     ret = listen(listen_sockfd,1);
 29     if(ret < 0)
 30     {
 31         perror("listen");
 32         return 0;
 33     }
 34 
 35     //初始化epoll
 36     int epfd = epoll_create(4);
 37     if(epfd < 0)                                                                                     
 38     {
 39         perror("epfd");
 40         return 0;
 41     }
 42 
 43     struct epoll_event ee;
 44     ee.events = EPOLLIN;
 45     ee.data.fd = listen_sockfd;
 46     epoll_ctl(epfd,EPOLL_CTL_ADD,listen_sockfd,&ee);
 47 
 48 
 49     while(1)
 50     {
 51         struct epoll_event arr[10];
 52         int ret = epoll_wait(epfd,arr,sizeof(arr)/sizeof(arr[0]),-1);
 53         if(ret < 0)
 54         {
 55             continue;
 56         }
 57         for(int i = 0;i < ret;++i)
 58         {                                                                                            
 59             if(arr[i].data.fd == listen_sockfd)
 60             {
 61                 struct sockaddr_in peer_addr;
 62                 socklen_t socklen = sizeof(peer_addr);
 63                 int new_sockfd = accept(listen_sockfd,(struct sockaddr*)&peer_addr,&socklen);
 64                 if(new_sockfd < 0)
 65                 {
 66                     perror("accept");
 67                     return 0;
 68                 }
 69 
 70                 struct epoll_event newee;
 71                 newee.data.fd = new_sockfd;
 72                 newee.events = EPOLLIN;
 73                 epoll_ctl(epfd,EPOLL_CTL_ADD,new_sockfd,&newee);
 74 
 75             }
 76             else
 77             {
 78                 char buf[1024] = {0};
 79                 ssize_t recv_ret = recv(arr[i].data.fd,buf,sizeof(buf)-1,0);                         
 80                 if(recv_ret < 0)
 81                 {
 82                     perror("recv");
 83                     continue;
 84                 }
 85                 else if(recv_ret == 0)
 86                 {
 87                     printf("client close");
 88                     close(arr[i].data.fd);
 89                     close(listen_sockfd);
 90                     return 0;
 91                 }
 92 
 93                 printf("client%d say:%s%d\n",arr[i].data.fd,buf,arr[i].data.fd);
 94 
 95                 memset(buf,'\0',sizeof(buf));
 96                 sprintf(buf,"hello,i am server,i recv client%d",arr[i].data.fd);
 97                 ssize_t send_ret = send(arr[i].data.fd,buf,strlen(buf),0);
 98                 if(send_ret < 0)
 99                 {
100                     perror("send");                                                                  
101                     continue;
102                 }
103 
104             }
105         }
106 
107     }
108     close(listen_sockfd);
109     close(epfd);
110     return 0;
111 }                                     

让程序跑起来可以看到一个服务端可以同时和两个客户端进行正常通信

服务端:
高级IO:五种高阶IO模型、多路转接(select、poll、epoll)_第16张图片
客户端1:
高级IO:五种高阶IO模型、多路转接(select、poll、epoll)_第17张图片
客户端2:
高级IO:五种高阶IO模型、多路转接(select、poll、epoll)_第18张图片

你可能感兴趣的:(linux—网络,多路转接,select,epoll,poll,高阶IO模型)