IO知识总结

IO,即`Input/Output`,指的是程序从外部设备或者网络读取数据到用户态内存/从用户态内存写数据到外部设备或者网络的过程。 ## 普通的IO过程 一般的IO,其流程为, 1. Java进程调用`read()` `write()`系统调用函数,进入内核态; 2. 内核中的相关程序将数据从设备缓冲区拷贝到内核缓冲区中; 3. 把数据从内核缓冲区拷贝到进程的地址空间中去 这就完成了一次`Input`,`Output`反之。 以磁盘IO为例,一次普通IO的流程如下: [![一次普通IO的流程.jpg](https://z1.ax1x.com/2023/12/08/pigjMLD.jpg)](https://imgse.com/i/pigjMLD) 这里有两个耗时的操作,一是从设备拷贝数据到内核缓冲区(磁盘准备数据慢,这里的设备缓冲区就是磁盘控制器的缓冲区,内核缓冲区就是`PageCache`),二是从内核缓冲区拷贝数据到进程的用户态内存空间(涉及到内核态到用户态CPU上下文的切换)。 内核缓冲区的作用是解决第二个问题,一次性拷贝一批数据,从而避免频繁且缓慢的磁盘IO或者与其他设备的IO。 字节缓冲流诸如`BufferedInputStream`作用是解决第一个问题,一次性从内核缓冲区拷贝一批数据到进程的缓冲区中,这个缓冲区位于进程的地址空间,之后接着取数据,如果缓冲区中还有数据,就无需系统调用而拿到数据,避免了大量的系统调用开销。 这是普通的IO操作,除此之外还有各种方式用于加快IO,譬如DMA、零拷贝技术等。 ## 网络IO 服务端如何实现高并发、海量连接与网络IO的方式有着千丝万缕的联系,与磁盘IO不同的是,网络IO是从网卡拿数据,仅此而已 在讨论网络IO的方式之前,我们应该先对阻塞/非阻塞、同步/异步的概念有一个比较清晰的认识: ### 阻塞IO与非阻塞IO 如上所述,一次IO过程中数据的流动大体可以分为两步 1. 硬件(磁盘、网卡等)到内核缓冲区 2. 内核缓冲区到用户态进程空间 通过进程在输入数据的时候,在第一步(也就是硬件到内核缓冲区)是否发生阻塞等待,可以将网络IO分为阻塞IO和非阻塞IO 具体来说,用户态进程发起了读写请求,但是内核态数据还未准备就绪(磁盘、网卡还没准备好数据), 1. 如果进程需要阻塞等待,直到内核数据准备好,才返回,则为阻塞IO; 2. 如果内核立马返回,不会阻塞进程,则为非阻塞IO; ### 同步IO与异步IO 在一次IO中数据传输的两个步骤中,但凡有一处发生了阻塞,就被称为同步IO;如果两个步骤都不阻塞,则被称为异步IO。 网络IO的方式可分为三种,分别是`BIO` `NIO`与`AIO`. ### BIO `BIO`是同步阻塞的IO,在`BIO`的方式下, 1. 用户态进程发起读写请求,若内核中的数据未准备好,则进程阻塞等待数据; 2. 在阻塞等待期间不能处理其他的网络IO请求,故为了可以处理海量连接请求,需要为每个连接(具体表现为一个与套接字关联的对象实例)提供一个线程来处理IO; 每个连接一个线程来处理的方式消耗大量的系统资源,因为线程会占用大概几MB内存,而我们的内存却是有限的,这样的方式注定无法处理太多的请求,这样就限制住了并发数量。 ### NIO `NIO`是同步非阻塞IO,在`NIO`的方式下,相比`BIO`有如下优势: 1. `NIO`不需要为每个网络连接开一个线程来处理,而是使用一个线程监听多个网络连接,当有连接的数据准备就绪,则进行处理,大大减少了处理并发所需的线程数量; 2. `NIO`中,当进行IO操作时,程序可以立即返回,而不需要等待内核数据就绪,通过轮询或者监听,程序可以知道哪些连接已经准备好了数据或者可以写入数据了。针对就绪的连接执行数据处理操作,而不会阻塞某一个特定的IO上,因此称为非阻塞IO; `NIO`是需要内核提供支持的,在创建了连接后,调用`fcntl(sockfd, F_SETFL, flags | O_NONBLOCK)`将其设置为非阻塞。 但是`NIO`也有较为明显的缺点:因为要轮询确定内核数据有没有就绪,这会产生大量的系统调用(每一次轮询是一次系统调用),这会大量消耗系统资源。 ### AIO `AIO`是异步非阻塞IO,当进行读写的时候,进程只需要调用API的`read`或`write`方法,当IO结束,调用回调函数通知用户线程直接去取数据就好了,与`NIO`不同的是,AIO是把数据从内核拷贝到用户态也交给了系统线程去处理,整个IO过程无需用户线程参与。 ## IO多路复用 为了解决上面提到的`NIO`会导致大量系统调用的问题,出现了IO多路复用模型。 IO多路复用不是简单的一个线程管理多个网络连接,因为在未采用IO多路复用的`NIO`中,就可以做到一个线程管理多个网络连接(依次轮询它所管理的网络连接),那么IO多路复用的本质应该是什么呢? IO多路复用实际上复用的是系统调用,它可以使用有限的系统调用来管理多个网络连接,具体地说,将一批网络连接丢给内核,让内核告诉我,哪几个连接的数据准备好了,这样一次系统调用就可以检查多个网络连接。 在Linux中,IO多路复用的实现主要有`select` `poll` `epoll`,都是采用上述思想设计的,不过它们之间又略有不同。 ### select `select`在使用时其实是一个函数,传入我们想要监听的文件描述符,程序在调用`select`时会阻塞,直到有文件描述符就绪或者超时,函数返回。 `select`返回已经就绪的文件描述符并遍历,逐个执行IO操作。 `select`的缺点是单个进程可以监视的文件描述符的数量有限,在Linux上的限制是1024。 ### poll `poll`可以看做是`select`的升级版本,它不限制可以监听的文件描述符的最大数量。 ### epoll `select`和`poll`所共有的缺点是,用户需要每次都将海量的的文件描述符集合从用户态传递到内核态,让内核去检测哪些文件描述符就绪了。 由于频繁的大量文件描述符拷贝,这里是比较耗时的,于是就有了`epoll`. 在调用`epoll_wait()`的时候会阻塞,直到有文件描述符就绪被返回,线程遍历就绪的文件描述符,依次进行IO操作。 相比于`select` `poll`,`epoll`无须频繁地拷贝大量的文件描述符,因为`epoll`预先在内核中分配了空间,添加需要监听的文件描述符只需要传递新增的文件描述符即可,大大提高了效率。

版权声明: 如无特别声明,本文版权归 月梦の技术博客 所有,转载请注明本文链接。

(采用 CC BY-NC-SA 4.0 许可协议进行授权)

本文标题:《 IO及IO模型 》

本文链接:https://ymiir.netlify.app//draft/2023-12-07-IO%E5%8E%9F%E7%90%86

本文最后一次更新为 天前,文章中的某些内容可能已过时!