Linux-高级IO之select

简介: 五种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的讲解。

相关文章
|
2月前
|
网络协议 安全 Linux
Linux C/C++之IO多路复用(select)
这篇文章主要介绍了TCP的三次握手和四次挥手过程,TCP与UDP的区别,以及如何使用select函数实现IO多路复用,包括服务器监听多个客户端连接和简单聊天室场景的应用示例。
93 0
|
2月前
|
存储 Linux C语言
Linux C/C++之IO多路复用(aio)
这篇文章介绍了Linux中IO多路复用技术epoll和异步IO技术aio的区别、执行过程、编程模型以及具体的编程实现方式。
86 1
Linux C/C++之IO多路复用(aio)
|
4月前
|
缓存 安全 Linux
Linux 五种IO模型
Linux 五种IO模型
|
2月前
|
Linux C++
Linux C/C++之IO多路复用(poll,epoll)
这篇文章详细介绍了Linux下C/C++编程中IO多路复用的两种机制:poll和epoll,包括它们的比较、编程模型、函数原型以及如何使用这些机制实现服务器端和客户端之间的多个连接。
25 0
Linux C/C++之IO多路复用(poll,epoll)
|
3月前
|
网络协议 Java Linux
高并发编程必备知识IO多路复用技术select,poll讲解
高并发编程必备知识IO多路复用技术select,poll讲解
|
4月前
|
小程序 Linux 开发者
Linux之缓冲区与C库IO函数简单模拟
通过上述编程实例,可以对Linux系统中缓冲区和C库IO函数如何提高文件读写效率有了一个基本的了解。开发者需要根据应用程序的具体需求来选择合适的IO策略。
34 0
|
4月前
|
存储 IDE Linux
Linux源码阅读笔记14-IO体系结构与访问设备
Linux源码阅读笔记14-IO体系结构与访问设备
|
4月前
|
存储 Java
【IO面试题 四】、介绍一下Java的序列化与反序列化
Java的序列化与反序列化允许对象通过实现Serializable接口转换成字节序列并存储或传输,之后可以通过ObjectInputStream和ObjectOutputStream的方法将这些字节序列恢复成对象。
|
5月前
|
Java 大数据
解析Java中的NIO与传统IO的区别与应用
解析Java中的NIO与传统IO的区别与应用
|
3月前
|
Java 大数据 API
Java 流(Stream)、文件(File)和IO的区别
Java中的流(Stream)、文件(File)和输入/输出(I/O)是处理数据的关键概念。`File`类用于基本文件操作,如创建、删除和检查文件;流则提供了数据读写的抽象机制,适用于文件、内存和网络等多种数据源;I/O涵盖更广泛的输入输出操作,包括文件I/O、网络通信等,并支持异常处理和缓冲等功能。实际开发中,这三者常结合使用,以实现高效的数据处理。例如,`File`用于管理文件路径,`Stream`用于读写数据,I/O则处理复杂的输入输出需求。
下一篇
无影云桌面