记一次源码追踪分析,从Java到JNI,再到JVM的C++:fileChannel.map

记一次源码追踪分析,从Java到JNI,再到JVM的C++:fileChannel.map,第1张

文章目录
  • 前言
  • 问题/目的
    • 【问题1】Java中哪些API使用到了mmap
    • 【问题2】怎么知道该API使用到了mmap,如何追踪程序的系统调用
    • 【目的1】源码中分析验证,从Java到JNI,再到C++:fileChannel.map()使用的是系统调用mmap
    • 【目的2】源码验证分析:调用mmapedByteBuffer.put(Byte[])时JVM在搞些什么?mmap比普通的read/write快在哪?
  • 揭晓
    • 【答案1】mmap在Java NIO中的体现/使用
    • 【答案2】程序执行的系统调用追踪
  • 源码追踪分析,从Java到JNI,再到JVM的C++
    • 【目的1】寻源之旅:fileChannel.map()
    • 【目的2】寻源之旅:mmapedByteBuffer.put(Byte\[ \])
  • 总结


前言

在系统IO相关的系统调用有read/write,mmap,sendfile等这些。

  • 其中read/write是普通的读写,每次都需要将buffer从用户空间拷贝到内核空间;
  • mmap使用的是内存映射,会将磁盘文件对应的页映射(拷贝)到内核空间的page cache,并记录到用户进程的页表中,使得用户空间也可以像 *** 作用户空间一样 *** 作该文件的映射,最后再由 *** 作系统来讲该映射(脏页)回写到磁盘;
  • sendfile则使用的是零拷贝技术,在mmap的基础上,当发送数据的时候只拷贝fd和offset等元数据信息,而将数据主体直接拷贝至protocol buffer,实现了内核数据零冗余的零拷贝技术

本文地址:https://blog.csdn.net/weixin_54430656/article/details/124783246



问题/目的 【问题1】Java中哪些API使用到了mmap 【问题2】怎么知道该API使用到了mmap,如何追踪程序的系统调用 【目的1】源码中分析验证,从Java到JNI,再到C++:fileChannel.map()使用的是系统调用mmap 【目的2】源码验证分析:调用mmapedByteBuffer.put(Byte[])时JVM在搞些什么?mmap比普通的read/write快在哪?

揭晓 【答案1】mmap在Java NIO中的体现/使用

看一个例子

	// 1GB
	public static final int _GB = 1*1024*1024;
        File file = new File("filename");
        FileChannel fileChannel = new RandomAccessFile(file, "rw").getChannel();
        MappedByteBuffer mmapedByteBuffer = fileChannel.map(FileChannel.MapMode.READ_WRITE, 0, _GB);
        for (int i = 0; i < _GB; i++) {
            count++;
            mmapedByteBuffer.put((byte)0);
        }

其中fileChannel.map()底层使用的就是系统调用mmap,函数签名为:
public abstract MappedByteBuffer map(MapMode mode,long position, long size)throws IOException



【答案2】程序执行的系统调用追踪
/**
 * @author Tptogiar
 * @description
 * @date 2022/5/14 - 19:32
 */
public class TestMappedByteBuffer{


    public static final int _4kb = 4*1024;
    public static final int _GB  = 1*1024*1024;

    public static void main(String[] args) throws IOException, InterruptedException {
		// 为了方便在日志中找到本段代码的开始位置和结束位置,这里利用文件io来打开始标记
        FileInputStream startInput = null;
        try {
            startInput = new FileInputStream("start1.txt");
            startInput.read();
        } catch (IOException e) {
            e.printStackTrace();
        }
        
        File file = new File("filename");
        FileChannel fileChannel = new RandomAccessFile(file, "rw").getChannel();
        MappedByteBuffer map = fileChannel.map(FileChannel.MapMode.READ_WRITE, 0, _GB); //我们想分析的语句【问题2】
        for (int i = 0; i < _GB; i++) {
            map.put((byte)0); // 下文中需要分析的语句【目的2】
        }
        
        // 打结束标记
        FileInputStream endInput = null;
        try {
            endInput = new FileInputStream("end.txt");
            endInput.read();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

把上面这段代码编译后把“.class”文件拉到linux执行,并用linux上的strace工具记录其系统调用日志,拿到日志文件我们可以在日志中看到以下信息:

注:日志有3000多行,这里只选取我们关注的

// ...
// 看到了我们打的开始标志
openat(AT_FDCWD, "start1.txt", O_RDONLY) = -1 ENOENT (No such file or directory)


// ... 
// 打开文件,文件描述符fd为6
openat(AT_FDCWD, "filename", O_RDWR|O_CREAT, 0666) = 6
// 判断文件状态
fstat(6, {st_mode=S_IFREG|0644, st_size=1048576, ...}) = 0
// ... 
// 判断文件状态
fstat(6, {st_mode=S_IFREG|0644, st_size=1048576, ...}) = 0
// 进行内存映射
mmap(NULL, 1048576, PROT_READ|PROT_WRITE, MAP_SHARED, 6, 0) = 0x7f2fd6cd4000
// ...
// 程序退出
exit(0)  


// 看到了我们打的结束标志
openat(AT_FDCWD, "end.txt", O_RDONLY)   = -1 ENOENT (No such file or directory)

在上面程序的系统调用日志中我们确实看到了我们打的开始标志,结束标志。在开始标志和结束标志之间我们看到了我们的文件"filename"确实被打开了,文件描述符fd = 6;在打开文件后紧接着又执行了系统调用mmap,这一点我们Java代码一致,这样,我们就验证了我们【答案1】中的结论,可以开始我们的下文了



源码追踪分析,从Java到JNI,再到JVM的C++ 【目的1】寻源之旅:fileChannel.map()

我们知道我们执行Java代码fileChannel.map()确实会在底层调用系统调用,那怎么在源码中得到验证呢?怎么落脚于源码进行分析呢?下面开始我们的寻源之旅

  • FileChannelImpl.map()
    注:由于代码较长,这里代码中略去了一些我们不关注的,比如异常捕获等
public MappedByteBuffer map(MapMode mode, long position, long size)throws IOException{
    // ...
    try {
        // ...
        synchronized (positionLock) {
            // ...
            long mapPosition = position - pagePosition;
            mapSize = size + pagePosition;
            try {
                // !我们要找的语句就在这!
                addr = map0(imode, mapPosition, mapSize);
            } catch (OutOfMemoryError x) {
                // 如果内存不足,先尝试进行GC
                System.gc();
                try {
                    Thread.sleep(100);
                } catch (InterruptedException y) {
                    Thread.currentThread().interrupt();
                }
                try {
                // 再次试着mmap
                    addr = map0(imode, mapPosition, mapSize);
                } catch (OutOfMemoryError y) {
                    // After a second OOME, fail
                    throw new IOException("Map failed", y);
                }
            }
        }
       // ...
    } finally {
        // ...
    }
}

上面函数源码中真正执行mmap的语句是在addr = map0(imode, mapPosition, mapSize),于是我们寻着这里继续追踪

  • FileChannelImpl.map0()
// Creates a new mapping
private native long map0(int prot, long position, long length)throws IOException;

可以看到,该方法是一个native方法,所以后面的源码我们需要到这个FileChannelImpl.class对应的fileChannelImpl.c中去看,所以我们需要去找到JDK的源码

在JDK源码中我们找到fileChannelImpl.c文件

  • fileChannelImpl.c
    根据JNI的对应规则,我们找到该文件内对应的Java_sun_nio_ch_FileChannelImpl_map0方法,其源码如下:
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;
    }
    // !我们要找的语句就在这里!
    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);
}

