在信息交换的过程中,计算机都是对这些流进行数据的收发操作,简称为I/O操作(Input and Output)
通常用户进程中的一次完整I/O交互流程分为两阶段,首先是经过内核空间,也就是由操作系统处理;紧接着就是到用户空间,也就是交由应用程序。
内核空间中存放的是内核代码和数据,而进程的用户空间中存放的是用户程序的代码和数据。
操作系统和驱动程序运行在内核空间,应用程序运行在用户空间,两者不能简单地使用指针传递数据。因为Linux使用的虚拟内存机制,必须通过系统调用请求Kernel来协助完成I/O操作,内核会为每个I/O设备维护一个缓冲区,用户空间的数据可能被换出,所以当内核空间使用用户空间的指针时,对应的数据可能不在内存中。
一个网络输入操作通常包括两个不同阶段。
(1)等待网络数据到达网卡,然后将数据读取到内核缓冲区。
(2)从内核缓冲区复制数据,然后拷贝到用户空间。I/O有内存I/O、网络I/O和磁盘I/O三种,通常我们说的I/O指的是后两者。如下图所示是I/O通信过程的调度示意。
五种I/O模型,分别是:
阻塞I/O模型
非阻塞I/O模型
多路复用I/O模型
信号驱动I/O模型
异步I/O模型
当用户进程调用了recvfrom这个系统调用,内核就开始了I/O的第一个阶段:准备数据。对于网络I/O来说,很多时候数据在一开始还没有到达(比如,还没有收到一个完整的UDP包),这个时候内核就要等待足够的数据到来。而在用户进程这边,整个进程会被阻塞,当数据准备好时,它就会将数据从内核拷贝到用户内存,然后返回结果,用户进程才解除阻塞的状态,重新运行起来。几乎所有的开发者第一次接触到的网络编程都是从listen()、send()、recv()等接口开始的,这些接口都是阻塞型的
当用户进程发出read操作时,如果内核中的数据还没有准备好,那么它并不会阻塞用户进程,而是立刻返回一个error。从用户进程角度讲,它发起一个read操作后,并不需要等待,而是马上就得到了一个结果,用户进程判断结果是一个error时,它就知道数据还没有准备好。于是它可以再次发送read操作,一旦内核中的数据准备好了,并且再次收到了用户进程的系统调用,那么它会马上将数据拷贝到用户内存,然后返回,非阻塞型接口相比于阻塞型接口的显著差异在于,在被调用之后立即返回
多个进程的I/O可以注册到一个复用器(Selector)上,当用户进程调用该Selector,Selector会监听注册进来的所有I/O,如果Selector监听的所有I/O在内核缓冲区都没有可读数据,select调用进程会被阻塞,而当任一I/O在内核缓冲区中有可读数据时,select调用就会返回,而后select调用进程可以自己或通知另外的进程(注册进程)再次发起读取I/O,读取内核中准备好的数据,多个进程注册I/O后,只有一个select调用进程被阻塞。
多路复用I/O相对阻塞和非阻塞更难简单说明,所以额外解释一段,其实多路复用I/O模型和阻塞I/O模型并没有太大的不同,事实上,还更差一些,因为这里需要使用两个系统调用(select和recvfrom),而阻塞I/O模型只有一次系统调用(recvfrom)。但是,用Selector的优势在于它可以同时处理多个连接,所以如果处理的连接数不是很多,使用select/epoll的Web Server不一定比使用多线程加阻塞I/O的Web Server性能更好,可能延迟还更大,select/epoll的优势并不是对于单个连接能处理得更快,而是能处理更多的连接
信号驱动I/O是指进程预先告知内核,向内核注册一个信号处理函数,然后用户进程返回不阻塞,当内核数据就绪时会发送一个信号给进程,用户进程便在信号处理函数中调用I/O读取数据。从上图可以看出,实际上I/O内核拷贝到用户进程的过程还是阻塞的,信号驱动I/O并没有实现真正的异步,因为通知到进程之后,依然由进程来完成I/O操作。这和后面的异步I/O模型很容易混淆,需要理解I/O交互并结合五种I/O模型进行比较阅读
用户进程发起aio_read操作后,给内核传递与read相同的描述符、缓冲区指针、缓冲区大小三个参数及文件偏移,告诉内核当整个操作完成时,如何通知我们立刻就可以开始去做其他的事;而另一方面,从内核的角度,当它收到一个aio_read之后,首先它会立刻返回,所以不会对用户进程产生任何阻塞,内核会等待数据准备完成,然后将数据拷贝到用户内存,当这一切都完成之后,内核会给用户进程发送一个信号,告诉它aio_read操作完成。
异步I/O的工作机制是:告知内核启动某个操作,并让内核在整个操作完成后通知我们,这种模型与信号驱动I/O模型的区别在于,信号驱动I/O模型是由内核通知我们何时可以启动一个I/O操作,这个I/O操作由用户自定义的信号函数来实现,而异步I/O模型由内核告知我们I/O操作何时完成。
同步和异步其实是指CPU时间片的利用,主要看请求发起方对消息结果的获取是主动发起的,还是被动通知的,如下图所示。如果是请求方主动发起的,一直在等待应答结果(同步阻塞),或者可以先去处理其他事情,但要不断轮询查看发起的请求是否有应答结果(同步非阻塞),因为不管如何都要发起方主动获取消息结果,所以形式上还是同步操作。如果是由服务方通知的,也就是请求方发出请求后,要么一直等待通知(异步阻塞),要么先去干自己的事(异步非阻塞)。当事情处理完成后,服务方会主动通知请求方,它的请求已经完成,这就是异步。异步通知的方式一般通过状态改变、消息通知或者回调函数来完成,大多数时候采用的都是回调函数。
阻塞和非阻塞在计算机的世界里,通常指针对I/O的操作,如网络I/O和磁盘I/O等。那么什么是阻塞和非阻塞呢?简单地说,就是我们调用了一个函数后,在等待这个函数返回结果之前,当前的线程是处于挂起状态还是运行状态。如果是挂起状态,就意味着当前线程什么都不能干,就等着获取结果,这就是同步阻塞;如果仍然是运行状态,就意味着当前线程是可以继续处理其他任务的,但要时不时地看一下是否有结果了,这就是同步非阻塞
其实前四种I/O模型都是同步I/O操作,它们的区别在于第一阶段,而第二阶段是一样的:在数据从内核拷贝到应用缓冲区期间(用户空间),进程阻塞于recvfrom调用。有人可能会说,NIO(Non-Blocking I/O)并没有被阻塞。这里有个非常“狡猾”的地方,定义中所指的“I/O Operation”是指真实的I/O操作。NIO在执行recvfrom的时候,如果内核(Kernel)的数据没有准备好,这时候不会阻塞进程。但是,当内核(Kernel)中数据准备好的时候,recvfrom会将数据从内核(Kernel)拷贝到用户内存中,这个时候进程就被阻塞了。在这段时间内,进程是被阻塞的
从上图可以看出,阻塞程度:阻塞I/O>非阻塞I/O>多路复用I/O>信号驱动I/O>异步I/O,效率是由低到高的。最后,再看一下下表,从多维度总结了各I/O模型之间的差异,可以加深理解。
Java NIO和BIO之间第一个最大的区别是,BIO是面向流的,NIO是面向缓冲区的。Java BIO面向流意味着每次从流中读一个或多个字节,直至读取所有字节,它们没有被缓存在任何地方。此外,不能前后移动流中的数据。如果需要前后移动从流中读取的数据,需要先将它缓存到一个缓冲区。Java NIO的缓冲导向方法略有不同。数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动。这就增加了处理过程的灵活性。但是,还需要检查该缓冲区是否包含所有需要处理的数据。而且,要确保当更多的数据读入缓冲区时,不能覆盖缓冲区里尚未处理的数据。
Java BIO的各种流是阻塞的。这意味着,当一个线程调用read()或write()时,该线程被阻塞,直到有一些数据被读取,或数据完全写入。该线程在此期间不能再干任何事情。Java NIO的非阻塞模式,是一个线程从某通道(Channel)发送请求读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用,就什么都不会获取,而不是保持线程阻塞,所以直到数据变成可以读取之前,该线程可以继续做其他的事情。非阻塞写也是如此。一个线程请求写入某通道一些数据,但不需要等待它完全写入,这个线程同时可以去做别的事情。线程通常将非阻塞I/O的空闲时间用于在其他通道上执行I/O操作,所以一个单独的线程现在可以管理多个I/O通道。
Java NIO的选择器(Selector)允许一个单独的线程监视多个输入通道,可以注册多个通道使用一个选择器,然后使用一个单独的线程来“选择”通道:这些通道里已经有可以处理的输入,或者选择已准备写入的通道。这种选择机制使一个单独的线程很容易管理多个通道。
JDK 1.7(NIO2)才是实现真正的异步AIO(Asynchronous I/O)、把I/O读写操作完全交给操作系统,学习了Linux Epoll模式
AIO基本原理Java AIO处理API中,重要的三个类分别是:AsynchronousServerSocketChannel(服务端)、AsynchronousSocketChannel(客户端)及CompletionHandler(用户处理器)。CompletionHandler接口实现应用程序向操作系统发起I/O请求,当完成后处理具体逻辑,否则做自己该做的事情,“真正”的异步I/O需要操作系统更强的支持。
在多路复用I/O模型中,事件循环将文件句柄的状态事件通知给用户线程,由用户线程自行读取数据、处理数据。而在异步I/O模型中,当用户线程收到通知时,数据已经被内核读取完毕,并放在了用户线程指定的缓冲区内,内核在I/O完成后通知用户线程直接使用即可。异步I/O模型使用Proactor设计模式实现这一机制