零拷贝与JAVA实现

674 阅读9分钟

本文探究的几个问题

1.什么是零拷贝

2.零拷贝在java中的应用

3.Filechannel的写入与Fileoutputstream的写入的区别

一.PageCache与文件读写

Page cache是磁盘数据在内存中的缓存

PageCache是来自磁盘的需求

为什么需要PageCache

二.零拷贝

常规文件读写

几种零拷贝

零拷贝技术的发展很多样化,现有的零拷贝技术种类也非常多,而当前并没有一个适合于所有场景的零拷贝技术的出现。对于 Linux 来说,现存的零拷贝技术也比较多,这些零拷贝技术大部分存在于不同的 Linux 内核版本,有些旧的技术在不同的 Linux 内核版本间得到了很大的发展或者已经渐渐被新的技术所代替。本文针对这些零拷贝技术所适用的不同场景对它们进行了划分。概括起来,Linux 中的零拷贝技术主要有下面这几种:

1. 直接 I/O:对于这种数据传输方式来说,应用程序可以直接访问硬件存储,操作系统内核只是辅助数据传输:这类零拷贝技术针对的是操作系统内核并不需要对数据进行直接处理的情况,数据可以在应用程序地址空间的缓冲区和磁盘之间直接进行传输,完全不需要 Linux 操作系统内核提供的页缓存的支持。

1. 在数据传输的过程中,避免数据在操作系统内核地址空间的缓冲区和用户应用程序地址空间的缓冲区之间进行拷贝。有的时候,应用程序在数据进行传输的过程中不需要对数据进行访问,那么,将数据从 Linux 的页缓存拷贝到用户进程的缓冲区中就可以完全避免,传输的数据在页缓存中就可以得到处理。在某些特殊的情况下,这种零拷贝技术可以获得较好的性能。Linux 中提供类似的系统调用主要有 mmap(),sendfile() 以及 splice()。

1. 对数据在 Linux 的页缓存和用户进程的缓冲区之间的传输过程进行优化。该零拷贝技术侧重于灵活地处理数据在用户进程的缓冲区和操作系统的页缓存之间的拷贝操作。这种方法延续了传统的通信方式,但是更加灵活。在  Linux  中,该方法主要利用了写时复制技术。

三.FileChannel与FileOutPutStream的write的区别

1.FileOutPutStream:

java中write最终调用的本地函数


    private native void writeBytes(byte b[], int off, int len, boolean append)

        throws IOException;

native方法的实现:


  JNIEXPORT void JNICALL

Java_java_io_FileOutputStream_writeBytes(JNIEnv *env,

    jobject this, jbyteArray bytes, jint off, jint len, jboolean append) {

    writeBytes(env, this, bytes, off, len, append, fos_fd);

}

writeBytes的实现:


  void

writeBytes(JNIEnv *env, jobject this, jbyteArray bytes,

          jint off, jint len, jboolean append, jfieldID fid)

{

    jint n;

    char stackBuf[BUF_SIZE];

    char *buf = NULL;

    FD fd;

    if (IS_NULL(bytes)) {

        JNU_ThrowNullPointerException(env, NULL);

        return;

    }

    if (outOfBounds(env, off, len, bytes)) {

        JNU_ThrowByName(env, "java/lang/IndexOutOfBoundsException", NULL);

        return;

    }

    if (len == 0) {

        return;

    } else if (len > BUF_SIZE) {

        buf = malloc(len);

        if (buf == NULL) {

            JNU_ThrowOutOfMemoryError(env, NULL);

            return;

        }

    } else {

        buf = stackBuf;

    }

    (*env)->GetByteArrayRegion(env, bytes, off, len, (jbyte *)buf);

    if (!(*env)->ExceptionOccurred(env)) {

        off = 0;

        while (len > 0) {

            fd = GET_FD(this, fid);

            if (fd == -1) {

                JNU_ThrowIOException(env, "Stream Closed");

                break;

            }

            if (append == JNI_TRUE) {

                n = IO_Append(fd, buf+off, len);

            } else {

                n = IO_Write(fd, buf+off, len);

            }

            if (n == -1) {

                JNU_ThrowIOExceptionWithLastError(env, "Write error");

                break;

            }

            off += n;

            len -= n;

        }

    }

    if (buf != stackBuf) {

        free(buf);

    }

}

