我们调用的Java AIO底层也是要调用OS的AIO实现,而OS主要也就Windows和Linux这两大类,当然还有Solaris和mac这些小众的。

在 Windows 操作系统中,提供了一个叫做 I/O Completion Ports 的方案,通常简称为 IOCP,操作系统负责管理线程池,其性能非常优异,所以在 Windows 中 JDK 直接采用了 IOCP 的支持。

而在 Linux 中其实也是有AIO 的实现的,但是限制比较多,性能也一般,所以 JDK 采用了自建线程池的方式,也就是说JDK并没有用Linux提供的AIO。但是本文主要想聊的,就是Linux中的AIO实现。

Linux AIO主要有三种实现:

glibc 的 AIO 本质上是由多线程在用户态下模拟出来的异步IO,但是glibc下的POSIX AIO的bug太多,而且找不到相关资料,所以我们这里不详细讲。(我查了一下相关资料大多数是11年左右的那几篇文章,感兴趣的可以自己去了解一下)

而libeio的实现和glibc很像,也是由多线程在用户态下模拟出来的异步IO,是libev 的作者 Marc Alexander Lehmann大佬写的,一会会提及它

Linux 2.6以上的版本实现了内核级别的AIO,内核的AIO只能以 O_DIRECT(直接写入磁盘) 的方式做直接 IO(使用了虚拟文件系统,其他OS不一定能用)

glibc 的 AIO

异步请求被提交到request_queue中;

request_queue实际上是一个表结构,"行"是fd、"列"是具体的请求。也就是说,同一个fd的请求会被组织在一起;

异步请求有优先级概念,属于同一个fd的请求会按优先级排序,并且最终被按优先级顺序处理;

随着异步请求的提交,一些异步处理线程被动态创建。这些线程要做的事情就是从request_queue中取出请求,然后处理之;

为避免异步处理线程之间的竞争,同一个fd所对应的请求只由一个线程来处理;

异步处理线程同步地处理每一个请求,处理完成后在对应的aiocb中填充结果,然后触发可能的信号通知或回调函数(回调函数是需要创建新线程来调用的);

异步处理线程在完成某个fd的所有请求后,进入闲置状态;

异步处理线程在闲置状态时,如果request_queue中有新的fd加入,则重新投入工作,去处理这个新fd的请求(新fd和它上一次处理的fd可以不是同一个);

异步处理线程处于闲置状态一段时间后(没有新的请求),则会自动退出。等到再有新的请求时,再去动态创建;

更详细的可以自己去看Linux里面的源码是怎么写的

libeio 的 AIO

1. 主线程调用eio_init函数,主要是初始化req_queue(请求队列),res_queue(响应队列)以及对应的mutex(互斥锁)和cond(pthread,Linux多线程部分);

2-3. 所有的IO操作其实都是对eio_sumbit的调用,而eio_sumbit的职能是将IO操作封装为request并插入到req_queue;并调用cond_signal向worker线程发出reqwait已经OK的信号;

4. worker线程被创建后执行的函数为etp_proc,etp_proc启动后会一直等待reqwait条件的出现;

5-6. 当reqwait条件变量满足时,etp_proc从req_queue中取得一个待处理的request;并调用eio_execute来同步执行该IO操作;

7-8. eio_execute完成后,将response插入到res_queue队列中;同时调用want_poll来通知主线程request已经处理完毕;

9. 这里worker线程通知主线程的机制是通过向pipe[1]写一个byte数据;

10. 当主线程发现pipe[0]可读时,就调用eio_poll;

11. eio_poll从res_queue里取response,并调用该IO操作在init时设置的callback函数完成后续处理;

12. 在res_queue中没有待处理response时,调用done_poll;

13-14. done_poll从pipe[0]读出一个byte数据,该IO操作完成。

Linux libaio 内核级别AIO

首先是调用 io_setup 函数创建一个aio上下文 aio_context_t (对应内核中的kioctx),这个上下文包含等待队列等内容和一个存放 io_event 的 aio_ring_buffer

调用 io_submit 提交异步请求,每一个请求都会创建一个 iocb 结构用于描述这个请求(对应内核中的kiocb)

调用 aio_rw_vect_retry 提交请求到虚拟文件系统,这个方法调用了 file->f_op(open)->aio_read 或 file->f_op->aio_write 提交到了虚拟文件系统,提交完后 IO 请求立即返回,而不等待虚拟文件系统完成相应操作(这也就是为什么内核级别实现的aio不一定兼容其他OS的原因,因为使用了自有的文件系统)

调用wake_up_process唤醒被阻塞的进程(io_getevents的调用者)

最后然后调用aio_complete 将处理结果写回到对应的io_event中

io_getevents返回结果

内核级AIO与用户线程级别的AIO(glibc和libeio)的比较

从上面的流程可以看出,linux版本的异步IO实际上只是利用了CPU和IO设备可以异步工作的特性(IO请求提交的过程主要还是在调用者线程上同步完成的,请求提交后由于CPU与IO设备可以并行工作,所以调用流程可以返回,调用者可以继续做其他事情)。相比同步IO,并不会占用额外的CPU资源。

而glibc版本的异步IO则是利用了线程与线程之间可以异步工作的特性,使用了新的线程来完成IO请求,这种做法会额外占用CPU资源(对线程的创建、销毁、调度都存在CPU开销,并且调用者线程和异步处理线程之间还存在线程间通信的开销)。不过,IO请求提交的过程都由异步处理线程来完成了(而linux版本是调用者来完成的请求提交),调用者线程可以更快地响应其他事情。如果CPU资源很富足,这种实现倒也还不错。

当调用者连续调用异步IO接口,提交多个异步IO请求时。在glibc版本的异步IO中,同一个fd的读写请求由同一个异步处理线程来完成。而异步处理线程又是同步地、一个一个地去处理这些请求。所以,对于底层的IO调度器来说,它一次只能看到一个请求。处理完这个请求,异步处理线程才会提交下一个。

而内核实现的异步IO,则是直接将所有请求都提交给了IO调度器,IO调度器能看到所有的请求。请求多了,IO调度器使用的类电梯算法就能发挥更大的功效。请求少了,极端情况下(比如系统中的IO请求都集中在同一个fd上,并且不使用预读),IO调度器总是只能看到一个请求,那么电梯算法将退化成先来先服务算法,可能会极大的增加碰头移动的开销。

glibc版本的异步IO支持非direct-io,可以利用内核提供的page cache来提高效率。而linux版本只支持direct-io,cache的工作就只能靠用户程序来实现了。

顺便提一嘴,没有OS提供了本地文件的非阻塞IO(NIO),对于文件的读写,即使以O_NONBLOCK方式来打开一个文件,也会处于"阻塞"状态。因为文件时时刻刻处于可读状态。

不得不说,一旦把一样技术挖到比较深的地方的话,涉及到的就是各种OS的知识甚至C语言的东西了。而这对于我这种只会表面调包的码畜来说非常的不友好,毕竟我没有学过Linux内核相关的知识,没有Linux编程的基础(我甚至连C语言都不怎么熟悉)。希望以后能找时间补上。

(区区Linux AIO,可难不倒我名侦探野比大雄!)PS:如果有错的话希望大家指正,毕竟之前就写错了,虽然我知道根本没人看我blog。

参考资料:

