女装网站建设规划书,个人博客WordPress吗,宿迁房产网官网,四川建设信息网官网高并发四种IO模型的底层原理
1 IO读写的基本原理
为了避免用户进程直接操作内核#xff0c;保证内核安全#xff0c;操作系统将内存#xff08;虚拟内存#xff09;划分为两部分#xff1a;一部分是内核空间(Kernel-Space)#xff0c;另一部分是用户空间(User-Space)。在…高并发四种IO模型的底层原理
1 IO读写的基本原理
为了避免用户进程直接操作内核保证内核安全操作系统将内存虚拟内存划分为两部分一部分是内核空间(Kernel-Space)另一部分是用户空间(User-Space)。在Linux系统中内核模块运行在内核空间对应的进程处于内核态用户程序运行在用户空间对应的进程处于用户态。
操作系统的核心是内核程序它独立于普通的应用程序既有权限访问受保护的内核空间也有权限访问硬件设备而普通的应用程序并没有这样的权限。内核空间总是驻留在内存中是为操作系统的内核保留的。应用程序不允许直接在内核空间区域进行读写也不允许直接调用内核代码定义的函数。每个应用程序进程都有一个单独的用户空间对应的进程处于用户态用户态进程不能访问内核空间中的数据也不能直接调用内核函数因此需要将进程切换到内核态才能进行系统调用。
内核态进程可以执行任意命令调用系统的一切资源而用户态进程只能执行简单的运算不能直接调用系统资源那么问题来了用户态进程如何执行系统调用呢答案是用户态进程必须通过系统调用(System Call)向内核发出指令完成调用系统资源之类的操作。说明
如果没有特别声明本书后文所提到的内核是指操作系统的内核。用户程序进行IO的读写依赖于底层的IO读写基本上会用到底层的read和write两大系统调用。虽然在不同的操作系统中read和write两大系统调用的名称和形式可能不完全一样但是它们的基本功能是一样的。
操作系统层面的read系统调用并不是直接从物理设备把数据读取到应用的内存中write系统调用也不是直接把数据写入物理设备。上层应用无论是调用操作系统的read还是调用操作系统的write都会涉及缓冲区。具体来说上层应用通过操作系统的read系统调用把数据从内核缓冲区复制到应用程序的进程缓冲区通过操作系统的write系统调用把数据从应用程序的进程缓冲区复制到操作系统的内核缓冲区。
简单来说应用程序的IO操作实际上不是物理设备级别的读写而是缓存的复制。read和write两大系统调用都不负责数据在内核缓冲区和物理设备如磁盘、网卡等之间的交换。这个底层的读写交换操作是由操作系统内核(Kernel)来完成的。所以在应用程序中无论是对socket的IO操作还是对文件的IO操作都属于上层应用的开发它们在输入(Input)和输出(Output)维度上的执行流程是类似的都是在内核缓冲区和进程缓冲区之间进行数据交换。
2 内核缓冲区与进程缓冲区
为什么设置那么多的缓冲区导致读写过程那么麻烦呢
缓冲区的目的是减少与设备之间的频繁物理交换。计算机的外部物理设备与内存和CPU相比有着非常大的差距外部设备的直接读写涉及操作系统的中断。发生系统中断时需要保存之前的进程数据和状态等信息结束中断之后还需要恢复之前的进程数据和状态等信息。为了减少底层系统的频繁中断所导致的时间损耗、性能损耗出现了内核缓冲区。
操作系统会对内核缓冲区进行监控等待缓冲区达到一定数量的时候再进行IO设备的中断处理集中执行物理设备的实际IO操作通过这种机制来提升系统的性能。至于具体什么时候执行系统中断包括读中断、写中断则由操作系统的内核来决定应用程序不需要关心。
上层应用使用read系统调用时仅仅把数据从内核缓冲区复制到应用的缓冲区进程缓冲区上层应用使用write系统调用时仅仅把数据从应用的缓冲区复制到内核缓冲区。
内核缓冲区与应用缓冲区在数量上也不同。在Linux系统中操作系统内核只有一个内核缓冲区。每个用户程序进程都有自己独立的缓冲区叫作用户缓冲区或者进程缓冲区。在大多数情况下Linux系统中用户程序的IO读写程序并没有进行实际的IO操作而是在用户缓冲区和内核缓冲区之间直接进行数据的交换。
3 典型的系统调用流程
用户程序所使用的系统调用read和write并不是使数据在内核缓冲区和物理设备之间交换read调用把数据从内核缓冲区复制到应用的用户缓冲区write调用把数据从应用的用户缓冲区复制到内核缓冲区。两个系统调用的大致流程如图2-1所示。这里以read系统调用为例看一下一个完整输入流程的两个阶段
应用程序等待数据准备好。从内核缓冲区向用户缓冲区复制数据。
如果是读取一个socket套接字那么以上两个阶段的具体处理流程如下
第一个阶段应用程序等待数据通过网络到达网卡当所等待的分组到达时数据被操作系统复制到内核缓冲区中。这个工作由操作系统自动完成用户程序无感知。第二个阶段内核将数据从内核缓冲区复制到应用的用户缓冲区。
再具体一点如果是在Java客户端和服务端之间完成一次socket请求和响应包括read和write的数据交换其完整的流程如下
客户端发送请求Java客户端程序通过write系统调用将数据复制到内核缓冲区Linux将内核缓冲区的请求数据通过客户端机器的网卡发送出去。在服务端这份请求数据会从接收网卡中读取到服务端机器的内核缓冲区。服务端获取请求Java服务端程序通过read系统调用从Linux内核缓冲区读取数据再送入Java进程缓冲区。服务端业务处理Java服务器在自己的用户空间中完成客户端的请求所对应的业务处理。服务端返回数据Java服务器完成处理后构建好的响应数据将从用户缓冲区写入内核缓冲区这里用到的是write系统调用操作系统会负责将内核缓冲区的数据发送出去。发送给客户端服务端Linux系统将内核缓冲区中的数据写入网卡网卡通过底层的通信协议将数据发送给目标客户端。
由于生产环境的Java高并发应用基本都运行在Linux操作系统上因此以上案例中的操作系统以Linux作为实例。
4 四种主要的IO模型
服务端高并发IO编程往往要求的性能都非常高一般情况下需要选用高性能的IO模型。
4.1 同步阻塞IO
首先解释一下阻塞与非阻塞。阻塞IO指的是需要内核IO操作彻底完成后才返回到用户空间执行用户程序的操作指令。“阻塞”指的是用户程序发起IO请求的进程或者线程的执行状态。可以说传统的IO模型都是阻塞IO模型并且在Java中默认创建的socket都属于阻塞IO模型。
其次解释一下同步与异步。简单来说可以将同步与异步看成发起IO请求的两种方式。同步IO是指用户空间进程或者线程是主动发起IO请求的一方系统内核是被动接收方。异步IO则反过来系统内核是主动发起IO请求的一方用户空间是被动接收方。
同步阻塞IO(Blocking IO)指的是用户空间或者线程主动发起需要等待内核IO操作彻底完成后才返回到用户空间的IO操作。在IO操作过程中发起IO请求的用户进程或者线程处于阻塞状态。
在Java中
在Java应用程序进程中所有对socket连接进行的IO操作都是同步阻塞IO。在阻塞式IO模型中从Java应用程序发起IO系统调用开始一直到系统调用返回这段时间内发起IO请求的Java进程或者线程是阻塞的。直到返回成功后应用进程才能开始处理用户空间的缓冲区数据。
同步阻塞IO的具体流程如图2-2所示。举个例子在Java中发起一个socket的read操作的系统调用流程大致如下
(1)从Java进行IO读后发起read系统调用开始用户线程或者线程就进入阻塞状态。(2)当系统内核收到read系统调用后就开始准备数据。一开始数据可能还没有到达内核缓冲区例如还没有收到一个完整的socket数据包这时内核就要等待。(3)内核一直等到完整的数据到达就会将数据从内核缓冲区复制到用户缓冲区用户空间的内存然后内核返回结果例如返回复制到用户缓冲区中的字节数。(4)直到内核返回后用户线程才会解除阻塞的状态重新运行起来。阻塞IO的特点是在内核执行IO操作的两个阶段发起IO请求的用户进程或者线程被阻塞了。
阻塞IO的优点是应用程序开发非常简单在阻塞等待数据期间用户线程挂起基本不会占用CPU资源。
阻塞IO的缺点是一般情况下会为每个连接配备一个独立的线程一个线程维护一个连接的IO操作。在并发量小的情况下这样做没有什么问题。在高并发的应用场景下阻塞IO模型需要大量的线程来维护大量的网络连接内存、线程切换开销会非常巨大性能很低基本上是不可用的。
4.2 同步非阻塞IO
非阻塞IO(Non-Blocking IO, NIO)指的是用户空间的程序不需要等待内核IO操作彻底完成可以立即返回用户空间去执行后续的指令即发起IO请求的用户进程或者线程处于非阻塞状态与此同时内核会立即返回给用户一个IO状态值。
阻塞和非阻塞的区别是什么呢阻塞是指用户进程或者线程一直在等待而不能做别的事情非阻塞是指用户进程或者线程获得内核返回的状态值就返回自己的空间可以去做别的事情。在Java中非阻塞IO的socket被设置为NONBLOCK模式。说明
同步非阻塞IO也可以简称为NIO但是它不是Java编程中的NIO。Java编程中的NIO(New IO)类库组件所归属的不是基础IO模型中的NIO模型而是IO多路复用模型。同步非阻塞IO指的是用户进程主动发起不需要等待内核IO操作彻底完成就能立即返回用户空间的IO操作。在IO操作过程中发起IO请求的用户进程或者线程处于非阻塞状态。
在java中
在Linux系统下socket连接默认是阻塞模式可以将socket设置成非阻塞模式。在NIO模型中应用程序一旦开始IO系统调用就会出现以下两种情况
(1)在内核缓冲区中没有数据的情况下系统调用会立即返回一个调用失败的信息。(2)在内核缓冲区中有数据的情况下在数据的复制过程中系统调用是阻塞的直到完成数据从内核缓冲区复制到用户缓冲区。复制完成后系统调用返回成功用户进程或者线程可以开始处理用户空间的缓冲区数据。
同步非阻塞IO的流程如图2-3所示。举个例子发起一个非阻塞socket的read操作的系统调用流程如下
(1)在内核数据没有准备好的阶段用户线程发起IO请求时立即返回。所以为了读取最终的数据用户进程或者线程需要不断地发起IO系统调用。(2)内核数据到达后用户进程或者线程发起系统调用用户进程或者线程阻塞。内核开始复制数据它会将数据从内核缓冲区复制到用户缓冲区然后内核返回结果例如返回复制到的用户缓冲区的字节数。(3)用户进程或者线程读到数据后才会解除阻塞状态重新运行起来。也就是说用户空间需要经过多次尝试才能保证最终真正读到数据而后继续执行。
同步非阻塞IO的特点是应用程序的线程需要不断地进行IO系统调用轮询数据是否已经准备好如果没有准备好就继续轮询直到完成IO系统调用为止。
同步非阻塞IO的优点是每次发起的IO系统调用在内核等待数据过程中可以立即返回用户线程不会阻塞实时性较好。
同步非阻塞IO的缺点是不断地轮询内核这将占用大量的CPU时间效率低下。
总体来说在高并发应用场景中同步非阻塞IO是性能很低的也是基本不可用的一般Web服务器都不使用这种IO模型。在Java的实际开发中不会涉及这种IO模型但是此模型还是有价值的其作用在于其他IO模型中可以使用非阻塞IO模型作为基础以实现其高性能。
4.3 IO多路复用
概念理解
为了提高性能操作系统引入了一种新的系统调用专门用于查询IO文件描述符含socket连接的就绪状态。在Linux系统中新的系统调用为 select/epoll 系统调用。通过该系统调用一个用户进程或者线程可以监视多个文件描述符一旦某个描述符就绪一般是内核缓冲区可读/可写内核就能够将文件描述符的就绪状态返回给用户进程或者线程用户空间可以根据文件描述符的就绪状态进行相应的IO系统调用。
IO多路复用(IO Multiplexing)属于一种经典的Reactor模式实现有时也称为异步阻塞IO, Java中的Selector属于这种模型。
系统应用
如何避免同步非阻塞IO模型中轮询等待的问题呢答案是采用IO多路复用模型。
目前支持IO多路复用的系统调用有select、epoll等。几乎所有的操作系统都支持select系统调用它具有良好的跨平台特性。epoll是在Linux 2.6内核中提出的是select系统调用的Linux增强版本。
在IO多路复用模型中通过select/epoll系统调用单个应用程序的线程可以不断地轮询成百上千的socket连接的就绪状态当某个或者某些socket网络连接有IO就绪状态时就返回这些就绪的状态或者说就绪事件。
举个例子来说明IO多路复用模型的流程。发起一个多路复用IO的read操作的系统调用流程如下
(1)选择器注册。首先将需要read操作的目标文件描述符socket连接提前注册到Linux的select/epoll选择器中在Java中所对应的选择器类是Selector类。然后开启整个IO多路复用模型的轮询流程。(2)就绪状态的轮询。通过选择器的查询方法查询所有提前注册过的目标文件描述符socket连接的IO就绪状态。通过查询的系统调用内核会返回一个就绪的socket列表。当任何一个注册过的socket中的数据准备好或者就绪了就说明内核缓冲区有数据了内核将该socket加入就绪的列表中并且返回就绪事件。(3)用户线程获得了就绪状态的列表后根据其中的socket连接发起read系统调用用户线程阻塞。内核开始复制数据将数据从内核缓冲区复制到用户缓冲区。(4)复制完成后内核返回结果用户线程才会解除阻塞的状态用户线程读取到了数据继续执行。说明:
在用户进程进行IO就绪事件的轮询时需要调用选择器的select查询方法发起查询的用户进程或者线程是阻塞的。
当然如果使用了查询方法的非阻塞的重载版本发起查询的用户进程或者线程也不会阻塞重载版本会立即返回。IO多路复用模型的read系统调用流程如图2-4所示。IO多路复用模型的特点是IO多路复用模型的IO涉及两种系统调用一种是IO操作的系统调用另一种是select/epoll就绪查询系统调用。IO多路复用模型建立在操作系统的基础设施之上即操作系统的内核必须能够提供多路分离的系统调用select/epoll。
和NIO模型相似多路复用IO也需要轮询。负责select/epoll状态查询调用的线程需要不断地进行select/epoll轮询以找出达到IO操作就绪的socket连接。
IO多路复用模型与同步非阻塞IO模型是有密切关系的具体来说注册在选择器上的每一个可以查询的socket连接一般都设置成同步非阻塞模型只是这一点对于用户程序而言是无感知的。
IO多路复用模型的优点是一个选择器查询线程可以同时处理成千上万的网络连接所以用户程序不必创建大量的线程也不必维护这些线程从而大大减少了系统的开销。与一个线程维护一个连接的阻塞IO模式相比这一点是IO多路复用模型的最大优势。
通过JDK的源码可以看出Java语言的NIO组件在Linux系统上是使用epoll系统调用实现的。所以Java语言的NIO组件所使用的就是IO多路复用模型。
IO多路复用模型的缺点是本质上 select/epoll 系统调用是阻塞式的属于同步IO需要在读写事件就绪后由系统调用本身负责读写也就是说这个读写过程是阻塞的。要彻底地解除线程的阻塞就必须使用异步IO模型。
4.4 异步IO
异步IO(Asynchronous IO, AIO)指的是用户空间的线程变成被动接收者而内核空间成为主动调用者。在异步IO模型中当用户线程收到通知时数据已经被内核读取完毕并放在了用户缓冲区内内核在IO完成后通知用户线程直接使用即可。
异步IO类似于Java中典型的回调模式用户进程或者线程向内核空间注册了各种IO事件的回调函数由内核去主动调用。
系统应用
异步IO模型的基本流程是用户线程通过系统调用向内核注册某个IO操作。内核在整个IO操作包括数据准备、数据复制完成后通知用户程序用户执行后续的业务操作。
在异步IO模型中在整个内核的数据处理过程包括内核将数据从网络物理设备网卡读取到内核缓冲区、将内核缓冲区的数据复制到用户缓冲区中用户程序都不需要阻塞。
异步IO模型的流程如图2-5所示。举个例子发起一个异步IO的read操作的系统调用流程如下
(1)当用户线程发起了read系统调用后立刻就可以去做其他的事用户线程不阻塞。(2)内核开始IO的第一个阶段准备数据。准备好数据内核就会将数据从内核缓冲区复制到用户缓冲区。(3)内核会给用户线程发送一个信号(Signal)或者回调用户线程注册的回调方法告诉用户线程read系统调用已经完成数据已经读入用户缓冲区。(4)用户线程读取用户缓冲区的数据完成后续的业务操作。
异步IO模型的特点是在内核等待数据和复制数据的两个阶段用户线程都不是阻塞的。用户线程需要接收内核的IO操作完成的事件或者用户线程需要注册一个IO操作完成的回调函数。正因为如此异步IO有的时候也被称为信号驱动IO。
异步IO模型的缺点是应用程序仅需要进行事件的注册与接收其余的工作都留给了操作系统也就是说需要底层内核提供支持。
理论上来说异步IO是真正的异步输入输出它的吞吐量高于IO多路复用模型的吞吐量。就目前而言Windows系统下通过IOCP实现了真正的异步IO。在Linux系统下异步IO模型在2.6版本才引入JDK对它的支持目前并不完善因此异步IO在性能上没有明显的优势。
大多数高并发服务端的程序都是基于Linux系统的。因而目前这类高并发网络应用程序的开发大多采用IO多路复用模型。大名鼎鼎的Netty框架使用的就是IO多路复用模型而不是异步IO模型。
参考书名Java高并发核心编程 卷1NIO、Netty、Redis、ZooKeeper作者尼恩