高效IO模型

前言

IO过程总体分为两步,第一步:接收端->接受缓冲区等待数据的递达,发送端->等待发送缓冲区有可用空间,将发送数据放入发送缓冲区;第二步拷贝:接收端->将递达的数据从内核空间拷贝到用户空间的接受缓冲区,发送端->将发送的数据从用户空间的发送缓冲区拷贝至内核缓冲区;

所以高效IO本质是单位时间内拷贝的频率很高,重点是缩短等待的时间

一、五种IO模型

1.1 阻塞IO

阻塞IO是最常见的IO模型(在内核将数据准备好之前,系统调用会一直等待,所有的套接字,默认都是阻塞方式)
在这里插入图片描述

阻塞:本质是由OS发起,由OS执行,进程状态R->(S or T or D),进入等待队列当中,数据就绪后,进程被操作系统唤醒,再去执行recvfrom

1.2 非阻塞IO

如果内核还未将数据准备好,系统调用仍然会直接返回,并且返回EWOULDBLOCK

非阻塞IO往往需要程序员循环的方式反复尝试读写文件描述符, 这个过程称为轮询. 这对CPU来说是较大的浪费, 一般只有特定场景下才使用

在这里插入图片描述

非阻塞轮询的本质是:由用户发起,OS执行,做事件就绪(OS有数据)的检测工作

1.3 信号驱动IO

内核将数据准备好的时候,使用SIGIO信号通知应用程序进行IO操作
在这里插入图片描述

1.4 异步IO

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

在这里插入图片描述

1.5 IO多路转接

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

二、同步通信与异步通信

同步与异步关注的是消息通信机制

  • 所谓同步,就是在发出一个调用时,在没有得到结果之前,该调用就不返回,就得到返回值;换句话说,就是由调用者主动等待这个调用的结果;拷贝需要用户参与
  • 异步则是相反,调用在发出之后,这个调用就直接返回了,所有没有返回结果;换句话说,当一个异步过程调用发出后,调用者不会立刻得到结果;而是在调用发出后,被调用者通过状态、通知来通知调用者,或者通过回调函数处理这个调用。拷贝不需要用户参与,操作系统拷贝

这里的同步通信和进程之间的同步是完全不相干的概念

三、非阻塞IO

非阻塞IO可以open打开时设置flag为非阻塞,这里主要介绍打开后的fd如何设置为非阻塞

3.1 fcntl

一个文件描述符,默认都是阻塞IO,fcntl函数用于已经打开的fd

该函数是一个系统调用函数

man  2  fcntl
#include <unistd.h>
#include <fcntl.h>

int fcntl(int fd, int cmd, ... /* arg */ );

fcntl函数有5种功能:

  • 复制一个现有的描述符 ---- cmd=F_DUPFD
  • 获得/设置文件描述符标记–cmd=F_GETFD或F_SETFD
  • 获得/设置文件状态标记-----cmd=F_GETFL或F_SETFL
  • 获得/设置异步IO所有权-----cmd=F_GETOWN或F_SETOWN
  • 获得/设置记录锁--------------cmd=F_GETLK,F_SETLK或F_SETLKW

传入的cmd的值不同,后面追加的参数也不相同

目前只使用第三种功能,获取/设置文件状态标记,就可以将一个文件描述符设置为非阻塞

3.2 实现函数SetNoBlock

基于fcntl,实现一个SetNoBlock函数,将文件描述符设置为非阻塞

void SetNoBlock(int fd){
    
    
	int fl = fcntl(fd,F_GETFL);   //获取fd的状态标记
	if(fl < 0){
    
    
		perror("fcntl");
		return ;
	}
	fcntl(fd, F_SETFL, fl | O_NONBLOCK);  //将fl状态标记设置为非阻塞 
}

3.3 如何使用函数SetNoBlock

以轮询的方式读取标准输入

#include<iostream>                                                    
#include<unistd.h>
#include<fcntl.h>

bool SetNonBlock(int fd){
    
    
	   int fl = fcntl(fd,F_GETFL);
	   if(fl < 0){
    
    
	    std::cerr << "fcntl error" << std::endl;
	    return false;
	  }
	  fcntl(fd,F_SETFL, fl | O_NONBLOCK);
	  return true;
 }
int main()
{
    
    
#define NUM 1024
   SetNonBlock(0);
   while(true){
    
    
      char buffer[NUM];
      ssize_t size = read(0,buffer,sizeof(buffer)-1);
      if(size < 0){
    
    
          //不一定是出错了,有可能是底层没有数据
	     if(errno == EAGAIN || errno  == EWOULDBLOCK ){
    
    
	         std::cout << "底层的数据没有就绪,你在轮询检测一下,try again " << std::endl;
	         continue;
          }
	     if(errno == EINTR){
    
    
		      std::cout << "底层数据就绪未知,被信号中断 " << std::endl;
		     continue;
          }
	    else{
    
    
	  		  std::cerr << "read error: " << size << " errno: "<< errno << std::endl;  
	          break;
	     }
	  }

	  buffer[size] = 0;  //主动给字符串最后添加'\0'
	  std::cout << "#echo: " <<buffer <<  std::endl;  
	  
    }
     
     return 0;
}       

如果以非阻塞读取数据时,如果数据没有就绪,read是以出错的形式返回
如何判断是read真的出错?or 底层数据没有就绪 or read被信号中断

  • errno == EAGAIN || errno == EWOULDBLOCK 时,说明数据没有就绪
  • errno == EINTR ,说明被信号中断
 grep -ER 'EAGAIN | EWOULDBLOCK'   /usr/include/  查看宏定义

在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/weixin_54792212/article/details/126051186