文章目录

  • IO 是什么
  • I/O模型
    • 阻塞 IO
    • 非阻塞 IO
    • IO 多路复用
    • 信号驱动式 IO
    • 异步 IO
  • 五种 IO 模型对比

IO 是什么

IO 全称为 Input/Output ,翻译过来就是 输入/输出。

计算机角度的 IO
我们日常所说的输入输出,比较直观的意思就是计算机的输入/输出,计算机就是主体。

在计算机当中我们可以分为 5 个部分:运算器、控制器、存储器、输入十本、输出设备。

输入设备是向计算机输入数据和信息的设备,键盘,鼠标都是属于输入设备;
输出设备是向计算机硬件系统的终端设备,用于接收计算机数据的输出显示。像显示器、打印机就都是属于输出设备。

操作系统角度的 IO
我们要将内存中的数据写入到磁盘的话,那么主体就是一个程序。

操作系统就是负责计算机的资源管理和进程的调度,我们电脑运行着的应用程序,其实是需要经过操作系统,才能做出一些特殊的操作,如磁盘文件的读写,内存的读写等等。

真正的 IO 是在操作系统上执行的。即当前程序的 IO 操作分为两种动作:IO 调用和 IO 执行。IO 调用是由进程(应用程序的运行态)发起的,而 IO 执行是操作系统内核的工作。

而应用程序发起的一次 IO 操作是包含了两个阶段的:
IO 调用:应用程序进程向操作系统内核发起调用。
IO 执行:操作系统内核完成 IO 操作。

I/O模型

阻塞 IO

假设应用程序的进程发起 IO 调用,但是如果内核的数据还没有准备好的话,那么应用程序进程就一直在阻塞等待,一直等到内核数据准备好了,从内核拷贝到用户空间,才返回成功提示,此次 IO 操作,就称呼为 阻塞 I/O

阻塞 I/O 比较经典的应用就是 阻塞 socket,Java BIO。

优点:开发简单,容易入门;在阻塞等待期间,用户线程挂起,在挂起期间是不会占用 CPU 资源的。
缺点:一个线程维护一个 IO,是不适合大并发,在并发量大的时候需要创建大量的线程来维护网络的连接,内存、线程开销也非常大。

非阻塞 IO

就是在内核还没有准备好数据的时候就会返回错误码,而调用的程序是不会休眠的,而是不断轮询询问内核数据是否准备好。

如果数据没有准备好的话,不像阻塞式 IO 那样会一直被阻塞,而是返回一个错误码,数据准备好的时候,函数就成功返回。

应用程序对这样一个非阻塞描述符循环调用称为轮询。

非阻塞式 IO 的轮询会耗费大量的 CPU ,通常在专门提供某一功能的系统中才会被使用,通过为套接字的描述符属性设置为非阻塞式,可使用该功能。

非阻塞 IO 的流程:
● 应用进程向操作系统内核,发起 recvfrom() 读取数据。
● 操作系统内核数据没有准备好,立即返回 EWOULDBLOCK 错误码。
● 应用程序进程轮询调用,继续向操作系统内核发起 recvfrom 速去数据。
● 操作西游内核数据准备好了,从内核缓冲区拷贝到用户空间。
● 完成调用,返回成功提示。

recvfrom() 用来接收远程主机指定的 socket 传来的数据,并把数据传到由参数 buf 指向的内存空间。

优点:每次发起 IO 调用,在内核等待数据的过程中可以立即返回,用户线程不会阻塞,实时性较好。

缺点:多个线程不断轮询内核是否有数据,占用大量 CPU 的时间,效率不高。一般 Web 服务器不会采用此模式。在针对这个问题,就可以使用 IO 复用模型去解决这个问题。

IO 多路复用

类似于非阻塞,只不过轮询不是由用户线程去执行的,而是由内核去轮询的,内核监听程序监听到数据准备好后,调用内核函数复制数据到用户态。

select 这个系统调用,充当代理类的角色,不断轮询注册到它这里的所有需要的 IO 文件描述符,有结果时,把结果告诉被代理的 recvfrom 函数,它本身再亲自出马去拿数据。

IO 多路复用至少有两次系统调用,如果只有一个代理对象,性能上是不如前面的 IO 模型的,但是由于它可以同时监听很多套接字,所以性能比前两个都好。

IO 多路复用的核心思想:系统给我们提供了一个函数(如:select、poll、epool),它们可以同时监听多个 fd 的操作,任何一个返回内核数据就绪,应用进程再发起 recvfrom 系统调用。

fd : 文件描述符(File Descriptor),它是计算机科学中的一个术语,形式上是一个非负整数,当程序打开一个现有文件或者创建一个新文件的时候,内核向进程返回一个文件的描述符。

