IO模型讲解及IO多路复用详解
学习 Linux 时,经常可以看到两个词:User space(用户空间)和 Kernel space(内核空间)
简单说,Kernel space 是 Linux 内核的运行空间,User space 是用户程序的
运行空间。为了安全,它们是隔离的,即使用户的程序崩溃了,内核也不受影响
虚拟内存被操作系统划分成两块:内核空间和用户空间,内核空间是内核代码运行的地方,用户空间是用户程序代码
运行的地方。当进程运行在内核空间时就处于内核态,当进程运行在用户空间时就处于用户态。
Kernel space 可以执行任意命令,调用系统的一切资源;User space 只能执行简单的运算,不能直接调用系统 资源,必须通过系统接口(又称 system call),才能向内核发出指令。 通过系统接口,进程可以从用户空间切换到内核空间 str = "my string" // 用户空间 x = x + 2 file.write(str) // 切换到内核空间 y = x + 4 // 切换回用户空间 上面代码中,第一行和第二行都是简单的赋值运算,在 User space 执行。第三行需要写入文件,就要切换到 Kernel space,因为用户不能直接写文件,必须通过内核安排。第四行又是赋值运算,就切换回 User space。 查看 CPU 时间在 User space 与 Kernel Space 之间的分配情况,可以使用top命令。它的第三行输出就是 CPU 时间分配统计。
![](/i/l/?n=22&i=blog/1353113/202208/1353113-20220831115630834-1107282779.png)
ni:niceness 的缩写,CPU 消耗在 nice 进程(低优先级)的时间百分比 id:idle 的缩写,CPU 消耗在闲置进程的时间百分比,这个值越低,表示 CPU 越忙 wa:wait 的缩写,CPU 等待外部 I/O 的时间百分比,这段时间 CPU 不能干其他事,但是也没有执行运算,这个 值太高就说明外部设备有问题 hi:hardware interrupt 的缩写,CPU 响应硬件中断请求的时间百分比 si:software interrupt 的缩写,CPU 响应软件中断请求的时间百分比 st:stole time 的缩写,该项指标只对虚拟机有效,表示分配给当前虚拟机的 CPU 时间之中,被同一台物理机上 的其他虚拟机偷走的时间百分比 PIO与DMA 有必要简单地说说慢速I/O设备和内存之间的数据传输方式。 PIO 我们拿磁盘来说,很早以前,磁盘和内存之间的数据传输是需要CPU控制的,也就是说如果我们读取磁盘 文件到内存中,数据要经过CPU存储转发,这种方式称为PIO。显然这种方式非常不合理,需要占用大量的CPU 时间来读取文件,造成文件访问时系统几乎停止响应。 DMA 后来,DMA(直接内存访问,Direct Memory Access)取代了PIO,它可以不经过CPU而直接进行磁盘 和内存(内核空间)的数据交换。在DMA模式下,CPU只需要向DMA控制器下达指令,让DMA控制器来处理数据 的传送即可,DMA控制器通过系统总线来传输数据,传送完毕再通知CPU,这样就在很大程度上降低了CPU占有 率,大大节省了系统资源,而它的传输速度与PIO的差异其实并不十分明显,因为这主要取决于慢速设备的速 度。 可以肯定的是,PIO模式的计算机我们现在已经很少见到了。 缓存IO和直接IO 缓存IO:数据从磁盘先通过DMA copy到内核空间,再从内核空间通过cpu copy到用户空间 直接IO:数据从磁盘通过DMA copy到用户空间 缓存IO 缓存I/O又被称作标准I/O,大多数文件系统的默认I/O操作都 是缓存I/O。在Linux的缓存I/O机制中,数据先从磁盘复制到内核空间的缓冲区,然后从内核空间缓冲区 复制到应用程序的地址空间。 读操作: 操作系统检查内核的缓冲区有没有需要的数据,如果已经缓存了,那么就直接从缓存中返回;否则从磁盘中读 取,然后缓存在操作系统的缓存中。 写操作: 将数据从用户空间复制到内核空间的缓存中。这时对用户程序来说写操作就已经完成,至于什么时候再写到磁 盘中由操作系统决定,除非显示地调用了sync同步命令(详情参考《【珍藏】linux 同步IO: sync、fsync与 fdatasync》)。 缓存I/O的优点: 1)在一定程度上分离了内核空间和用户空间,保护系统本身的运行安全; 2)可以减少读盘的次数,从而提高性能。 缓存I/O的缺点: 在缓存 I/O 机制中,DMA 方式可以将数据直接从磁盘读到页缓存中,或者将数据从页缓存直接写回到磁盘 上,而不能直接在应用程序地址空间和磁盘之间进行数据传输,这样,数据在传输过程中需要在应用程序地址 空间(用户空间)和缓存(内核空间)之间进行多次数据拷贝操作,这些数据拷贝操作所带来的CPU以及内存 开销是非常大的。 直接IO 直接IO就是应用程序直接访问磁盘数据,而不经过内核缓冲 区,也就是绕过内核缓冲区,自己管理I/O缓存区,这样做的目 的是减少一次从内核缓冲区到用户程序缓存的数据复制。 引入内核缓冲区的目的在于提高磁盘文件的访问性能,因为当进程需要读取磁盘文件时,如果文件内容已经在内核缓 冲区中,那么就不需要再次访问磁盘;而当进程需要向文件中写入数据时,实际上只是写到了内核缓冲区便告诉进程 已经写成功,而真正写入磁盘是通过一定的策略进行延迟的。 然而,对于一些较复杂的应用,比如数据库服务器,它们为了充分提高性能,希望绕过内核缓冲区,由自己在用户 态空间实现并管理I/O缓冲区,包括缓存机制和写延迟机制等,以支持独特的查询机制,比如数据库可以根据更加 合理的策略来提高查询缓存命中率。另一方面,绕过内核缓冲区也可以减少系统内存的开销,因为内核缓冲区本身就 在使用系统内存。 应用程序直接访问磁盘数据,不经过操作系统内核数据缓冲区,这样做的目的是减少一次从内核缓冲区到用户程序 缓存的数据复制。这种方式通常是在对数据的缓存管理由应用程序实现的数据库管理系统中。 直接I/O的缺点就是如果访问的数据不在应用程序缓存中,那么每次数据都会直接从磁盘进行加载,这种直接加载 会非常缓慢。通常直接I/O跟异步I/O结合使用会得到较好的性能。
![](/i/l/?n=22&i=blog/1353113/202208/1353113-20220831140537283-1845181298.png)
![](/i/l/?n=22&i=blog/1353113/202208/1353113-20220831140556523-1608530757.png)
Linux提供了对这种需求的支持,即在open()系统调用中增加参数选项O_DIRECT,用它打开的文件便可以绕过内核缓冲区的直接访问,这样便有效避免了CPU和内存的多余时间开销 顺便提一下,与O_DIRECT类似的一个选项是O_SYNC,后者只对写数据有效,它将写入内核缓冲区的数据立即写入磁盘,将机器故障时数据的丢失减少到最小,但是它仍然要经过内核缓冲区。 IO访问方式 磁盘IO
![](/i/l/?n=22&i=blog/1353113/202208/1353113-20220831140907235-1528966629.png)
具体步骤: 当应用程序调用read接口时,操作系统检查在内核的高速缓存有没有需要的数据,如果已经缓存了,那么就直接从 缓存中返回,如果没有,则从磁盘中读取,然后缓存在操作系统的缓存中。 应用程序调用write接口时,将数据从用户地址空间复制到内核地址空间的缓存中,这时对用户程序来说,写操作已 经完成,至于什么时候再写到磁盘中,由操作系统决定,除非显示调用了sync同步命令
![](/i/l/?n=22&i=blog/1353113/202208/1353113-20220831141013734-1301654108.png)
网络IO 1)操作系统将数据从磁盘复制到操作系统内核的页缓存中
2)应用将数据从内核缓存复制到应用的缓存中
3)应用将数据写回内核的Socket缓存中
4)操作系统将数据从Socket缓存区复制到网卡缓存,然后将其通过网络发出
![](/i/l/?n=22&i=blog/1353113/202208/1353113-20220831141147396-1244910020.png)
1、当调用read系统调用时,通过DMA(Direct Memory Access)将数据copy到内核模式
2、然后由CPU控制将内核模式数据copy到用户模式下的 buffer中
3、read调用完成后,write调用首先将用户模式下 buffer中的数据copy到内核模式下的socket buffer中
4、最后通过DMA copy将内核模式下的socket buffer中的数据copy到网卡设备中传送。 从上面的过程可以看出,数据白白从内核模式到用户模式走了一圈,浪费了两次copy,而这两次copy都是CPU,copy,即占用CPU资源。 磁盘IO和网络IO对比 首先,磁盘IO主要的延时是由(以15000rpm硬盘为例): 机械转动延时(机械磁盘的主要性能瓶颈,平均为 2ms) + 寻址延时(2~3ms) + 块传输延时(一般4k每块,40m/s的传输速度,延时一般为0.1ms) 决定。(平均 为5ms) 而网络IO主要延是由: 服务器响应延时 + 带宽限制 + 网络延时 + 跳转路由延时 + 本地接收延时 决定。(一般 为几十到几千毫秒,受环境干扰极大) 所以两者一般来说网络IO延时要大于磁盘IO的延时 Socket网络编程 客户端 public class SocketClient { public static void main(String args[]) throws Exception { // 要连接的服务端IP地址和端口 String host = "127.0.0.1"; int port = 55533; // 与服务端建立连接 Socket socket = new Socket(host, port); // 建立连接后获得输出流 OutputStream outputStream = socket.getOutputStream(); String message="你好 yiwangzhibujian"; socket.getOutputStream().write(message.getBytes("UTF-8")); outputStream.close(); socket.close(); } } 服务端 public class SocketServer { public static void main(String[] args) throws Exception { // 监听指定的端口 int port = 55533; ServerSocket server = new ServerSocket(port); // server将一直等待连接的到来 System.out.println("server将一直等待连接的到来"); Socket socket = server.accept(); // 建立好连接后,从socket中获取输入流,并建立缓冲区进行读取 InputStream inputStream = socket.getInputStream(); byte[] bytes = new byte[1024]; int len; StringBuilder sb = new StringBuilder(); while ((len = inputStream.read(bytes)) != -1) { //注意指定编码格式,发送方和接收方一定要统一,建议使用UTF-8 sb.append(new String(bytes, 0, len,"UTF-8")); } System.out.println("get message from client: " + sb); inputStream.close(); socket.close(); server.close(); } } public class SocketServer { public static void main(String args[]) throws IOException { // 监听指定的端口 int port = 55533; ServerSocket server = new ServerSocket(port); // server将一直等待连接的到来 System.out.println("server将一直等待连接的到来"); while(true){ Socket socket = server.accept(); // 建立好连接后,从socket中获取输入流,并建立缓冲区进行读取 InputStream inputStream = socket.getInputStream(); byte[] bytes = new byte[1024]; int len; StringBuilder sb = new StringBuilder(); while ((len = inputStream.read(bytes)) != -1) { // 注意指定编码格式,发送方和接收方一定要统一,建议使用UTF-8 sb.append(new String(bytes, 0, len, "UTF-8")); } System.out.println("get message from client: " + sb); inputStream.close(); socket.close(); } } } public class SocketServer { public static void main(String args[]) throws Exception { // 监听指定的端口 int port = 55533; ServerSocket server = new ServerSocket(port); // server将一直等待连接的到来 System.out.println("server将一直等待连接的到来"); //如果使用多线程,那就需要线程池,防止并发过高时创建过多线程耗尽资源 ExecutorService threadPool = Executors.newFixedThreadPool(100); while (true) { 1 2 3 4 5 6 7 8 9 10 11 12同步IO和异步IO 同步和异步是针对应用程序和内核的交互而言的,同步指的是用户进程触发IO操作并等待或者轮询的去查看IO操作 是否就绪,而异步是指用户进程触发IO操作以后便开始做自己的事情,而当IO操作已经完成的时候会得到IO完成的 通知。 阻塞IO和非阻塞IO 阻塞方式下读取或者写入函数将一直等待,而非阻塞方式下,读取或者写入函数会立即返回一个状态值。 Socket socket = server.accept(); Runnable runnable=()->{ try { // 建立好连接后,从socket中获取输入流,并建立缓冲区进行读取 InputStream inputStream = socket.getInputStream(); byte[] bytes = new byte[1024]; int len; StringBuilder sb = new StringBuilder(); while ((len = inputStream.read(bytes)) != -1) { // 注意指定编码格式,发送方和接收方一定要统一,建议使用UTF-8 sb.append(new String(bytes, 0, len, "UTF-8")); } System.out.println("get message from client: " + sb); inputStream.close(); socket.close(); } catch (Exception e) { e.printStackTrace(); } }; threadPool.submit(runnable); } } } 同步IO和异步IO 同步和异步是针对应用程序和内核的交互而言的,同步指的是用户进程触发IO操作并等待或者轮询的去查看IO操作 是否就绪,而异步是指用户进程触发IO操作以后便开始做自己的事情,而当IO操作已经完成的时候会得到IO完成的 通知。 指的是用户空间和内核空间数据交互的方式 同步:用户空间要的数据,必须等到内核空间给它才做其他事情 异步:用户空间要的数据,不需要等到内核空间给它,才做其他事情。内核空间会异步通知用户进程,并把数据 直接给到用户空间。 阻塞IO和非阻塞IO 阻塞方式下读取或者写入函数将一直等待,而非阻塞方式下,读取或者写入函数会立即返回一个状态值。 指的是用户就和内核空间IO操作的方式 堵塞:用户空间通过系统调用(systemcall)和内核空间发送IO操作时,该调用是堵塞的 非堵塞:用户空间通过系统调用(systemcall)和内核空间发送IO操作时,该调用是不堵塞的,直接返回的, 只是返回时,可能没有数据而已 IO设计模式之Reactor和Proactor 平时接触的开源产品如Redis、ACE,事件模型都使用的Reactor模式;而同样做事件处理的Proactor,由于操作系 统的原因,相关的开源产品也少;这里学习下其模型结构,重点对比下两者的异同点; 反应器Reactor 概述 反应器设计模式(Reactor pattern)是一种为处理并发服务请求,并将请求提交到 一个或者多个服务处理程序的事件设计模式。当客户端请求抵达后,服务处理程序 使用多路分配策略,由一个非阻塞的线程来接收所有的请求,然后派发这些请求至 相关的工作线程进行处理 Reactor模式主要包含下面几部分内容: 初始事件分发器(Initialization Dispatcher):用于管理Event Handler,定义注册、移除 EventHandler等。它还作为Reactor模式的入口调用Synchronous Event Demultiplexer的select方法以阻 塞等待事件返回,当阻塞等待返回时,根据事件发生的Handle将其分发给对应的Event Handler处理,即回调 EventHandler中的handle_event()方法 同步(多路)事件分离器(Synchronous Event Demultiplexer):无限循环等待新事件的到来,一旦发现 有新的事件到来,就会通知初始事件分发器去调取特定的事件处理器。 系统处理程序(Handles):操作系统中的句柄,是对资源在操作系统层面上的一种抽象,它可以是打开的文 件、一个连接(Socket)、Timer等。由于Reactor模式一般使用在网络编程中,因而这里一般指Socket Handle,即一个网络连接(Connection,在Java NIO中的Channel)。这个Channel注册到Synchronous Event Demultiplexer中,以监听Handle中发生的事件,对ServerSocketChannnel可以是CONNECT事件,对 SocketChannel可以是READ、WRITE、CLOSE事件等。 事件处理器(Event Handler): 定义事件处理方法,以供Initialization Dispatcher回调使用。 对于Reactor模式,可以将其看做由两部分组成,一部分是由Boss组成,另一部分是由worker组成。Boss就像老板 一样,主要是拉活儿、谈项目,一旦Boss接到活儿了,就下发给下面的work去处理。也可以看做是项目经理和程序 员之间的关系。 对于Reactor模式,可以将其看做由两部分组成,一部分是由Boss组成,另一部分是由worker组成。Boss就像老板 一样,主要是拉活儿、谈项目,一旦Boss接到活儿了,就下发给下面的work去处理。也可以看做是项目经理和程序 员之间的关系。 Reactor模式的处理: 服务器端启动一条单线程,用于轮询IO操作是否就绪,当有就绪的才进行相应的读写操作,这样的话就减少了服务器产 生大量的线程,也不会出现线程之间的切换产生的性能消耗。(目前JAVA的NIO就采用的此种模式,这里引申出一个问 题:在多核情况下NIO的扩展问题) 以上两种处理方式都是基于同步的,多线程的处理是我们传统模式下对高并发的处 理方式,Reactor模式的处理是现今面对高并发和高性能一种主流的处理方式。
![](/i/l/?n=22&i=blog/1353113/202208/1353113-20220831142310310-1617382405.png)
![](/i/l/?n=22&i=blog/1353113/202208/1353113-20220831142415008-1266499446.png)
Proactor模式 运用于异步I/O操作,Proactor模式中,应用程序不需要进行实际的读写过程,它只需要从缓存区读取或者写入即 可,操作系统会读取缓存区或者写入缓存区到真正的IO设备. Proactor中写入操作和读取操作,只不过感兴趣的事件是写入完成事件。 Proactor模式结构
![](/i/l/?n=22&i=blog/1353113/202208/1353113-20220831142509979-1087190425.png)
![](/i/l/?n=22&i=blog/1353113/202208/1353113-20220831142829755-2089451994.png)
![](/i/l/?n=22&i=blog/1353113/202208/1353113-20220831143026461-208776982.png)
![](/i/l/?n=22&i=blog/1353113/202208/1353113-20220831143230857-49669411.png)
标签:缓存,空间,内核,IO,spring2,磁盘,CPU From: https://www.cnblogs.com/zhouyideboke/p/16643024.html