理解Java中的零拷貝技術原理:MappedByteBuffer與FileChannel.transferTo

零拷貝技術主要包括mmap和sendfile公荧,在RocketMQ雹顺、Kafka這類高性能消息隊列中間件中有應用,在Netty這種高性能網絡通信框架中也有應用龟虎。在Java里mmap和sendfile分別對應MappedByteBuffer和FileChannel.transferTo()河爹,兩者都是Java的nio包提供的能力匠璧。

MappedByteBuffer與mmap

理解mmap內存文件映射需要理解虛擬內存或者說內存虛擬化,實際可以認為是零拷貝技術的一個基石咸这。
使用虛擬化技術夷恍,可以做到讓多個虛擬地址映射到同一片物理地址,這樣硬件設備驅動就可以做到通過DMA對一片同時對內核和應用都可見的內存區(qū)域進行讀寫了媳维。這樣的意義在于酿雪,由于這樣的內存區(qū)域對內核和應用都可見,應用程序才能做到直接操作內核內存去完成一些以往需要到自己應用程序的用戶態(tài)內存進行中轉的讀寫邏輯侄刽。

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個字節(jié)
                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);
            /*建立內存映射州丹,用戶態(tài)虛擬內存與文件讀取到的os文件系統(tǒng)內核態(tài)內存映射到相同的物理內存地址
                這樣應用程序讀寫用戶態(tài)內存相當于就是讀寫內核態(tài)內存醋安,從而讀寫文件
                內核態(tài)內存與磁盤文件之間由os的文件系統(tǒng)管理杂彭,讀的時候在內核內存就直接讀、不在就缺頁中斷吓揪,內核置換頁亲怠;
                寫的話直接寫到內核態(tài)內存里,由os負責或手工flush到磁盤柠辞。*/
            MappedByteBuffer mappedButeBuffer = fileChannel.map(MapMode.READ_WRITE, 0, f.length());
            // 使用內存映射從內核態(tài)內存直接讀取到byte[] buffer里赁炎,因為是內存映射、所以不會發(fā)生從內核態(tài)復制到用戶態(tài)內存的過程钾腺。
            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按內存映射的方式讀取文件內容讥裤。

相比傳統(tǒng)的InputStream方式放棒,使用FileChannel.map()建立內存文件映射,用戶態(tài)虛擬內存與文件通過DMA存入的os文件系統(tǒng)的內核態(tài)內存己英、映射到相同的物理內存地址间螟。 這樣應用程序讀寫用戶態(tài)內存相當于就是讀寫內核態(tài)內存。這也就是相當于讀寫文件:原因在于內核態(tài)內存與磁盤文件之間由os的文件系統(tǒng)管理损肛,讀的時候在內核內存就直接讀厢破、不在就缺頁中斷,內核置換頁治拿; 寫的話直接寫到內核態(tài)內存里摩泪,由os負責或手工flush到磁盤。

內存文件映射建立后得到MappedByteBuffer劫谅,之后代碼里使用MappedByteBuffer.get(byte[])將文件內容從內核態(tài)內存直接讀取到byte[]里见坑,因為是虛擬內存映射、所以不會發(fā)生從內核態(tài)復制到用戶態(tài)內存的過程捏检。

FileChannle.transferTo()與sendfile

transferTo好比將兩個流的channel直接進行連接荞驴,而不是像傳統(tǒng)的方式那樣從一個讀出來再寫到另一個去,直接走內核態(tài)的copy贯城,不用經過用戶態(tài)熊楼。底層實際是將文件通過DMA讀取到os文件系統(tǒng)的內核態(tài)內存之后、不復制到用戶態(tài)內存而是直接transfer到內核態(tài)的Socket緩沖區(qū)能犯、再通過DMA寫到網卡通過網絡發(fā)送出去鲫骗。

對應到操作系統(tǒng)層面底層使用的是sendfile內核調用,從Linux2.1開始提供悲雳。Linux2.4之后支持所謂“scatter-gather”特性挎峦,甚至內核態(tài)的copy都不用,target內核態(tài)緩沖已經記錄了src內核態(tài)緩沖的地址合瓢,相當于使用DMA直接從src往device(控制臺坦胶、文件、網路等等)進行輸出。也就是說上面提到的從文件系統(tǒng)內核態(tài)內存到Socket緩沖區(qū)內核態(tài)內存這步也省了顿苇,直接從文件系統(tǒng)內核態(tài)內存通過DMA就寫到了網卡峭咒。

底層系統(tǒng)的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 中,這個偏移量會隨著偏移增加遗增,直到文件發(fā)送完為止叫惊,當然在程序中需要用如 while() 這樣的語句來控制。
第 4 個參數 count做修,表示要傳送的字節(jié)數(在以下示例中霍狰,是 1G 文件的大小,即 buf.st_size)

需要注意的是in_fd必須是一個文件饰及,而out_fd可以是文件和網絡socket等可寫的句柄蔗坯、但底層從2.6內核開始必須是socket了。從這里可以基本可以看出sendfile的使用場景跟它的名字一樣燎含,發(fā)送文件到網絡宾濒。

