【Linux系统】I/O多路复用

发布于:2025-06-25 ⋅ 阅读:(19) ⋅ 点赞:(0)

IO模型

IO模型即用什么样的通道进行数据的发送和接收。
Java共支持3种网络变成IO模式:BIO、NIO、AIO,为Java语言对操作系统的各种IO模型的封装。在使用这些API的时候,不涉及操作系统。

同步与异步

  • 同步:即发起一个调用后,被调用者处理完请求之前,调用不返回。
  • 异步:即发起一个调用后,立刻得到被调用者的回应表示已接收到请求,但是被调用者并没有返回结果,此时可以处理其他请求,被调用者通常依靠事件、回调等机制来通知调用者其返回结果。
  • 两者区别最大在于异步的话调用者不需要等待结果处理,被调用者会通过回调等机制来通知调用者返回结果。

阻塞和非阻塞

  • 阻塞:即发起一个请求,调用者一直等待请求结果返回,也就是当前线程会被挂起,无法从事其它任务,只有当条件就绪才能继续。
  • 非阻塞:即发起一个请求,调用者不用一直等着结果返回,可以先去干其他事情。

同步异步与阻塞非阻塞

例子:老张拿两个水壶(不会响的水壶和会响的水壶)烧水。

  • 同步异步,对水壶而言。普通水壶不能提示水烧开了,属于同步;响的水壶能提示水烧开了,属于异步。
  • 阻塞非阻塞,对老张而言。干等水烧开的老张,属于阻塞;干其他事情等水开的老张,属于非阻塞。

一般异步都会配合非阻塞使用,否则无法发挥异步的效用

常见的I/O模型对比

所有的系统I/O都分为两个阶段:等待就绪和操作。
等待就绪的阻塞是不使用CPU的,是在“空等”;而真正的读操作的阻塞是使用CPU的,真正在“干活”,而这个过程非常快,属于memory copy,带宽通常在1GB/s级别以上,可以理解为基本不耗时。
如下几种常见的I/O模型对比:
常见的IO模型

  • 以socket.read()函数为例子:
    • BIO:如果TCP RecvBuffer里没有数据,函数会一直阻塞,直到收到数据,返回读到的数据。
    • NIO:如果TCP RecvBuffer里有数据,就把数据从网卡读入内存,并且返回给用户;反之则直接返回0,永远不会阻塞。
    • AIO:不但等待就绪是非阻塞的,数据从网卡到内存的过程也是异步的。

BIO(Blocking I/O)

同步阻塞I/O模式,数据的读取写入必须阻塞在一个线程内等待其完成(一个客户端连接对于一个处理线程)。

  • 传统BIO
    传统bio

由一个独立的Acceptor线程负责监听客户端的连接。一般通过while(true)循环中服务端会调用accept()方法等待客户端连接的方式监听请求,请求一旦收到一个连接请求,就可以建立通信套接字在这个通信套接字上进行读写操作,此时不能再接收其他客户端连接请求,只能等待当前连接的客户端的操作执行完成。
如果要让BIO通信模型能够同时处理多个客户端的请求,就必须使用多线程(因为socket.accept()、socket.read()、socket.write()涉及的三个主要函数都是同步阻塞的),当一个连接在处理I/O时,系统是阻塞的。开启多线程,就可以让CPU去处理更多的事情。
即接收到客户端连接请求后为每个客户端创建一个新的线程进行链路处理,处理完成后,通过输出流返回给客户端,线程销毁。此即典型的-请求-应答通信模型。
使用多线程的本质:利用多核;当I/O阻塞系统,但CPU空闲时,可以利用多线程使用CPU资源。
在Java虚拟机中,线程是宝贵的资源,主要体现在:

  1. 线程的销毁成本很高,尤其在Linux系统中,线程本质上就是一个进程。
  2. 线程本身占用较大内存,像Java的线程栈,一般至少分配512k-1M的空间。
  3. 线程的切换成本很高。操作系统发生线程切换时,需要保留线程的上下文,然后执行系统调用。如果线程数过高,可能执行线程切换的时间甚至会大于线程执行的时间。
  4. 容易造成锯齿状的系统负载。系统的负载是用活动线程数和CPU核心数,一旦线程数量高但外部网环境不是很稳定,就很容易造成大量请求的结果同时返回,激活大量阻塞进程从而使系统负载压力过大。
  • 伪异步I/O
    为了上述一个链路需要一个线程处理的问题,优化为通过一个线程池来处理多个客户端的请求接入,形成客户端个数M。
    伪异步
    当有新的客户端接入时,将客户端的Socket封装成一个Task(该任务实现java.lang.Runnable接口)投递到后端的线程池中进行处理,JDK的线程池维护一个消息队列和N个活跃进程,对消息队列中的任务进行处理。
    由于线程池可以设置消息队列的大小和最大线程数,因此,它的资源占用是可控的,无论多少个客户端并发访问,都不会导致资源的耗尽和宕机。
    利用线程池,避免了为每个请求都创建一个独立线程造成的线程资源耗尽问题。但是因为底层仍是BIO模型,因此无法从根本上解决问题。
    缺点:
    1.IO代码里read操作是阻塞操作,如果连接不做数据读写操作会导致线程阻塞,浪费资源。
    2.如果线程很多,会导致服务器线程太多,压力太大。
  • 应用场景:BIO适用于连接数目比较小且固定的架构,这种对服务器资源要求比较高。

