I/O多路复用 - select

Posted TangguTae

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了I/O多路复用 - select相关的知识,希望对你有一定的参考价值。

目录

I/O的分类

阻塞I/O

非阻塞I/O

信号驱动I/O

多路复用I/O

select

select的参数

select的返回值

select版本的服务器

select的特点

缺点

优点


基础的I/O可以看看之前的一篇文章

Linux操作系统-系统级IO_TangguTae的博客-CSDN博客

一次I/O的过程可以分为等待+拷贝两个步骤。

例如:

读I/O = 首先等待读事件就绪(有数据可读),然后将数据从内核空间拷贝到用户空间。

写I/O = 首先等待写事件就绪(有缓冲区可拷贝),然后用户空间数据拷贝至内核空间。

I/O的分类

阻塞I/O

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

这种阻塞的感觉其实是用户所感受到的,在内核层面,该进程将会被挂起,放到阻塞队列当中。

例如read函数,当读事件不满足时,就会被阻塞,直到读事件满足,此时再将数据从内核拷贝到用户空间。

非阻塞I/O

非阻塞I/O就和阻塞I/O不同,不会一直在原地等待,如果内核的数据还未准备好,系统任然会返回,返回错误码EAGAIN或者EWOULDBLOCK。

此时,如果要获得数据,则需要进行循环检测返回值,直到读取成功。

但是这样也会有问题:循环检测的方式比较消耗CPU的资源

将I/O变成非阻塞I/O可以用fcntl函数

第一个参数是所要修改的文件描述符 ,如何修改取决于后面的cmd参数。

如果想要变成非阻塞,则需要带上O_NONBLOCK属性。

信号驱动I/O

而在信号驱动I/O 中,当文件描述符上可执行I/O 操作时,进程请求内核为自己发送一个信号。之后进程就可以执行任何其他的任务直到I/O 就绪为止,此时内核会发送信号给进程。

这个信号就是29号信号SIGIO

 对应的文件描述符需要进行属性的修改

这里需要添加O_ASYNC属性。

除此之外,还需要在该进程里自定义信号捕捉函数,让内核发送SIGIO信号通知进程进行数据的拷贝,信号相关的可以参考

Linux操作系统 - 信号_TangguTae的博客-CSDN博客_linux 系统信号

多路复用I/O

对于之前的I/O,都是在单个进程只在一个文件描述符上执行I/O操作,每次I/O 系统调用都会阻塞直到完成数据传输。整个过程的效率其实是非常低。

I/O 多路复用允许进程同时检查多个文件描述符以找出它们中的任何一个是否可执行I/O 操作。

所对应的系统调用为select、poll和epoll。

本文章重点讨论一下select。

上述I/O都是同步I/O。

异步I/O

在内核进行数据拷贝完成后再通知进程,因此进程不会一直等待数据传输到内核或者等待操作完成。这使得进程可以同I/O 操作一起并行处理其他的任务。简单的说就是内核帮进程把数据都准备好了(等待+拷贝),进程不会被阻塞。

 

select

之前的I/O都是对单个文件描述符进行操作,而select可以一次性等多个文件描述符(普通文件、管道,FIFO,套接字等等),用select来检测所等待的文件描述符是否为就绪态,从而通知上层应用对数据进行读或者写的操作。所以说select只负责I/O过程中的等待这一过程。

select的参数

第一个参数是文件描述符,其值为所需要等待的文件描述符中最大的文件描述符的值+1

第二个参数到第四个参数的类型都是一样的,都为fd_set* 类型(文件描述符的集合),这三个参数既是输入型参数也是输出型参数。

作为输入性参数时:

readfds表示的是需要关心哪些文件描述符上的读事件

writefds表示要关心哪些文件描述符上的写事件

exceptfds表示要关心哪些文件描述符是否有异常事件

作为输出型参数时,三个参数返回值表示有哪些文件描述符上所对应的事件准备就绪。

fd_set本质上是一个位图,需要哪些文件描述符被检测时,将对应的位置置为1。

第五个参数timeout是一个timeval类型的参数

