1.IO 简介
IO(即 Input/Output 首字母的简写)是计算机系统与外部世界之间进行数据交换的过程。在计算机科学和信息科学领域,IO 是指从外部设备(如磁盘、键盘、显示器)读取(Input)数据到计算机内部,或将计算机内部处理的数据输出(Output)到外部设备的过程。 通常用户进程中的一个完整 IO 分为两阶段:用户进程空间<-->内核空间、内核空间<-->设备空间(磁盘、网络等)。IO(Input/Output)在计算机科学中包含以下几种类型的操作(通常开发中所说的 IO 泛指文件 IO 和网络 IO):
- 文件 IO(File I/O):文件 IO 是指对文件的读取和写入操作。通过文件 IO,计算机可以从文件中读取数据或将数据写入到文件中。
- 网络 IO(Network I/O):网络 IO 是指计算机与网络中其他设备进行数据交换的操作。通过网络 IO,计算机可以通过网络连接发送和接收数据,例如通过网络下载文件或与远程服务器通信。
- 控制台 IO(Console I/O):控制台 IO 是指与计算机的控制台交互的操作。它包括从键盘读取输入和将输出显示在屏幕上。
- 内存 IO(Memory I/O):内存 IO 是指在计算机内存中读取和写入数据的操作。通过内存 IO,计算机可以在内存中读取和修改数据,而无需进行磁盘或网络 IO。
在 LINUX 中由于进程无法直接操作 I/O 设备,其必须通过系统调用请求 kernel(内核)来协助完成 I/O 操作;由于设备 IO 一般速度较慢,内核会为每个 I/O 设备维护一个缓冲区作为缓存用于提升 IO 性能。对于一个输入操作来说,进程 IO 系统调用后,内核首先会检查缓冲区是否存在相应的缓存数据,如果内核缓冲区存在缓存数据则直接复制到进程空间,否则将从 IO 设备中读取。因此,对于一个网络输入操作通常包括两个不同阶段:
- 等待网络数据到达网卡 → 读取到内核缓冲区,数据就绪。
- 从内核缓冲区复制数据到进程空间。
2.五种 IO 模型
在《UNIX 网络编程》这本书中将 IO 操作分为五种 IO 模型,五种 IO 模型分别是阻塞 IO 模型、非阻塞 IO 模型、IO 复用模型、信号驱动的 IO 模型、异步 IO 模型。在五种 IO 模型中只有异步 IO 模型是异步 IO 操作,其余模型均为同步 IO 操作。
2.1 阻塞 IO 模型(Blocking I/O)
在阻塞 IO 模型中,应用程序发起 IO 操作后,线程会被阻塞(即挂起)直到操作完成。当进行读取操作时,如果没有数据可读,则线程会一直等待,直到有数据到达;当进行写入操作时,如果写缓冲区已满,则线程会一直等待,直到有空闲的缓冲区可用。
阻塞 IO 模型最典型的应用是阻塞 Socket、Java BIO,实现简单适用于并发量小的场景,在大规模并发场景下会导致资源浪费和性能问题。
2.2 非阻塞 IO 模型(Non-blocking I/O)
在非阻塞 IO 模型中,应用程序发起 IO 操作后,会立即返回,而不会阻塞等待操作完成。当进行读取操作时,如果没有数据可读,则会立即返回一个错误码或空值。当进行写入操作时,如果写缓冲区已满,则会立即返回一个错误码或异常。非阻塞 IO 模型需要通过循环轮询的方式来检查 IO 操作的就绪状态,通常依赖于底层 API(如 select、poll、epoll 等)来实现。非阻塞 IO 模型适用并发量较小、且不需要及时响应的网络应用开发。
2.3 IO 复用模型(Multiplexing I/O)
IO 复用模型允许应用程序同时监视多个 IO 操作的就绪状态,通过阻塞等待多个 IO 事件中的任意一个事件发生。当某个 IO 事件就绪时,操作系统通知应用程序进行处理,可以通过轮询、回调或信号等方式进行处理。常见的 IO 复用机制有 select、poll、epoll 等,这些机制允许应用程序同时监视多个文件描述符的状态,提高了并发处理能力。在 Linux 中 IO 复用的实现方式主要有 select、poll 和 epoll 三种:
- Select:注册 IO、阻塞扫描,监听的 IO 最大连接数不能多于 FD_SIZE。
- Select 是最早的一种 IO 复用机制,支持将多个文件描述符(通常是 socket)放入一个文件描述符集合中,并通过 select 函数等待其中任何一个文件描述符就绪。
- Select 是最早的一种 IO 复用机制,支持将多个文件描述符(通常是 socket)放入一个文件描述符集合中,并通过 select 函数等待其中任何一个文件描述符就绪。
- Select 的缺点是效率较低,它需要线性扫描所有文件描述符,且有文件描述符数量限制。
- Poll:原理和 Select 相似,无数量限制,IO 数量越大扫描线性性能越差。
- poll 是 select 的改进版,通过使用更灵活的数据结构(struct pollfd)来存储文件描述符信息,解决了 select 中文件描述符数量的限制问题。
- poll 函数会阻塞,直到集合中的任何一个文件描述符就绪或超时时间到达。
- poll 的工作方式类似于 select,但由于使用了更高效的数据结构,它在大规模并发连接的情况下表现更好。
- Epoll:Epoll 是 Linux 特有的高性能 IO 复用机制(Linux2.6 后内核支持),相对于 select 和 poll,在处理大量并发连接时表现更出色。事件驱动不阻塞,mmap 实现内核与用户空间的消息传递,数量很大。
- Epoll 使用 epoll_create 函数创建一个 epoll 对象,并使用 epoll_ctl 函数注册和删除文件描述符。
- 通过 epoll_wait 函数等待事件的发生,它会阻塞,直到有文件描述符就绪或超时时间到达。
- Epoll 提供了两种工作模式:边缘触发(EPOLLET)和水平触发(EPOLLONESHOT)。Epoll 默认采用边缘触发(EPOLLET)模式,只在状态变化时通知应用程序,减少了无效的系统调用,因此资源占用更低。
Redis 和 Nginx 在 Linux 中均采用 Epoll 作为 IO 复用机制来提升性能,提高并发的网络连接数量。
2.4 信号驱动式 IO(Signal-driven I/O)
信号驱动 IO 模型中,应用程序通过注册信号处理函数,当 IO 操作就绪时,操作系统会发送一个信号来通知应用程序进行处理。在信号驱动 IO 模型中,应用程序不需要进行轮询或阻塞等待,而是通过信号处理函数来处理 IO 事件。信号驱动 IO 模型适用于一些异步的、事件驱动的场景,但在实践中较少使用,因为信号驱动 IO 的实现比较复杂且容易出错。
2.5 异步式 IO(Asynchronous I/O)
异步 IO 模型中,应用程序发起 IO 操作后,不需要等待操作完成,而是继续执行其他任务。当 IO 操作完成时,操作系统会通知应用程序,并通过回调函数或事件处理器来处理已完成的 IO 操作。异步 IO 模型通常需要使用特定的异步 IO API,并通过回调或事件处理器来处理 IO 事件。它提供了高并发、高吞吐量的 IO 处理能力,适用于复杂的并发场景。
3.五种 IO 模型对比
阻塞 IO 模型:
- 特点:应用程序发起 IO 操作后会一直阻塞等待操作完成。
- 优点:使用简单,易于理解和实现。
- 缺点:单线程只能处理一个 IO 操作,效率低下,不能充分利用系统资源。
非阻塞 IO 模型:
- 特点:应用程序发起 IO 操作后立即返回,通过轮询来检查操作的就绪状态。
- 优点:相对于阻塞 IO,能够处理多个 IO 操作,提高了并发性能。
- 缺点:轮询的方式会带来较高的 CPU 开销,可能导致性能下降。
IO 复用(多路复用)模型:
- 特点:通过 IO 复用机制同时监视多个 IO 操作的就绪状态。
- 优点:能够处理大量并发连接,通过事件驱动模型提高了并发性能。
- 缺点:实现复杂,需要使用特定的 API(如 select、poll、epoll 等)。
信号驱动 IO 模型:
- 特点:应用程序通过注册信号处理函数,在 IO 操作就绪时接收到系统发送的信号。
- 优点:相对于轮询的方式,减少了 CPU 开销,适用于异步、事件驱动的场景。
- 缺点:实现较复杂,信号的传递和处理可能带来一些额外的开销。
异步 IO 模型:
- 特点:应用程序发起 IO 操作后不需要等待操作完成,通过回调函数或事件处理器处理操作结果。
- 优点:高并发、高吞吐量,适用于复杂的并发场景。
- 缺点:实现较复杂,需要使用特定的异步 IO API,需要处理回调和事件驱动的机制。
Java知识库