I/O模型html
在开始NIO的学习以前,先对I/O的模型有一个理解,这对NIO的学习是绝对有好处的。我画一张图,简单表示一下数据从外部磁盘向运行中进程的内存区域移动的过程:编程
这张图片明显忽略了不少细节,只涉及了基本操做,下面分析一下这张图。缓存
用户空间和内核空间安全
一个计算机一般有必定大小的内存空间,如一台计算机有4GB的地址空间,可是程序并不能彻底使用这些地址空间,由于这些地址空间是被划分为用户空间和内核空间的。程序只能使用用户空间的内存,这里所说的使用是指程序可以申请的内存空间,并非真正访问的地址空间。下面看下什么是用户空间和内核空间:网络
一、用户空间dom
用户空间是常规进程所在的区域,什么是常规进程,打开任务管理器看到的就是常规进程:异步
JVM就是常规进程,驻守于用户空间,用户空间是非特权区域,好比在该区域执行的代码不能直接访问硬件设备。分布式
二、内核空间函数
内核空间主要是指操做系统运行时所使用的用于程序调度、虚拟内存的使用或者链接硬件资源等的程序逻辑。内核代码有特别的权利,好比它能与设备控制器通信,控制着整个用于区域进程的运行状态。和I/O相关的一点是:全部I/O都直接或间接经过内核空间。性能
那么,为何要划分用户空间和内核空间呢?这也是为了保证操做系统的稳定性和安全性。用户程序不能够直接访问硬件资源,若是用户程序须要访问硬件资源,必须调用操做系统提供的接口,这个调用接口的过程也就是系统调用。每一次系统调用都会存在两个内存空间之间的相互切换,一般的网络传输也是一次系统调用,经过网络传输的数据先是从内核空间接收到远程主机的数据,而后再从内核空间复制到用户空间,供用户程序使用。这种从内核空间到用户控件的数据复制很费时,虽然保住了程序运行的安全性和稳定性,可是牺牲了一部分的效率。
最后,如何分配用户空间和内核空间的比例也是一个问题,是更多地分配给用户空间供用户程序使用,仍是首先保住内核有足够的空间来运行,仍是要平衡一下。在当前的Windows 32位操做系统中,默认用户空间:内核空间的比例是1:1,而在32位Linux系统中的默认比例是3:1(3GB用户空间、1GB内核空间)。
进程执行I/O操做的步骤
缓冲区,以及缓冲区如何工做,是全部I/O的基础。所谓"输入/输出"讲的无非也就是把数据移入或移出缓冲区。
进程执行I/O操做,归结起来,就是向操做系统发出请求,让它要么把缓冲区里的数据排干净(写),要么用数据把缓冲区填满(读)。进程利用这一机制处理全部数据进出操做,操做系统内部处理这一任务的机制,其复杂程度可能超乎想像,但就概念而言,却很是直白易懂,从上面的图,能够总结一下进程执行I/O操做的几步:
一、进程使用底层函数read(),创建和执行适当的系统调用,要求其缓冲区被填满,此时控制权移交给内核
二、内核随即向磁盘控制硬件发出命令,要求其从磁盘读取数据
三、磁盘控制器和数据直接写入内核内存缓冲区,这一步经过DMA完成,无需主CPU协助。这里多提一句,关于DMA,能够百度一下,它是现代电脑的重要特点,它容许不一样速度的硬件装置来沟通,而不须要依赖于CPU的大量中断负载,大大提高了整个系统的效率
四、一盘磁盘控制器把缓冲区填满,内核随即把数据从内核空间的临时缓冲区拷贝到进程执行read()调用时指定的缓冲区
五、进程从用户空间的缓冲区中拿到数据
固然,若是内核空间里已经有数据了,那么该数据只须要简单地拷贝出来便可。至于为何不能直接让磁盘控制器把数据送到用户空间的缓冲区呢?最简单的一个理由就是,硬件一般不能直接访问用户空间。
同步和异步、阻塞和非阻塞
有了上面对于I/O的解读,咱们来看一下同步和异步、阻塞和非阻塞两组概念的区别,主要两者在关注点上有所不一样。
一、同步和异步
同步和异步这个概念比较广,不只仅是在I/O,其余的还有诸如同步调用/异步调用、同步请求/异步请求,都是一个意思。同步和异步,关注的是消息通讯机制。
所谓同步,就是在发出一个"调用请求"时,在没有获得结果以前,该"调用请求"就不返回,可是一旦调用返回就获得返回值了。换句话说,就是由"调用者"主动等待"调用"的结果。像咱们平时写的,方法A调用Math.random()方法、方法B调用String.substring()方法都是同步调用,由于调用者主动在等待这些方法的返回。
所谓异步,则正好相反,"调用"发出以后,这个调用就直接返回了,全部没有返回结果。换句话说,当一个异步调用请求发出以后,调用者不会马上获得结果,所以异步调用适用于那些对数据一致性要求不是很高的场景,好比模块A更新了缓存中的某个值,模块B将某个内容分享到新浪微博,这些模块的关注点更可能是"作了这件事"而不是"作了这件事是否立刻成功",用分布式的话说,就是牺牲了系统的强一致性而提升了整个系统的可用性及分区容错性。若是这种场景下,咱们但愿获取异步调用的结果,"被调用者"能够经过状态、通知来通知调用者,或经过回调函数处理这个调用,对应Java中的有Future/FutureTask、wait/notify。
二、阻塞和非阻塞
阻塞和非阻塞关注的是程序在等待调用结果时的状态。
阻塞调用指的是调用结果返回以前,当前线程会被挂起,调用线程只有在获得结果以后才会返回。
非阻塞调用指的是在不能当即获得结果以前,该调用不会阻塞当前线程。
Linux网络I/O模型
因为绝大多数的Java应用都部署在Linux系统上,所以这里谈一下Linux网络I/O模型。
Linux的内核将全部外部设备都看作一个文件来操做,对一个文件的读写操做会调用内核提供的系统命令,返回一个file descriptor(fd,文件描述符)。而对一个Socket的读写也会有相应的描述符,称为Socketfd(Socket描述符),描述符就是一个数字,它指向内核中的一个结构体(结构体,C/C++数据类型,相似Java中的类,存储各类不一样类型的数据,这里存储的是文件路径、数据区等一些属性)。
根据UNIX网络编程对I/O模型的分类,UNIX提供了5种I/O模型,分别为:
一、阻塞I/O模型
阻塞I/O模型就是最经常使用的I/O模型,缺省状况下全部的文件操做都是阻塞的,以Socket来说解此模型:在用户空间中调用recvfrom,其系统调用直到数据包到达且被复制到应用进程的缓冲区或者发生错误时才返回,在此期间会一直等待,进程在从调用recvfrom开始到它返回的整段时间内都是被阻塞的,所以被称为阻塞I/O。
二、非阻塞I/O模型
recvfrom从用户空间到内核空间的时候,若是该缓冲区没有数据的话,就直接返回一个EWOULDBOCK错误,通常都对非阻塞I/O模型进行轮询检查这个状态,看内核空间是否是有数据到来,有数据到来则从内核空间复制数据到用户空间。
三、I/O复用模型
Linux提供select/poll,进程经过将一个或者多个fd传递给select或poll系统调用,阻塞在select操做上,这样select/poll能够帮助咱们侦测多个fd是否处于就绪状态。select/poll是顺序扫描fd是否就绪,并且支持的fd数量有限,所以它的使用受到了一些制约。Linux还提供了一个epoll系统调用,epoll使用基于事件驱动方式替代顺序扫描,所以性能更高。当有fd就绪时,当即会掉函数rollback。
四、信号驱动I/O模型
首先开启Socket信号驱动I/O功能,并经过系统调用sigaction执行一个信号处理函数(此系统调用当即返回,进程继续工做,它是非阻塞的)。当数据准备就绪时,就为进程生成一个SIGIO信号,经过信号会掉通知应用程序调用recvfrom来读取数据,并通知主循环函数来处理数据。
五、异步I/O
告知内核启动某个操做,并让内核在整个操做完成后(包括将数据从内核复制到用户本身的缓冲区)通知开发者。这种模型与信号驱动I/O模型的主要区别是:信号驱动I/O模型由内核通知开发者什么时候能够开始一个I/O操做,异步I/O模型由内核通知开发者I/O操做什么时候已经完成。