注册网站步骤,Wordpress实现中英文,杭州手机建设网站,济南网站模板目录 本章目标
一、用户空间和内核空间
二、PIO与DMA
三、缓存IO和直接IO
1. 缓存IO
2. 直接IO
三、IO访问方式
1. 磁盘IO
2. 网络IO
3. 磁盘IO和网络IO对比
四、Socket网络编程
1. 客户端
2. 服务端
示例一
示例二
示例三
五、同步IO和异步IO
六、阻塞IO和非…目录 本章目标
一、用户空间和内核空间
二、PIO与DMA
三、缓存IO和直接IO
1. 缓存IO
2. 直接IO
三、IO访问方式
1. 磁盘IO
2. 网络IO
3. 磁盘IO和网络IO对比
四、Socket网络编程
1. 客户端
2. 服务端
示例一
示例二
示例三
五、同步IO和异步IO
六、阻塞IO和非阻塞IO
七、IO设计模式之Reactor和Proactor
1. 反应器Reactor
1.1. 简介
1.2. 为什么使用Reactor模式
1.3. Reactor模式结构
1.4. 业务流程及时序图
2. Proactor模式
2.1. Proactor模式结构
2.2. 业务流程及时序图
3. 对比两者的区别
3.1. 主动和被动
3.2. 实现
3.3. 优缺点
优点
缺点
4. 适用场景
八、漫谈五种IO模型
1. 高性能IO模型浅析
2. IO模型举例理解1
3. IO模型举例理解2
4. 五种IO模型介绍
4.1. 同步阻塞IO
4.2. 同步非阻塞IO
4.3. IO多路复用
4.4. 异步IO
九、Redis IO多路复用技术以及epoll实现原理
1. 为什么Redis中要使用I/O多路复用呢
2. epoll实现机制
3. redis epoll底层实现
优势 本章目标
理解Linux中的用户空间和内核空间理解内存与磁盘的PIO和DMA交互方式理解缓存IO和直接IO的方式及区别理解磁盘IO和网络IO的访问方式及区别理解同步IO和异步IO的区别理解堵塞IO和非堵塞IO的区别回顾Socket网络编程中如何利用多线程提高并发能力理解IO设计模式之Reactor和Proactor以及它们的区别理解五种IO模型理解Redis底层关于IO多路复用之epoll实现原理
一、用户空间和内核空间
学习 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 时间分配统计。 这一行有 8 项统计指标。 其中第一项24.8 ususer 的缩写就是 CPU 消耗在 User space 的时间百分比
第二项0.5 sysystem 的 缩写是消耗在 Kernel space 的时间百分比。
随便也说一下其他 6 个指标的含义。
niniceness 的缩写CPU 消耗在 nice 进程低优先级的时间百分比
ididle 的缩写CPU 消耗在闲置进程的时间百分比这个值越低表示 CPU 越忙
wawait 的缩写CPU 等待外部 I/O 的时间百分比这段时间 CPU 不能干其他事
但是也没有执行运算这个 值太高就说明外部设备有问题
hihardware interrupt 的缩写CPU 响应硬件中断请求的时间百分比
sisoftware interrupt 的缩写CPU 响应软件中断请求的时间百分比
ststole 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到用户空间
1. 缓存IO
缓存I/O又被称作标准I/O大多数文件系统的默认I/O操作都是缓存I/O。
在Linux的缓存I/O机制中数据先从磁盘复制到内核空间的缓冲区然后从内核空间缓冲区 复制到应用程序的地
址空间。
读操作操作系统检查内核的缓冲区有没有需要的数据如果已经缓存了那么就直接从缓存中返回否则从磁盘中读 取然后缓存在操作系统的缓存中。写操作将数据从用户空间复制到内核空间的缓存中。这时对用户程序来说写操作就已经完成至于什么时候再写到磁 盘中由操作系统决定除非显示地调用了sync同步命令详情参考《【珍藏】linux 同步IO: sync、fsync与 fdatasync》。缓存I/O的优点 在一定程度上分离了内核空间和用户空间保护系统本身的运行安全可以减少读盘的次数从而提高性能。
缓存I/O的缺点在缓存 I/O 机制中DMA 方式可以将数据直接从磁盘读到页缓存中或者将数据从页缓存直接写回到磁盘 上而不能直接在应用程序地址空间和磁盘之间进行数据传输这样数据在传输过程中需要在应用程序地址 空间用户空间和缓存内核空间之间进行多次数据拷贝操作这些数据拷贝操作所带来的CPU以及内存 开销是非常大的。
2. 直接IO
直接IO就是应用程序直接访问磁盘数据而不经过内核缓冲 区也就是绕过内核缓冲区,自己管理
I/O缓存区这样做的目 的是减少一次从内核缓冲区到用户程序缓存的数据复制。 引入内核缓冲区的目的在于提高磁盘文件的访问性能因为当进程需要读取磁盘文件时如果文件
内容已经在内核缓 冲区中那么就不需要再次访问磁盘而当进程需要向文件中写入数据时实
际上只是写到了内核缓冲区便告诉进程 已经写成功而真正写入磁盘是通过一定的策略进行延迟
的。 然而对于一些较复杂的应用比如数据库服务器它们为了充分提高性能希望绕过内核缓冲
区由自己在用户态空间实现并管理I/O缓冲区包括缓存机制和写延迟机制等以支持独特的查
询机制**比如数据库可以根据更加 合理的策略来提高查询缓存命中率。
另一方面绕过内核缓冲区也可以减少系统内存的开销因为内核缓冲区本身就 在使用系统内
存。 应用程序直接访问磁盘数据不经过操作系统内核数据缓冲区这样做的目的是减少一次从内核缓
冲区到用户程序 缓存的数据复制。
这种方式通常是在对数据的缓存管理由应用程序实现的数据库管理系统中。 直接I/O的缺点就是如果访问的数据不在应用程序缓存中那么每次数据都会直接从磁盘进行加
载这种直接加载 会非常缓慢。通常直接I/O跟异步I/O结合使用会得到较好的性能。 访问步骤 Linux提供了对这种需求的支持即在open()系统调用中增加参数选项O_DIRECT用它打开的文
件便可以绕过内核 缓冲区的直接访问这样便有效避免了CPU和内存的多余时间开销。
顺便提一下与O_DIRECT类似的一个选项是O_SYNC后者只对写数据有效它将写入内核缓
冲区的数据立即写入磁盘将机器故障时数据的丢失减少到最小但是它仍然要经过内核缓冲区。
三、IO访问方式
1. 磁盘IO 具体步骤
当应用程序调用read接口时操作系统检查在内核的高速缓存有没有需要的数据
如果已经缓存了那么就直接从 缓存中返回如果没有则从磁盘中读取然后缓存在操作系统
的缓存中。 应用程序调用write接口时将数据从用户地址空间复制到内核地址空间的缓存中这时对用户程序
来说写操作已 经完成至于什么时候再写到磁盘中由操作系统决定除非显示调用了sync同
步命令。 2. 网络IO
操作系统将数据从磁盘复制到操作系统内核的页缓存中应用将数据从内核缓存复制到应用的缓存中应用 将数据写回内核的Socket缓存中操作系统将数据从Socket缓存区复制到网卡缓存然后将其通过网络发出 当调用read系统调用时通过DMADirect Memory Access将数据copy到内核模式然后由CPU控制将内 核模式数据copy到用户模式下的 buffer中read调用完成后write调用首先将用户模式下 buffer中的数据 copy到内核模式下的socket buffer中最后通过DMA copy将内核模式下的socket buffer中的数据copy到网 卡设备中传送。
从上面的过程可以看出数据白白从内核模式到用户模式走了一圈浪费了两次copy而这两次
copy都是CPU
copy即占用CPU资源。
3. 磁盘IO和网络IO对比
首先磁盘IO主要的延时是由以15000rpm硬盘为例
机械转动延时机械磁盘的主要性能瓶颈平均为 2ms 寻址延时2~3ms 块传输延时
一般4k每块40m/s的传输速度延时一般为0.1ms) 决定。平均 为5ms 而网络IO主要延是由 服务器响应延时 带宽限制 网络延时 跳转路由延时 本地接收延时
决定。一般 为几十到几千毫秒受环境干扰极大
所以两者一般来说网络IO延时要大于磁盘IO的延时
四、Socket网络编程
1. 客户端
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();}
}2. 服务端
示例一
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-8sb.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-8sb.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) {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-8sb.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由于操作系 统的原因相关的开源产品也少
这里学习下其模型结构重点对比下两者的异同点
1. 反应器Reactor
1.1. 简介
反应器设计模式(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模式一般使用在网络编程中因而这里一般指SocketHandle即一个网络连接Connection在Java NIO中的Channel。这个Channel注册到SynchronousEvent Demultiplexer中以监听Handle中发生的事件对ServerSocketChannnel可以是CONNECT事件对 SocketChannel可以是READ、WRITE、CLOSE事件等。事件处理器(Event Handler) 定义事件处理方法以供Initialization Dispatcher回调使用。对于Reactor模式可以将其看做由两部分组成一部分是由Boss组成另一部分是由worker组成。Boss就像老板 一样主要是拉活儿、谈项目一旦Boss接到活儿了就下发给下面的work去处理。也可以看做是项目经理和程序 员之间的关系。
1.2. 为什么使用Reactor模式
并发系统常使用reactor模式代替常用的多线程的处理方式节省系统的资源提高 系统的吞吐
量。例如在高并发的情况下既可以使用多处理处理方式也可以使用Reactor处理方式。
1、多线程的处理
为每个单独到来的请求专门启动一条线程这样的话造成系统的开销很大并且在单核的机上多线程并不
能提高系 统的性能除非在有一些阻塞的情况发生。否则线程切换的开销会使处理的速度变慢。
2、Reactor模式的处理
服务器端启动一条单线程用于轮询IO操作是否就绪当有就绪的才进行相应的读写操作这样的
话就减少了服务器产 生大量的线程也不会出现线程之间的切换产生的性能消耗。(目前JAVA的
NIO就采用的此种模式这里引申出一个问 题在多核情况下NIO的扩展问题)
以上两种处理方式都是基于同步的多线程的处理是我们传统模式下对高并发的处 理方式
Reactor模式的处理是现今面对高并发和高性能一种主流的处理方式。
1.3. Reactor模式结构 Reactor包含如下角色
Handle 句柄用来标识socket连接或是打开文件Synchronous Event Demultiplexer同步事件多路分解器
由操作系统内核实现的一个函数用于阻塞等 待发生在句柄集合上的一个或多个事件如
select/epoll
Event Handler事件处理接口Concrete Event HandlerA实现应用程序所提供的特定事件处理逻辑Reactor反应器定义一个接口实现以下功能 供应用程序注册和删除关注的事件句柄运行事 件循环有就绪事件到来时分发事件到之前注册的回调函数上处理Initiation Dispatcher用于管理Event Handler即EventHandler的容器用以注册、移除 EventHandler等另外它还作为Reactor模式的入口调用Synchronous Event Demultiplexer的select方 法以阻塞等待事件返回当阻塞等待返回时根据事件发生的Handle将其分发给对应的Event Handler处理 即回调EventHandler中的handle_event()方法。
1.4. 业务流程及时序图 应用启动将关注的事件handle注册到Reactor中调用Reactor进入无限事件循环等待注册的事件到来事件到来select返回Reactor将事件分发到之前注册的回调函数中处理
2. Proactor模式
运用于异步I/O操作Proactor模式中应用程序不需要进行实际的读写过程
它只需要从缓存区读取或者写入即 可操作系统会读取缓存区或者写入缓存区到真正的IO设备.
Proactor中写入操作和读取操作只不过感兴趣的事件是写入完成事件。
2.1. Proactor模式结构 Proactor主动器模式包含如下角色
Handle 句柄用来标识socket连接或是打开文件Asynchronous Operation Processor异步操作处理器负责执行异步操作一般由操作系统内核实现Asynchronous Operation异步操作Completion Event Queue完成事件队列异步操作完成的结果放到队列中等待后续使用Proactor主动器为应用程序进程提供事件循环从完成事件队列中取出异步操作的结果分发调用相应
的 后续处理逻辑
Completion Handler完成事件接口一般是由回调函数组成的接口Concrete Completion Handler完成事件处理逻辑实现接口定义特定的应用处理逻辑
2.2. 业务流程及时序图 应用程序启动调用异步操作处理器提供的异步操作接口函数调用之后应用程序和异步操作处理就独立运 行应用程序可以调用新的异步操作而其它操作可以并发进行应用程序启动Proactor主动器进行无限的事件循环等待完成事件到来异步操作处理器执行异步操作完成后将结果放入到完成事件队列主动器从完成事件队列中取出结果分发到相应的完成事件回调函数处理逻辑中
3. 对比两者的区别
3.1. 主动和被动
以主动写为例
Reactor将handle放到select()等待可写就绪然后调用write()写入数据写完处理后续逻辑Proactor调用aoi_write后立刻返回由内核负责写操作写完后调用相应的回调函数处理后续逻辑
可以看出Reactor被动的等待指示事件的到来并做出反应
它有一个等待的过程做什么都要先放入到监听事件集 合中等待handler可用时再进行操作
Proactor直接调用异步读写操作调用完后立刻返回
3.2. 实现
Reactor实现了一个被动的事件分离和分发模型服务等待请求事件的到来再通过不受间断的同
步处理事件从而 做出反应
Proactor实现了一个主动的事件分离和分发模型这种设计允许多个任务并发的执行从而提高吞
吐量并可执行 耗时长的任务各个任务间互不影响
3.3. 优缺点
优点
Reactor实现相对简单对于耗时短的处理场景处理高效 操作系统可以在多个事件源上等待并
且避免了多线程 编程相关的性能开销和
编程复杂性 事件的串行化对应用是透明的可以顺序的同步执行而不需要加锁 事务分 离将
与应用无关的多路分解和分配机制和与应用相关的回调函数分离开来
Proactor性能更高能够处理耗时长的并发场景
缺点
Reactor处理耗时长的操作会造成事件分发的阻塞影响到后续事件的处理
Proactor实现逻辑复杂依赖操作系统对异步的支持目前实现了纯异步操作的操作系统少实现
优秀的如windows IOCP但由于其windows系统用于服务器的局限性目前应用范围较小而
Unix/Linux系统对纯异步的支 持有限应用事件驱动的主流还是通过select/epoll来实现
4. 适用场景
Reactor同时接收多个服务请求并且依次同步的处理它们的事件驱动程序
Proactor异步接收和同时处理多 个服务请求的事件驱动程序
八、漫谈五种IO模型
1. 高性能IO模型浅析
服务器端编程经常需要构造高性能的IO模型常见的IO模型有四种
1同步阻塞IOBlocking IO即传统的IO模型。
2同步非阻塞IONon-blocking IO默认创建的socket都是阻塞的非阻塞IO要求socket被
设置为NONBLOCK。注意这里所说的NIO并非Java的NIONew IO库。
3IO多路复用IO Multiplexing即经典的Reactor设计模式有时也称为异步阻塞IOJava
中的Selector和Linux中的epoll都是这种模型。
4异步IOAsynchronous IO即经典的Proactor设计模式也称为异步非阻塞IO。
2. IO模型举例理解1
阻塞IO, 给女神发一条短信, 说我来找你了, 然后就默默的一直等着女神下楼, 这个期间除了等待你
不 会做其他事情,属于备胎做法.
非阻塞IO, 给女神发短信, 如果不回, 接着再发, 一直发到女神下楼, 这个期间你除了发短信等待不会
做其他事情, 属于专一做法.
IO多路复用, 是找一个宿管大妈来帮你监视下楼的女生, 这个期间你可以些其他的事情. 例如可以顺
便 看看其他妹子,玩玩王者荣耀, 上个厕所等等. IO复用又包括 select, poll, epoll 模式. 那么它们 的
区别是什么? 3.1 select大妈每一个女生下楼, select大妈都不知道这个是不是你的女神, 她需要 一个
一个询问, 并且select大妈能力还有限, 最多一次帮你监视1024个妹子 3.2 poll大妈不限制盯着 女生
的数量, 只要是经过宿舍楼门口的女生, 都会帮你去问是不是你女神 3.3 epoll大妈不限制盯着女 生的数量, 并且也不需要一个一个去问. 那么如何做呢? epoll大妈会为每个
进宿舍楼的女生脸上贴上一 个大字条,上面写上女生自己的名字, 只要女生下楼了, epoll大妈就知道
这个是不是你女神了, 然后大 妈再通知你.
上面这些同步IO有一个共同点就是, 当女神走出宿舍门口的时候, 你已经站在宿舍门口等着女神的,
此时你属 于同步等待状态
接下来是异步IO的情况 你告诉女神我来了, 然后你就去王者荣耀了, 一直到女神下楼了, 发现找不见
你了, 女神再给你打电话通知你, 说我下楼了, 你在哪呢? 这时候你才来到宿舍门口. 此时属于逆袭做法
3. IO模型举例理解2
1、阻塞I/O模型
老李去火车站买票排队三天买到一张退票。 耗费在车站吃喝拉撒睡 3天其他事一件没 干。
2、非阻塞I/O模型
老李去火车站买票隔12小时去火车站问有没有退票三天后买到一张票。耗费往返车 站6次
路上6小时其他时间做了好多事。
3、I/O复用模型
select/poll老李去火车站买票委托黄牛然后每隔6小时电话黄牛询问黄牛三天内 买到票然后老李去火车站交钱领票。 耗费往返车站2次路上2小时黄牛手续费100元打电话17次epoll老李去火车站买票委托黄牛黄牛买到后即通知老李去领然后老李去火车站交钱领票。耗费 往返车站2次路上2小时黄牛手续费100元无需打电话
4、信号驱动I/O模型
老李去火车站买票给售票员留下电话有票后售票员电话通知老李然后老李去火车 站交钱领票。
耗费往返车站2次路上2小时免黄牛费100元无需打电话
5、异步I/O模型
老李去火车站买票给售票员留下电话有票后售票员电话通知老李并快递送票上门。
耗 费往返车站1次路上1小时免黄牛费100元无需打电话
4. 五种IO模型介绍
4.1. 同步阻塞IO
同步阻塞IO模型是最简单的IO模型用户线程在内核进行IO操作时被阻塞。 图1 同步阻塞IO
如图1所示用户线程通过系统调用read发起IO读操作由用户空间转到内核空间。
内核等到数据包到达后然后将 接收的数据拷贝到用户空间完成read操作。 用户线程使用同步阻塞IO模型的伪代码描述为
{read(socket, buffer);process(buffer);}
即用户需要等待read将socket中的数据读取到buffer后才继续处理接收的数据。
整个IO请求的过程中用户线程 是被阻塞的这导致用户在发起IO请求时不能做任何事情对
CPU的资源利用率不够。
4.2. 同步非阻塞IO
同步非阻塞IO是在同步阻塞IO的基础上将socket设置为NONBLOCK。
这样做用户线程可以在发起IO请求后可以立即 返回。 图2 同步非阻塞IO 如图2所示由于socket是非阻塞的方式因此用户线程发起IO请求时立即返回。
但并未读取到任何数据用户线程 需要不断地发起IO请求直到数据到达后才真正读取到数
据继续执行。 用户线程使用同步非阻塞IO模型的伪代码描述为
{while(read(socket, buffer) ! SUCCESS);process(buffer);
}
即用户需要不断地调用read尝试读取socket中的数据直到读取成功后才继续处理接收的数
据。整个IO请求的 过程中虽然用户线程每次发起IO请求后可以立即返回但是为了等到数据
仍需要不断地轮询、重复请求消耗了大量的CPU的资源。
一般很少直接使用这种模型而是在其他IO模型中使用非阻塞IO这一特性。
4.3. IO多路复用
IO多路复用模型是建立在内核提供的多路分离函数select基础之上的使用select函数可以避免同
步非阻塞IO模型 中轮询等待的问题。 图3 多路分离函数select 如图3所示用户首先将需要进行IO操作的socket添加到select中然后阻塞等待select系统调用返
回。
当数据到 达时socket被激活select函数返回。用户线程正式发起read请求读取数据并继续执
行。 从流程上来看使用select函数进行IO请求和同步阻塞模型没有太大的区别甚至还多了添加监视
socket以及调 用select函数的额外操作效率更差。但是使用select以后最大的优势是用户可
以在一个线程内同时处理多个 socket的IO请求。用户可以注册多个socket然后不断地调用select
读取被激活的socket即可达到在同一个线 程内同时处理多个IO请求的目的。而在同步阻塞模型
中必须通过多线程的方式才能达到这个目的。 用户线程使用select函数的伪代码描述为
{select(socket);while(1) {sockets select();for(socket in sockets) {if(can_read(socket)) {read(socket, buffer);process(buffer);}}}
}
其中while循环前将socket添加到select监视中然后在while内一直调用select获取被激活的
socket一旦socket可读便调用read函数将socket中的数据读取出来。 然而使用select函数的优点并不仅限于此。虽然上述方式允许单线程内处理多个IO请求但是每
个IO请求的过程 还是阻塞的在select函数上阻塞平均时间甚至比同步阻塞IO模型还要长。
如果用户线程只注册自己 感兴趣的socket或者IO请求然后去做自己的事情等到数据到来时再
进行处理 则可提高CPU的利用率。
IO多路复用模型使用了Reactor设计模式实现了这一机制。 图5 IO多路复用 如图5所示通过Reactor的方式可以将用户线程轮询IO操作状态的工作统一交给handle_events
事件循环进行处 理。
用户线程注册事件处理器之后可以继续执行做其他的工作异步而Reactor线程负责调用内核
的select函数 检查socket状态。
当有socket被激活时则通知相应的用户线程或执行用户线程的回调函数执行
handle_event进行数据读取、处理的工作。
由于select函数是阻塞的因此多路IO复用模型也被称为异步阻塞IO模 型。注意这里的所说的阻
塞是指select函数执行时线程被阻塞而不是指socket。一般在使用IO多路复用模型 时socket都
是设置为NONBLOCK的不过这并不会产生影响因为用户发起IO请求时数据已经到达了用
户线程 一定不会被阻塞。 用户线程使用IO多路复用模型的伪代码描述为 void UserEventHandler::handle_event() {if(can_read(socket)) {read(socket, buffer);process(buffer);}}{Reactor.register(new UserEventHandler(socket));}
用户需要重写EventHandler的handle_event函数进行读取数据、处理数据的工作
用户线程只需要将自己的 EventHandler注册到Reactor即可。
Reactor中handle_events事件循环的伪代码大致如下。 Reactor::handle_events() {while(1) {sockets select();for(socket in sockets) {get_event_handler(socket).handle_event();}}}
事件循环不断地调用select获取被激活的socket然后根据获取socket对应的EventHandler执行
器handle_event函数即可。
IO多路复用是最常使用的IO模型但是其异步程度还不够“彻底”
因为它使用了会阻塞线程的select系统调用。
因此IO多路复用只能称为异步阻塞IO而非真正的异步IO。
4.4. 异步IO
“真正”的异步IO需要操作系统更强的支持。在IO多路复用模型中事件循环将文件句柄的状态事件
通知给用户线 程由用户线程自行读取数据、处理数据。而在异步IO模型中当用户线程收到通
知时数据已经被内核读取完毕 并放在了用户线程指定的缓冲区内内核在IO完成后通知用户
线程直接使用即可。
异步IO模型使用了Proactor设计模式实现了这一机制 图7 异步IO 如图7所示异步IO模型中用户线程直接使用内核提供的异步IO API发起read请求且发起后立
即返回继续执行 用户线程代码。
不过此时用户线程已经将调用的AsynchronousOperation和CompletionHandler注册到内核然后
操作系统开启独立的内核线程去处理IO操作。当read请求的数据到达时由内核负责读取socket中
的数据并写入 用户指定的缓冲区中。最后内核将read的数据和用户线程注册的
CompletionHandler分发给内部Proactor Proactor将IO完成的信息通知给用户线程一般通过调
用用户线程注册的完成事件处理函数完成异步IO。 用户线程使用异步IO模型的伪代码描述为 void UserCompletionHandler::handle_event(buffer) {process(buffer);}{aio_read(socket, new UserCompletionHandler);}
用户需要重写CompletionHandler的handle_event函数进行处理数据的工作参数buffer表示
Proactor已经准备 好的数据用户线程直接调用内核提供的异步IO API并将重写的
CompletionHandler注册即可。 相比于IO多路复用模型异步IO并不十分常用不少高性能并发服务程序使用IO多路复用模型多
线程任务处理的架 构基本可以满足需求。况且目前操作系统对异步IO的支持并非特别完善更多
的是采用IO多路复用模型模拟异步IO 的方式IO事件触发时不直接通知用户线程而是将数据读
写完毕后放到用户指定的缓冲区中。
Java7之后已经支 持了异步IO感兴趣的读者可以尝试使用。
九、Redis IO多路复用技术以及epoll实现原理
redis 是一个单线程却性能非常好的内存数据库 主要用来作为缓存系统。
redis 采用网络IO多路复用技术来保证在多连接的时候 系统的高吞吐量。
1. 为什么Redis中要使用I/O多路复用呢
首先Redis 是跑在单线程中的所有的操作都是按照顺序线性执行的但是由于读写操作等待用
户输入或输出都 是阻塞的所以 I/O 操作在一般情况下往往不能直接返回这会导致某一文件的
I/O 阻塞导致整个进程无法对其 它客户提供服务而 I/O 多路复用就是为了解决这个问题而出现
的。
selectpollepoll都是IO多路复用的机制。I/O多路复用就通过一种机制可以监视多个描述
符一旦某个 描述符就绪能够通知程进行相应的操作。
redis的io模型主要是基于epoll实现的不过它也提供了 select和kqueue的实 现默认采用epoll。
那么epoll到底是个什么东西呢 我们一起来看看
2. epoll实现机制
设想一下如下场景
有100万个客户端同时与一个服务器进程保持着TCP连接。而每一时刻通常只有几百上千个TCP
连接是活跃的(事实上大部分场景都是这种情况)。如何实现这样的高并发
在select/poll时代服务器进程每次都把这100万个连接告诉操作系统(从用户态复制句柄数据结构
到内核态)让操作系统内核去查询这些套接字上是否有事件发生轮询完后再将句柄数据复制
到用户态让服务器应用程序轮询 处理已发生的网络事件这一过程资源消耗较大因此
select/poll一般只能处理几千的并发连接。 如果没有I/O事件产生我们的程序就会阻塞在select处。但是依然有个问题我们从select那里仅
仅知道了有I/O事件发生了但却并不知道是那几个流可能有一个多个甚至全部我们
只能无差别轮询所有流找出能 读出数据或者写入数据的流对他们进行操作。 但是使用select我们有O(n)的无差别轮询复杂度同时处理的流越多每一次无差别轮询时间就
越长 总结select和poll的缺点如下
每次调用select/poll都需要把fd集合从用户态拷贝到内核态这个开销在fd很多时会很大同时每次调用select/poll都需要在内核遍历传递进来的所有fd这个开销在fd很多时也很大针对select支持的文件描述符数量太小了默认是1024 4. select返回的是含有整个句柄的数组应用程序需要遍历整个数组才能发现哪些句柄发生了事件select的触发方式是水平触发应用程序如果没有完成对一个已经就绪的文件描述符进行IO操作那么之后每次select调用还是会将这些文件描述符通知进程。 相比select模型poll使用链表保存文件描述符因此没有了监视文件数量的限制但其他三个缺点依然存在。epoll的设计和实现与select完全不同。epoll是poll的一种优化返回后不需要对所有的fd进行遍历在内核中维持了fd的列表。
select和poll是将这个内核列表维持在用户态然后传递到内核中。与poll/select不同 epoll不再是
一个单独的系统调用而是由epoll_create/epoll_ctl/epoll_wait三个系统调用组成后面将会 看到
这样做的好处。epoll在2.6以后的内核才支持。 epoll通过在Linux内核中申请一个简易的文件系统(文件系统一般用什么数据结构实现B树)。
把原先的 select/poll调用分成了3个部分
调用epoll_create()建立一个epoll对象(在epoll文件系统中为这个句柄对象分配资源)调用epoll_ctl向epoll对象中添加这100万个连接的套接字调用epoll_wait收集发生的事件的连接
如此一来要实现上面说是的场景只需要在进程启动时建立一个epoll对象然后在需要的时候向
这个epoll对象 中添加或者删除连接。同时epoll_wait的效率也非常高因为调用epoll_wait时
并没有一股脑的向操作系统复 制这100万个连接的句柄数据内核也不需要去遍历全部的连接。 总结epoll的优点如下
epoll 没有最大并发连接的限制上限是最大可以打开文件的数目这个数字一般远大于 2048, 一般来说这 个数目和系统内存关系很大 具体数目可以 cat /proc/sys/fs/file-max 察看。效率提升 epoll 最大的优点就在于它只管你“活跃”的连接 而跟连接总数无关因此在实际的网络环境中epoll 的效率就会远远高于 select 和 poll 。内存拷贝 epoll 在这点上使用了“共享内存”这个内存拷贝也省略了。
3. redis epoll底层实现
当某一进程调用epoll_create方法时Linux内核会创建一个eventpoll结构体这个结构体中有两个
成员与epoll的使用方式密切相关。
eventpoll结构体如下所示 每一个epoll对象都有一个独立的eventpoll结构体用于存放通过epoll_ctl方法向epoll对象中添加进来的事 件。
这些事件都会挂载在红黑树中如此重复添加的事件就可以通过红黑树而高效的识别出来
(红黑树的插入时间 效率是lgn其中n为树的高度)。 而所有添加到epoll中的事件都会与设备(网卡)驱动程序建立回调关系也就是说当相应的事件发
生时会调用这个 回调方法。这个回调方法在内核中叫ep_poll_callback,它会将发生的事件添加到
rdlist双链表中。 在epoll中对于每一个事件都会建立一个epitem结构体如下所示 当调用epoll_wait检查是否有事件发生时只需要检查eventpoll对象中的rdlist双链表中是否有
epitem元素即可。如果rdlist不为空则把发生的事件复制到用户态同时将事件数量返回给用户。
优势
1、不用重复传递
我们调用epoll_wait时就相当于以往调用select/poll但是这时却不用传递socket句柄给 内核
因为内核已经在epoll_ctl中拿到了要监控的句柄列表。
2、在内核里一切皆文件
所以epoll向内核注册了一个文件系统用于存储上述的被监控socket。
当你调用 epoll_create时就会在这个虚拟的epoll文件系统里创建一个file结点。当然这个file不是
普通文件它只 服务于epoll。 poll在被内核初始化时操作系统启动同时会开辟出epoll自己的内核高速cache区用于安置
每一个我们想监控的socket这些socket会以红黑树的形式保存在内核cache里以支持快速的查
找、插入、删除。
这个内核高速cache区就是建立连续的物理内存页然后在之上建立slab层简单的说就是物
理上分配好你 想要的size的内存对象每次使用时都是使用空闲的已分配好的对象。
3、极其高效的原因
这是由于我们在调用epoll_create时内核除了帮我们在epoll文件系统里建了个file结点在内核
cache里 建了个红黑树用于存储以后epoll_ctl传来的socket外还会再建立一个list链表用于存储
准备就绪的事 件当epoll_wait调用时仅仅观察这个list链表里有没有数据即可。有数据就返回
没有数据就sleep等 到timeout时间到后即使链表没数据也返回。所以epoll_wait非常高效。 这个准备就绪list链表是怎么维护的呢
当我们执行epoll_ctl时除了把socket放到epoll文件系统里file对象对应的红黑树上之外还会给内
核中断处 理程序注册一个回调函数告诉内核如果这个句柄的中断到了就把它放到准备就绪
list链表里。所以当一个socket上有数据到了内核在把网卡上的数据copy到内核中后就来把
socket插入到准备就绪链表里了。
注好好 理解这句话 从上面这句可以看出epoll的基础就是回调呀 如此一颗红黑树一张准备就绪句柄链表少量的内核cache就帮我们解决了大并发下的
socket处理问题。
执 行epoll_create时创建了红黑树和就绪链表执行epoll_ctl时如果增加 socket句柄则检查
在红黑树中是否存在存在立即返回不存在则添加到树干 上然后向内核注册回调函数用于
当中断事件来临时向准备就绪链表中插入数 据。执行epoll_wait时立刻返回准备就绪链表里的数据
即可。 最后看看epoll独有的两种模式LT和ET。无论是LT和ET模式都适用于以上所说的流程。
区别是LT模式下只要 一个句柄上的事件一次没有处理完会在以后调用epoll_wait时次次返回
这个句柄而ET模式仅在第一次返回。 关于LTET有一端描述LT和ET都是电子里面的术语ET是边缘触发LT是水平触发一个表
示只有在变化的边 际触发一个表示在某个阶段都会触发。 LT, ET这件事怎么做到的呢当一个socket句柄上有事件时内核会把该句柄插入上面所说的准备
就绪list链表这时我们调用epoll_wait会把准备就绪的socket拷贝到用户态内存然后清空准备
就绪list链表最后epoll_wait干了件事就是检查这些socket如果不是ET模式就是LT模式的
句柄了并且这些socket上确实有未处理的事件时又把该句柄放回到刚刚清空的准备就绪链表
了。 所以非ET的句柄只要它上面还有事件 epoll_wait每次都会返回这个句柄。
从上面这段可以看出LT还有个回放的过程低效了