可以看出writeBytes会将java内存的byte拷贝到c空间的内存中,如果一次写入超过8192字节,将会涉及到内存的申请与释放,写入最终调用的是IO_Write或者IO_Append


#define IO_Write handleWrite

#define IO_Append handleWrite

这两者最终都调用handleWrite


ssize_t

handleWrite(FD fd, const void *buf, jint len)

{

    ssize_t result;

    RESTARTABLE(write(fd, buf, len), result);

    return result;

}

可以看出最终调用系统函数write。

FileOutPutStream的写入大概流程为将java空间的数据拷贝到c空间,期间如果写入大小小于8192则会调用栈空间的缓存,否则将申请内存,最后调用write系统函数

2.FileChannel的写入

FileChannelImpl.class


public int write(ByteBuffer var1) throws IOException {

        this.ensureOpen();

        if (!this.writable) {

            throw new NonWritableChannelException();

        } else {

            Object var2 = this.positionLock;

            synchronized(this.positionLock) {

                int var3 = 0;

                int var4 = -1;

                byte var5;

                try {

                    this.begin();

                    var4 = this.threads.add();

                    if (this.isOpen()) {

                        do {

                            var3 = IOUtil.write(this.fd, var1, -1L, this.nd);

                        } while(var3 == -3 && this.isOpen());

                        int var12 = IOStatus.normalize(var3);

                        return var12;

                    }

                    var5 = 0;

                } finally {

                    this.threads.remove(var4);

                    this.end(var3 > 0);

                    assert IOStatus.check(var3);

                }

                return var5;

            }

        }

    }

IOUtil.class


static int write(FileDescriptor var0, ByteBuffer var1, long var2, NativeDispatcher var4) throws IOException {

        if (var1 instanceof DirectBuffer) {

            return writeFromNativeBuffer(var0, var1, var2, var4);

        } else {

            int var5 = var1.position();

            int var6 = var1.limit();

            assert var5 <= var6;

            int var7 = var5 <= var6 ? var6 - var5 : 0;

            ByteBuffer var8 = Util.getTemporaryDirectBuffer(var7);

            int var10;

            try {

                var8.put(var1);

                var8.flip();

                var1.position(var5);

                int var9 = writeFromNativeBuffer(var0, var8, var2, var4);

                if (var9 > 0) {

                    var1.position(var5 + var9);

                }

                var10 = var9;

            } finally {

                Util.offerFirstTemporaryDirectBuffer(var8);

            }

            return var10;

        }

    }


private static int writeFromNativeBuffer(FileDescriptor var0, ByteBuffer var1, long var2, NativeDispatcher var4) throws IOException {

        int var5 = var1.position();

        int var6 = var1.limit();

        assert var5 <= var6;

        int var7 = var5 <= var6 ? var6 - var5 : 0;

        boolean var8 = false;

        if (var7 == 0) {

            return 0;

        } else {

            int var9;

            if (var2 != -1L) {

                var9 = var4.pwrite(var0, ((DirectBuffer)var1).address() + (long)var5, var7, var2);

            } else {

                var9 = var4.write(var0, ((DirectBuffer)var1).address() + (long)var5, var7);

            }

            if (var9 > 0) {

                var1.position(var5 + var9);

            }

            return var9;

        }

    }

FileDispatcherImpl.class


int write(FileDescriptor var1, long var2, int var4) throws IOException {

        return write0(var1, var2, var4, this.append);

    }

static native int write0(FileDescriptor var0, long var1, int var3, boolean var4) throws IOException;

我们看到FileChannel的写入,如果buffer不是直接内存的buffer将复制一份到直接内存里。然后交出直接内存的地址空间交给FileDispatcherImpl来写入