NIO(Non Blocking IO)

同步非阻塞,服务器实现模式为一个线程可以处理多个请求(连接),客户端发送的连接请求都会注册到多路复用selector上,多路复用器轮询到连接有IO请求就进行处理。
它支持面向缓冲,基于通道的I/O操作方法。对于低负载、低并发的应用程序,可以使用同步阻塞I/O来提升开发速率和更好的维护性;对于高负载、高并发的(网络)应用,应使用NIO的非阻塞模式来开发。

  • 核心组件:
    • Channel(通道):类似于流,每个channel对应一个buffer缓冲区。channel会注册到selector上,由selector根据channel读写事件的发生将其交由某个空闲的线程处理。
    • Buffer(缓冲区):NIO的Channel和Buffer既可以读也可以写。
    • Selector(选择器):可以对应一个或多个线程。
  • NIO服务端程序:
    • 创建一个ServerSocketChannel和Selector,并将ServerSocketChannel注册到Selector上;
    • Selector通过select()方法监听channel事件,当客户端连接时,selector监听到连接事件,获取到ServerSocketChannel注册时绑定的selectionKey;
    • selectionKey通过channel()方法可以获取绑定ServerSocketChannel;
    • ServerSocketChannel通过accept()方法得到SocketChannel;
    • 将SocketChannel注册到Selector上,关心read事件;
    • 注册后返回一个SelectionKey,会和该SocketChannel关联;
    • Selector继续通过select()方法监听事件,当客户端发送数据给服务端,Selector监听到read事件,获取到SocketChannel注册时绑定的socketChannel;
    • SelectionKey通过channel()方法可以获取绑定的socketChannel;
    • 将socketChannel里的数据读取出来;
    • 用socketChannel将服务端数据写回客户端。
  • NIO和IO的区别:
    • IO流是阻塞的,NIO流不是阻塞的;
    • IO面向流,NIO面向缓冲区;
      • 在面向流的I/O中,可以直接将数据写入或将数据直接读到stream对象中。虽然Stream中也有buffer开通的扩展类,但只是流的包装类,还未从流读到缓冲区。
      • NIO是直接读到buffer中进行操作。在NIO中,所有数据(读取、写入)都是用缓冲区处理的。
    • NIO通过Channel(通道)进行读写;通道的读写是双向的,而流是单向。无论读写,通道只能与Buffer交互。因为Buffer,通道可以异步读写。
    • NIO有选择器,IO没有;选择器用于使用单线程处理多个通道。因此,它需要较少的线程来处理这些通道。线程之间的切换多余操作系统来说是昂贵的。因此,为了提供系统效率。
  • 应用场景:适用于连接数目多且连接比较短(轻操作)的架构。
  • NIO模型的selector负责监听各种I/O事件,然后转交给后端线程去处理。NIO相对于BIO非阻塞的体现就在:BIO的后端线程需要阻塞等待客户端写数据(比如read方法),如果客户端不写数据,线程就会阻塞。NIO把等到客户端操作这一步骤教给了selector,selector负责轮询所有已注册的客户端,发现有数据才转交给后端线程处理,后端线程不需要做任何阻塞等待。
  • Redis就是典型的NIO线程模型,selector收集所有的事件并且转给后端线程,线程连续执行所有事件命令并将结果写回客户端。

