当前位置: 首页 > 科技观察

10分钟搞懂JavaNIO底层原理

时间:2023-03-11 21:42:11 科技观察

很多写在前面的朋友都被javaIO模型看得有点头晕了。一会儿有4个模型,一会儿有5个模型。很多朋友也对nio这个词感到困惑。有一段时间javanio不叫非阻塞io,有一段时间javanio叫非阻塞io。它是什么?很多朋友对异步和非阻塞感到困惑。没有阻塞,不是异步的吗?这太难了。在这篇文章中,我将从底层开始,给大家简单介绍一下java的四大io模型。需要面试的,或者还没想通的,完全有福了。一、JavaIO读写原理无论是Socket读写还是文件读写,Java层面的应用开发还是Linux系统的底层开发,都属于输入输入输出输出的处理,简称IO读写。原则上和加工流程,是一致的。区别在于参数不同。用户程序读写IO,基本用到两大系统调用,read&write。操作系统可能有不同的名称,但功能是相同的。首先强调一个基础知识:read系统调用并不是直接从物理设备中读取数据到内存中。write系统调用并不直接向物理设备写入数据。read系统调用是将数据从内核缓冲区复制到进程缓冲区;write系统调用是将数据从进程缓冲区拷贝到内核缓冲区。这两个系统调用都不负责在内核缓冲区和磁盘之间交换数据。底层的读写交换是由操作系统内核完成的。1、kernelbuffer和processbufferbuffer的目的是减少频繁的系统IO调用。大家都知道系统调用需要保存之前的流程数据和状态信息,而调用结束后需要恢复之前的信息。为了减少这种耗时耗性能的系统调用,出现了缓冲区。有了缓冲区,操作系统使用read函数将数据从内核缓冲区复制到进程缓冲区,write将数据从进程缓冲区复制到内核缓冲区。等待缓冲区达到一定数量后再进行IO调用以提高性能。至于何时读取和存储,由内核决定,用户程序无需关心。在Linux系统中,系统内核也有一个缓冲区,称为内核缓冲区。每个进程都有自己独立的缓冲区,称为进程缓冲区。因此,用户程序的IO读写程序,在大多数情况下,并不进行实际的IO操作,而是读写自己的进程缓冲区。2.javaIO读写底层流程用户程序读写IO,基本都是使用系统调用read&write。Read将数据从内核缓冲区复制到进程缓冲区,Write将数据从进程缓冲区复制到内核缓冲区。区域,它们不等同于内核缓冲区和磁盘之间的数据交换。在这里插入一张图片描述首先我们来看一个典型的Java服务器处理网络请求的典型过程:(1)客户端请求:Linux通过网卡读取客户端的请求数据,并将数据读入内核缓冲区。(2)获取请求数据:服务器从内核缓冲区读取数据到Java进程缓冲区。(3)服务器端业务处理:Java服务器在自己的用户空间处理客户端的请求。(4)服务器返回的数据:将Java服务器构造好的响应从用户缓冲区写入系统缓冲区。(5)发送给客户端:Linux内核通过网络I/O将内核缓冲区中的数据写入网卡,网卡通过底层通信协议将数据发送给目标客户端。2.四种主要的IO模型服务器端编程往往需要构建高性能的IO模型。常见的IO模型有四种:1.同步阻塞IO(BlockingIO)首先在这里解释一下阻塞和非阻塞:blockingIO,意思是内核IO操作需要完全完成才返回用户空间执行用户的操作。阻塞是指用户空间程序的执行状态,用户空间程序需要等待IO操作完全完成。传统的IO模型是同步阻塞IO。在java中,默认创建的所有套接字都是阻塞的。其次,解释一下同步和异步:同步IO是用户空间和内核空间之间发起调用的一种方式。同步IO是指用户空间线程是主动发起IO请求的一方,内核空间是被动的接收者。而异步IO则意味着kernel内核是发起IO请求的一方,用户线程是被动的接收者。2.同步非阻塞IO(Non-blockingIO)非阻塞IO是指用户程序不需要等待内核IO操作完成,内核立即返回一个状态值给用户,而用户空间不需要等到内核IO操作完全完成。它可以立即返回用户空间,执行用户操作,并且处于非阻塞状态。简单来说:阻塞就是用户空间(调用线程)一直在等待,什么都不做;非阻塞是指用户空间(调用线程)拿到状态就返回,IO操作一做完就可以做。如果你能做到,那就去做吧。非阻塞IO需要将socket设置为NONBLOCK。强调一下,这里说的NIO(synchronousnon-blockingIO)模型并不是Java的NIO(NewIO)库。3.IO多路复用(IOMultiplexing)是Reactor经典的设计模式,有时也称为异步阻塞IO。Java中的Selector和Linux中的epoll都是这种模型。4.异步IO(AsynchronousIO)异步IO是指用户空间和内核空间之间调用方式的倒置。用户空间线程成为被动接受者,内核空间是主动调用者。这一点有点类似于回调模式,是Java中的典型模式。用户空间线程向内核空间注册各种IO事件回调函数,内核主动调用。3、同步阻塞IO(BlockingIO)在linux中的Java进程中,默认所有socket都是阻塞IO。在阻塞I/O模型中,应用程序被阻塞,无法进行IO系统调用,直到系统调用返回。返回成功后,应用进程开始处理用户空间的缓存数据。这里插上图片描述举个栗子,发起一个阻塞的socketreadread操作系统调用,过程大致是这样的:(1)当用户线程调用read系统调用时,内核(kernel)启动第一阶段IO:准备数据。很多时候,一开始数据还没有到达(比如还没有收到一个完整的Socket数据包),此时内核会等待足够的数据到达。(2)当内核等待数据准备好时,会将数据从内核缓冲区复制到用户缓冲区(用户内存),然后内核返回结果。(3)从开始IO读取的read系统调用开始,用户线程进入阻塞状态。直到内核返回结果,用户线程才释放block的状态,重新开始运行。所以,阻塞IO的特点就是在内核执行IO的两个阶段,用户线程都是阻塞的。BIO的优点:程序简单,用户线程在阻塞等待数据的过程中被挂起。用户线程基本不占用CPU资源。BIO的缺点:一般情况下,每个连接都配备一个独立的线程,或者一个线程维护一个成功连接的IO流的读写。在小并发的情况下,这是没有问题的。但是在高并发场景下,需要大量的线程来维护大量的网络连接,内存和线程切换的开销会很大。所以,基本上,BIO模型在高并发场景下是用不上的。4、同步非阻塞NIO(NoneBlockingIO)在Linux系统下,可以设置socket使其成为非阻塞。在NIO模型中,一旦应用程序启动IO系统调用,就会出现以下两种情况:当内核缓冲区没有数据时,系统调用会立即返回,并返回调用失败信息。在内核缓冲区有数据的情况下,它会被阻塞,直到数据从内核缓冲区复制到用户进程缓冲区。拷贝完成后,系统调用返回成功,应用进程开始处理用户空间的缓存数据。在这里插入图片描述举个栗子。发起一个非阻塞的socketreadread操作系统调用,流程是这样的:当内核数据还没有准备好,当用户线程发起IO请求时,立即返回。用户线程需要不断发起IO系统调用。内核数据到达后,用户线程发起系统调用,用户线程阻塞。内核开始复制数据。它会将内核内核缓冲区中的数据复制到用户缓冲区(用户内存),然后内核返回结果。用户线程释放块的状态并再次运行。经过多次尝试,用户线程终于读取到数据并继续执行。NIO的特点:应用程序的线程需要不断的执行I/O系统调用,轮询数据是否准备好,如果没有,继续轮询直到系统调用完成。NIO的优点:每次发起的IO系统调用都可以在内核等待数据的同时立即返回。不会阻塞用户线程,实时性更好。NIO的缺点:需要反复发起IO系统调用。这种连续的轮询会不断的向内核查询,会占用大量的CPU时间,系统资源的利用率低。总之,NIO模型在高并发场景下是不行的。普通的网络服务器不使用这种IO模型。一般很少直接使用这种模型,而是在其他IO模型中使用非阻塞IO特性。在java的实际开发中,是不会涉及到这个IO模型的。还是那句话,JavaNIO(NewIO)并不是IO模型中的NIO模型,而是另一种模型,叫做IO多路复用模型(IOmultiplexing)。五、IO多路复用模型(I/Omultiplexing)同步非阻塞NIO模型如何避免轮询和等待的问题?这就是IO多路复用模型。IO多路复用模型是通过一个新的系统调用,一个进程可以监听多个文件描述符。一旦一个描述符就绪(通常是内核缓冲区可读/可写),内核kernel就可以通知程序进行相应的IO系统调用。目前支持IO多路复用的系统调用,包括select、epoll等,select系统调用目前几乎所有操作系统都支持,具有良好的跨平台特性。epoll是在linux2.6内核中提出的,是linuxselect系统调用的增强版。IO多路复用模型的基本原理是select/epoll系统调用。单个线程连续轮询数百个负责select/epoll系统调用的套接字连接。当数据到达一个或多个套接字网络连接时,返回这些可读写的连接。因此,好处是显而易见的——通过一次select/epoll系统调用,可以查询一个甚至上百个可读写的网络连接。举个栗子。发起一个multiplexedIOreadread操作系统调用,流程如下:insertpicturedescriptionhere在这种模式下,首先不对read系统调用进行select/epoll系统调用。当然,这里有个前提,目标网络连接需要事先注册到select/epoll的可查询socket列表中。然后,就可以开始整个IO多路复用模型的读取过程了。(1)进行select/epoll系统调用查询可读连接。内核会查询所有select可查询的socket列表,当任意一个socket中的数据准备好后,select就会返回。当用户进程调用select时,整个线程会被阻塞(blocked)。(2)用户线程获得目标连接后,发起read系统调用,用户线程阻塞。内核开始复制数据。它会将内核内核缓冲区中的数据复制到用户缓冲区(用户内存),然后内核返回结果。(3)用户线程释放block状态,用户线程最终读取数据继续执行。多路复用IO的特点:IO多路复用模型是基于操作系统内核可以提供的多路复用系统调用select/epoll。多路复用IO需要两个系统调用(systemcalls),一个是select/epollquerycall,一个是IOreadcall。与NIO模型类似,多路复用IO需要轮询。负责select/epoll查询调用的线程需要不断的进行select/epoll轮询,找出可以进行IO操作的连接。另外,多路复用IO模型与之前的NIO模型有关。对于每一个可以查询的socket,一般都设置为非阻塞模型。只有这一点对用户程序是透明的(不知道)。多路复用IO的优点:使用select/epoll的优点是可以同时处理上千个连接。与一个线程维护一个连接相比,I/O多路复用技术最大的优势是系统不必创建线程或维护这些线程,从而大大降低了系统开销。Java的NIO(newIO)技术采用了IO多路复用模型。在Linux系统上,使用epoll系统调用。多路复用IO的缺点:select/epoll系统调用本质上是同步IO和阻塞IO。读写事件就绪后都需要负责读写,也就是读写进程阻塞。如何完全解锁线程?那就是异步IO模型。6、异步IO模型(asynchronousIO)如何进一步提高效率,缓解最后一点阻塞?这就是异步IO模型,全称是asynchronousI/O,简称AIO。AIO的基本流程是:用户线程通过系统调用通知内核开始一个IO操作,用户线程返回。整个IO操作(包括数据准备和数据拷贝)完成后,kernel内核通知用户程序,由用户进行后续的业务操作。内核的数据准备是从网络物理设备(网卡)读取数据到内核缓冲区;内核的数据拷贝就是将内核缓冲区中的数据拷贝到用户程序空间的缓冲区中。此处插入图片说明(1)当用户线程调用read系统调用后,可以立即开始做其他事情,用户线程不会阻塞。(2)内核(kernel)开始IO的第一阶段:准备数据。当内核等待数据准备好时,它会将数据从内核缓冲区复制到用户缓冲区(用户内存)。(3)内核会向用户线程发送信号,或者回调用户线程注册的回调接口,告诉用户线程读操作完成。(4)用户线程读取用户缓冲区中的数据,完成后续的业务操作。异步IO模型的特点:在kernel内核的等待数据和拷贝数据两个阶段,用户线程不会阻塞(blocked)。用户线程需要接受内核IO操作完成的事件,或者向操作系统内核注册IO操作完成的回调函数。因此,异步IO有时也被称为信号驱动IO。异步IO模型的缺点:需要完成事件的注册和传递。在这里,底层操作系统需要提供很多支持,做很多工作。目前Windows系统通过IOCP实现真正的异步I/O。但就目前的行业情况来看,Windows系统很少被用作百万级或高并发应用的服务器操作系统。Linux系统下,异步IO模型在2.6版本才引入,目前还不完善。所以,这也是linux下的。在实现高并发网络编程时,IO多路复用模型是主要模式。总结一下:四种IO模型,理论上越往后阻塞越少,效率最好。在四种I/O模型中,前三种是同步I/O,因为它们之间的实际I/O操作会阻塞线程。只有最后一个才是真正的异步I/O模型。不幸的是,目前的Linux操作系统还不够完善。