java aio实现_深入理解Java AIO(三)—— Linux中的AIO实现相关推荐

  1. java 异常机制_深入理解Java异常处理机制

    一.引子 try-catch-finally恐怕是大家再熟悉不过的语句了,而且感觉用起来也是很简单,逻辑上似乎也是很容易理解.不过,我亲自体验的"教训"告诉我,这个东西可不是想象中 ...

  2. java事件处理模型_从零开始理解JAVA事件处理机制(3)

    我们连续写了两小节的教师-学生的例子,必然觉得无聊死了,这样的例子我们就是玩上100遍,还是不知道该怎么写真实的代码.那从本节开始,我们开始往真实代码上面去靠拢. 事件最容易理解的例子是鼠标事件:我们 ...

  3. java对象实例_深入理解Java对象实例生成的例子!(转)

    深入理解Java对象实例生成的例子!(转)[@more@]代码如下: class A { public int Avar; public A() { System.out.println(" ...

  4. java 常量折叠_深入理解Java虚拟机之早期编译器优化

    Javac编译器 Javac编译器是一个由Java语言编写的程序 Javac的源码与调试 从Sun Javac的代码来看,编译器大致分为3个过程: 解析与填充符号表的过程 插入式注解处理器的注解处理过 ...

  5. sas java 虚拟机异常_深入理解JAVA虚拟机之异常诊断

    常见的JAVA虚拟机HotSpot虚拟机运行时数据库由5部分构成:方法区,堆,虚拟机栈,本地方法栈,程序计数器.下面列举各个部分可能出现的异常及其出现原因. 1.方法区存放的已被虚拟机加载的类型信息, ...

  6. java class教程_深入理解Java Class文件格式(七)

    本专栏列前面的一系列博客, 对Class文件中的一部分数据项进行了介绍. 本文将会继续介绍class文件中未讲解的信息. 先回顾一下上面一篇文章. 在上一篇博客中, 我们介绍了: this_class ...

  7. java 准备 解析_深入理解JAVA虚拟机学习笔记24——类加载的准备和解析

    每天进步一点点! 今天我们一起看一下类加载的准备阶段和解析阶段. 先看一下准备阶段:主要任务是在方法区中为类变量(仅static修饰变量,不包含实例变量)分配内存并设置类变量初始化的阶段. 这里面的区 ...

  8. java异常机制_全面理解java异常机制

    在理想状态下,程序会按照我们预想的步骤一步一步的执行,但是即使你是大牛,你也不可避免出错,所以java为我们提供了异常机制.本文将会从以下几个方面介绍java中的异常机制: 异常机制的层次结构 异常的 ...

  9. java虚拟机编程_深入理解Java虚拟机(一)

    一.运行时数据区域 1.程序计数器: 当前线程执行字节码的行号指示器(通过改变计数器的值来选择下条需要执行的字节码指令) 每个线程有独立的程序计数器(线程私有,为了切换线程时能恢复到挣钱的执行位置) ...

最新文章

  1. Android OpenGL使用GLSurfaceView预览视频
  2. python装饰器简单理解
  3. 转载-centos网络配置(手动设置,自动获取)的2种方法
  4. 模仿mongodb采用xml+json实现小型数据库
  5. [iOS]利用Appicon and Launchimage Maker生成并配置iOSApp的图标和启动页
  6. Note - Shader - 2
  7. POJ 3481 treap
  8. vue:webpack + vue-cil 中 proxyTable 处理跨域
  9. 如何共享计算机磁盘,扩展群集共享磁盘的分区 - Windows Server | Microsoft Docs
  10. 凸优化第二章凸集 2.6 对偶锥与广义不等式
  11. 在售后技术服务里,Kubernetes到底是什么? | 凌云时刻
  12. 深入浅出数据分析读书笔记(head first into data analysis)
  13. 动态切换 web 报表中的统计图类型
  14. grldr文件引导windows xp、ubuntu系统
  15. 任意大小 内存池 c语言,C语言内存池使用模型-1 - Mr.南柯 - 51Testing软件测试网 51Testing软件测试网-软件测试人的精神家园...
  16. oracle 按照lead,Oracle之Lead和Lag偏移量函数使用详解
  17. 英雄联盟 LCUAPI
  18. [1150]Linux服务器上使用rz命令上传文件报:Segmentation Fault
  19. 【微信小程序】实现页面跳转功能
  20. SAP所有模块用户出口(User Exits) 三

热门文章

  1. EventLog Analyzer:高效保护网络安全的强大工具
  2. VS 防反编译(混淆)dll、exe
  3. 博客园客户端UAP开发随笔 -- App UI设计的三大纪律八项注意
  4. 编译器之语法分析器(syntax analyzer)
  5. 树莓派+MediaPipe+PCA9685+自制摄像机云台实现人脸跟踪移动
  6. 数据仓库之维度表和事实表
  7. python里面Nose和pytest的区别
  8. REVIT建模如何 一键生成局部三维视图
  9. 教育培训python
  10. 实验实例 —逻辑门设计