1. 前言
最近在学习 Java NIO 方面的知识, 为了加深理解特地去看了 Unix/Linux I/O 方面的知识, 并写了一些代码进行验证在本文接下来的一章中, 我将通过举例的方式向大家介绍五种 I/O 模型如果大家是第一次了解 I/O 模型方面的知识, 理解起来会有一定的难度所以在看文章的同时, 我更建议大家动手去实现这些 I/O 模型, 感觉会不一样好了, 下面咱们一起进入正题吧
2. I/O 模型
本章将向大家介绍五种 I/O 模型, 包括阻塞 I/O 非阻塞 I/OI/O 复用信号驱动式 I/O 异步 I/O 等本文的内容参考了 UNIX 网络编程, 文中所用部分图片也是来自于本书关于 UNIX 网络编程这本书, 我想就不用多说了很多写网络编程方面的文章一般都会参考该书, 本文也不例外如果大家想进深入学习网络编程, 建议去读读这本书
2.1 阻塞 I/O 模型
阻塞 I/O 是最简单的 I/O 模型, 一般表现为进程或线程等待某个条件, 如果条件不满足, 则一直等下去条件满足, 则进行下一步操作相关示意图如下:
上图中, 应用进程通过系统调用 recvfrom 接收数据, 但由于内核还未准备好数据报, 应用进程就阻塞住了直到内核准备好数据报, recvfrom 完成数据报复制工作, 应用进程才能结束阻塞状态
这里简单解释一下应用进程和内核的关系内核即操作系统内核, 用于控制计算机硬件同时将用户态的程序和底层硬件隔离开, 以保障整个计算机系统的稳定运转 (如果用户态的程序可以控制底层硬件, 那么一些病毒就会针对硬件进行破坏, 比如 CIH 病毒) 应用进程即用户态进程, 运行于操作系统之上, 通过系统调用与操作系统进行交互上图中, 内核指的是 TCP/IP 等协议及相关驱动程序客户端发送的请求, 并不是直接送达给应用程序, 而是要先经过内核内核将请求数据缓存在内核空间, 应用进程通过 recvfrom 调用, 将数据从内核空间拷贝到自己的进程空间内大致示意图如下:
阻塞 I/O 理解起来并不难, 不过这里还是举个例子类比一下假设大家日常工作流程设这样的 (其实就是我日常工作的流程), 我们写好代码后, 本地测试无误, 通过邮件的方式, 告知运维同学发布服务运维同学通过发布脚本打包代码, 重启服务(心疼我司的人肉运维) 一般项目比较大时, 重启一次比较耗时而运维同学又有点死脑筋, 非要等这个服务重启好, 再去做其他事结果一天等待的时间比真正工作的时间还要长, 然后就被开了运维同学用这个例子告诉我们, 阻塞式 I/O 效率不太好
2.2 非阻塞 I/O 模型
与阻塞 I/O 模型相反, 在非阻塞 I/O 模型下应用进程与内核交互, 目的未达到时, 不再一味的等着, 而是直接返回然后通过轮询的方式, 不停的去问内核数据准备好没示意图如下:
上图中, 应用进程通过 recvfrom 系统调用不停的去和内核交互, 直到内核准备好数据报从上面的流程中可以看出, 应用进程进入轮询状态时等同于阻塞, 所以非阻塞的 I/O 似乎并没有提高进程工作效率
再用上面的例子进行类比公司辞退了上一个怠工的运维同学后, 又招了一个运维同学这个运维同学每次重启服务, 隔一分钟去看一下, 然后进入发呆状态虽然真正的工作时间增加了, 但是没用啊, 等待的时间还是太长了被公司发现后, 又被辞了
2.3 I/O 复用模型
随着因特网走出实验室, 走出大学, 走进了千家万户各种服务提供商也随之面临着很大的压力, 用户量的快速发展, 导致大家的服务器压力巨大在当时, 可选的路无非有两条, 一是通过多进程的方式压榨单机性能, 但这治标不治本, 毕竟不能创建无限个进程二是扩充硬件资源, 这个可以解决问题, 但是需要很多钱, 只有土豪公司可以这样做在这样一个棘手的情况下, I/O 复用模型应运而生, 使得单机效率较之以往有了很大的提高
I/O 复用模型出现后, 又经过了两次改进第一版的系统调用是 select, 第二版是 poll, 现在在用的是第三版的 epoll 本节不打算对三者进行比较说明, 如果大家感兴趣可以等待我的下一篇文章, 我将会在下一篇文章中简析三者的优缺点本节接下来将以 select 函数为例, 简述该函数的使用过程
select 有三个文件描述符集 (readfds), 分别是可读文件描述符集(writefds) 可写文件描述符集和异常文件描述符集 (exceptfds) 应用程序可将某个 socket (文件描述符)设置到感兴趣的文件描述符集中, 并调用 select 等待所感兴趣的事件发生比如某个 socket 处于可读状态了, 此时应用进程就可调用 recvfrom 函数把数据从内核空间拷贝到进程空间内, 无需再等待内核准备数据了示意图如下:
一般情况下, 应用进程会将多个 socket 设置到感兴趣的文件描述符集中, 并调用 select 等待所关注的事件 (比如可读可写) 处于就绪状态当某些 socket 处于就绪状态后, select 返回处于就绪状态的 sockct 数量注意这里返回的是 socket 的数量, 并不是具体的 socket 应用程序需要自己去确定哪些 socket 处于就绪状态了, 确定之后即可进行后续操作
I/O 复用本身不是很好理解, 所以这里还是举例说明吧话说公司的运维部连续辞退两个运维同学后, 运维部的 leader 觉得需要亲自监督一下大家工作于是 leader 在周会上和大家说, 从下周开始, 所有的发布邮件都由他接收, 并由他转发给相关运维同学, 同时也由他重启服务各位运维同学需要告诉 leader 各自所负责监控的项目, 服务重启好后, leader 会通过内部沟通工具通知相关运维同学至于服务重启的结果(成功或失败),leader 不关心, 需要运维同学自己去看运维同学看好后, 需要把结果回复给开发同学
上面的流程可能有点啰嗦, 所以还是看图吧
把上面的流程进行分步, 如下:
开发同学将发布邮件发送给运维 leader, 并指明这个邮件应该转发给谁
运维告诉 leader, 如果有发给我的邮件, 请发送给我
leader 把邮件转发给相关的运维同学, 并着手重启服务
运维同学看完邮件, 告诉 leader 某某服务重启好后, 请告诉我
服务重启好, leader 通知运维同学 xx 服务启动好了
运维同学查看服务启动情况, 并返回信息给开发同学
这种方式为什么可以提高工作效率呢? 原因在于运维同学一股脑把他所负责的几十个项目都告诉了 leader, 由 leader 重启服务, 并通知运维同学运维同学这个时候等待 leader 的通知, 只要其中一个或几个服务重启好了, 运维同学就回接到通知, 然后就可去干活了而不是像以前一样, 非要等某个服务重启好再进行后面的工作
说一下上面例子的角色扮演开发同学是客户端, leader 是内核开发同学发的邮件相当于网络请求, leader 接收邮件, 并重启服务, 相当于内核准备数据运维同学是服务端应用进程, 告诉 leader 自己感兴趣的事情, 并在最后将事情的处理结果返回给开发同学
不知道大家有没有理解上面的例子, I/O 复用本身可能就不太好理解, 所以看不懂也不要气馁另外, 上面的例子只是为了说明情况, 现实中并不会是这样干, 不然 leader 要累死了如果大家觉得上面的例子不太好, 我建议大家去看看权威资料 UNIX 网络编程同时, 如果能用 select 写个简单的 tcp 服务器, 有助于加深对 I/O 复用的理解如果不会写, 也可以参考我写的代码 select_server.c
2.4 信号驱动式 I/O 模型
信号驱动式 I/O 模型是指, 应用进程告诉内核, 如果某个 socket 的某个事件发生时, 请向我发一个信号在收到信号后, 信号对应的处理函数会进行后续处理示意图如下:
再用之前的例子进行说明某个运维同学比较聪明, 他写了一个监控系统重启服务的过程由监控系统来做, 做好后, 监控系统会给他发个通知在此之前, 运维同学可以去做其他的事情, 不用一直发呆等着了运维同学收到通知后, 首先去检查服务重启情况, 接着再给开发同学回复邮件就行了
相比之前的工作方式, 是不是感觉这种方式更合理从流程上来说, 这种方式确实更合理进程在信号到来之前, 可以去做其他事情, 而不用忙等但现实中, 这种 I/O 模型用的并不多
2.5 异步 I/O 模型
异步 I/O 是指应用进程把文件描述符传给内核后, 啥都不管了, 完全由内核去操作这个文件描述符内核完成相关操作后, 会发信号告诉应用进程, 某某 I/O 操作我完成了, 你现在可以进行后续操作了示意图如下:
上图通过 aio_read 把文件描述符数据缓存空间, 以及信号告诉内核, 当文件描述符处于可读状态时, 内核会亲自将数据从内核空间拷贝到应用进程指定的缓存空间呢拷贝完在告诉进程 I/O 操作结束, 你可以直接使用数据了
接着上一节的例子进行类比, 运维小哥升级了他的监控系统此时, 监控系统不光可以监控服务重启状态, 还能把重启结果整理好, 发送给开发小哥而运维小哥要做的事情就更简单了, 收收邮件, 点点监控系统上的发布按钮然后就可以悠哉悠哉的继续睡觉了, 一天一天的就这么过去了
2.6 总结
上面介绍了 5 种 I/O 模型, 也通过举例的形式对每种模型进行了补充说明, 不知道大家看懂没抛开上面的 I/O 模型不谈, 如果某种 I/O 模型能让进程的工作的时间大于等待的时间, 那么这种模型就是高效的模型在服务端请求量变大时, 通过 I/O 复用模型可以让进程进入繁忙的工作状态中, 减少忙等, 进而提高了效率
I/O 复用模型结果数次改进, 目前性能已经很好了, 也得到了广泛应用像 Nginx,lighttd 等服务器软件都选用该模型好了, 关于 I/O 模型就说到这里
最后附一张几种 I/O 模型的对比图:
3. 写在最后
前面简述了几种 I/O 模型, 并辅以例子进行说明关于 I/O 模型的文章, 网上有很多大家也是各开脑洞, 用了不同的例子进行类比说明, 包括但不限于送外卖送快递飞机调度等等在写这篇文章前, 我也是绞尽脑汁, 希望想一个不同的例子, 不然如果和别人的太像, 免不了有抄袭的嫌疑除此之外, 举的例子还要尽量是大家都知道的, 同时又能说明问题所以这篇文章想例子想的也是挺累的另外, 限于本人语言水平, 文中有些地方可能未能描述清楚如果给大家造成了困扰, 在这里说声抱歉最后声明一下, 本文的例子拿运维同学举例, 本人并无意黑运维同学我们公司运维自动化程度不高, 运维同事们还是很辛苦的, 心疼 5 分钟
来源: https://www.cnblogs.com/nullllun/p/8434330.html