0
点赞
收藏
分享

微信扫一扫

五种IO模型

1kesou 2022-03-12 阅读 120

引入

在《Unix网络编程》一书中提到了五种IO模型,分别是:阻塞IO、非阻塞IO、多路复用IO、信号驱动IO以及异步IO,其中前4种IO都属于同步IO。
这5种IO同时也被称为Linux的五种网络IO模型。由于服务器端一般都是使用的Linux操作系统,所以了解这几种io模型是学习java网络io模型BIO(IO),NIO,AIO的基础。

操作系统的内核态和用户态

用户态字面理解就是用户使用的空间,内核态则是操作系统使用的空间。在IO中(文件IO或者是网络IO),都是由用户去调用Read读取内核态中的数据,读取数据到用户态;而write则是将数据从用户态写到内核态中,由内核去写入文件或者是通过网络IO(网卡)发送数据。

文件描述符fd

内核(kernel)利用文件描述符(file descriptor)来访问文件。文件描述符是非负整数。打开现存文件或新建文件时,内核会返回一个文件描述符。读写文件也需要使用文件描述符来指定待读写的文件。
实际上,文件描述符是一个索引值,指向内核为每一个进程所维护的该进程打开文件的记录表。当程序打开一个现有文件或者创建一个新文件时,内核向进程返回一个文件描述符。

IO操作过程:

对于一次IO操作(以read举例),数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间。所以说,当一个read操作发生时,它会经历两个阶段:

  1. 第一阶段:等待数据准备
  2. 第二阶段:将数据从内核拷贝到进程中

网络IO的本质是socket的读取,socket在linux系统被抽象为流,IO可以理解为对流的操作。对于socket流而言:

  1. 第一步:通常涉及等待网络上的数据分组到达,然后被复制到内核的某个缓冲区。
  2. 第二步:把数据从内核缓冲区复制到应用进程缓冲区。

同步IO和异步IO?

首先先讲下阻塞和非阻塞:
阻塞
阻塞调用是指调用结果返回之前,当前线程会被挂起(线程进入非可执行状态,在这个状态下,cpu不会给线程分配时间片,即线程暂停运行)。函数只有在得到结果之后才会返回。
非阻塞
非阻塞和阻塞的概念相对应,指在不能立刻得到结果之前,该函数不会阻塞当前线程,而会立刻返回。
阻塞IO和非阻塞IO的区别就在于: 应用程序的调用是否立即返回

然后是同步和异步:
同步
所谓同步,就是在发出一个功能调用时,在没有得到结果之前,该调用就不返回。也就是必须一件一件事做,等前一件做完了才能做下一件事。
有人也许会把阻塞调用和同步调用等同起来,实际上他是不同的。对于同步调用来说,很多时候当前线程还是激活的,只是从逻辑上当前函数没有返回,它还会抢占cpu去执行其他逻辑,也会主动检测io是否准备好。
异步
异步的概念和同步相对。当一个异步过程调用发出后,调用者不能立刻得到结果。实际处理这个调用的部件在完成后,通过状态、通知和回调来通知调用者。

对于IO来说,同步IO和异步IO的区别就在于: 数据拷贝的时候进程是否阻塞
同步IO:应用程序主动向内核查询是否有可用数据,如果有自己负责把数据从内核copy到用户空间,拷贝的过程中进程阻塞。
异步IO:应用程序向内核发起读数据请求需要:(1)告诉内核数据存放位置(2)注册回调函数,当内核完成数据copy后调用回调通知应用程序取数据。因为数据copy由内核完成的,所以拷贝的时候进程不阻塞。

换句话说,同步IO/异步IO最大区别:同步IO数据从内核空间到用户空间的copy动作是由应用程序自己完成。而异步IO则是注册回调函数并告知内核用户空间缓冲区存放地址,数据copy由内核完成。

参考:简述同步IO和异步IO的区别

五种IO模型

1、(同步)阻塞IO模型

在这里插入图片描述
阻塞IO是最传统的一种IO模型,即在读写数据过程中会发生阻塞现象。

当用户进程发出IO请求之后,内核会去查看数据是否就绪,如果没有就绪就会等待数据就绪,而用户进程就会处于阻塞状态,用户进程交出CPU。当内核等到数据就绪之后,进程就会将内核中的数据拷贝到用户内存,然后内核返回结果给用户线程,用户线程才解除block状态。
在这里插入图片描述

2、(同步)非阻塞IO模型

