支撑百万并发的“零拷贝”技术,你了解吗?( 八 )


下面是和内存映射相关的核心代码:

  1. public MappedByteBuffer map(MapMode mode, long position, long size) throws IOException {
  2. int pagePosition = (int)(position % allocationGranularity);
  3. long mapPosition = position - pagePosition;
  4. long mapSize = size + pagePosition;
  5. try {
  6. addr = map0(imode, mapPosition, mapSize);
  7. } catch (OutOfMemoryError x) {
  8. System.gc();
  9. try {
  10. Thread.sleep(100);
  11. } catch (InterruptedException y) {
  12. Thread.currentThread().interrupt();
  13. }
  14. try {
  15. addr = map0(imode, mapPosition, mapSize);
  16. } catch (OutOfMemoryError y) {
  17. throw new IOException("Map failed", y);
  18. }
  19. }
  20. int isize = (int)size;
  21. Unmapper um = new Unmapper(addr, mapSize, isize, mfd);
  22. if ((!writable) || (imode == MAP_RO)) {
  23. return Util.newMappedByteBufferR(isize, addr + pagePosition, mfd, um);
  24. } else {
  25. return Util.newMappedByteBuffer(isize, addr + pagePosition, mfd, um);
  26. }
  27. }
map() 方法通过本地方法 map0() 为文件分配一块虚拟内存,作为它的内存映射区域,然后返回这块内存映射区域的起始地址:
  • 文件映射需要在 Java 堆中创建一个 MappedByteBuffer 的实例 。如果第一次文件映射导致 OOM,则手动触发垃圾回收,休眠 100ms 后再尝试映射,如果失败则抛出异常 。
  • 通过 Util 的 newMappedByteBuffer(可读可写)方法或者 newMappedByteBufferR(仅读)方法反射创建一个 DirectByteBuffer 实例,其中 DirectByteBuffer 是 MappedByteBuffer 的子类 。
map() 方法返回的是内存映射区域的起始地址,通过(起始地址+偏移量)就可以获取指定内存的数据 。
这样一定程度上替代了 read() 或 write() 方法,底层直接采用 sun.misc.Unsafe 类的 getByte() 和 putByte() 方法对数据进行读写 。
  1. private native long map0(int prot, long position, long mapSize) throws IOException;
上面是本地方法(native method)map0 的定义,它通过 JNI(Java Native Interface)调用底层 C 的实现 。
这个 native 函数(Java_sun_nio_ch_FileChannelImpl_map0)的实现位于 JDK 源码包下的 native/sun/nio/ch/FileChannelImpl.c 这个源文件里面 。
  1. JNIEXPORT jlong JNICALL
  2. Java_sun_nio_ch_FileChannelImpl_map0(JNIEnv *env, jobject this,
  3. jint prot, jlong off, jlong len)
  4. {
  5. void *mapAddress = 0;
  6. jobject fdo = (*env)->GetObjectField(env, this, chan_fd);
  7. jint fd = fdval(env, fdo);
  8. int protections = 0;
  9. int flags = 0;
  10. if (prot == sun_nio_ch_FileChannelImpl_MAP_RO) {
  11. protections = PROT_READ;
  12. flags = MAP_SHARED;
  13. } else if (prot == sun_nio_ch_FileChannelImpl_MAP_RW) {
  14. protections = PROT_WRITE | PROT_READ;
  15. flags = MAP_SHARED;
  16. } else if (prot == sun_nio_ch_FileChannelImpl_MAP_PV) {
  17. protections = PROT_WRITE | PROT_READ;
  18. flags = MAP_PRIVATE;
  19. }
  20. mapAddress = mmap64(
  21. 0, /* Let OS decide location */
  22. len, /* Number of bytes to map */
  23. protections, /* File permissions */
  24. flags, /* Changes are shared */
  25. fd, /* File descriptor of mapped file */
  26. off); /* Offset into file */
  27. if (mapAddress == MAP_FAILED) {
  28. if (errno == ENOMEM) {
  29. JNU_ThrowOutOfMemoryError(env, "Map failed");
  30. return IOS_THROWN;
  31. }
  32. return handle(env, -1, "Map failed");
  33. }
  34. return ((jlong) (unsigned long) mapAddress);
  35. }
可以看出 map0() 函数最终是通过 mmap64() 这个函数对 Linux 底层内核发出内存映射的调用,mmap64() 函数的原型如下:
  1. #include <sys/mman.h>
  2. void *mmap64(void *addr, size_t len, int prot, int flags, int fd, off64_t offset);
下面详细介绍一下 mmap64() 函数各个参数的含义以及参数可选值:
addr:文件在用户进程空间的内存映射区中的起始地址,是一个建议的参数,通常可设置为 0 或 NULL,此时由内核去决定真实的起始地址 。
当 flags 为 MAP_FIXED 时,addr 就是一个必选的参数,即需要提供一个存在的地址 。
len:文件需要进行内存映射的字节长度 。
prot:控制用户进程对内存映射区的访问权限:
  • PROT_READ:读权限 。
  • PROT_WRITE:写权限 。
  • PROT_EXEC:执行权限 。
  • PROT_NONE:无权限 。
flags:控制内存映射区的修改是否被多个进程共享: