理解Java中的零拷贝技术原理:MappedByteBuffer与FileChannel.transferTo

零拷贝技术主要包括mmap和sendfile,在RocketMQ、Kafka这类高性能消息队列中间件中有应用,在Netty这种高性能网络通信框架中也有应用。在Java里mmap和sendfile分别对应MappedByteBuffer和FileChannel.transferTo(),两者都是Java的nio包提供的能力。

MappedByteBuffer与mmap

理解mmap内存文件映射需要理解虚拟内存或者说内存虚拟化,实际可以认为是零拷贝技术的一个基石。
使用虚拟化技术,可以做到让多个虚拟地址映射到同一片物理地址,这样硬件设备驱动就可以做到通过DMA对一片同时对内核和应用都可见的内存区域进行读写了。这样的意义在于,由于这样的内存区域对内核和应用都可见,应用程序才能做到直接操作内核内存去完成一些以往需要到自己应用程序的用户态内存进行中转的读写逻辑。

Java里的mmap内存文件映射能力是通过MappedByteBuffer = FileChannel.map()这样一个操作提供的,下面来看一下示例代码:

/**
 * mmap内存映射在Java中的使用 FileChannel.map() -> MappedByteBuffer
 */
@Slf4j
public class MmapTest {

    private static String filepath = "D:\\Media\\test.txt";

    private static File f = new File(filepath);

