- 前言
- 问题/目的
- 【问题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好
…
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)