AIO(Asynchronous I/O)

异步非阻塞,由操作系统完成后回调通知服务端程序启动线程去处理,一般适用于连接数较多且连接时间较长的应用。
异步IO是基于事件和回调机制实现的,即应用操作之后会直接返回,不会堵塞,当后台处理完成,操作系统会通知相应的线程进行后续的操作。除了AIO,其他的IO类型都是同步的。

  • 应用场景:适用于连接数目多且连接比较长(重操作)的架构。

BIO、NIO、AIO三者对比:

BIO NIO AIO
IO模型 同步阻塞 同步非阻塞(多路复用) 异步非阻塞
编程难度 简单 复杂 复杂
可靠性
吞吐量

一些Linux操作系统中的基础概念

用户空间/内核空间

操作系统的核心是内核,独立于普通的应用程序,可以访问受保护的内存空间,也有访问底层硬件设备的所有权限。为了保证用户进程不能直接操作内核(kernel),保证内核的安全,操作系统将虚拟空间划分为两部分,一部分为内核空间,一部分为用户空间。

进程切换

为了控制进程的执行,内核必须有能力挂起正在CPU上运行的进程,并恢复以前挂起的某个进程的执行。这种行为被称为进程切换。因此可以说,任何进程都是在操作系统内核的支持下运行的。
从一个进程的运行转到另一个进程的运行,需要经过以下变化:

  • 保存处理机上下文,包括程序计数器和其他寄存器。
  • 更新PCB信息。
  • 把进程的PCB移入相应的消息队列,如就绪、在某事件阻塞等队列。
  • 选择另一个进程执行,并更新其PCB。
  • 更新内存管理的数据结构。
  • 恢复处理机上下文。

进程阻塞

正在执行的进程,由于期待的某些事件并未发生,如请求系统资源失败、等待某种操作的完成、新数据尚未到达或无新工作等,则由系统自动执行阻塞原语(BLOCK),使自己由运行状态变为阻塞状态。
进程的阻塞是进程自身的一种主动行为,因此只有处于运行态的进程(获得了CPU资源),才可能将其转为阻塞状态。当进程进入阻塞状态时,是不占用CPU资源的。

文件描述符号

文件描述符,是一个用语表述指向文件的引用的抽象化概念。文件描述符在形式上是一个非负整数。它是一个索引值,指向内核为每一个进程所维护的该进程打开文件的记录表。

缓存I/O

缓存I/O又称为标准I/O,大多数文件系统的默认I/O操作都是缓存I/O。在Linux的缓存I/O机制中,操作系统会将I/O的数据缓存在文件系统的页缓存中,然后才会从系统内核的缓冲区拷贝到应用程序的地址空间。
缺点:数据在传输过程中需要在应用程序地址空间和内核进行多次数据拷贝操作,这些数据拷贝操作所带来的CPU以及内存开销非常大。

IO多路复用

什么是IO多路复用?

多路是指网络连接,复用是指同一个线程。

  • IO多路复用是一种同步IO模型,实现一个线程可以监视多个文件句柄;
  • 一旦某个文件句柄就绪,就能够通知应用程序进行相应的读写操作;
  • 没有文件句柄就绪就会阻塞应用程序,交出CPU。

Nginx的IO模型

Nignx支持多种并发模型,并发模型的具体实现根据系统平台而有所不同。在支持多种并发模型的平台上,niginx自动选择最高效的模型。


网站公告

今日签到

点亮在社区的每一天
去签到