Linux-高级IO之select

举报
End、断弦 发表于 2022/04/01 18:25:56 2022/04/01
【摘要】 五种IO模型 阻塞IO 非阻塞IO 信号驱动 多路转接 异步IO 同步和异步通信 I/O多路转接之select 简单的select服务器 总结

@TOC

五种IO模型

阻塞IO

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

在这里插入图片描述
例如:A在钓鱼时,会一直盯着鱼漂,当鱼漂动时,拉动鱼竿,其余的时间都在盯着鱼漂。

非阻塞IO

非阻塞IO: 如果内核还未将数据准备好, 系统调用仍然会直接返回, 并且返回EWOULDBLOCK错误码

在这里插入图片描述
非阻塞IO往往需要程序员循环的方式反复尝试读写文件描述符, 这个过程称为轮询. 这对CPU来说是较大的浪费, 一般只有特定场景下才使用。
例如:B也来钓鱼,B一遍玩手机一遍看着鱼漂,还时不时的问A钓没钓到鱼。B不是一直在等还可以玩手机的等。

信号驱动

信号驱动IO: 内核将数据准备好的时候, 使用SIGIO信号通知应用程序进行IO操作

在这里插入图片描述
例:此时C也来钓鱼,C将鱼竿上系了一个铃铛,C在看书,当铃铛响了C开始拉动鱼竿。C就像是信号驱动IO一样。

多路转接

IO多路转接: 虽然从流程图上看起来和阻塞IO类似. 实际上最核心在于IO多路转接能够同时等待多个文件描述符的就绪状态

在这里插入图片描述
例:此时D拿着20根鱼竿来钓鱼,D来回看着就可以了。20根鱼竿鱼上钩的几率远大于之前的3个人。

异步IO

异步IO: 由内核在数据拷贝完成时, 通知应用程序(而信号驱动是告诉应用程序何时可以开始拷贝数据).

在这里插入图片描述
例:E带着手下来钓鱼,E说:你在这钓,把桶钓满了拿回来给我。E然后开车走了。E根本不用在这里等待。

总结:

  • 任何IO都包括2个步骤:1是等,2是拷贝
  • 读IO=等待读事件就绪+内核数据拷贝到用户空间
  • 写IO=等待写事件就绪+用户空间数据拷贝到内核空间

高级IO的本质:尽可能减少等的时间比重

同步和异步通信

同步概念:

所谓同步,就是在发出一个调用时,在没有得到结果之前,该调用就不返回. 但是一旦调用返回,就得到返回值了; 换句话说,就是由调用者主动等待这个调用的结果

异步概念:

异步则是相反,调用在发出之后,这个调用就直接返回了,所以没有返回结果; 换句话说,当一个异步过程调用发出后,调用者不会立刻得到结果; 而是在调用发出后,被调用者通过状态、通知来通知调用者,或通过回调函数处理这个调用

还需注意多进程和多线程同步和异步之间的区别:

进程/线程同步也是进程/线程之间直接的制约关系
是为完成某种任务而建立的两个或多个线程,这个线程需要在某些位置上协调他们的工作次序而等待、传递信息所产生的制约关系. 尤其是在访问临界资源的时候。

I/O多路转接之select

系统提供select函数来实现多路复用输入/输出模型.

  • select系统调用是用来让我们的程序监视多个文件描述符的状态变化的
  • 程序会停在select这里等待,直到被监视的文件描述符有一个或多个发生了状态改变

select函数原型

int select(int nfds, fd_set *readfds, fd_set *writefds,
           fd_set *exceptfds, struct timeval *timeout)

参数说明:

  • 参数nfds是需要监视的最大的文件描述符值+1;
  • rdset,wrset,exset分别对应于需要检测的可读文件描述符的集合,可写文件描述符的集 合及异常文件描述符的集合;
  • 参数timeout为结构timeval,用来设置select()的等待时间

参数timeout取值:

  • NULL:则表示select()没有timeout,select将一直被阻塞,直到某个文件描述符上发生了件;
  • 0:仅检测描述符集合的状态,然后立即返回,并不等待外部事件的发生。
  • 特定的时间值:如果在指定的时间段里没有事件发生,select将超时返回。

在这里插入图片描述

fd_set结构

用命令vim /usr/include/sys/select.h查看源码:

 53 /* The fd_set member is required to be an array of longs.  */
 54 typedef long int __fd_mask;

 64 typedef struct
 65   {
 66     /* XPG4.2 requires this member name.  Otherwise avoid the name
 67        from the global namespace.  */
 68 #ifdef __USE_XOPEN
 69     __fd_mask fds_bits[__FD_SETSIZE / __NFDBITS];
 70 # define __FDS_BITS(set) ((set)->fds_bits)
 71 #else
 72     __fd_mask __fds_bits[__FD_SETSIZE / __NFDBITS];
 73 # define __FDS_BITS(set) ((set)->__fds_bits)
 74 #endif
 75   } fd_set;