在多路复用里包括:
● select:线性扫描所有监听的文件描述符,不管他们是不是活跃的,有最大数据的限制(32位系统里面为 1024,64位操作系统里面为 2048)

● poll:和 select 一样,不过数据结构不同,需要分配一个 pollfd 结构数组,维护在内核中,它没有大小限制,不过需要很多复制操作。

● epoll:用于代替 select 和 poll 。它没有大小限制,使用一个文件描述符(fd)管理多个文件描述符,使用红黑树存储,同时用事件驱动代替了轮询。epoll_ctl 中注册的文件描述符在时间触发的时候会通过回调机制激活该文件描述符。epoll_wait 便会收到通知,最后,epoll 还采用了 mmap 虚拟内存映射技术较少用户态和内核态数据传输的开销。
epoll 这里去掉了遍历文件描述符的坑爹操作,而是采用了监听事件回调的机制,这就是 epoll 最大的亮点。

优点:系统不必创建维护大量的线程,只使用一个线程,一个选择器即可同时处理成千上万个连接,大大较少了系统的开销。

缺点:在本质上,elect/poll系统调用的是阻塞式的,是属于同步 IO,需要在读写操作就绪后,由系统调用进行阻塞的读写。

select、poll、epoll区别

selectpollepoll
底层数据结构数组链表红黑数和双链表
获取就绪的fd遍历遍历事件回调
事件复杂度O(n)O(n)O(1)
最大连接数1024无限制无限制
fd数据拷贝每次调用 select ,需要将 fd 数据从用户空间拷贝到内核空间每次调用 poll,需要将 fd 数据从用户空间拷贝到内核空间使用内存映射(mmap),不需要从用户空间频繁拷贝 fd 数据到内核空间。

信号驱动式 IO

在上面 epoll 明显的优化了 IO 的执行效率,但在进程中调用 epoll_wait() 时,仍然可能会被阻塞。在这能不能不用我们老是去询问数据是否准备好,等发出请求后了,数据准备好了通知我就就行了,就诞生了 型号驱动 IO 模型

执行流程:
使用信号,内核在数据准备就绪的时候通过信号哎进行通知。

首先开启信号驱动 IO 套接字,并使用 sigaction 系统调用来安装信号处理程序,内核直接返回,不会阻塞用户态。

数据准备好时,内核会发送 SIGIO 信号,收到信号后开始进行 IO 操作。

说通俗一定就是:信号驱动下不会再主动询问的方式去确认数据是否就绪,而是向内核发送一个信号,然后应用用户进程就可以去做别的事,不用阻塞,当内核数据准备好后,载通过信号通知应用进程,数据准备好后的可读状态。应用用户进程收到信号之后,立即调用 recvfrom。去读区数据。

信号驱动 IO 模型,在应用进程发出信号后,是立即返回的,不会阻塞进程。它已经有一处操作的感觉了。但是上面的流程图当中。发现数据复制到应用缓冲的时候,应用进程还是阻塞的。回过头来看下这些IO在数据从内核复制到应用缓冲的时候,都是阻塞的。

异步 IO

异步 IO 依赖信号处理程序来进行通知。

不过异步 IO 与前面 IO 模型不同的是:前面的都是数据准备阶段的阻塞与非阻塞,异步 IO 模型通知的是 IO 操作已经完成,而不是数据准备完成。返回的不是处理结果,而是表示提示成功l类似的意思。

异步 IO 才是真正的非阻塞,主进程只负责做自己的事情,等 IO 操作完成(数据成功从内核缓存区复制到应用程序缓冲区)时通过回调函数对数据进行处理。

unix 中异步 IO 函数以 aio_或 lio_ 打头。

异步 IO 的优化思路是很简单的,只需要向内核发送一次请求,就可以完成数据状态的询问和数据拷贝的所有操作,并且不用阻塞等待结果。

优点:真正实现了异步非阻塞,吞吐量在这几种模式中是最高的。

缺点:应用程序主需要进行事件的注册与接收,其余工作都是交给了操作系统的内核,所以需要内核提供支持,在 Linux 系统中,异步 IO 在其2.6才引入,到目前为止也还不是非常完善,在底层的实现仍然使用的是epoll,与 IO 多路复用相同,因此在性能上是没有明显占优的。

五种 IO 模型对比

掐面 4 种 IO 模型的主要区别是在第一阶段,它们第二阶段是一样的;数据从内核缓冲去复制到调用者缓冲区期间都被阻塞住。

前面四种 IO 都是同步 IO;IO操作导致请求进程阻塞,直到 IO 操作完成。

异步 IO:IO 操作不导致请求进程阻塞。

IO 模型
阻塞 I/O 模型同步阻塞
非阻塞 I/O 模型同步非阻塞
I/O 多路复用模型同步阻塞
信号驱动 I/O 模型同步非阻塞
异步 I/O(AIO)模型异步非阻塞