什么是IO?
IO:Input/Output,即数据的读取(接收)/写入(发送)操作,针对不同的数据存储媒介,大致可以分为网络 IO 和磁盘 IO 两种。在 Linux 系统中,为了保证系统安全,操作系统将虚拟内存划分为内核空间和用户空间两部分。因此用户进程无法直接操作IO设备资源,需要通过系统调用完成对应的IO操作。
1、I/O多路复用简介
I/O 多路复用(I/O Multiplexing)是一种高效处理多个 I/O 操作的技术,常用于提高网络服务器或应用程序的并发处理能力。它允许一个线程或进程同时监控多个 I/O 操作,而无需为每个操作创建独立的线程或进程。
以下是对 I/O 多路复用的简介及其用途的详细说明:
阻塞IO和非阻塞IO都是早期最常见的网络编程模型,但是他们有着致命的缺点。考虑如下场景:
说明:
每个用户请求都需要使用一个单独的线程进行服务,同时还要请求应用B才能完成业务逻辑。
由于不知道应用B的响应数据何时会返回,那么只能选择阻塞IO或者非阻塞IO进行轮询。
然而阻塞IO会导致线程被挂起,非阻塞IO会导致线程一直处于轮询状态。这两种情况都会导致线程无法被释放或者复用。随着用户请求数的增多,应用A不得不创建更多的线程。然而对于操作系统来说,可以创建的线程是有上限的,并且过多的线程会导致线程切换的时间变多,严重时可能导致系统卡死,无法对外提供服务。这也是著名的C10K问题。
为了解决这个问题,于是人们就提出了方案:由一个或者几个线程去监控多个网络请求,由他们去完成数据准备阶段的操作。当有数据准备就绪之后再分配对应的线程去读取数据,这样就可以使用少量的线程维护大量的网络请求,这就是IO多路复用。
IO 多路复用的复用指的是复用线程,而不是IO连接;目的是让少量线程能够处理多个IO连接。
2、IO多路复用特点
- 一个或一组线程(线程池)处理多个TCP连接
- IO多路复用使用两个系统调用(select/poll/epoll和recvfrom),blocking IO只调用了recvfrom
- select/poll/epoll核心是可以同时处理多个connection,而不是更快,所以连接数不高的话,性能不一定比多线程+阻塞IO好。
- select是内核提供的多路分离函数,使用它可以避免同步非阻塞IO中轮询等待问题。
IO多路复用是一种同步IO模型,实现一个线程可以监视多个文件句柄。一旦某个文件句柄就绪,就能够通知应用程序进行相应的读写操作;没有文件句柄就绪时会阻塞应用程序,交出CPU。
IO是指网络 IO,多路指多个TCP连接(即 socket),复用指复用一个或几个线程。
意思说一个或一组线程处理多个 TCP 连接。最大优势是减少系统开销小,不必创建过多的进程/线程,也不必维护这些进程/线程。
IO 多路复用的三种实现方式:select、poll、epoll
举例说明:
假设你是一名老师,让30个学生完成一道题目,并检查他们的结果是否正确,现在有以下几种情况:
1、按顺序逐个检查,先检查A,然后B,之后C、D…这中间如果有学生卡住,后面的学生都会被耽搁
2、找来30个助教,每个助教负责检查一个学生
3、站在讲台上,那个学生写完了举手示意,谁举手就去检查谁
上述场景1对应传统单线程socket模型,缺点非常明显:同时只能处理一个客户端的请求,多余的请求可以通过队列的方式保存起来,后续一次遍历处理。但如果处理某个请求时阻塞了,那么后续所有请求的处理都会阻塞。
上述场景2对应传统多线程socket模型,一般都是通过主线程阻塞等待客户端连接,每个客户端连接创建新的工作线程来处理请求的方式实现。当并发量不是很大时,这种处理方式还可以使用。一旦并发量很大,频繁创建的线程会带来巨大的资源消耗以及上下文切换消耗。
上述场景3就可以理解为I/O多路复用技术︰将客户端对应socket的fd(文件描述符)注册到select或poll或epoll上,当socket流就绪时,select线程就会执行:轮询找到就绪的socket,将它返回给应用,执行相应流处理。
从这里也就可以看出,IO多路复用技术的核心是减少服务端线程的创建,通过使用较少线程处理所有请求的方式提高整体效率。
常见I/O模型
同步阻塞IO(Blocking IO):即传统IO模型
同步非阻塞IO(Non-blocking IO):默认常见的socket都是阻塞的,非阻塞IO要求socket被设置成NONBLOCK
IO多路复用(IO Multiplexing):即经典的Reactor设计模式,也被称为异步阻塞IO,Java中的selector和linux中的epoll都是这种模型
异步IO(Asychronous IO):即Proactor设计模式,也被称为异步非阻塞IO
同步和异步是指内核通知用户线程的方式。
用户进程/线程和内核是以传输层为分割线的
传输层以上是指用户进程
传输层以下(包括传输层)是指内核(处理所有通信细节,发送数据,等待确认,给无序到达的数据排序等,这四层是操作系统内核的一部分)
同步
用户线程发起IO请求后需要等待或者轮询内核IO操作,完成后才能继续执行
异步
用户线程发起IO请求后仍继续执行,当内核IO操作完成后回通知用户线程,或调用用户线程注册的回调函数。
阻塞和非阻塞是指用户线程调用内核IO操作的方式。
阻塞
IO操作需要彻底完成后才能返回用户空间
非阻塞
IO操作被调用后立即返回给用户一个状态值,无需等待IO操作彻底完成
3、 I/O 多路复用三种实现方式说明
I/O 多路复用是一种机制,通过这种机制,应用程序可以监控多个 I/O 流(如网络连接、文件描述符等)的状态,判断它们是否可以进行读写操作,而不需要为每个 I/O 流创建独立的线程或进程。
其核心思想是将多个 I/O 操作的状态集中在一个地方进行管理,从而提高效率和资源利用率。
主要方法
- select
最早的 I/O 多路复用机制,通过检查一组文件描述符来判断它们是否可以进行读写操作。select 方法存在一些限制,例如文件描述符的数量限制和性能瓶颈。 - poll
poll 是对 select 的改进,克服了 select 的一些限制,如文件描述符数量的限制。它使用一个事件数组来跟踪文件描述符的状态。 - epoll
epoll 是 Linux 下的一种高效 I/O 多路复用机制,适用于大量并发连接的场景。它解决了 select 和 poll 在处理大量连接时的性能问题,支持水平触发(Level Triggered)和边缘触发(Edge Triggered)模式。 - kqueue
kqueue 是 FreeBSD、macOS 和其他一些 BSD 系统上的 I/O 多路复用机制,类似于 epoll,提供了高效的事件通知机制。 - IOCP
I/O 完成端口(I/O Completion Ports)是 Windows 上的 I/O 多路复用机制,适用于处理大量并发 I/O 操作的场景。
3、 I/O 多路复用的用途
提高并发性能
- 高效管理大量连接
在网络服务器中,I/O 多路复用可以高效地管理大量并发连接。通过单线程处理多个连接,减少了线程切换的开销,从而提高了整体性能。 - 资源节约
避免为每个连接创建一个独立的线程或进程,减少了系统资源的消耗,如内存和 CPU 时间。
提高响应能力 - 低延迟处理
I/O 多路复用能够在事件发生时立即通知应用程序进行处理,从而减少了等待时间和处理延迟,提高了系统的响应能力。 - 异步处理
支持异步处理 I/O 操作,使得应用程序可以在处理 I/O 操作时继续进行其他工作,而不是阻塞等待。
实现高效的网络服务 - 高性能服务器
许多高性能网络服务器(如 Nginx 和 Redis)使用 I/O 多路复用技术来处理大量并发连接和请求,以实现高吞吐量和低延迟。 - 事件驱动模型
现代的事件驱动框架和库(如 Node.js)依赖于 I/O 多路复用技术来处理并发 I/O 操作,从而实现高效的事件处理和回调机制。
5. I/O 多路复用的工作流程
1.注册事件
应用程序将感兴趣的 I/O 流(如网络连接)的状态(读、写、异常等)注册到 I/O 多路复用机制中。
2.等待事件
应用程序调用 I/O 多路复用接口,等待事件的发生。此时,应用程序会被挂起,直到注册的 I/O 流中有事件发生。
3.处理事件
当事件发生时,I/O 多路复用机制会通知应用程序。应用程序可以检查哪些 I/O 流有事件发生,并进行相应的处理。
4.重复循环
应用程序处理完事件后,可以继续注册新的事件,重复等待和处理过程。
总结
I/O 多路复用通过集中管理多个 I/O操作,避免了传统的多线程或多进程模型带来的资源开销,提高了并发处理能力和系统性能。
它广泛应用于网络服务器、高性能应用程序和事件驱动模型中,帮助实现高效的I/O 操作和响应。