select的过程:
在这里插入图片描述

简单的select服务器

将创建套接字单独封装成一个类

Sock.hpp

  1 #pragma once                                                                                                                        
  2 
  3 
  4 #include <iostream>
  5 #include <string>
  6 
  7 #include <sys/socket.h>
  8 #include <netinet/in.h>
  9 #include <sys/types.h>
 10 #include <arpa/inet.h>
 11 #include <unistd.h>
 12 #include <stdlib.h>
 13 #include <strings.h>
 14 #include <sys/select.h>
 15 
 16 using namespace std;
 17 
 18 class Sock{
 19     public:
 20       static int Socket(){
 21           int sock = socket(AF_INET, SOCK_STREAM, 0);
 22               if(sock < 0){
 23               cerr << "socket error" << endl;
 24               exit(2);
 25               }
 26           return sock;
 27 }
 28     static void Bind(int sock, int port){
 29           struct sockaddr_in local;
 30           bzero(&local, sizeof(local));
 31 
 32           local.sin_family = AF_INET;
 33           local.sin_port = htons(port);
 34           local.sin_addr.s_addr = htonl(INADDR_ANY);
 35           if(bind(sock, (struct sockaddr*)&local, sizeof(local)) < 0){
 36             cerr << "bind error" << endl;
 37             exit(3);
 38           }
 39                                                                                               
 40 }
 41     static void Listen(int sock){
 42         if(listen(sock, 5) < 0){
 43         cerr << "listen error" << endl;
 44         exit(4);
 45         }
 46                                           
 47 }
 48     static int Accept(int sock){
 49         struct sockaddr_in peer;
 50         socklen_t len = sizeof(peer);
 51         int fd = accept(sock, (struct sockaddr*)&peer, &len);
 52           if(fd < 0){
 53               cerr << "accpet error" << endl;
 54           }
 55 
 56         return fd;
 57         }
 58     static void Setsockopt(int sock){
 59         int opt = 1;
 60         setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
 61     }
 62 };

selectServer.hpp

    1 #pragma once                                                                                                                      
    2 
    3 #include"Sock.hpp"
    4 #include<sys/select.h>
    5 #define NUM (sizeof(fd_set)*8)
    6 #define DFL_FD -1
    7 
    8 
    9 //#define DFL_PORT 8080
   10 #define NUM (sizeof(fd_set)*8)
   11 #define DFL_FD -1
   12 
   13 class SelectServer {
   14   private:
   15         int lsock; 
   16         int port;
   17         int fd_array[NUM];
   18   public:
   19       SelectServer(int _p = 8080) :port(_p)
   20       {}
   21      void InitServer(){
   22        //初始化数组来保存已经打开的文件描述符
W> 23         for (int i = 0; i < NUM; i++) {
   24             fd_array[i] = DFL_FD;                            
   25             }
   26         lsock = Sock::Socket();
   27         Sock::Setsockopt(lsock);
   28         Sock::Bind(lsock, port);
   29         Sock::Listen(lsock);
   30        
   31         fd_array[0] = lsock;//将第一个元素设置成监听套接字
   32 }
   33      void AddFd2Array(int sock){
   34           int i = 0;
W> 35           for (; i < NUM; i++){
   36           if (fd_array[i] == DFL_FD) {  
      37              break;
   38              }
   39           }
W> 40           if (i >= NUM) {
   41             cerr << "fd array is full, close sock" << endl;
   42             close(sock);
   43           }
   44           else{
   45             fd_array[i] = sock;
   46             cout << "fd: " << sock << " add to select ..." << endl;
   47                                                                         
   48           }
   49                                           
   50 }
   51      void DefFdFromArray(int index){
W> 52           if (index >= 0 && index < NUM){
   53               fd_array[index] = DFL_FD;
   54                                                         
   55               }
   56 }
   57     void HandlerEvents(fd_set* rfds){
   58       //先判断文件描述符是否合法
W> 59          for (int i = 0; i < NUM; i++){
   60               if(fd_array[i] == DFL_FD){
   61                  continue;                                                     
   62               }//分为监听套接字和普通的套接字
   63               if(FD_ISSET(fd_array[i], rfds)) {
   64                    if (fd_array[i] == lsock) {
   65                                                                                                                                   
   66                   int sock = Sock::Accept(lsock);
   67                       if (sock >= 0){                                                                   
   68                         cout << "get a new link ..." << endl;
   69                         AddFd2Array(sock);//监听套接字不能直接就读数据,要先添加到数组中
   70                                           //有可能连接建立好后不发数据了

  71                         }
   72                                                                                 
   73                     }
   74               else{                
   75                     char buf[10240];
   76                     ssize_t s = recv(fd_array[i], buf, sizeof(buf), 0);
   77                     if (s > 0) {
   78                         buf[s] = 0;
   79                         cout << "client# " << buf << endl;
   80                                                                                                                                                              }
   81              else if (s == 0) {
   82                      cout << "clien quit" << endl;
   83                      //客户端退出关闭文件描述符和将位图中的清0
   84                      close(fd_array[i]);  
   85                      DefFdFromArray(i);}
   86              else {}
   87              }
   88            }
   89                                                 
   90          }
   91 }
   92       void Start(){
   93            int maxfd = DFL_FD;
   94            for (;;) {
   95                 fd_set rfds;
   96                 FD_ZERO(&rfds);//在栈上先将位图清0
   97                 cout << "fd_array: ";
W> 98                 for (int i = 0; i < NUM; i++) {
   99                     if (fd_array[i] != DFL_FD) {                                                                                  
  100                     cout << fd_array[i] << " ";
  101                     FD_SET(fd_array[i], &rfds);
  102                     if (maxfd < fd_array[i]){
  103                       maxfd = fd_array[i];//更新最大的maxfd
  104                     }
  105                   }
  106                }
  107                 cout << endl;
  108                 cout << "begin select ..." << endl;
  109                 switch (select(maxfd + 1, &rfds, nullptr, nullptr, nullptr)){
  110                   //select的3种返回值
  111                   case 0:
  112                      cout << "timeout ..." << endl; 
  113                      break;
  114                   case -1:        
  115                      cerr << "select error!" << endl; 
  116                      break; 
  117                   default:
  118                      //事件就绪
  119                      HandlerEvents(&rfds);
  120                      break;
  121                 }
  122           }
  123 }
  124      ~SelectServer()
  125     {}
  126 
  127 };                                     

