场景:9点上班时,发现9点半有一个需求评审会
理解JAVA NIO之前先了解下Linux IO类型是非常有帮助的。
缓存 I/O
缓存 I/O 又被称作标准 I/O,大多数文件系统的默认 I/O 操作都是缓存 I/O。在 Linux 的缓存 I/O 机制中,操作系统会将 I/O 的数据缓存在文件系统的页缓存( page cache )中,也就是说,数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间。
缓存 I/O 的缺点:
数据在传输过程中需要在应用程序地址空间和内核进行多次数据拷贝操作,这些数据拷贝操作所带来的 CPU 以及内存开销是非常大的。
IO模型
阻塞 I/O(blocking IO)
在linux中,默认情况下所有的socket都是blocking,一个典型的读操作流程大概是这样:
当用户进程调用了recvfrom这个系统调用,kernel就开始了IO的第一个阶段:准备数据(对于网络IO来说,很多时候数据在一开始还没有到达。比如,还没有收到一个完整的UDP包。这个时候kernel就要等待足够的数据到来)。这个过程需要等待,也就是说数据被拷贝到操作系统内核的缓冲区中是需要一个过程的。而在用户进程这边,整个进程会被阻塞(当然,是进程自己选择的阻塞)。当kernel一直等到数据准备好了,它就会将数据从kernel中拷贝到用户内存,然后kernel返回结果,用户进程才解除block的状态,重新运行起来。
blocking IO的特点就是在IO执行的两个阶段都被block了
非阻塞 I/O(nonblocking IO)
linux下,可以通过设置socket使其变为non-blocking。当对一个non-blocking socket执行读操作时,流程是这个样子
当用户进程发出read操作时,如果kernel中的数据还没有准备好,那么它并不会block用户进程,而是立刻返回一个error。从用户进程角度讲 ,它发起一个read操作后,并不需要等待,而是马上就得到了一个结果。用户进程判断结果是一个error时,它就知道数据还没有准备好,于是它可以再次发送read操作。一旦kernel中的数据准备好了,并且又再次收到了用户进程的system call,那么它马上就将数据拷贝到了用户内存,然后返回。
所以,nonblocking IO的特点是用户进程需要不断的主动询问kernel数据好了没有。
I/O 多路复用( IO multiplexing)
IO multiplexing就是我们说的select,poll,epoll,有些地方也称这种IO方式为event driven IO。select/epoll的好处就在于单个process就可以同时处理多个网络连接的IO。它的基本原理就是select,poll,epoll这个function会不断的轮询所负责的所有socket,当某个socket有数据到达了,就通知用户进程。
当用户进程调用了select,那么整个进程会被block,而同时,kernel会“监视”所有select负责的socket,当任何一个socket中的数据准备好了,select就会返回。这个时候用户进程再调用read操作,将数据从kernel拷贝到用户进程。
所以,I/O 多路复用的特点是通过一种机制一个进程能同时等待多个文件描述符,而这些文件描述符(套接字描述符)其中的任意一个进入读就绪状态,select()函数就可以返回。
这个图和blocking IO的图其实并没有太大的不同,事实上,还更差一些。因为这里需要使用两个system call (select 和 recvfrom),而blocking IO只调用了一个system call (recvfrom)。但是,用select的优势在于它可以同时处理多个connection。
所以,如果处理的连接数不是很高的话,使用select/epoll的web server不一定比使用multi-threading + blocking IO的web server性能更好,可能延迟还更大。select/epoll的优势并不是对于单个连接能处理得更快,而是在于能处理更多的连接。
在IO multiplexing Model中,实际中,对于每一个socket,一般都设置成为non-blocking,但是,如上图所示,整个用户的process其实是一直被block的。只不过process是被select这个函数block,而不是被socket IO给block。
epoll
相对于select和poll来说,epoll更加灵活,没有描述符限制。epoll使用一个文件描述符管理多个描述符,将用户关系的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的copy只需一次。
epoll对文件描述符的操作有两种模式:LT(level trigger)和ET(edge trigger)。LT模式是默认模式,LT模式与ET模式的区别如下:
- LT模式:当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序可以不立即处理该事件。下次调用epoll_wait时,会再次响应应用程序并通知此事件。
- ET模式:当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序必须立即处理该事件。如果不处理,下次调用epoll_wait时,不会再次响应应用程序并通知此事件。
ET模式在很大程度上减少了epoll事件被重复触发的次数,因此效率要比LT模式高。epoll工作在ET模式的时候,必须使用非阻塞套接口,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。
JAVA NIO
有了上述背景知识理解java nio就相当容易了。
Java NIO 由以下几个核心部分组成:
- Buffers
- Channels
- Selectors
工作过程:有一个线程selector,负责检查channel的事件(比如读数据,就是否已经将数据从磁盘等读到内核空间;得向Selector注册Channel,然后调用它的select()方法)是否就绪,如果检查到某事件就绪,用户进程就可以进行相应处理(如可以将数据从channel写到buffer)。
Buffers
缓冲区本质上是一块可以写入数据,然后可以从中读取数据的内存。这块用户空间内存被包装成NIO Buffer对象,并提供了一组方法,用来方便的访问该块内存。
为了理解Buffer的工作原理,需要熟悉它的三个属性:
- capacity
- position
- limit
position和limit的含义取决于Buffer处在读模式还是写模式。不管Buffer处在什么模式,capacity的含义总是一样的。
capacity
buffer的大小,这个比较容易理解,就是buffer的大小,分配好之后就不再变了。
position
- 写数据到Buffer中时,position表示当前(也就是下一次读或者写)的位置。初始的position值为0.当一个数据写到Buffer后, position会向前移动到下一个可插入数据的Buffer单元。
- position最大可为capacity -1.
- 当读取数据时,也是从某个特定位置开始读。
limit
- 在写模式下,Buffer的limit表示最多能往Buffer里写多少数据。 写模式下,limit等于Buffer的capacity。
- 当切换Buffer到读模式时, limit表示你最多能读到多少数据。因此,当切换Buffer到读模式时,limit会被设置成写模式下的position值。
读写buffer基本就是针对于这些参数展开的,使用Buffer读写数据一般遵循以下四个步骤:
- 写入数据到Buffer
- 调用flip()方法,flip方法将Buffer从写模式切换到读模式。其实调用flip()方法会将position设回0,并将limit设置成之前写position的值。
- 从Buffer中读取数据
- 调用clear()方法或者compact()方法
一旦读完了所有的数据,就需要清空缓冲区,让它可以再次被写入。有两种方式能清空缓冲区:调用clear()或compact()方法。
- clear()方法会清空整个缓冲区。
- compact()方法只会清除已经读过的数据。任何未读的数据都被移到缓冲区的起始处,新写入的数据将放到缓冲区未读数据的后面。
其它常用方法:
- rewind()方法:Buffer.rewind()将position设回0,所以你可以重读Buffer中的所有数据。
- mark()与reset()方法:通过调用Buffer.mark()方法,可以标记Buffer中的一个特定position。之后可以通过调用Buffer.reset()方法恢复到这个position。
示例代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
package com.meituan.bpdata.nio; import java.io.IOException; import java.io.RandomAccessFile; import java.nio.ByteBuffer; import java.nio.channels.FileChannel; /** * @author jhaoniu * @description * @date 15-11-25 上午10:18 */ public class BufferDemo { public static void main(String[] args) throws IOException { /** for i in `seq 0 9`;do printf $i >>/opt/tmp/test.txt ;done */ RandomAccessFile aFile = new RandomAccessFile("/opt/tmp/test.txt", "rw"); FileChannel inChannel = aFile.getChannel(); /** create buffer with capacity of 10 bytes*/ ByteBuffer buf = ByteBuffer.allocate(10); /** read into buffer.*/ int readBytes = inChannel.read(buf); System.out.println("readBytes:" + readBytes); /** 准备读, position设回0,并将limit设置成之前position的值 */ buf.flip(); while (buf.hasRemaining()) { System.out.println((char) (buf.get())); } /** position设回0 可以重新读 */ buf.rewind(); while (buf.hasRemaining()) { System.out.println((char) (buf.get())); if ((char) (buf.get()) == '5') { /** 在6处做一个mark*/ buf.mark(); } } /** reset 到mark的位置*/ buf.reset(); while (buf.hasRemaining()) { System.out.println((char) (buf.get())); } /** 将buf转为自己数组 */ System.out.println(new String(buf.array())); buf.clear(); //make buffer ready for writing } } |
Channels
Java NIO的通道类似流,但又有些不同:
- 既可以从通道中读取数据,又可以写数据到通道。但流的读写通常是单向的。
- 通道可以异步地读写。
- 通道中的数据总是要先读到一个Buffer,或者总是要从一个Buffer中写入
Selectors
为了将Channel和Selector配合使用,必须将channel注册到selector上。通过SelectableChannel.register()方法来实现
1 2 3 4 5 |
selector = Selector.open(); servChannel = ServerSocketChannel.open(); servChannel.configureBlocking(false); servChannel.socket().bind(new InetSocketAddress(port), 1024); servChannel.register(selector, SelectionKey.OP_ACCEPT); |
一旦向Selector注册了一或多个通道,就可以调用几个重载的select()方法。这些方法返回你所感兴趣的事件(如连接、接受、读或写)已经准备就绪的那些通道。换句话说,如果你对“读就绪”的通道感兴趣,select()方法会返回读事件已经就绪的那些通道。
1 2 3 |
selector.select(3000); Set<SelectionKey> selectedKeys = selector.selectedKeys(); Iterator<SelectionKey> it = selectedKeys.iterator(); //返回就绪的SelectionKey |
处理就绪的通道
1 2 3 4 5 6 7 8 9 |
if (key.isAcceptable()) { // TODO } if (key.isReadable()) { // Read the data SocketChannel sc = (SocketChannel) key.channel(); ByteBuffer readBuffer = ByteBuffer.allocate(1024); int readBytes = sc.read(readBuffer); ..... |
java nio的优势
- 读写数据都是面向缓冲区的(buffer本质是一块内存区域),数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中,读写一个buffer相对于基于字节流和字符流进行操作来说效率高。
- 用单个线程来处理多个Channels的好处是,只需要更少的线程来处理通道。如果不采用nio,我们可能需要使用更多的线程来完成类似的功能,对于操作系统来说,线程之间上下文切换的开销很大,而且每个线程都要占用系统的一些资源(如内存)。
参考:
http://www.ibm.com/developerworks/cn/linux/l-cn-directio/
http://www.cnblogs.com/bigwangdi/p/3182958.html
http://segmentfault.com/a/1190000003063859
http://ifeve.com/selectors/
相关推荐
在进行异步I/O编程时,使用工具如调试器、日志记录、性能分析器等可以帮助理解程序的行为和优化性能。对于初学者来说,阅读和分析开源项目源码也是很好的学习途径,例如查看上述链接的博客文章《异步I/O处理》...
IOCP(I/O Completion Port,I/O完成端口)是Windows操作系统提供的一种高效的异步I/O模型,尤其适用于处理大量并发I/O操作。在这个场景中,"MFC IOCP模型异步IO"指的是使用MFC来实现基于IOCP的异步I/O操作。 IOCP...
在Linux操作系统中,I/O...在实际项目中,你可能还会遇到更多复杂的情况,比如多线程下的I/O同步、异步I/O、缓冲I/O等,这些都是Linux I/O编程的深入话题。不过,从基础实验"open.c"开始,你已经迈出了坚实的第一步。
Libevent和Libuv是两个流行的事件库,它们提供了抽象层以简化跨平台的异步I/O编程。Libevent的《libevent 2.0 book》和《Libevent深入浅出》可以帮助理解其设计理念和使用方法,而Libuv的Design Overview提供了更...
异步I/O和完成端口则提供了最高的并发性和效率,但需要更复杂的编程技巧。 在Windows_Socket_IO模型的压缩包中,你可能会找到以下内容: - 阻塞I/O的示例代码,展示如何创建和使用一个简单的socket进行数据传输。 -...
Netty是一款强大的、高性能的异步I/O框架,专为构建网络应用而设计。它是一个开源的Java库,广泛应用于服务器和客户端的开发,尤其在处理高并发、低延迟的网络服务时,Netty的表现尤为突出。Netty的核心是基于NIO...
除了基本的I/O操作外,还有其他高级I/O机制,例如异步I/O、内存映射文件和缓冲I/O等。异步I/O允许程序在等待I/O操作完成时继续执行其他任务,提高了效率。内存映射文件将文件内容映射到进程的虚拟地址空间,使得访问...
在IT领域,异步I/O(Asynchronous Input/Output)是一种高效的编程模型,尤其是在处理大量I/O密集型任务时,如文件下载。Python作为一种强大的编程语言,提供了多种方式实现异步编程,本项目"一个异步 I/O 下载工具...
- 重叠I/O是Windows特有的另一种异步I/O模型,使用`WSASend`和`WSARecv`函数,允许在I/O操作进行的同时执行其他计算任务。 - 通过配合I/O完成端口(IOCP)使用,可以实现高度并发的网络服务,特别适合服务器端开发...
### 异步I/O编程概览 #### 一、引言 本次讲座由Henrik Thostrup Jensen在2006年4月20日进行,主题为《异步I/O编程》。讲座旨在探讨异步I/O的概念、优势、挑战以及如何在实际编程中应用这些技术。 #### 二、什么是...
通过《Java I/O, 2nd Edition》这本著作,读者将能够深入理解Java的I/O系统,掌握高效、安全的I/O编程技巧,无论是处理文件、网络通信还是序列化,都能游刃有余。对于Java开发者来说,这是一本不可多得的参考资料。
- **原理**:重叠I/O是Windows系统提供的高级异步I/O模型,它允许I/O操作在发出后立即返回,而无需等待操作完成。 - **使用结构**:使用`OVERLAPPED`结构体记录I/O操作的状态,并通过`WSASend`和`WSARecv`等函数...
常见的I/O模型有阻塞I/O、非阻塞I/O、I/O多路复用、信号驱动I/O以及异步I/O。在WINSOCK中,这些模型都被支持,并且可以根据应用场景选择合适的模型。 1. **阻塞I/O**:这是最基本的模型,当一个套接字进行读写操作...
在IT领域,异步I/O(Asynchronous Input/Output)是一种高效的编程模型,它允许程序在等待I/O操作完成时继续...通过理解和运用这个库,开发者可以更好地理解和实践Linux下的异步I/O编程,提升软件的效率和用户体验。
在编程模型方面,异步I/O方式最适合与事件驱动模型相结合。事件驱动模型通过事件队列管理多个事件的处理,每当有I/O操作完成时,就会触发相应的事件,从而激活特定的事件处理程序。这种方式大大简化了并发控制,并...
而Node.js推崇的是非阻塞I/O模型,通过事件驱动和回调函数实现异步操作,以提高系统的并发能力。 总结来说,同步与异步是关于处理结果获取方式的不同策略,而阻塞与非阻塞是关于处理过程中线程状态管理的差异。在...
同步I/O在进行数据传输时会阻塞进程,直到I/O操作完成,而异步I/O则在发起I/O请求后,操作系统会负责后续的数据传输过程,不会阻塞进程,程序可以继续执行其他任务,当I/O操作完成时,操作系统通过回调函数或Future...
学习Node.js异步I/O的开发者需要注意,虽然单线程模型简化了并发编程的复杂性,但也带来了单点故障的风险,如果某个操作耗时过长,它将阻塞整个事件循环。因此,开发者应当尽量避免在回调中执行繁重的计算任务,合理...
重叠I/O模型是一种非阻塞I/O模型,通过使用`OVERLAPPED`结构来异步执行I/O操作。这种方式非常适合高并发场景。 **示例代码:** ```c // 创建重叠结构 OVERLAPPED ovl; ZeroMemory(&ovl, sizeof(OVERLAPPED)); // ...
2. **事件驱动**:基于事件驱动的模型,如Node.js,通过事件循环机制来处理异步I/O,适用于需要处理大量并发连接的场景。 3. **Promise/Future**:这种模式通过Promise或Future对象来封装异步操作的结果,提供了链式...