高效IO——五种IO模型概念和非阻塞IO

Posted 两片空白

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了高效IO——五种IO模型概念和非阻塞IO相关的知识,希望对你有一定的参考价值。

目录

前言:

一.五种IO模型

二.同步通信和异步通信对比

三.阻塞和非阻塞

四.其它高级IO

        4.1 非阻塞IO


前言:

        内存于外设进行数据交互叫做IO。

        IO的过程中主要由两个动作,一个是等待,一个是拷贝。

比如:

        读IO,就是在等待可以读数据的条件,条件成立将数据从内核空间拷贝到用户空间。

        写IO,就是在等待可以写数据的条件,条件成立将数据从用户空间拷贝到内核空间。

        高效IO的本质是:单位时间内,尽可能的减少等待的比重。

一.五种IO模型

  • 阻塞IO:在内核数据准备好之前,系统调用会一直等待。

        所有套接字,默认方式都是等待。阻塞IO是最常见的IO模型。

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

        非阻塞IO需要程序员编码时,以循环的方式反复去尝试读写文件描述符,这个过程叫轮询。这对CPU来说是一种较大的浪费,一般只有在特定的情况下才使用。

  • 信号驱动IO:内核将数据准备好之后,使用SIGIO信号,通知进程进行IO操作。

        SIGIO信号:文件描述符准备就绪, 可以开始进行输入/输出操作,默认捕捉动作是忽略。我们可以自定义捕捉动作为recvfrom或者sendto。当信号到来时,就可以将数据从内核拷贝到用户,或者从用户拷贝到内核。   

        注意:数据拷贝的过程还是需要进程来做。

  • IO多路转接:通过系统调用,可以同时等待多个文件描述符的就绪状态。至少一个就绪,就可以进行读和写了。

        通过调用select/pol/epoll可以实现同一时刻等待多个文件描述符的就绪状态。

select/pol/epoll也是负责等待,那为什么效率提高了呢?

        因为同一时间等待多个文件,文件就绪的概率增加了,等待的时间就减少了。

        当等待完毕后,调用read或者write进行IO就不需要等待了。

  • 异步IO:由内核在数据拷贝完成时,通知进程,使用数据。

        不需要进程等待和将数据空内核空间拷贝到用户空间,这些动作都是内核做的,当内核拷贝完成后,通知进程来使用数据即可。

        这个和信号驱动IO不同,信号驱动IO,进程需要自己将数据从内核拷贝到用户。

总结:

前四种IO归类为同步IO,最后一个归类为异步IO。

任何IO过程中,都包含两个动作,一个是等待,另外一个是拷贝,在实际生活中,等待消耗的时间远远大于拷贝的时间。让IO高效,最核心的办法就是让等待的时间尽量少。

二.同步通信和异步通信对比

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

  • 同步通信:就是在发出一个调用时,在没有得到结果之前,该调用不会返回。一旦返回就得到返回值。调用者主动等待这个调用的结果。需要调用者自己参与。
  • 异步通信:调用在发出后,调用直接返回,没有返回结果。而被调用者通过状态,通知来通知调用者,或者通过回调函数处理这个调用。不需要调用者自己参与。

        拿上面的信号驱动IO举例,信号驱动IO的情况很像异步IO,但是,信号通信IO最后还需要调用者对数据进行拷贝,所以属于同步IO。

        在多线程和多进程中,也有同步和互斥的概念。和这里是完全不相同的。

  • 进程或线程的同步,是进程和线程之间的一种制约关系。
  • 多线程之间的同步,是让多个线程之间,可以按照一定次序运行。使得条件不满足时等待,满足时,可以立马执行。调高效率。

三.阻塞和非阻塞

        阻塞和非阻塞,是指条件不满足时,等待方式的不同。

  • 阻塞:在调用结果返回前,当前进程会被挂起等待,进程被放入等待队列,状态设为非R。调用线程只有在得到结果后才返回。
  • 非阻塞:在不能得到结果之前,当前线程不会被挂起等待,而是会直接返回。

四.其它高级IO

        非阻塞IO,记录锁,系统V流机制,IO多路转接(IO多路复用),readv和writev函数以及存储映射IO(mmap),这些统称为高级IO。

        下面介绍两种IO,非阻塞IOIO多路转接

        4.1 非阻塞IO

        一个文件描述符,默认情况下都是阻塞IO。调用fcntl函数可以改变文件描述符为非阻塞IO。

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

int fcntl(int fd, int cmd, ... /* arg */ );
参数作用
fd打开的文件描述符
cmd具体操作
...可变参数

fcntl函数的功能:

  • 复制一个现有的描述符,对应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

我们可以使用第三个功能,获得/设置文件状态标记,就可以将一个文件描述符设置为非阻塞。

使用如下:

void SetNoBlack(int fd){
  //获得文件状态
  int fl = fcntl(fd,F_GETFL);
  if(fl < 0){
    perror("fcntl error\\n");
    return;
  }
  //设置文件状态
  fcntl(fd, F_SETFL, fl | O_NONBLOCK);

}
  • 使用F_GETFL将当前的文件描述符属性取出来,是一个位图。
  • 然后使用F_SETFL将文件描述符设置回去,设置回去的同时加上 O_NONBLOCK参数。

轮询方式读取标准输入:

        轮询方式读取:read当读条件不满足是,实际是阻塞的。当设置为非阻塞后,不断循环调用read,就是在轮询检测read条件是否满足。

        说明:read非阻塞状态下,当返回值为小于0时,说明是读的条件不满足,而不是read调用失败。当读条件不满足时,不仅read的返回值回小于0,还会将全局变量错误码errno设置为EAGAIN(一个宏,值为11)。

#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>

void SetNoBlack(int fd){
  //获得文件状态
  int fl = fcntl(fd,F_GETFL);
  if(fl < 0){
    perror("fcntl error\\n");
    return;
  }
  //设置文件状态
  fcntl(fd, F_SETFL, fl | O_NONBLOCK);

}

int main(){
  //将标准输入设置为非阻塞
  SetNoBlack(0);
  char c = 0;
  //轮询检测read
  while(1){
    sleep(1);
    //printf("begin to read\\n");
    ssize_t n = read(0, &c, 1);
    if(n > 0){
      printf("%c\\n",c);
    }
    //并不是read错误,而是read的条件不满足
    else if(n < 0 && errno == EAGAIN){
      printf("read cond is not met....\\n");
    }
    else{
      perror("read error\\n");
    }
    printf("------------------\\n");

  }


  return 0;
}

 

为了看起来方便,我将多路转接IO写在了另外一篇博客。

以上是关于高效IO——五种IO模型概念和非阻塞IO的主要内容,如果未能解决你的问题,请参考以下文章

聊聊 Linux 中的五种 IO 模型

详解--高级IO

Linux五种IO模型

五种IO模型和BIO,NIO,AIO

Linux五种IO模型(同步 阻塞概念)

Linux设备驱动中的IO模型---阻塞和非阻塞IO