javaaio实现_深⼊理解JavaAIO(三)——Linux中的AIO实现我们调⽤的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的所有请求后,进⼊闲置状态;
java上下文context异步处理线程在闲置状态时,如果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。
参考资料:
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论