這是我參與8月更文挑戰的第12天,活動詳情查看:8月更文挑戰java
零拷貝(Zero-Copy)是一種 I/O
操做優化技術,能夠快速高效地將數據從文件系統移動到網絡接口,而不須要將其從內核空間複製到用戶空間。其在 FTP
或者 HTTP
等協議中能夠顯著地提高性能。可是須要注意的是,並非全部的操做系統都支持這一特性,目前只有在使用 NIO
和 Epoll
傳輸時纔可以使用該特性。markdown
須要注意,它不能用於實現了數據加密或者壓縮的文件系統上,只有傳輸文件的原始內容。這類原始內容也包括加密了的文件內容。網絡
若是服務端要提供文件傳輸的功能,咱們能想到的最簡單的方式是:將磁盤上的文件讀取出來,而後經過網絡協議發送給客戶端。併發
傳統 I/O 的工做方式是,數據讀取和寫入是從用戶空間到內核空間來回複製,而內核空間的數據是經過操做系統層面的 I/O 接口從磁盤讀取或寫入。socket
代碼一般以下,通常會須要兩個系統調用:高併發
read(file, tmp_buf, len);
write(socket, tmp_buf, len);
複製代碼
代碼很簡單,雖然就兩行代碼,可是這裏面發生了很多的事情。post
首先,期間共發生了 4 次用戶態與內核態的上下文切換,由於發生了兩次系統調用,一次是 read()
,一次是 write()
,每次系統調用都得先從用戶態切換到內核態,等內核完成任務後,再從內核態切換回用戶態。性能
上下文切換到成本並不小,一次切換須要耗時幾十納秒到幾微秒,雖然時間看上去很短,可是在高併發的場景下,這類時間容易被累積和放大,從而影響系統的性能。優化
其次,還發生了 4 次數據拷貝,其中兩次是 DMA 的拷貝,另外兩次則是經過 CPU 拷貝的,下面說一下這個過程:加密
第一次拷貝
,把磁盤上的數據拷貝到操做系統內核的緩衝區裏,這個拷貝的過程是經過 DMA 搬運的。第二次拷貝
,把內核緩衝區的數據拷貝到用戶的緩衝區裏,因而咱們應用程序就可使用這部分數據了,這個拷貝到過程是由 CPU 完成的。第三次拷貝
,把剛纔拷貝到用戶的緩衝區裏的數據,再拷貝到內核的 socket 的緩衝區裏,這個過程依然仍是由 CPU 搬運的。第四次拷貝
,把內核的 socket 緩衝區裏的數據,拷貝到網卡的緩衝區裏,這個過程又是由 DMA 搬運的。這種簡單又傳統的文件傳輸方式,存在冗餘的上文切換和數據拷貝,在高併發系統裏是很是糟糕的,多了不少沒必要要的開銷,會嚴重影響系統性能。
因此,要想提升文件傳輸的性能,就須要減小「用戶態與內核態的上下文切換」和「內存拷貝」的次數。
零拷貝主要是用來解決操做系統在處理 I/O 操做時,頻繁複制數據的問題。關於零拷貝主要技術有 mmap+write
、sendfile
和splice
等幾種方式。
在瞭解零拷貝技術以前,先了解虛擬內存的概念。
全部現代操做系統都使用虛擬內存,使用虛擬地址取代物理地址,主要有如下幾點好處:
利用上述的第一條特性能夠優化,能夠把內核空間和用戶空間的虛擬地址映射到同一個物理地址,這樣在 I/O 操做時就不須要來回複製了。
以下圖展現了虛擬內存的原理。
使用mmap/write
方式替換原來的傳統I/O方式,就是利用了虛擬內存的特性。下圖展現了mmap/write
原理:
整個流程的核心區別就是,把數據讀取到內核緩衝區後,應用程序進行寫入操做時,直接把內核的Read Buffer
的數據複製到Socket Buffer
以便寫入,此次內核之間的複製也是須要CPU的參與的。
上述流程就是少了一個 CPU COPY,提高了 I/O 的速度。不過發現上下文的切換仍是4次並無減小,這是由於仍是要應用程序發起write
操做。
那能不能減小上下文切換呢?這就須要
sendfile
方式來進一步優化了。
從 Linux 2.1 版本開始,Linux 引入了 sendfile
來簡化操做。sendfile
方式能夠替換上面的mmap/write
方式來進一步優化。
sendfile
將如下操做:
mmap();
write();
複製代碼
替換爲:
sendfile();
複製代碼
這樣就減小了上下文切換,由於少了一個應用程序發起write
操做,直接發起sendfile
操做。
下圖展現了sendfile
原理:
sendfile
方式只有三次數據複製(其中只有一次 CPU COPY)以及2次上下文切換。
那能不能把 CPU COPY 減小到沒有呢?這樣須要帶有
scatter/gather
的sendfile
方式了。
Linux 2.4 內核進行了優化,提供了帶有 scatter/gather
的 sendfile 操做,這個操做能夠把最後一次 CPU COPY
去除。其原理就是在內核空間 Read BUffer 和 Socket Buffer 不作數據複製,而是將 Read Buffer 的內存地址、偏移量記錄到相應的 Socket Buffer 中,這樣就不須要複製。其本質和虛擬內存的解決方法思路一致,就是內存地址的記錄。
下圖展現了scatter/gather 的 sendfile 的原理:
scatter/gather 的 sendfile 只有兩次數據複製(都是 DMA COPY)及 2 次上下文切換。CUP COPY 已經徹底沒有。不過這一種收集複製功能是須要硬件及驅動程序支持的。
splice
調用和sendfile
很是類似,用戶應用程序必須擁有兩個已經打開的文件描述符,一個表示輸入設備,一個表示輸出設備。與sendfile
不一樣的是,splice
容許任意兩個文件互相鏈接,而並不僅是文件與socket
進行數據傳輸。對於從一個文件描述符發送數據到socket
這種特例來講,一直都是使用sendfile
系統調用,而splice
一直以來就只是一種機制,它並不只限於sendfile
的功能。也就是說 sendfile 是 splice 的一個子集。
在 Linux 2.6.17 版本引入了 splice,而在 Linux 2.6.23 版本中, sendfile 機制的實現已經沒有了,可是其 API 及相應的功能還在,只不過 API 及相應的功能是利用了 splice 機制來實現的。
和 sendfile 不一樣的是,splice 不須要硬件支持。
不管是傳統的 I/O 方式,仍是引入了零拷貝以後,2 次 DMA copy
是都少不了的。由於兩次 DMA 都是依賴硬件完成的。因此,所謂的零拷貝,都是爲了減小 CPU copy 及減小了上下文的切換。
下圖展現了各類零拷貝技術的對比圖:
CPU拷貝 | DMA拷貝 | 系統調用 | 上下文切換 | |
---|---|---|---|---|
傳統方法 | 2 | 2 | read/write | 4 |
內存映射 | 1 | 2 | mmap/write | 4 |
sendfile | 1 | 2 | sendfile | 2 |
scatter/gather copy | 0 | 2 | sendfile | 2 |
splice | 0 | 2 | splice | 0 |
我是一個正在被打擊還在努力前進的碼農。若是文章對你有幫助,記得點贊、關注喲,謝謝!