在这里插入图片描述
当用户进程发起一个IO操作后,并不需要等待,而是马上就得到了一个结果。如果结果是一个error时,它就知道数据还没有准备好,进程在返回之后,可以干点别的事情,然后它可以再次发送IO操作。一旦内核中的数据准备好了,并且又再次收到了用户进程的请求,那么进程就会将内核中的数据拷贝到用户内存,然后返回。需要注意,拷贝数据整个过程,进程仍然是属于阻塞的状态。

在这里插入图片描述

3、IO多路复用模型

在这里插入图片描述
所谓I/O多路复用机制,就是说通过一种机制,可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。这种机制的使用需要额外的功能来配合: select、poll、epoll。
select、poll,epoll本质上都是同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的。

Linux 用 select/poll/epoll 函数实现 IO 复用模型,这些函数也会使进程阻塞,但是和阻塞IO所不同的是这些函数可以同时阻塞多个IO操作。而且可以同时对多个读操作、写操作的IO函数进行检测。直到有数据可读或可写时,才真正调用IO操作函数。

  • select时间复杂度O(n),它仅仅知道了,有I/O事件发生了,却并不知道是哪几个流(可能有一个,多个,甚至全部),我们只能无差别轮询所有流,找出能读出数据,或者写入数据的流,对他们进行操作。所以select具有O(n)的无差别轮询复杂度,同时处理的流越多,无差别轮询时间就越长。
  • poll(翻译:轮询)时间复杂度O(n),poll本质上和select没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态,
    但是它没有最大连接数的限制,原因是它是基于链表来存储的.
  • epoll时间复杂度O(1),epoll可以理解为event poll,不同于忙轮询和无差别轮询,epoll会把哪个流发生了怎样的I/O事件通知我们。所以我们说epoll实际上是事件驱动(每个事件关联上fd)的,此时我们对这些流的操作都是有意义的。(复杂度降低到了O(1))。

在多路复用IO模型中,会有一个内核线程不断去轮询多个socket的状态,只有当真正读写事件发生时,才真正调用实际的IO读写操作。因为在多路复用IO模型中,只需要使用一个线程就可以管理多个socket,系统不需要建立新的进程或者线程,也不必维护这些线程和进程,并且只有在真正有读写事件进行时,才会使用IO资源,所以它大大减少了资源占用。
在这里插入图片描述

4.信号驱动IO模型

在这里插入图片描述

Linux 用socket进行信号驱动 IO,用户线程发起一个IO请求操作会给对应的socket安装一个信号处理函数,进程继续运行并不阻塞,当IO事件就绪,进程收到SIGIO 信号,然后处理 IO 事件。
在这里插入图片描述内核在第一个阶段是异步,在第二个阶段是同步;与非阻塞IO的区别在于它提供了消息通知机制,不需要用户进程不断的轮询检查,减少了系统API的调用次数,提高了效率。

这个一般用于UDP中,对TCP套接口几乎是没用的,原因是该信号产生得过于频繁,并且该信号的出现并没有告诉我们发生了什么事情。在UDP上,SIGIO信号会在下面两个事件的时候产生:
1 数据报到达套接字
2 套接字上发生错误
因此我们很容易判断SIGIO出现的时候,如果不是发生错误,那么就是有数据报到达了。
而在TCP上,由于TCP是双工的,它的信号产生过于频繁,并且信号的出现几乎没有告诉我们发生了什么事情。因此对于TCP套接字,SIGIO信号是没有什么使用的。

5、异步IO模型

前面四种IO模型实际上都属于同步IO,只有最后这一种是真正的异步IO,因为无论是多路复用IO还是信号驱动模型,IO操作的第2个阶段都会引起用户线程阻塞,也就是内核进行数据拷贝到进程中 的过程都会让用户线程阻塞。

Linux中,可以调用 aio_read 函数告诉内核描述字缓冲区指针和缓冲区的大小、文件偏移及通知的方
式,然后无论内核数据是否准备好,都会直接返回,用户态进程可以去做别的事情,当内核将数据拷贝到缓冲区之后,再通知应用程序。IO两个阶段,进程都是非阻塞的。
在这里插入图片描述Linux提供了AIO库函数实现异步,但是用的很少。目前有很多开源的异步IO库,例如libevent、libev、libuv。

参考:聊聊Linux 五种IO模型
Linux的5种网络IO模型详解 这篇博客描述上有些问题

举报

相关推荐

0 条评论