Java nio是在jdk1.4引入的,在jdk1.4以前java只有BIO。BIO代表同步阻塞IO和NIO代表的是同步非阻塞IO。
《UNIX网络编程》提出一个输入操作包含两个不同的阶段(6.2 I/O模型)
- 等待数据准备好
- 从内核向进程复制数据。
对于一个套接字上的输入操作,第一步通常涉及等待数据从网络中到达,当所等待分组到达时,它被复制到内核中的某个缓冲区。第二个就是把数据从内核缓冲区复制到应用进程缓冲区。
Linux中内存分为用户空间、内核空间。程序都在用户空间进行内存分配,为了系统的安全性,用户空间不能直接操作硬件资源,用户空间必须通过调用系统提供的API接口进行访问。每次系统调用都需要两个内存空间的数据拷贝。
Linux提供了五种I/O模型
阻塞式I/O模型
如图,在进程在发起系统调用时,数据从准备好到复制完成,线程都是同步调用,就相当于你调用一个userinfo.setList(假设数据很大)这个方法,如果花费了1秒钟,那么这一过程中这个线程相当于sleep。不能进行任何其他操作,只能等内核处理完后,调用notify()进行唤醒。
非阻塞式I/O模型
如图非阻塞模式下,应用进程进行内核调用时,内核立即返回,而不是等待数据复制完成。
I/O复用模型
如图,用户线程在进行系统调用时阻塞于select,等待数据变为可读,然后调用recvfrom将数据复制到应用进程缓冲区。Linux提供了三种select、poll、epoll,后面讲这三个的区别。
信号驱动式模型
信号驱动I/O进行系统调用时也是立即返回,与非阻塞模式相比在数据变为可读时,内核会回调应用线程,然后应用线程再调用recvfrom将数据复制到应用进程缓冲区。
异步I/O
相比于信号驱动,异步线程不关注数据是否已经准备就绪,而是关注于复制是否完成。在应用调用后立即返回,当内核将数据复制到用户缓冲区后会发送信号告诉应用线程可以进行操作。
五种I/O模式对比,前四种模式主要区别与第一阶段,第二阶段都是一样的,在数据从内核复制到调用者的缓冲区中,都需要调用recvfrom调用。异步I/O这两个模型都需要处理。
同步I/O模型,导致请求进程阻塞,知道I/O操作完成。
异步I/O模型,不导致请求进程阻塞。
I/O复用。
Linux提供select/poll,进程通过将一个或者多个fd传递给select或poll系统调用,阻塞在select操作上,这样select/poll可以帮助我们侦测多个fd是否处于就绪状态。select/poll是顺序扫描fd是否就绪,而且支持的fd数量有限,因此它的使用受到了一些制约。Linux还提供了一个epoll系统调用,epoll使用基于事件驱动方式替代顺序扫描,因此性能更高。当有fd就绪时,立即会掉函数callback。
第一阶段为select阶段,select有如下缺点:
* 单个进程能够监视的文件描述符数量存在最大限制,通常是1024,当然可以更改数量,但是数量越多性能越差
* 内核/用户空间的内存拷贝问题,select需要复制大量句柄数据结构从而产生巨大开销
* select返回的是含有整个句柄的数组,应用程序需要遍历整个数组才能发现哪些句柄发生了事件
* select的触发方式是水平触发,应用程序如果没有完成对一个已经就绪的文件描述符进行I/O操作,那么之后每次select调用还是会将这些文件描述符通知进程
相比select,poll使用链表保存文件描述符,因此没有了监视文件数量的限制,但是其它三个缺点依旧存在。
综上,再总结一下,select和poll的实现机制差不多是一样的,只不过函数不同、参数不同,但是基本流程是相同的:
1. 复制用户数据到内核空间
2. 估计超时时间
3. 遍历每个文件并调用f_op->poll()取得文件状态
4. 遍历完成检查状态,如果有就绪的文件则跳转至5、如果有信号产生则重新启动select或者poll、否则挂起进程并等待超时或唤醒超时或再次遍历每个文件状态
5. 将所有文件的就绪状态复制到用户空间
6. 清理申请的资源
epoll函数是第三个阶段,它改进了select与poll的所有缺点,epoll将select与poll分为了三个部分:
1. epoll_ecreate()简历一个epoll对象
2. epoll_ctl向epoll对象中添加socket套接字顺便给内核中断处理程序注册一个callback,高速内核,当文件描述符上有事件到达(或者中断)的时候就调用这个callback
3. 调用epoll_wait收集发生事件的链接
在实现上epoll()的三个核心点是:
1. 使用mmap共享内存,即用户空间和内核空间共享的一块物理地址,这样当内核空间要对文件描述符上的事件进行检查时就不需要来回拷贝数据了
2. 红黑树,用于存储文件描述符,当内核初始化epoll时,会开辟出一块内核高速cache区,这块区域用于存储我们需要监管的所有Socket描述符,由于红黑树的数据结构,对文件描述符增删查效率大为提高
3. rdlist,就绪描述符链表区,这是一个双向链表,epoll_wait()函数返回的也就是这个就绪链表,上面的epoll_ctl说了添加对象的时候会注册一个callback,这个callbakc的作用实际就是将描述符放入rdlist中,所以当一个socket上的数据到达的时候内核就会把网卡上的数据复制到内核,然后把socket描述符插入到就绪链表rdlist中
java NIO 基于I/O复用模型