我们要找的语句就上面代码中的mapAddress = mmap64(0,len,protections,flags,fd,off),至于为什么不是直接的mmap,而是mmap64,是因为这里的mmap64是一个宏,在文件上方有其定义,如下:

#define mmap64 mmap

至此,我们就在源码中得到验证了我们【问题2】中的结论:fileChannelImpl.map()底层使用的是mmap系统调用



【目的2】寻源之旅:mmapedByteBuffer.put(Byte[ ])

接着我们来看看当我们调用mmapedByteBuffer.put(Byte[])JVM底层在搞些什么动作

  • MappedByteBuffer
    首先我们得知道,当我们执行MappedByteBuffer map = fileChannel.map(FileChannel.MapMode.READ_WRITE, 0, _GB)时,实际返回的对象是DirectByteBuffer类的实例,因为MappedByteBuffer为抽象类,且只有DirectByteBuffer继承了它,看下面两图就明白了


  • DirectByteBuffer
    于是我们找到DirectByteBuffer内的put(Byte[ ])方法
public ByteBuffer put(byte x) {
    unsafe.putByte(ix(nextPutIndex()), ((x)));
    return this;
}

可以看到该方法内实际是调用Unsafe类内的putByte方法来实现功能的,所以我们还得去看Unsafe类

  • Unsafe.class
public native void    putByte(long address, byte x);

该方法在Unsafe内是一个native方法,所以所以我们还得去看unsafe.cpp文件内对应的实现

  • unsafe.cpp

在JDK源码中,我们找到unsafe.cpp

在这份源码内,没有使用JNI内普通加前缀的方法来形成对应关系

不过我们还是能顺着源码的蛛丝轨迹找到我们要找的方法

注意到源码中有这样的注册机制,所以我们可以知道我们要找的代码就是上图中标注的代码

顺藤摸瓜,我们就找到了该方法的定义

UNSAFE_ENTRY(void, Unsafe_SetNative##Type(JNIEnv *env, jobject unsafe, jlong addr, java_type x)) \
  UnsafeWrapper("Unsafe_SetNative"#Type); \
  JavaThread* t = JavaThread::current(); \
  t->set_doing_unsafe_access(true); \
  void* p = addr_from_java(addr); \
  *(volatile native_type*)p = x; \
  t->set_doing_unsafe_access(false); \
UNSAFE_END \

该方法内主要的逻辑语句就是以下两句:

  // 获取地址
  void* p = addr_from_java(addr); \
  //设置值
  *(volatile native_type*)p = x; \

至此,我们就知道:其实我们调用mmapedByteBuffer.put(Byte[ ])时,JVM底层并不需要涉及到系统调用(这里也可以用strace工具追踪从而得到验证)。也就是说通过mmap映射的空间在内核空间和用户空间是共享的,我们在用户空间只需要像平时使用用户空间那样就行了————获取地址,设置值,而不涉及用户态,内核态的切换

总结

fileChannelImpl.map()底层用调用系统函数mmap

fileChannelImpl.map()返回的其实不是MappedByteBuffer类对象,而是DirectByteBuffer类对象

在linux上可以通过strace来追踪系统调用

JNI中“.class”文件内方法与“.cpp”文件内函数的对应关系不止是前缀对应的方法,还可以是注册的方式,这一点的追寻代码的时候有很大帮助

directByteBuffer.put()方法底层并没有涉及系统调用,也就不需要涉及切态的性能开销(其底层知识执行获取地址,设置值的 *** 作),所以mmap的性能就比普通读写read/write好

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/langs/921055.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-05-16
下一篇 2022-05-16

发表评论

登录后才能评论

评论列表(0条)

保存