select.cc

  1 #include "selectServer.hpp"
  2                                                                                                                                     
  3 void Usage(string proc)
  4 {
  5       cout <<"Usage:\n\t"<< proc << " port" << endl;
  6 
  7 }
  8 
  9 int main(int argc, char *argv[])
 10 {
 11   cout << sizeof(fd_set)*8 << endl;
 12   if(argc != 2)
 13   {
 14      Usage(argv[0]);
 15      exit(1);
 16   }
 17   SelectServer *ssvr = new SelectServer(atoi(argv[1]));
 18   ssvr->InitServer();
 19   ssvr->Start();
 20 
 21    return 0;
 22 }

下面来测试一下:

在这里插入图片描述
在用手机测试一下:
在这里插入图片描述

总结

select就绪条件

读就绪

  • socket内核中, 接收缓冲区中的字节数, 大于等于低水位标记SO_RCVLOWAT. 此时可以无阻塞的读该文件描述符, 并且返回值大于0;
  • socket TCP通信中, 对端关闭连接, 此时对该socket读, 则返回0;
  • 监听的socket上有新的连接请求;
  • socket上有未处理的错误

写就绪

  • socket内核中, 发送缓冲区中的可用字节数(发送缓冲区的空闲位置大小), 大于等于低水位标记SO_SNDLOWAT, 此时可以无阻塞的写, 并且返回值大于0
  • socket的写操作被关闭(close或者shutdown). 对一个写操作被关闭的socket进行写操作, 会触发SIGPIPE信号;
  • socket使用非阻塞connect连接成功或失败之后;
  • socket上有未读取的错误;

select特点

可监控的文件描述符个数取决与sizeof(fd_set)的值.服务器上sizeof(fd_set)=512,每bit表示一个文件描述符,则服务器上支持的最大文件描述符是512*8=4096.
将fd加入select监控集的同时,还要再使用一个数据结构array保存放到select监控集中的fd,

  • 一是用于再select 返回后,array作为源数据和fd_set进行FD_ISSET判断。
  • 二是select返回后会把以前加入的但并无事件发生的fd清空,则每次开始select前都要重新从array取得fd逐一加入(FD_ZERO最先),扫描array的同时取得fd最大值maxfd,用于select的第一个参数。

select缺点

  • 每次调用select, 都需要手动设置fd集合, 从接口使用角度来说也非常不便.
  • 每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大
  • 同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大
  • select支持的文件描述符数量太小

本篇文章到这就结束了,后面还会有poll,epoll的讲解。

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。