timeout用来设定select阻塞的时间的上限。

如果结构体当中的tv_sec和tv_usec都为0的话,表示此时的select是非阻塞的等待。当设置大于0时,为特定时间,如果在指定的时间段里没有事件发生,select将超时返回。当设置为nullptr的时候,select将会阻塞等待,直到有有关心的文件描述符时间就绪时就返回。

select的返回值

select的返回值有三种情况

1、返回值为-1时

表示有错误发生。并且会设置对应的错误码errno,可能包括的错误为EBADF、ENTER等。EBADF表示有非法的文件描述符,ENTER表示该调用被信号中断了。

2、返回值为0时

在任何文件描述符成为就绪态之前select()调用已经超时。

3、返回值大于0时

表示已经就绪的文件描述符有多少个。他所统计的是在readfds、writefds和exceptfds三个集合中被标记就绪态文件描述符的总数。

select版本的服务器

对网络编程不太熟悉的可以看看下面的文章。

Linux操作系统 - 网络编程socket(1)_TangguTae的博客-CSDN博客

​​​​​​​​​​​​​​Linux操作系统 - 网络编程socket(2)_TangguTae的博客-CSDN博客

实现select版本的服务器的基本流程

1、 先按照基本的socket套接字编程的流程进行服务器的初始化,包括套接字的创建、端口号的绑定、设置为监听套接字等步骤。

#include<iostream>
#include<sys/select.h>
#include<cstring>
#include<unistd.h>                                  
#include<sys/socket.h>
#include<sys/types.h>
#include<netinet/in.h>
#include<arpa/inet.h>
using namespace std;
const int NUM = sizeof(fd_set)*8;//由于fd_set是位图,所以每一个字节表示8个文件描述符
#define DEF_NUM -1//默认的文件描述符的值

class SelectServer

  private:
    int lsock;//监听套接字
    int fd_array[NUM];//用来存储需要监视的文件描述符
    int port;
  public:
    SelectServer(int _port)
      :lsock(-1)
      ,port(_port)
    
      memset(fd_array,DEF_NUM,sizeof(fd_array));//初始化数组
    
    ~SelectServer()
    
      close(lsock);
    
    void InitServer()//初始化服务器
    
      //基本流程和socket编程一样
      lsock = socket(AF_INET,SOCK_STREAM,0);
      if(lsock < 0)
       
        cerr<<"sock error!"<<endl;
        exit(-1);
      
      sockaddr_in local;
      local.sin_family = AF_INET;
      local.sin_port = htons(port);
      local.sin_addr.s_addr = INADDR_ANY;

      if(bind(lsock,(struct sockaddr*)&local,sizeof(local)) < 0)
      
        cerr<<"bind error!"<<endl;
        exit(-1);
      
      if(listen(lsock,5) < 0)
      
        cerr<<"listen error!"<<endl;
        exit(-1);
      
      fd_array[0] = lsock;//首先将监听套接字放入文件描述符数组中
      cout<<"InitServer successful..."<<endl;
    

2、初始化服务器的任务完成以后就得开始对文件描述进行监听。

引入一组对文件描述符集操作的函数

FD_CLR函数

用来清除描述词组set中相关fd 的位

FD_ISSET函数

用来测试描述词组set中相关fd 的位是否为真

FD_SET函数

用来设置描述词组set中相关fd的位

FD_ZERO函数

用来清除描述词组set的全部位

注意:fd_set参数每次 select之后需要重新更新一下,因为他的值已作为返回参数被修改过,此时需要重新添加需要检测的文件描述符。

