2018-07-24
JAVA
NIO LINUX
前面已经讲了Selector
,SocketChannel
和DirectBuffer
, 这些是NIO网络编程中最核心的组件
接下来我们会再讲几点非核心的优化(不代表不重要, 只是API不占NIO设计的大头):
这两项本质上都基于零拷贝(zero copy)
技术。
零拷贝(Zero-Copy)是指计算机在执行操作时,CPU不需要先将数据从某处内存复制到一个特定区域,从而节省CPU时钟周期和内存带宽 —-维基百科
拿常用的网络文件传输过程举个栗子:
read
读取磁盘文件内容到内核缓冲区DMA copy
给网卡发送画个图:
可以清楚得看到,有2次copy是没必要的, 就是上面的2和3,还会平白增加2次用户态和内核态上下文切换, 在高并发场景下,这些会很致命。
解决上面这个问题有几个思路
页缓存
和应用进程缓冲区
的传输1和2都是避免应用程序地址空间和内核地址空间两者之间的缓冲区拷贝, 3是从传输的角度优化,因为DMA进行数据传输基本不需要CPU参与,但是用户地址空间的缓冲区和内核的页缓存
传输没有类似DMA的手段, 3就是从这个角度优化。
直接I/O
和传输优化
都涉及到硬件层面我们暂且不讲,主要讲避免上下文切换和数据来回拷贝
这个思路, Linux内核提供了
如图,mmap
将buffer映射到了用户空间,操作的是同一块内存,也不需要切换了, 但是mmap
有个缺点就是, 如果其他进程在向这个文件write
, 那么会被认为是一个错误的存储访问
而sendfile
则没有映射, 保留了mmap
的不需要来回拷贝优点,适用于应用进程不需要对读取的数据做任何处理的场景,如图:
2.6以后还提供了splice
, splice可以在内核态将数据整块的从A复制到B地址。
NIO中通过FileChannel
来提供Zero-Copy
的支持,分别是
FileChannel.map
的基本用法如下:
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 |
/** * 测试FileChannel的用法 * * @author sound2gd * */ public class FileChannnelTest { public static void main(String[] args) { File file = new File("src/com/cris/chapter15/f6/FileChannnelTest.java"); try ( // FileInputStream打开的FileChannel只能读取 FileChannel fc = new FileInputStream(file).getChannel(); // FileOutputStream打开的FileChannel只能写入 FileChannel fo = new FileOutputStream("src/com/cris/chapter15/f6/a.txt").getChannel();) { // 将FileChannel的数据全部映射成ByteBuffer MappedByteBuffer mbb = fc.map(MapMode.READ_ONLY, 0, file.length()); // 使用UTF-8的字符集来创建解码器 Charset charset = Charset.forName("UTF-8"); // 直接将buffer里的数据全部输出 fo.write(mbb); mbb.clear(); // 创建解码器 CharsetDecoder decoder = charset.newDecoder(); // 使用解码器将byteBuffer转换为CharBuffer CharBuffer decode = decoder.decode(mbb); System.out.println(decode); } catch (Exception e) { } } } |
这就是一个基本的例子,用于文件复制,可以看到fo.write(mbb)
的时候,是将mbb Buffer
的数据输出到另一个文件的,看起来就像是
在内存中,而不是在文件里, 这就是内存映射文件.
我们来看看map的实现
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 |
public MappedByteBuffer map(MapMode mode, long position, long size) ...省略非关键代码 try { // 调用map0这个native方法 addr = map0(imode, mapPosition, mapSize); } catch (OutOfMemoryError x) { // An OutOfMemoryError may indicate that we've exhausted memory // so force gc and re-attempt map // gc下防止内存不够 System.gc(); try { // 等待gc结束 Thread.sleep(100); } catch (InterruptedException y) { Thread.currentThread().interrupt(); } try { // 再试一次 addr = map0(imode, mapPosition, mapSize); } catch (OutOfMemoryError y) { // After a second OOME, fail throw new IOException("Map failed", y); } } ... } private native long map0(int prot, long position, long length) throws IOException; |
打开FileChannelImpl.c
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 |
JNIEXPORT jlong JNICALL Java_sun_nio_ch_FileChannelImpl_map0(JNIEnv *env, jobject this, jint prot, jlong off, jlong len) { void *mapAddress = 0; jobject fdo = (*env)->GetObjectField(env, this, chan_fd); jint fd = fdval(env, fdo); int protections = 0; int flags = 0; if (prot == sun_nio_ch_FileChannelImpl_MAP_RO) { protections = PROT_READ; flags = MAP_SHARED; } else if (prot == sun_nio_ch_FileChannelImpl_MAP_RW) { protections = PROT_WRITE | PROT_READ; flags = MAP_SHARED; } else if (prot == sun_nio_ch_FileChannelImpl_MAP_PV) { protections = PROT_WRITE | PROT_READ; flags = MAP_PRIVATE; } // 所以还是使用的mmap这个API mapAddress = mmap64( 0, /* Let OS decide location */ len, /* Number of bytes to map */ protections, /* File permissions */ flags, /* Changes are shared */ fd, /* File descriptor of mapped file */ off); /* Offset into file */ if (mapAddress == MAP_FAILED) { if (errno == ENOMEM) { JNU_ThrowOutOfMemoryError(env, "Map failed"); return IOS_THROWN; } return handle(env, -1, "Map failed"); } return ((jlong) (unsigned long) mapAddress); } |
可以看到,还是使用的我们mmap
的api, 了解一些底层知识还是有必要的, JVM很多东西都是对底层的一层封装.
另一个API transferTo
同理,最后调用的是transferTo0
方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
JNIEXPORT jlong JNICALL Java_sun_nio_ch_FileChannelImpl_transferTo0(JNIEnv *env, jobject this, jint srcFD, jlong position, jlong count, jint dstFD) { off64_t offset = (off64_t)position; // 调用sendfile方法 jlong n = sendfile64(dstFD, srcFD, &offset, (size_t)count); if (n < 0) { if (errno == EAGAIN) return IOS_UNAVAILABLE; if ((errno == EINVAL) && ((ssize_t)count >= 0)) return IOS_UNSUPPORTED_CASE; if (errno == EINTR) { return IOS_INTERRUPTED; } JNU_ThrowIOExceptionWithLastError(env, "Transfer failed"); return IOS_THROWN; } return n; } |
可以看到封装的是sendfile
这个方法,这里看的是jvm在linux系统的的实现。
本文主要介绍了Linux中Zero-Copy零拷贝
的概念,分类和解决方案。
同时介绍了NIO对Zero-Copy
的支持, 分别是FileChannel.map
以及FileChannel.transferTo
.
在高并发场景下,这点提升是很关键的,著名框架Netty
, Kafka
都大量使用了零拷贝的API, 是其高性能的原因之一。