關于Linux、JDK、Netty中NIO與零拷貝那些事

      網友投稿 737 2025-03-31

      最近項目中經常使用到NIO和零拷貝技術提升性能,對整個流程做了詳細梳理,特借鑒一篇文章分享如下。本文主要基于https://www.163.com/dy/article/G56MPDNO0521POB7.html修改,在此表示TKS。

      一、先理解內核空間與用戶空間

      linux 按照特權等級,把進程的運行空間分為內核空間和用戶空間,分別對應著下圖中, CPU 特權等級分為4個,linux 使用 Ring 0 和 Ring 3。

      內核空間(Ring 0)具有最高權限,可以直接訪問所有資源,;

      用戶空間(Ring 3)只能訪問受限資源,不能直接訪問內存等硬件設備,必須通過系統調用陷入到內核中,才能訪問這些特權資源。

      上面的Ring圖可以簡化成:

      內核從本質上看是一種軟件——控制計算機的硬件資源,并提供上層應用程序運行的環境。用戶態即上層應用程序的活動空間,應用程序的執行必須依托于內核提供的資源,包括CPU資源、存儲資源、I/O資源等。為了使上層應用能夠訪問到這些資源,內核必須為上層應用提供訪問的接口:即系統調用。

      系統調用是操作系統的最小功能單位,通過提供一些基本功能的接口供應用程序調用來調度內核空間管理的資源

      Shell是一個特殊的應用程序,俗稱命令行,本質上是一個命令解釋器,它下通系統調用,上通各種應用,通常充當著一種“膠水”的角色,來連接各個小功能程序,讓不同程序能夠以一個清晰的接口協同工作,從而增強各個程序的功能。通常短短的幾行Shell腳本就可以實現一個非常大的功能,原因就是這些Shell語句通常都對系統調用做了一層封裝。

      二、用戶態與內核態切換的損失

      當程序運行從用戶態切換到內核態,那么處在用戶態的線程需要先保存當前的數據以及運行的指令,方便回到用戶態時繼續執行,這中間還有很多其他的事情需要做,例如CPU寄存器需要保存和加載, 系統調度器的代碼需要執行, TLB實例需要重新加載, CPU 的pipeline需要刷掉。

      TLB

      頁表一般都很大,并且存放在內存中,所以處理器引入MMU后,讀取指令、數據需要訪問兩次內存:首先通過查詢頁表得到物理地址,然后訪問該物理地址讀取指令、數據。為了減少因為MMU導致的處理器性能下降,引入了TLB,TLB是Translation Lookaside Buffer的簡稱,可翻譯為“地址轉換后援緩沖器”,也可簡稱為“快表”。簡單地說,TLB就是頁表的Cache,其中存儲了當前最可能被訪問到的頁表項,其內容是部分頁表項的一個副本。只有在TLB無法完成地址翻譯任務時,才會到內存中查詢頁表,這樣就減少了頁表查詢導致的處理器性能下降。

      頁表

      MMU

      TLB中的項由兩部分組成:標識和數據。標識中存放的是虛地址的一部分,而數據部分中存放物理頁號、存儲保護信息以及其他一些輔助信息。虛地址與TLB中項的映射方式有三種:全關聯方式、直接映射方式、分組關聯方式。OR1200處理器中實現的是直接映射方式,所以本書只對直接映射方式作介紹。直接映射方式是指每一個虛擬地址只能映射到TLB中唯一的一個表項。假設內存頁大小是8KB,TLB中有64項,采用直接映射方式時的TLB變換原理如圖所示:

      CPU 的Pipeline

      在CPU中由5—6個不同功能的電路單元組成一條指令處理流水線,然后將一條指令分成5—6步后再由這些電路單元分別執行,這樣就能實現在一個CPU時鐘周期完成一條指令,因此提高CPU的運算速度運算速度。

      三、文件傳送的基本流程 1、DMA之前的文件拷貝流程

      DMA之前傳統的IO拷貝時序圖:

      使用I/O 中斷方式讀取數據步驟:

      用戶進程向 CPU 發起 read 系統調用讀取數據,由用戶態切換為內核態,然后一直阻塞等待數據的返回;

      CPU 在接收到指令以后對磁盤發起 I/O 請求,將磁盤數據先放入磁盤控制器緩沖區;

      數據準備完成以后,磁盤向 CPU 發起 I/O 中斷;

      CPU 收到 I/O 中斷以后將磁盤緩沖區中的數據拷貝到內核緩沖區,然后再從內核緩沖區拷貝到用戶緩沖區;

      用戶進程由內核態切換回用戶態,解除阻塞狀態,然后等待 CPU 的下一個執行時間鐘。

      2、DMA之后文件讀取流程 2.1 DMA復制與CPU復制的區別

      在 DMA 技術出現之前,應用程序與磁盤之間的 I/O 操作都是通過 CPU 的中斷完成的。每次用戶進程讀取磁盤數據時,都需要 CPU 中斷將數據讀進暫存器,然后發起 I/O 請求等待數據讀取和拷貝完成,然后寫進其它地方,每次的 I/O 中斷都導致 CPU 的上下文切換。

      DMA(Direct Memory Access,直接存儲器訪問) ,在DMA之前的CPU復制,需要CPU將數據讀進暫存器(區別于寄存器),然后寫進其它地方,這個過程中,CPU被擠占,而DMA在拷貝時不影響CPU去運行其他任務。

      具體流程:CPU對DMA控制器初始化,向I/O接口發出操作命令,I/O接口提出DMA請求。DMA控制器對DMA請求判別優先級及屏蔽,向總線裁決邏輯提出總線請求。當CPU執行完當前總線周期即可釋放總線控制權。此時,總線裁決邏輯輸出總線應答,表示DMA已經響應,通過DMA控制器通知I/O接口開始DMA傳輸。

      2.2 DMA復制流程

      系統從磁盤上讀取數據,DMA復制進內核的頁緩存,

      然后通過CPU復制讀取給用戶的緩存空間,

      然后通過CPU寫進Socket緩沖區域,

      最后通過DMA復制傳輸進入網絡。

      2.3 DMA拷貝示意圖

      由圖可知:DMA拷貝,需要經過四次數據拷貝,四次上下文切換,即使使用了DMA來處理與硬件的通訊,CPU仍然需要處理兩次數據拷貝,與此同時,在用戶態與內核態也發生了多次上下文切換,無疑也加重了CPU負擔。

      2.4 DMA下的IO拷貝時序圖

      CPU 從繁重的 I/O 操作中解脫,數據讀取操作的流程如下:

      用戶進程向 CPU 發起 read 系統調用讀取數據,由用戶態切換為內核態,然后一直阻塞等待數據的返回;

      CPU 在接收到指令以后對 DMA 磁盤控制器發起調度指令;

      DMA 磁盤控制器對磁盤發起 I/O 請求,將磁盤數據先放入磁盤控制器緩沖區,CPU 全程不參與此過程;

      數據讀取完成后,DMA 磁盤控制器會接受到磁盤的通知,將數據從磁盤控制器緩沖區拷貝到內核緩沖區;

      DMA 磁盤控制器向 CPU 發出數據讀完的信號,由 CPU 負責將數據從內核緩沖區拷貝到用戶緩沖區;

      用戶進程由內核態切換回用戶態,解除阻塞狀態,然后等待 CPU 的下一個執行時間鐘。

      四、零拷貝流程

      零拷貝,主要是指CPU拷貝

      1、 零拷貝的原理

      Linux 零拷貝技術主要有 3 個實現思路:用戶態直接 I/O、減少數據拷貝次數以及寫時復制技術。

      1.1 用戶態直接 I/O

      應用程序可以直接訪問硬件存儲,操作系統內核只是輔助數據傳輸。這種方式依舊存在用戶空間和內核空間的上下文切換,硬件上的數據直接拷貝至了用戶空間,不經過內核空間。因此,直接 I/O不存在內核空間緩沖區和用戶空間緩沖區之間的數據拷貝。

      1.2 減少數據拷貝次數

      在數據傳輸過程中,避免數據在用戶空間緩沖區和系統內核空間緩沖區之間的CPU拷貝,以及數據在系統內核空間內的CPU拷貝,這也是當前主流零拷貝技術的實現思路。

      1.3 寫時復制技術

      寫時復制指的是當多個進程共享同一塊數據時,如果其中一個進程需要對這份數據進行修改,那么將其拷貝到自己的進程地址空間中,如果只是數據讀取操作則不需要進行拷貝操作。

      2 、用戶態直接 I/O

      用戶態直接 I/O 使得應用進程或運行在用戶態(user space)下的庫函數直接訪問硬件設備,數據直接跨過內核進行傳輸直接從用戶態地址空間寫入到磁盤中,內核在數據傳輸過程除了進行必要的虛擬存儲配置工作之外,不參與任何其他工作,這種方式能夠直接繞過內核,極大提高了性能。對于一些應用程序,例如:數據庫。他們更傾向于自己的緩存機制,這樣可以提供更好的緩沖機制提高數據庫的讀寫性能。

      2.1 直接I/O圖示

      2.2 直接I/O 設計與實現

      要在塊設備中執行直接 I/O,進程必須在打開文件的時候設置對文件的訪問模式為O_DIRECT,這樣就等于告訴操作系統進程在接下來使用read() 或者write() 系統調用去讀寫文件的時候使用的是直接 I/O方式,所傳輸的數據均不經過操作系統內核緩存空間。使用直接 I/O讀寫數據必須要注意緩沖區對齊( buffer alignment )以及緩沖區的大小的問題,即對應 read() 以及 write() 系統調用的第二個和第三個參數。這里邊說的對齊指的是文件系統塊大小的對齊,緩沖區的大小也必須是該塊大小的整數倍。

      2.3 直接I/O 缺點

      這種方法只能適用于那些不需要內核緩沖區處理的應用程序,這些應用程序通常在進程地址空間有自己的數據緩存機制,稱為自緩存應用程序,如數據庫管理系統就是一個代表。

      這種方法直接操作磁盤 I/O,由于 CPU 和磁盤 I/O 之間的執行時間差距,會造成資源的浪費,解決這個問題需要進一步 I/O 結合使用。

      3、 減少數據拷貝之mmap

      一種零拷貝方式是使用 mmap + write 代替原來的 read + write 方式,減少了 1 次 CPU 拷貝操作。mmap 是 Linux 提供的一種內存映射文件方法,即將一個進程的地址空間中的一段虛擬地址映射到磁盤文件地址,mmap + write 的偽代碼如下:

      tmp_buf = mmap(file_fd, len);write(socket_fd, tmp_buf, len);

      使用 mmap 的目的是將內核中讀緩沖區(read buffer)的地址與用戶空間的緩沖區(user buffer)進行映射,從而實現內核緩沖區與應用程序內存的共享,省去了將數據從內核讀緩沖區(read buffer)拷貝到用戶緩沖區(user buffer)的過程,然而內核讀緩沖區(read buffer)仍需將數據到內核寫緩沖區(socket buffer)。

      3.1 mmap減少數據拷貝流程圖

      3.2 mmap+write拷貝流程

      基于 mmap + write 系統調用的零拷貝方式,整個拷貝過程會發生 4 次上下文切換,1 次 CPU 拷貝和 2 次 DMA 拷貝,用戶程序讀寫數據的流程如下:

      用戶進程通過 mmap() 函數向內核 (kernel) 發起系統調用,上下文從用戶態 (user space) 切換為內核態(kernel space);

      將用戶進程的內核空間的讀緩沖區 (read buffer) 與用戶空間的緩存區 (user buffer) 進行內存地址映射;

      CPU 利用 DMA 控制器將數據從主存或硬盤拷貝到內核空間 (kernel space) 的讀緩沖區 (read buffer);

      上下文從內核態 (kernel space) 切換回用戶態 (user space),mmap 系統調用執行返回;

      用戶進程通過write() 函數向內核 (kernel) 發起系統調用,上下文從用戶態 (user space) 切換為內核態(kernel space);

      CPU 將讀緩沖區 (read buffer) 中的數據拷貝到的網絡緩沖區 (socket buffer) ;

      CPU 利用 DMA 控制器將數據從網絡緩沖區 (socket buffer) 拷貝到網卡進行數據傳輸;

      上下文從內核態 (kernel space) 切換回用戶態 (user space) ,write 系統調用執行返回;

      3.3 mmap+write拷貝缺陷:

      mmap 主要的用處是提高 I/O 性能,特別是針對大文件。對于小文件,內存映射文件反而會導致碎片空間的浪費,因為內存映射總是要對齊頁邊界,最小單位是 4 KB,一個 5 KB 的文件將會映射占用 8 KB 內存,也就會浪費 3 KB 內存。

      另外 mmap 隱藏著一個陷阱,當使用 mmap 映射一個文件時,如果這個文件被另一個進程所截獲,那么 write 系統調用會因為訪問非法地址被 SIGBUS 信號終止,SIGBUS 默認會殺死進程并產生一個 coredump,如果服務器被這樣終止那損失就可能不小。

      解決這個問題通常使用文件的租借鎖:首先為文件申請一個租借鎖,當其他進程想要截斷這個文件時,內核會發送一個實時的 RT_SIGNAL_LEASE 信號,告訴當前進程有進程在試圖破壞文件,這樣 write 在被 SIGBUS 殺死之前,會被中斷,返回已經寫入的字節數,并設置 errno 為 success。

      通常的做法是在 mmap 之前加鎖,操作完之后解鎖。

      4、減少數據拷貝之sendfile

      sendfile 系統調用在 Linux 內核版本 2.1 中被引入,目的是簡化通過網絡在兩個通道之間進行的數據傳輸過程。sendfile 系統調用的引入,不僅減少了 CPU 拷貝的次數,還減少了上下文切換的次數,它的偽代碼如下:

      sendfile(socket_fd, file_fd, len);

      通過 sendfile 系統調用,數據可以直接在內核空間內部進行 I/O 傳輸,從而省去了數據在用戶空間和內核空間之間的來回拷貝。與 mmap 內存映射方式不同的是,sendfile 調用中 I/O 數據對用戶空間是完全不可見的。也就是說,這是一次完全意義上的數據傳輸過程。

      4.1 sendfile拷貝圖示

      4.2 sendfile拷貝流程

      基于 sendfile 系統調用的零拷貝方式,整個拷貝過程會發生 2 次上下文切換,1 次 CPU 拷貝和 2 次 DMA 拷貝,用戶程序讀寫數據的流程如下:

      用戶進程通過 sendfile() 函數向內核 (kernel) 發起系統調用,上下文從用戶態 (user space) 切換為內核態(kernel space)。

      CPU 利用 DMA 控制器將數據從主存或硬盤拷貝到內核空間 (kernel space) 的讀緩沖區 (read buffer)。

      CPU 將讀緩沖區 (read buffer) 中的數據拷貝到的網絡緩沖區 (socket buffer)。

      CPU 利用 DMA 控制器將數據從網絡緩沖區 (socket buffer) 拷貝到網卡進行數據傳輸。

      上下文從內核態 (kernel space) 切換回用戶態 (user space),sendfile 系統調用執行返回。

      相比較于 mmap 內存映射的方式,sendfile 少了 2 次上下文切換,但是仍然有 1 次 CPU 拷貝操作。sendfile 存在的問題是用戶程序不能對數據進行修改,而只是單純地完成了一次數據傳輸過程。

      4.3 sendfile拷貝缺點

      只能適用于那些不需要用戶態處理的應用程序。

      5、減少數據拷貝之sendfile + DMA

      常規 sendfile 還有一次內核態的拷貝操作,能不能也把這次拷貝給去掉呢?

      還真有,這種 DMA 輔助的 sendfile。

      Linux 2.4 版本的內核對 sendfile 系統調用進行修改,為 DMA 拷貝引入了 gather 操作。它將內核空間 (kernel space) 的讀緩沖區(read buffer) 中對應的數據描述信息(內存地址、地址偏移量) 記錄到相應的網絡緩沖區( (socket buffer) 中,由 DMA 根據內存地址、地址偏移量將數據批量地從讀緩沖區 (read buffer) 拷貝到網卡設備中,這樣就省去了內核空間中僅剩的 1 次 CPU 拷貝操作,sendfile 的偽代碼如下:

      Copysendfile(socket_fd, file_fd, len);

      在硬件的支持下,sendfile 拷貝方式不再從內核緩沖區的數據拷貝到 socket 緩沖區,取而代之的僅僅是緩沖區文件描述符和數據長度的拷貝,這樣 DMA 引擎直接利用 gather 操作將頁緩存中數據打包發送到網絡中即可,本質就是和虛擬內存映射的思路類似。

      5.1 sendfile + DMA示意圖

      5.2 sendfile+DMA拷貝流程

      基于 sendfile + DMA gather copy 系統調用的零拷貝方式,整個拷貝過程會發生 2 次上下文切換、0 次 CPU 拷貝以及 2 次 DMA 拷貝,用戶程序讀寫數據的流程如下:

      用戶進程通過 sendfile()函數向內核 (kernel) 發起系統調用,上下文從用戶態 (user space) 切換為內核態(kernel space)。

      CPU 利用 DMA 控制器將數據從主存或硬盤拷貝到內核空間 (kernel space) 的讀緩沖區 (read buffer)。

      CPU 把讀緩沖區 (read buffer) 的文件描述符(file descriptor)和數據長度拷貝到網絡緩沖區(socket buffer)。

      基于已拷貝的文件描述符 (file descriptor) 和數據長度,CPU 利用 DMA 控制器的 gather/scatter 操作直接批量地將數據從內核的讀緩沖區 (read buffer) 拷貝到網卡進行數據傳輸。

      上下文從內核態 (kernel space) 切換回用戶態 (user space),sendfile 系統調用執行返回。

      5.3 sendfile+DMA拷貝缺點

      sendfile + DMA gather copy 拷貝方式同樣存在用戶程序不能對數據進行修改的問題,而且本身需要硬件的支持,它只適用于將數據從文件拷貝到 socket 套接字上的傳輸過程。

      6、減少數據拷貝之splice

      sendfile 只適用于將數據從文件拷貝到 socket 套接字上,同時需要硬件的支持,這也限定了它的使用范圍。Linux 在 2.6.17 版本引入 splice 系統調用,不僅不需要硬件支持,還實現了兩個文件描述符之間的數據零拷貝。splice 的偽代碼如下:

      Copysplice(fd_in, off_in, fd_out, off_out, len, flags);

      splice 系統調用可以在內核空間的讀緩沖區 (read buffer) 和網絡緩沖區 (socket buffer) 之間建立管道 (pipeline),從而避免了兩者之間的 CPU 拷貝操作。

      6.1 splice流程示意圖

      6.2 splice流程

      基于 splice 系統調用的零拷貝方式,整個拷貝過程會發生 2 次上下文切換,0 次 CPU 拷貝以及 2 次 DMA 拷貝,用戶程序讀寫數據的流程如下:

      用戶進程通過 splice() 函數向內核(kernel)發起系統調用,上下文從用戶態 (user space) 切換為內核態(kernel space);

      CPU 利用 DMA 控制器將數據從主存或硬盤拷貝到內核空間 (kernel space) 的讀緩沖區 (read buffer);

      CPU 在內核空間的讀緩沖區 (read buffer) 和網絡緩沖區(socket buffer)之間建立管道 (pipeline);

      CPU 利用 DMA 控制器將數據從網絡緩沖區 (socket buffer) 拷貝到網卡進行數據傳輸;

      上下文從內核態 (kernel space) 切換回用戶態 (user space),splice 系統調用執行返回。

      splice 拷貝方式也同樣存在用戶程序不能對數據進行修改的問題。除此之外,它使用了 Linux 的管道緩沖機制,可以用于任意兩個文件描述符中傳輸數據,但是它的兩個文件描述符參數中有一個必須是管道設備。

      7、寫時復制

      在某些情況下,內核緩沖區可能被多個進程所共享,如果某個進程想要這個共享區進行 write 操作,由于 write 不提供任何的鎖操作,那么就會對共享區中的數據造成破壞,寫時復制的引入就是 Linux 用來保護數據的。

      寫時復制指的是當多個進程共享同一塊數據時,如果其中一個進程需要對這份數據進行修改,那么就需要將其拷貝到自己的進程地址空間中。這樣做并不影響其他進程對這塊數據的操作,每個進程要修改的時候才會進行拷貝,所以叫寫時拷貝。這種方法在某種程度上能夠降低系統開銷,如果某個進程永遠不會對所訪問的數據進行更改,那么也就永遠不需要拷貝。

      缺點:

      需要 MMU 的支持,MMU 需要知道進程地址空間中哪些頁面是只讀的,當需要往這些頁面寫數據時,發出一個異常給操作系統內核,內核會分配新的存儲空間來供寫入的需求。

      8、緩沖區共享

      緩沖區共享方式完全改寫了傳統的 I/O 操作,傳統的 Linux I/O 接口支持數據在應用程序地址空間和操作系統內核之間交換,這種交換操作導致所有的數據都需要進行拷貝。

      如果采用 fbufs 這種方法,需要交換的是包含數據的緩沖區,這樣就消除了多余的拷貝操作。應用程序將 fbuf 傳遞給操作系統內核,這樣就能減少傳統的 write 系統調用所產生的數據拷貝開銷。

      同樣的應用程序通過 fbuf 來接收數據,這樣也可以減少傳統 read 系統調用所產生的數據拷貝開銷。

      fbuf 的思想是每個進程都維護著一個緩沖區池,這個緩沖區池能被同時映射到用戶空間 (user space) 和內核態 (kernel space),內核和用戶共享這個緩沖區池,這樣就避免了一系列的拷貝操作。

      缺點:

      緩沖區共享的難度在于管理共享緩沖區池需要應用程序、網絡軟件以及設備驅動程序之間的緊密合作,而且如何改寫 API 目前還處于試驗階段并不成熟。

      9、Linux零拷貝對比

      無論是傳統 I/O 拷貝方式還是引入零拷貝的方式,2 次 DMA Copy 是都少不了的,因為兩次 DMA 都是依賴硬件完成的。下面從 CPU 拷貝次數、DMA 拷貝次數以及系統調用幾個方面總結一下上述幾種 I/O 拷貝方式的差別。

      五、Netty中的零拷貝 1、JDK零拷貝 - MappedByteBuffer

      關于Linux、JDK、Netty中NIO與零拷貝那些事

      MappedByteBuffer 是 NIO 基于內存映射 (mmap) 這種零拷貝方式的提供的一種實現,它繼承自 ByteBuffer。FileChannel 定義了一個 map()方法,它可以把一個文件從 position 位置開始的 size 大小的區域映射為內存映像文件。抽象方法 map() 方法在 FileChannel 中的定義如下:

      Copypublic abstract MappedByteBuffer map(MapMode mode, long position, long size) throws IOException;

      mode:限定內存映射區域(MappedByteBuffer)對內存映像文件的訪問模式,包括只可讀(READ_ONLY)、可讀可寫(READ_WRITE)和寫時拷貝(PRIVATE)三種模式。

      position:文件映射的起始地址,對應內存映射區域(MappedByteBuffer)的首地址。

      size:文件映射的字節長度,從 position 往后的字節數,對應內存映射區域(MappedByteBuffer)的大小。

      MappedByteBuffer 相比 ByteBuffer 新增了 fore()、load() 和 isLoad() 三個重要的方法:

      fore():對于處于 READ_WRITE 模式下的緩沖區,把對緩沖區內容的修改強制刷新到本地文件。

      load():將緩沖區的內容載入物理內存中,并返回這個緩沖區的引用。

      isLoaded():如果緩沖區的內容在物理內存中,則返回 true,否則返回 false。

      下面給出一個利用 MappedByteBuffer 對文件進行讀寫的使用示例:

      Copyprivate final static String CONTENT = "我要測試零拷貝寫入數據";private final static String FILE_NAME = "/Users/yangyue/Downloads/1.txt";public static void main(String[] args) { Path path = Paths.get(FILE_NAME); byte[] bytes = CONTENT.getBytes(Charset.forName("UTF-8")); try (FileChannel fileChannel = FileChannel.open(path, StandardOpenOption.READ, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING)) { MappedByteBuffer mappedByteBuffer = fileChannel.map(READ_WRITE, 0, bytes.length); if (mappedByteBuffer != null) { mappedByteBuffer.put(bytes); mappedByteBuffer.force(); } } catch (IOException e) { e.printStackTrace(); }}

      打開文件通道 fileChannel 并提供讀權限、寫權限和數據清空權限,通過 fileChannel 映射到一個可寫的內存緩沖區 mappedByteBuffer,將目標數據寫入 mappedByteBuffer,通過 force() 方法把緩沖區更改的內容強制寫入本地文件。

      測試讀文件:

      Copypublic static void read(){ Path path = Paths.get(FILE_NAME); int length = CONTENT.getBytes(Charset.forName("UTF-8")).length; try (FileChannel fileChannel = FileChannel.open(path, StandardOpenOption.READ)) { MappedByteBuffer mappedByteBuffer = fileChannel.map(READ_ONLY, 0, length); if (mappedByteBuffer != null) { byte[] bytes = new byte[length]; mappedByteBuffer.get(bytes); String content = new String(bytes, StandardCharsets.UTF_8); System.out.println(content); } } catch (IOException e) { e.printStackTrace(); }}

      map()方法是java.nio.channels.FileChannel 的抽象方法,由子類 sun.nio.ch.FileChannelImpl.java 實現,下面是和內存映射相關的核心代碼:

      Copypublic MappedByteBuffer map(MapMode var1, long var2, long var4) throws IOException { if (var4 == 0L) { var7 = 0L; FileDescriptor var38 = new FileDescriptor(); if (this.writable && var6 != 0) { var17 = Util.newMappedByteBuffer(0, 0L, var38, (Runnable)null); return var17; } var17 = Util.newMappedByteBufferR(0, 0L, var38, (Runnable)null); return var17; } var12 = (int)(var2 % allocationGranularity); long var36 = var2 - (long)var12; var10 = var4 + (long)var12; try { var7 = this.map0(var6, var36, var10); } catch (OutOfMemoryError var31) { System.gc(); try { Thread.sleep(100L); } catch (InterruptedException var30) { Thread.currentThread().interrupt(); } try { var7 = this.map0(var6, var36, var10); } catch (OutOfMemoryError var29) { throw new IOException("Map failed", var29); } } FileDescriptor var13; try { var13 = this.nd.duplicateForMapping(this.fd); } catch (IOException var28) { unmap0(var7, var10); throw var28; } assert IOStatus.checkAll(var7); assert var7 % allocationGranularity == 0L; int var35 = (int)var4; FileChannelImpl.Unmapper var15 = new FileChannelImpl.Unmapper(var7, var10, var35, var13); if (this.writable && var6 != 0) { var37 = Util.newMappedByteBuffer(var35, var7 + (long)var12, var13, var15); return var37; } else { var37 = Util.newMappedByteBufferR(var35, var7 + (long)var12, var13, var15); return var37; }}

      map()方法通過本地方法 map0()為文件分配一塊虛擬內存,作為它的內存映射區域,然后返回這塊內存映射區域的起始地址。

      文件映射需要在 Java 堆中創建一個 MappedByteBuffer 的實例。如果第一次文件映射導致 OOM,則手動觸發垃圾回收,休眠 100ms 后再嘗試映射,如果失敗則拋出異常。

      通過 Util 的 newMappedByteBuffer (可讀可寫)方法或者 newMappedByteBufferR(僅讀) 方法方法反射創建一個 DirectByteBuffer 實例,其中 DirectByteBuffer 是 MappedByteBuffer 的子類。

      map() 方法返回的是內存映射區域的起始地址,通過(起始地址 + 偏移量)就可以獲取指定內存的數據。這樣一定程度上替代了read() 或 write()方法,底層直接采用 sun.misc.Unsafe 類的 getByte()和putByte()方法對數據進行讀寫。

      Copyprivate 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 這個源文件里面:https://github.com/openjdk/jdk/blob/a619f36d115f1c6ebda15d7165de95dc44ebb1fd/src/java.base/windows/native/libnio/ch/FileChannelImpl.c

      MappedByteBuffer 的特點和不足之處:

      MappedByteBuffer 使用是堆外的虛擬內存,因此分配(map)的內存大小不受 JVM 的 -Xmx 參數限制,但是也是有大小限制的。

      如果當文件超出 Integer.MAX_VALUE 字節限制時,可以通過 position 參數重新 map 文件后面的內容。

      MappedByteBuffer 在處理大文件時性能的確很高,但也存在內存占用、文件關閉不確定等問題,被其打開的文件只有在垃圾回收的才會被關閉,而且這個時間點是不確定的。

      MappedByteBuffer 提供了文件映射內存的 mmap() 方法,也提供了釋放映射內存的 unmap() 方法。然而 unmap() 是 FileChannelImpl 中的私有方法,無法直接顯示調用。因此,用戶程序需要通過 Java 反射的調用 sun.misc.Cleaner 類的 clean() 方法手動釋放映射占用的內存區域。

      2、JDK零拷貝之DirectByteBuffer

      DirectByteBuffer 是 Java NIO 用于實現堆外內存的一個很重要的類,而 Netty 用 DirectByteBuffer 作為PooledDirectByteBuf 和 UnpooledDirectByteBuf 的內部數據容器(區別于 HeapByteBuf 直接用 byte[] 作為數據容器)。

      DirectByteBuffer 的對象引用位于 Java 內存模型的堆里面,JVM 可以對 DirectByteBuffer 的對象進行內存分配和回收管理,一般使用 DirectByteBuffer 的靜態方法 allocateDirect()創建 DirectByteBuffer 實例并分配內存。

      Copypublic static ByteBuffer allocateDirect(int capacity) { return new DirectByteBuffer(capacity);}

      DirectByteBuffer 內存分配是調用底層的 Unsafe 類提供的基礎方法 allocateMemory()直接分配堆外內存:

      CopyDirectByteBuffer(int cap) { // package-private super(-1, 0, cap, cap); boolean pa = VM.isDirectMemoryPageAligned(); int ps = Bits.pageSize(); long size = Math.max(1L, (long)cap + (pa ? ps : 0)); Bits.reserveMemory(size, cap); long base = 0; try { base = unsafe.allocateMemory(size); } catch (OutOfMemoryError x) { Bits.unreserveMemory(size, cap); throw x; } unsafe.setMemory(base, size, (byte) 0); if (pa && (base % ps != 0)) { // Round up to page boundary address = base + ps - (base & (ps - 1)); } else { address = base; } cleaner = Cleaner.create(this, new Deallocator(base, size, cap)); att = null;}

      那么 DirectByteBuffer 和零拷貝有什么關系?我們看一下 DirectByteBuffer 的類名:

      Copyclass DirectByteBuffer extends MappedByteBuffer implements DirectBuffer { }

      可以看到她繼承了 MappedByteBuffer,而 MappedByteBuffer 的 map() 方法會通過 Util.newMappedByteBuffer()來創建一個緩沖區實例。

      3、基于 sendfile 實現的 FileChannel

      FileChannel 是一個用于文件讀寫、映射和操作的通道,同時它在并發環境下是線程安全的,基于 FileInputStream、FileOutputStream 或者 RandomAccessFile 的 getChannel()方法可以創建并打開一個文件通道。FileChannel 定義了 transferFrom()和 transferTo()兩個抽象方法,它通過在通道和通道之間建立連接實現數據傳輸的。

      transferTo():通過 FileChannel 把文件里面的源數據寫入一個 WritableByteChannel 的目的通道。

      transferFrom():把一個源通道 ReadableByteChannel 中的數據讀取到當前 FileChannel 的文件里面。

      這兩個方法也是 java.nio.channels.FileChannel 的抽象方法,由子類 sun.nio.ch.FileChannelImpl.java 實現。transferTo() 和transferFrom() 底層都是基于 sendfile 實現數據傳輸的,其中 FileChannelImpl.java 定義了 3 個常量,用于標示當前操作系統的內核是否支持 sendfile 以及 sendfile 的相關特性。

      Copyprivate static volatile boolean transferSupported = true;private static volatile boolean pipeSupported = true;private static volatile boolean fileSupported = true;

      transferSupported:用于標記當前的系統內核是否支持sendfile()調用,默認為 true。

      pipeSupported:用于標記當前的系統內核是否支持文件描述符(fd)基于管道(pipe)的sendfile()調用,默認為 true。

      fileSupported:用于標記當前的系統內核是否支持文件描述符(fd)基于文件(file)的 sendfile()調用,默認為 true。

      4、Netty零拷貝

      Netty 中的零拷貝和上面提到的操作系統層面上的零拷貝不太一樣, 我們所說的 Netty 零拷貝完全是基于(Java 層面)用戶態的,它的更多的是偏向于數據操作優化這樣的概念,具體表現在以下幾個方面:

      Netty 通過 DefaultFileRegion 類對java.nio.channels.FileChannel 的 tranferTo()方法進行包裝,在文件傳輸時可以將文件緩沖區的數據直接發送到目的通道(Channel);

      ByteBuf 可以通過 wrap 操作把字節數組、ByteBuf、ByteBuffer 包裝成一個 ByteBuf 對象, 進而避免了拷貝操作;

      ByteBuf 支持 slice 操作, 因此可以將 ByteBuf 分解為多個共享同一個存儲區域的 ByteBuf,避免了內存的拷貝;

      Netty 提供了 CompositeByteBuf 類,它可以將多個 ByteBuf 合并為一個邏輯上的 ByteBuf,避免了各個 ByteBuf 之間的拷貝。

      Linux

      版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。

      版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。

      上一篇:系統流程圖制作模板大全(流程圖制作模版)
      下一篇:“Excel批處理”成批修改文件名 以幫助您組織文件!
      相關文章
      久久狠狠爱亚洲综合影院| 日韩亚洲Av人人夜夜澡人人爽| 在线电影你懂的亚洲| 亚洲人成图片小说网站| 亚洲精品无码久久久| 亚洲成A人片77777国产| 国产精品亚洲一区二区三区久久| 亚洲日韩AV一区二区三区中文| 亚洲一区在线免费观看| 亚洲同性男gay网站在线观看| 亚洲色图.com| 亚洲精品影院久久久久久| 亚洲色图古典武侠| 亚洲第一成年人网站| 亚洲欧洲日本精品| 亚洲综合久久1区2区3区| 亚洲精品高清国产麻豆专区| 自怕偷自怕亚洲精品| 亚洲综合激情视频| 亚洲av乱码一区二区三区| ass亚洲**毛茸茸pics| 2020天堂在线亚洲精品专区| 国产午夜亚洲精品国产| 亚洲人成无码网站在线观看| 亚洲日韩精品无码专区| 亚洲av最新在线观看网址| 国产精品亚洲专区无码不卡| 亚洲第一区精品日韩在线播放| 亚洲国产成人精品91久久久| 亚洲日本中文字幕一区二区三区 | 亚洲视频在线免费看| 亚洲色成人网一二三区| 亚洲乱人伦精品图片| 亚洲国产91在线| 亚洲第一街区偷拍街拍| 亚洲精品一级无码鲁丝片| 亚洲人成人网站色www| 亚洲视频.com| 亚洲一区二区三区免费观看| 亚洲色成人网站WWW永久四虎| 永久亚洲成a人片777777|