void ServerStart()

  int max_fd = DEF_NUM;//每次得统计一下文件描述符数组中的最大的文件描述符的数值
  while(true)
  
    fd_set rfds;
    FD_ZERO(&rfds);//先进行清空一下文件文件描述符集参数
    for(int i=0;i < NUM;i++)
    
      if(fd_array[i]!=DEF_NUM)//如果存在文件描述符,则设置到对应的文件描述符集中
      
        FD_SET(fd_array[i],&rfds);
        if(max_fd < fd_array[i])//更新一下最大文件描述符集
          max_fd = fd_array[i];
      
    
    //进行阻塞等待,将timeval参数设置为nullptr
    switch(select(max_fd+1,&rfds,nullptr,nullptr,nullptr))
    
      case 0:
        cout<<"select timeout!!!"<<endl;
        break;
      case -1:
        cout<<"select error!!!"<<endl;
        break;
      default:
        HanderEvents(&rfds);//说明有对应的文件描述符就绪
        break;
                                                                    
  

3、处理就绪的文件描述符

检测文件描述符数组中哪个文件描述符已经就绪,需要判断是监听套接字已就绪还是其他的套接字准备就绪。

void HanderEvents(fd_set* rfds)

  for(int i=0;i < NUM;i++)
  
    if(fd_array[i]==DEF_NUM)
      continue;
    if(FD_ISSET(fd_array[i],rfds))
    
      if(fd_array[i]==lsock)//如果是监听套接字就绪,说明有新的连接到来
      
        sockaddr_in remote;
        socklen_t len=sizeof(remote);
        int sock = accept(lsock,(struct sockaddr*)&remote,&len);
        if(sock >= 0)
        
          cout<<"get a new link..., sock = "<<sock<<endl;
          AddFd2Array(sock);//将新的套接字添加到数组当中
        
        //此时千万不能调用read函数去读sock,不然会被阻塞到
      
      else
      
        //有数据的到来
        int& sock = fd_array[i];
        char buf[1024] = '0';
        ssize_t s = recv(sock,buf,sizeof(buf)-1,0);//此时的recv不会被阻塞住
        if(s > 0)
        
          buf[s-1] = '\\0';
          cout<<"recv data from socket "<<sock<<" is "<<buf<<endl;
        
        else if(s==0)
        
          cout<<"socket "<<sock<<" is exit!"<<endl;
          close(sock);
          sock = DEF_NUM;
        
        else
        
          cout<<"recv error!!!"<<endl;
          close(sock);
          sock = DEF_NUM;
        
      
    
  

//将文件描述符添加到文件描述符数组中
void AddFd2Array(int fd)         

  for(int i=0;i < NUM;i++)
  
    if(fd_array[i]==DEF_NUM)
    
      fd_array[i] = fd;
      break;
    
  


运行结果

 

 

甚至可以尝试用网页访问

获得到一个http的请求。

上面的服务器还存在很多问题,在接收数据那里buf的大小不一定一次就能把数据读完,或者当数据量很小的时候,一次读取的字节数又太多了存在粘包的问题。

select的特点

对于fd_set所能检测的最大文件描述符的数量是有限制的,在linux上是由一个常量所决定的(FD_SETSIZE),该值的大小为1024。不过这里有牵扯到一个问题,文件描述符的资源是一个进程所拥有的,一个进程肯定所对应的能打开的文件描述符资源是有上限的。不过所对应进程的这个文件描述符的上限是可以扩展的。详情可以参考linux源码中的task_struct结构体中的fdtable。

缺点

1、等待的文件描述的数量是有上限的。

2、select需要频繁的与内核交互数据(拷贝),会降低效率。

3、select 每次调用都要重新设置fd_set参数,即添加需要监视的文件描述符。

4、select检测就绪的文件描述符表需要遍历文件描述符数组才能知道是哪个文件描述符就绪。

优点

优点就是主要集中在多路复用的I/O的优点。

1、select可以同时等待多个fd,而且只负责等待,任何一个时刻上fd就绪的概率就增加了,降低了I/O等待所消耗的时间,可以提高效率。当进行数据拷贝时就不会被阻塞住。

2、适合有大量连接的场景,但是这些连接并不是特别的活跃。

以上是关于I/O多路复用 - select的主要内容,如果未能解决你的问题,请参考以下文章

I/O多路复用 - select

Python—I/O多路复用

I/O多路复用之Select

I/O多路复用——select

浅谈网络I/O多路复用模型 select & poll & epoll

I/O多路复用