    // 使用java io包中的缓冲输入流BufferedInputStream
    public static void readFile() {
        try {
            BufferedInputStream bis = new BufferedInputStream(new FileInputStream(f));
            byte[] buffer = new byte[1024];
            int len = 0;
            while ((len = bis.read(buffer)) != -1) { // 从bis读到byte[] buffer里,读了len个字节
                log.info("从BufferedInputStream读了{}", new String(buffer, 0, len, "UTF-8"));
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    // 使用内存映射
    public static void mmapReadFile() {
        f = new File(filepath);
        int bufferSize = (int) f.length();
        byte[] buffer = new byte[bufferSize];
        FileChannel fileChannel;
        try {
            // fileChannel = new RandomAccessFile(f, "rw").getChannel();
            fileChannel = FileChannel.open(Paths.get(filepath), StandardOpenOption.READ, StandardOpenOption.WRITE);
            /*建立内存映射,用户态虚拟内存与文件读取到的os文件系统内核态内存映射到相同的物理内存地址
                这样应用程序读写用户态内存相当于就是读写内核态内存,从而读写文件
                内核态内存与磁盘文件之间由os的文件系统管理,读的时候在内核内存就直接读、不在就缺页中断,内核置换页;
                写的话直接写到内核态内存里,由os负责或手工flush到磁盘。*/
            MappedByteBuffer mappedButeBuffer = fileChannel.map(MapMode.READ_WRITE, 0, f.length());
            // 使用内存映射从内核态内存直接读取到byte[] buffer里,因为是内存映射、所以不会发生从内核态复制到用户态内存的过程。
            ByteBuffer byteBuffer = mappedButeBuffer.get(buffer);
            log.info("从MappedByteBuffer读了{}", new String(buffer, 0, bufferSize, "UTF-8"));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        readFile();
        mmapReadFile();
    }
}

上面的代码很简单,比较使用InputStream读文件内容以及使用MappedByteBuffer按内存映射的方式读取文件内容。

相比传统的InputStream方式,使用FileChannel.map()建立内存文件映射,用户态虚拟内存与文件通过DMA存入的os文件系统的内核态内存、映射到相同的物理内存地址。 这样应用程序读写用户态内存相当于就是读写内核态内存。这也就是相当于读写文件:原因在于内核态内存与磁盘文件之间由os的文件系统管理,读的时候在内核内存就直接读、不在就缺页中断,内核置换页; 写的话直接写到内核态内存里,由os负责或手工flush到磁盘。

内存文件映射建立后得到MappedByteBuffer,之后代码里使用MappedByteBuffer.get(byte[])将文件内容从内核态内存直接读取到byte[]里,因为是虚拟内存映射、所以不会发生从内核态复制到用户态内存的过程。

FileChannle.transferTo()与sendfile

transferTo好比将两个流的channel直接进行连接,而不是像传统的方式那样从一个读出来再写到另一个去,直接走内核态的copy,不用经过用户态。底层实际是将文件通过DMA读取到os文件系统的内核态内存之后、不复制到用户态内存而是直接transfer到内核态的Socket缓冲区、再通过DMA写到网卡通过网络发送出去。

对应到操作系统层面底层使用的是sendfile内核调用,从Linux2.1开始提供。Linux2.4之后支持所谓“scatter-gather”特性,甚至内核态的copy都不用,target内核态缓冲已经记录了src内核态缓冲的地址,相当于使用DMA直接从src往device(控制台、文件、网路等等)进行输出。也就是说上面提到的从文件系统内核态内存到Socket缓冲区内核态内存这步也省了,直接从文件系统内核态内存通过DMA就写到了网卡。

底层系统的sendfile API:

#include <sys/sendfile.h>
ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);

说明:
第 1 个参数 out_fd,在 2.6 内核里,必须指向一个 socket 。
第 2 个参数 in_fd,是一个要拷贝文件的文件fd。
第 3 个参数 offset, 是一个偏移量,它在不断的 sendfile 中,这个偏移量会随着偏移增加,直到文件发送完为止,当然在程序中需要用如 while() 这样的语句来控制。
第 4 个参数 count,表示要传送的字节数(在以下示例中,是 1G 文件的大小,即 buf.st_size)

需要注意的是in_fd必须是一个文件,而out_fd可以是文件和网络socket等可写的句柄、但底层从2.6内核开始必须是socket了。从这里可以基本可以看出sendfile的使用场景跟它的名字一样,发送文件到网络。

在Java里,sendfile技术对应的是FileChannel.transferTo()方法。下面看一下例子程序:

@Slf4j
public class TestServer {

    private ServerSocket ss;

    public TestServer(int port) throws Exception {
        ss = new ServerSocket(port);
    }

    public void doAccept() throws Exception {
        log.info("TestServer start ...");
        while (true) {
            Socket client = ss.accept();
            log.info("recv a connection " + client);
            new Worker(client).start();
        }
    }

    class Worker extends Thread {
        Socket client;
        byte[] buffer = new byte[1024];

        Worker(Socket socket) {
            client = socket;
        }

        @Override
        public void run() {
            try {
                BufferedInputStream bis = new BufferedInputStream(client.getInputStream());
                BufferedOutputStream bos = new BufferedOutputStream(client.getOutputStream());
                int len = 0;
                while ((len = bis.read(buffer)) != -1) {
                    log.info(new String(buffer, 0, len, "UTF-8"));
                    bos.write(buffer, 0, len);
                }
                client.shutdownInput();
                bos.flush();
                client.shutdownOutput();
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                if (null != client)
                    try {
                        client.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
            }
        }
    }

    public static void main(String[] args) {
        try {
            TestServer server = new TestServer(6687);
            server.doAccept();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

我们要模拟一个客户端使用FileChannel.transferTo发送文件到服务端,上面是一个简单的SockerServer服务端程序,做的事情也很简单,把收到文件后把文件内容再返回给客户端,下面再看下客户端:

/**
 * sendfile在Java中的应用
 */
@Slf4j
public class SendFileTest {

    private static File file = new File("D:\\Media\\test.txt");

    public static void sendStream() {
        Socket socket = null;
        try {
            socket = new Socket("127.0.0.1", 6687);
            FileInputStream fis = new FileInputStream(file);
            OutputStream os = socket.getOutputStream();
            InputStream is = socket.getInputStream();
            byte[] buffer = new byte[1024];
            int len = 0;
            while ((len = fis.read(buffer)) != -1) {
                os.write(buffer, 0, len);
            }
            os.flush();
            socket.shutdownOutput();
            log.info("发送完毕flush and shutdownOutput");

            byte[] readBuf = new byte[1024];
            is.read(readBuf);
            log.info("OutputStream发送文件收到回复{}", new String(readBuf, "UTF-8"));
            socket.shutdownInput();
            log.info("读取回复完毕,shutdownInput");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (socket != null)
                try {
                    socket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
        }
    }

    public static void sendfile() {
        try {
            SocketChannel socketChannel = SocketChannel.open();
            socketChannel.connect(new InetSocketAddress("127.0.0.1", 6687));
            FileInputStream fis = new FileInputStream(file);
            FileChannel fileChannel = fis.getChannel();
            // 从FileChannel直接transfer到SocketChannel,直接在内核态完成数据copy
            fileChannel.transferTo(0, file.length(), socketChannel);
            socketChannel.shutdownOutput();
            ByteBuffer buffer = ByteBuffer.allocate(1024);
            fileChannel.read(buffer);
            log.info("sendfile发送文件收到回复{}", new String(buffer.array(), "UTF-8"));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        sendStream();
        sendfile();
    }
}

客户端先后用了传统的socket OutputStream方法和FileChannel.transferTo方法发送文件,并显示服务端的返回。

参考

理论:

sendfile“零拷贝”、mmap内存映射、DMA - 简书 (jianshu.com)

什么是零拷贝?mmap与sendFile的区别是什么?_The Mamba Mentality的博客-CSDN博客_mmap和sendfile

linux零拷贝原理,RocketMQ&Kafka使用对比 - 云+社区 - 腾讯云 (tencent.com)

浅析Linux中的零拷贝技术 - 简书 (jianshu.com)

代码:

java 零拷贝-- MMAP,sendFile,Channel - 简书 (jianshu.com)

☕【Java深层系列】「并发编程系列」深入分析和研究MappedByteBuffer的实现原理和开发指南 - InfoQ 写作平台

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,294评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,493评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,790评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,595评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,718评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,906评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,053评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,797评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,250评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,570评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,711评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,388评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,018评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,796评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,023评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,461评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,595评论 2 350

推荐阅读更多精彩内容