一台服务器把本机磁盘文件的内容发送到客户端,一般分为两个步骤:
传统的WEB服务器在收到请求后,从磁盘读取数据,然后将数据写到网卡,通过网卡发送给客户端,这一读一写的过程中就涉及数据的拷贝:
read和write两个操作发生了两次系统调用,每次系统调用都得先从用户态切换到内核态,等内核完成任务后,再从内核态切换回用户态,也就是消息发送过程中一共发生了4次用户态与内核态的上下文切换;另外还发生了4次数据拷贝,其中两次是DMA的拷贝,另外两次则是通过CPU拷贝的;要想提高文件传输的性能,就需要减少用户态与内核态的上下文切换和内存拷贝的次数;
零拷贝技术实现的方式通常有mmap+write和sendfile两种;
mmap() 系统调用函数在调用进程的虚拟地址空间中创建一个新映射,这个映射会直接把内核缓冲区里的数据映射到用户空间,这样就不用从内核空间到用户空间来回复制数据了;
mmap + write的方式依然需要4次用户态与内核态的上下文切换,但是少了一次内存拷贝(CPU把数据从内核态缓冲区拷贝到用户缓冲区,四次CPU状态切换,两次DMA拷贝,一次内存拷贝),RocketMQ选择了这种mmap + write方式,因为这种方式即使频繁调用,使用小块文件传输,效果会比sendfile更好;但是这样不能很好的利用DMA方式,会比sendfile多消耗CPU, mmap映射的内存分配与释放的安全性控制复杂,需要避免JVM Crash问题;
通过使用sendfile(),数据可以直接在内核空间进行传输,因此避免了用户空间和内核空间之间来回复制拷贝,同时由于使用sendfile替代了read + write从而节省了一次系统调用,也就是2次用户态与内核态的上下文切换,整个过程发生了2次用户态与内核态的上下文切换和3次内存拷贝;(两次CPU状态切换,一次CPU内存拷贝,两次DMA拷贝)
变化后发生了2次用户态与内核态的上下文切换和2次内存拷贝;
public MappedByteBuffer map(MapMode var1, long var2, long var4) throws IOException{
...
//调用map0方法完成映射,并返回内存地址
var7 = this.map0(var6, var36, var10);
...
//根据内存地址创建MappedByteBuffer对象,供java层面的操作
var37 = Util.newMappedByteBuffer(var35, var7 + (long)var12, var13, var15);
return var37;
...
}
private native long map0(int var1, long var2, long var4) throws IOException;
#define mmap64 mmap
JNIEXPORT jlong JNICALL
Java_sun_nio_ch_FileChannelImpl_map0(JNIEnv *env, jobject this,
jint prot, jlong off, jlong len){
...
//这里调用的是mmap64,但是在文件开头define了mmap64就是mmap方法
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 */
...
//返回映射完成的内存地址
return ((jlong) (unsigned long) mapAddress);
}
rocketmq创建mappedFile对象后,会调用其init方法,完成了最终的映射操作。调用的方法是fileChannel.map;fileChannel.map最底层调用就是linux的系统方法mmap(mmap系统方法:为进程创建虚拟地址空间映射);
rocketmq在创建完mmap映射后,还会作一个预热,mappedFile.warmMappedFile方法调用底层的mlock方法:
public void mlock() {
final long address = ((DirectBuffer) (this.mappedByteBuffer)).address();
Pointer pointer = new Pointer(address);
int ret = LibC.INSTANCE.mlock(pointer, new NativeLong(this.fileSize));
int ret = LibC.INSTANCE.madvise(pointer, new NativeLong(this.fileSize),
LibC.MADV_WILLNEED);
}
mlock方法主要做了2个系统方法的调用,mlock和madvise: mlock系统方法:锁定内存中的虚拟地址空间,防止其被交换系统的swap空间中;swap空间就是磁盘上的一块空间,当内存不够用时,系统会将部分内存中不常用的数据放到磁盘上;mmap本身就是为了提高读写性能,如果被映射的内存数据被放到了磁盘上,那就失去了mmap的意义了,所以要做一个mlock进行内存的锁定;
madvise系统方法:该方法功能很多,主要是给系统内核提供内存处理建议,可以根据需要传入参数;在rocketmq中,传入的参数是MADV_WILLNEE,该参数的意思是告诉系统内核,这块内存一会儿就会用到,于是系统就会提前加载被映射的文件数据到内存中,这样就不会在需要使用的时候才去读取磁盘,影响性能;
在实际存储消息的时候,无论是使用堆外内存还是直接使用mappedByteBuffer,都需要额外的刷盘任务负责保证数据写入磁盘,调用不同的force方法会得到不同的效果;
MappedByteBuffer底层使用了操作系统的mmap机制,FileChannel#map()方法就会返回MappedByteBuffer;DirectByteBuffer虽然实现了MappedByteBuffer,不过DirectByteBuffer默认并没有直接使用mmap机制;
// FileChannelImpl.force方法
JNIEXPORT jint JNICALL
Java_sun_nio_ch_FileDispatcherImpl_force0(JNIEnv *env, jobject this,
jobject fdo, jboolean md){
...
//fsync系统方法:将内核内存中有修改的数据同步到相应文件的磁盘空间
result = fsync(fd);
...
}
// MappedByteBuffer的force方法
JNIEXPORT void JNICALL
Java_java_nio_MappedByteBuffer_force0(JNIEnv *env, jobject obj, jobject fdo,
jlong address, jlong len){
//msync系统方法:将mmap映射的内存空间中的修改同步到文件系统中
int result = msync(a, (size_t)len, MS_SYNC);
...
}