在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發(fā)送文件到服務端鼎兽,上面是一個簡單的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("發(fā)送完畢f(xié)lush and shutdownOutput");

            byte[] readBuf = new byte[1024];
            is.read(readBuf);
            log.info("OutputStream發(fā)送文件收到回復{}", 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尚粘,直接在內核態(tài)完成數據copy
            fileChannel.transferTo(0, file.length(), socketChannel);
            socketChannel.shutdownOutput();
            ByteBuffer buffer = ByteBuffer.allocate(1024);
            fileChannel.read(buffer);
            log.info("sendfile發(fā)送文件收到回復{}", new String(buffer.array(), "UTF-8"));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

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

客戶端先后用了傳統(tǒng)的socket OutputStream方法和FileChannel.transferTo方法發(fā)送文件择卦,并顯示服務端的返回。

參考

理論:

sendfile“零拷貝”郎嫁、mmap內存映射秉继、DMA - 簡書 (jianshu.com)

什么是零拷貝?mmap與sendFile的區(qū)別是什么泽铛?_The Mamba Mentality的博客-CSDN博客_mmap和sendfile

linux零拷貝原理尚辑,RocketMQ&Kafka使用對比 - 云+社區(qū) - 騰訊云 (tencent.com)

淺析Linux中的零拷貝技術 - 簡書 (jianshu.com)

代碼:

java 零拷貝-- MMAP,sendFile,Channel - 簡書 (jianshu.com)

?【Java深層系列】「并發(fā)編程系列」深入分析和研究MappedByteBuffer的實現(xiàn)原理和開發(fā)指南 - InfoQ 寫作平臺

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市盔腔,隨后出現(xiàn)的幾起案子杠茬,更是在濱河造成了極大的恐慌月褥,老刑警劉巖,帶你破解...
    沈念sama閱讀 221,406評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件瓢喉,死亡現(xiàn)場離奇詭異宁赤,居然都是意外死亡,警方通過查閱死者的電腦和手機栓票,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,395評論 3 398
  • 文/潘曉璐 我一進店門决左,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人走贪,你說我怎么就攤上這事佛猛。” “怎么了坠狡?”我有些...
    開封第一講書人閱讀 167,815評論 0 360
  • 文/不壞的土叔 我叫張陵挚躯,是天一觀的道長。 經常有香客問我擦秽,道長,這世上最難降的妖魔是什么漩勤? 我笑而不...
    開封第一講書人閱讀 59,537評論 1 296
  • 正文 為了忘掉前任感挥,我火速辦了婚禮,結果婚禮上越败,老公的妹妹穿的比我還像新娘触幼。我一直安慰自己,他們只是感情好究飞,可當我...
    茶點故事閱讀 68,536評論 6 397
  • 文/花漫 我一把揭開白布置谦。 她就那樣靜靜地躺著,像睡著了一般亿傅。 火紅的嫁衣襯著肌膚如雪媒峡。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,184評論 1 308
  • 那天葵擎,我揣著相機與錄音谅阿,去河邊找鬼。 笑死酬滤,一個胖子當著我的面吹牛签餐,可吹牛的內容都是我干的。 我是一名探鬼主播盯串,決...
    沈念sama閱讀 40,776評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼氯檐,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了体捏?” 一聲冷哼從身側響起冠摄,我...
    開封第一講書人閱讀 39,668評論 0 276
  • 序言:老撾萬榮一對情侶失蹤糯崎,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后耗拓,有當地人在樹林里發(fā)現(xiàn)了一具尸體拇颅,經...
    沈念sama閱讀 46,212評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 38,299評論 3 340
  • 正文 我和宋清朗相戀三年乔询,在試婚紗的時候發(fā)現(xiàn)自己被綠了樟插。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,438評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡竿刁,死狀恐怖黄锤,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情食拜,我是刑警寧澤鸵熟,帶...
    沈念sama閱讀 36,128評論 5 349
  • 正文 年R本政府宣布,位于F島的核電站负甸,受9級特大地震影響流强,放射性物質發(fā)生泄漏。R本人自食惡果不足惜呻待,卻給世界環(huán)境...
    茶點故事閱讀 41,807評論 3 333
  • 文/蒙蒙 一打月、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧蚕捉,春花似錦奏篙、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,279評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至敛熬,卻和暖如春肺稀,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背应民。 一陣腳步聲響...
    開封第一講書人閱讀 33,395評論 1 272
  • 我被黑心中介騙來泰國打工盹靴, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人瑞妇。 一個月前我還...
    沈念sama閱讀 48,827評論 3 376
  • 正文 我出身青樓稿静,卻偏偏與公主長得像,于是被迫代替她去往敵國和親辕狰。 傳聞我的和親對象是個殘疾皇子改备,可洞房花燭夜當晚...
    茶點故事閱讀 45,446評論 2 359

推薦閱讀更多精彩內容