看下FileDispatcherImpl.c是如何实现的


JNIEXPORT jint JNICALL

Java_sun_nio_ch_FileDispatcherImpl_write0(JNIEnv *env, jclass clazz,

                              jobject fdo, jlong address, jint len)

{

    jint fd = fdval(env, fdo);

    void *buf = (void *)jlong_to_ptr(address);

    return convertReturnVal(env, write(fd, buf, len), JNI_FALSE);

}

最终还是调用系统函数write方法

对比总结:

1. FileChannel和FileOutPutStream最终都是调用write系统函数,FileChannel的普通write和零拷贝没关系

1. 我们发现不管是FileChannel还是FileoutputStream,都会涉及到拷贝,根据RednaxelaFX的回答是本地native方法要保证write的数据被钉住,不能因为gc使得对象地址被改变。 www.zhihu.com/question/30…

1. FileChannel可以利用直接内存,如果直接内存能重复利用,将比FileoutPutStream少一次拷贝与内存申请释放

四.FileChannel与零拷贝

FileChannel中的transferTo/From使用的是sendfile

FileChannelImpl.class


    private native long transferTo0(FileDescriptor var1, long var2, long var4, FileDescriptor var6);

FileChannelImpl.c


JNIEXPORT jlong JNICALL

Java_sun_nio_ch_FileChannelImpl_transferTo0(JNIEnv *env, jobject this,

                                            jobject srcFDO,

                                            jlong position, jlong count,

                                            jobject dstFDO)

{

    jint srcFD = fdval(env, srcFDO);

    jint dstFD = fdval(env, dstFDO);

#if defined(__linux__)

    off64_t offset = (off64_t)position;

    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;

可以看出最后调用sendfile64系统函数

五.FileChannel,MMAP的使用


mmap java:

public static void main(String[] args) throws IOException {

RandomAccessFile  fo = new RandomAccessFile(new File("a.txt"),"rw");

long count=1024*1024*50;

MappedByteBuffer buffer=fo.getChannel().map(MapMode.PRIVATE, 0, count);

Stopwatch stopWatch = Stopwatch.createStarted();

for (int i = 0; i < count; i++) {

buffer.put((byte) 1);

        }

stopWatch.stop();

fo.close();

System.out.println(stopWatch.elapsed(TimeUnit.MILLISECONDS));

}


sendfile

public class SendFileTest {

public static void main(String[] args) throws IOException {

RandomAccessFile fo = new RandomAccessFile(new File("a.txt"), "rw");

FileChannel readChannel = new FileInputStream(new File("out.txt)).getChannel();

Stopwatch stopWatch = Stopwatch.createStarted();

fo.getChannel().transferFrom(readChannel, 0, readChannel.size());

stopWatch.stop();

fo.close();

System.out.println(stopWatch.elapsed(TimeUnit.MILLISECONDS));

}

}

mmap和sendfile对比:

sendfile() 系统调用不需要将数据拷贝或者映射到应用程序地址空间中去,所以 sendfile() 只是适用于应用程序地址空间不需要对所访问数据进行处理的情况。相对于 mmap() 方法来说,因为 sendfile 传输的数据没有越过用户应用程序 / 操作系统内核的边界线,所以 sendfile () 也极大地减少了存储管理的开销。但是,sendfile () 也有很多局限性,如下所列:

1. sendfile() 局限于基于文件服务的网络应用程序,比如 web 服务器。据说,在 Linux 内核中实现 sendfile() 只是为了在其他平台上使用 sendfile() 的 Apache 程序。

1. 由于网络传输具有异步性,很难在 sendfile () 系统调用的接收端进行配对的实现方式,所以数据传输的接收端一般没有用到这种技术。

1. 基于性能的考虑来说,sendfile () 仍然需要有一次从文件到 socket 缓冲区的 CPU 拷贝操作,这就导致页缓存有可能会被传输的数据所污染。