深入理解Java AIO(三)—— 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简单来说是依赖epoll实现的伪AIO,一张图加一小段说明就能讲完。但是我们还是先讲一些前置知识来铺垫一下,最后再聊Linux的AIO。(当然本文不会深入到源码级别,只是讲大概的流程,要说为什么,当然是因为看C语言写的东西实在是太累了,特别是看源码,何况还可能混了汇编)
前置知识
缓存 I/O
缓存 I/O 又被称作标准 I/O,大多数文件系统的默认 I/O 操作都是缓存 I/O。在 Linux 的缓存 I/O 机制中,操作系统会将 I/O 的数据缓存在文件系统的页缓存( page cache )中,也就是说,数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间。
缓存的两个阶段
- 等待数据准备(等待数据进入内核缓存)
- 将数据从内核缓存拷贝到进程内存中
进程的阻塞
正在执行的进程,由于期待的某些事件未发生,如请求系统资源失败、等待某种操作的完成、新数据尚未到达或无新工作做等,则由系统自动执行阻塞原语(Block),使自己由运行状态变为阻塞状态。可见,进程的阻塞是进程自身的一种主动行为,也因此只有处于运行态的进程(获得CPU),才可能将其转为阻塞状态。当进程进入阻塞状态,是不占用CPU资源的。
Linux的五种IO模型
阻塞 I/O
比较简单就不介绍了,有看过我之前的深入理解NIO系列就会懂
非阻塞 I/O
简单说就是用户进程需要不断的主动询问kernel数据好了没有。
I/O 多路复用
我们的select、poll和epoll都属于IO多路复用的模型
这里提一下,它们都属于同步非阻塞IO,为什么同步我们后面会解释。
信号驱动式IO
首先我们允许Socket进行信号驱动IO,并安装一个信号处理函数,进程继续运行并不阻塞。当数据准备好时,进程会收到一个SIGIO信号,可以在信号处理函数中调用I/O操作函数处理数据。
异步 I/O
用户进程发起read操作之后,立刻就可以开始去做其它的事。而另一方面,从kernel的角度,当它受到一个asynchronous read之后,首先它会立刻返回,所以不会对用户进程产生任何block。然后,kernel会等待数据准备完成,然后将数据拷贝到用户内存,当这一切都完成之后,kernel会给用户进程发送一个signal,告诉它read操作完成了。
这就是我们本节的重点,我们心心念念的AIO了。虽然在第三节才讲有点晚,但是铺垫了这么多,我想要说的,就是AIO它到底异步在哪里?或者说什么是异步?
异步和同步的区别
在说明synchronous IO和asynchronous IO的区别之前,需要先给出两者的定义。POSIX的定义是这样子的:
- A synchronous I/O operation causes the requesting process to be blocked until that I/O operation completes;
- An asynchronous I/O operation does not cause the requesting process to be blocked;
两者的区别就在于synchronous IO做”IO operation”的时候会将process阻塞。按照这个定义,之前所述的blocking IO,non-blocking IO,IO multiplexing都属于同步IO。
有人会说,non-blocking IO并没有被block啊。这里有个非常“狡猾”的地方,定义中所指的”IO operation”是指真实的IO操作,就是例子中的recvfrom这个system call。non-blocking IO在执行recvfrom这个system call的时候,如果kernel的数据没有准备好,这时候不会block进程。但是,当kernel中数据准备好的时候,recvfrom会将数据从kernel拷贝到用户内存中,这个时候进程是被block了。
而asynchronous IO则不一样,当进程发起IO 操作之后,就直接返回再也不理睬了,直到kernel发送一个信号,告诉进程说IO完成。在这整个过程中,进程完全没有被block。
通过上面的图片,可以发现non-blocking IO和asynchronous IO的区别还是很明显的。在non-blocking IO中,虽然进程大部分时间都不会被block,但是它仍然要求进程去主动的check,并且当数据准备完成以后,也需要进程主动的再次调用recvfrom来将数据拷贝到用户内存。而asynchronous IO则完全不同。它就像是用户进程将整个IO操作交给了他人(kernel)完成,然后他人做完后发信号通知。在此期间,用户进程不需要去检查IO操作的状态,也不需要主动的去拷贝数据。
Linux的AIO实现
最后我们再来聊一下Linux的伪AIO实现:
- 主线程往epoll内核事件表上注册socket上的读就绪
- 主线程调用epoll_wait等待socket上有数据可读
- 当socket上有数据可读时,epoll_wait通知主线程,主线程从socket上循环读取数据,直到没有更多数据可读,然后将读取到的数据封装成一个请求对象并插入到请求队列
- 睡眠在请求队列上的某个工作线程被唤醒,他获得请求对象并处理客户请求,然后往epoll内核事件表中注册socket上的写就绪
- 主线程调用epoll_wait等待socket可写
- 当socket可写时,epoll_wait通知主线程,主线程往socket上写入服务器处理客户请求结果
(解开个几把,太难了,有些东西还是含糊不清没挖透)
参考资料: https://segmentfault.com/a/1190000003063859 https://www.cnblogs.com/tianzeng/p/10779425.html