假如如今有這樣一個需求:須要將磁盤中的一個文件經過網絡傳輸到另外一個設備上,咱們看看各類實現方式中數據流的傳遞過程。node
在"Java零拷貝一步曲"中咱們也展現過這個過程,這個過程產生的系統消耗是:web
若是應用程序能夠直接訪問網絡接口存儲,那麼在應用程序訪問數據以前存儲總線就不須要被遍歷,數據傳輸所引發的開銷將會是最小的。應用程序或者運行在用戶模式下的庫函數能夠直接訪問硬件設備的存儲,操做系統內核除了進行必要的虛擬存儲配置工做以外,不參與數據傳輸過程當中的其它任何事情。直接 I/O 使得數據能夠直接在應用程序和外圍設備之間進行傳輸,徹底不須要操做系統內核頁緩存的支持。
圖 1. 使用直接 I/O 的數據傳輸
緩存
在 Linux 中,減小拷貝次數的一種方法是調用 mmap() 來代替調用 read,好比:bash
tmp_buf = mmap(file, len);
write(socket, tmp_buf, len);
複製代碼
首先,應用程序調用了 mmap() 以後,數據會先經過 DMA 拷貝到操做系統內核的緩衝區中去。接着,應用程序跟操做系統共享這個緩衝區,這樣,操做系統內核和應用程序存儲空間就不須要再進行任何的數據拷貝操做。應用程序調用了 write() 以後,操做系統內核將數據從原來的內核緩衝區中拷貝到與 socket 相關的內核緩衝區中。接下來,數據從內核 socket 緩衝區拷貝到協議引擎中去,這是第三次數據拷貝操做。服務器
圖 2. 利用 mmap() 代替 read()
網絡
經過使用 mmap() 來代替 read(), 已經能夠減半操做系統須要進行數據拷貝的次數。當大量數據須要傳輸的時候,這樣作就會有一個比較好的效率。可是,這種改進也是須要代價的,使用 mma()p 實際上是存在潛在的問題的。當對文件進行了內存映射,而後調用 write() 系統調用,若是此時其餘的進程截斷了這個文件,那麼 write() 系統調用將會被總線錯誤信號 SIGBUS 中斷,由於此時正在執行的是一個錯誤的存儲訪問。這個信號將會致使進程被殺死,解決這個問題能夠經過如下這兩種方法:異步
使用 mmap 是 POSIX 兼容的,可是使用 mmap 並不必定能得到理想的數據傳輸性能。數據傳輸的過程當中仍然須要一次 CPU 拷貝操做,並且映射操做也是一個開銷很大的虛擬存儲操做,這種操做須要經過更改頁表以及沖刷 TLB (使得 TLB 的內容無效)來維持存儲的一致性。可是,由於映射一般適用於較大範圍,因此對於相同長度的數據來講,映射所帶來的開銷遠遠低於 CPU 拷貝所帶來的開銷。socket
爲了簡化用戶接口,同時還要繼續保留 mmap()/write() 技術的優勢:減小 CPU 的拷貝次數,Linux 在版本 2.1 中引入了 sendfile() 這個系統調用。函數
sendfile() 不只減小了數據拷貝操做,它也減小了上下文切換。首先:sendfile() 系統調用利用 DMA 引擎將文件中的數據拷貝到操做系統內核緩衝區中,而後數據被拷貝到與 socket 相關的內核緩衝區中去。接下來,DMA 引擎將數據從內核 socket 緩衝區中拷貝到協議引擎中去。若是在用戶調用 sendfile () 系統調用進行數據傳輸的過程當中有其餘進程截斷了該文件,那麼 sendfile () 系統調用會簡單地返回給用戶應用程序中斷前所傳輸的字節數,errno 會被設置爲 success。若是在調用 sendfile() 以前操做系統對文件加上了租借鎖,那麼 sendfile() 的操做和返回狀態將會和 mmap()/write () 同樣。性能
圖 3. 利用 sendfile () 進行數據傳輸
sendfile() 系統調用不須要將數據拷貝或者映射到應用程序地址空間中去,因此 sendfile() 只是適用於應用程序地址空間不須要對所訪問數據進行處理的狀況。相對於 mmap() 方法來講,由於 sendfile 傳輸的數據沒有越過用戶應用程序 / 操做系統內核的邊界線,因此 sendfile () 也極大地減小了存儲管理的開銷。可是,sendfile () 也有不少侷限性,以下所列:
上小節介紹的 sendfile() 技術在進行數據傳輸仍然還須要一次多餘的數據拷貝操做,經過引入一點硬件上的幫助,這僅有的一次數據拷貝操做也能夠避免。爲了不操做系統內核形成的數據副本,須要用到一個支持收集操做的網絡接口,這也就是說,待傳輸的數據能夠分散在存儲的不一樣位置上,而不須要在連續存儲中存放。這樣一來,從文件中讀出的數據就根本不須要被拷貝到 socket 緩衝區中去,而只是須要將緩衝區描述符傳到網絡協議棧中去,以後其在緩衝區中創建起數據包的相關結構,而後經過 DMA 收集拷貝功能將全部的數據結合成一個網絡數據包。網卡的 DMA 引擎會在一次操做中從多個位置讀取包頭和數據。Linux 2.4 版本中的 socket 緩衝區就能夠知足這種條件,這也就是用於 Linux 中的衆所周知的零拷貝技術,這種方法不但減小了由於屢次上下文切換所帶來開銷,同時也減小了處理器形成的數據副本的個數。對於用戶應用程序來講,代碼沒有任何改變。首先,sendfile() 系統調用利用 DMA 引擎將文件內容拷貝到內核緩衝區去;而後,將帶有文件位置和長度信息的緩衝區描述符添加到 socket 緩衝區中去,此過程不須要將數據從操做系統內核緩衝區拷貝到 socket 緩衝區中,DMA 引擎會將數據直接從內核緩衝區拷貝到協議引擎中去,這樣就避免了最後一次數據拷貝。
圖 4. 帶有 DMA 收集拷貝功能的 sendfile
經過這種方法,CPU 在數據傳輸的過程當中不但避免了數據拷貝操做,理論上,CPU 也永遠不會跟傳輸的數據有任何關聯,這對於 CPU 的性能來講起到了積極的做用:首先,高速緩衝存儲器沒有受到污染;其次,高速緩衝存儲器的一致性不須要維護,高速緩衝存儲器在 DMA 進行數據傳輸前或者傳輸後不須要被刷新。然而實際上,後者實現起來很是困難。源緩衝區有多是頁緩存的一部分,這也就是說通常的讀操做能夠訪問它,並且該訪問也能夠是經過傳統方式進行的。只要存儲區域能夠被 CPU 訪問到,那麼高速緩衝存儲器的一致性就須要經過 DMA 傳輸以前沖刷新高速緩衝存儲器來維護。並且,這種數據收集拷貝功能的實現是須要硬件以及設備驅動程序支持的。
splice() 是 Linux 中與 mmap() 和 sendfile() 相似的一種方法。它也能夠用於用戶應用程序地址空間和操做系統地址空間之間的數據傳輸。splice() 適用於能夠肯定數據傳輸路徑的用戶應用程序,它不須要利用用戶地址空間的緩衝區進行顯式的數據傳輸操做。那麼,當數據只是從一個地方傳送到另外一個地方,過程當中所傳輸的數據不須要通過用戶應用程序的處理的時候,spice() 就成爲了一種比較好的選擇。splice() 能夠在操做系統地址空間中整塊地移動數據,從而減小大多數數據拷貝操做。並且,splice() 進行數據傳輸能夠經過異步的方式來進行,用戶應用程序能夠先從系統調用返回,而操做系統內核進程會控制數據傳輸過程繼續進行下去。splice() 能夠被當作是相似於基於流的管道的實現,管道可使得兩個文件描述符相互鏈接,splice 的調用者則能夠控制兩個設備(或者協議棧)在操做系統內核中的相互鏈接。
splice() 系統調用和 sendfile() 很是相似,用戶應用程序必須擁有兩個已經打開的文件描述符,一個用於表示輸入設備,一個用於表示輸出設備。與 sendfile() 不一樣的是,splice() 容許任意兩個文件之間互相鏈接,而並不僅是文件到 socket 進行數據傳輸。對於從一個文件描述符發送數據到 socket 這種特例來講,一直都是使用 sendfile() 這個系統調用,而 splice 一直以來就只是一種機制,它並不只限於 sendfile() 的功能。也就是說,sendfile() 只是 splice() 的一個子集,在 Linux 2.6.23 中,sendfile() 這種機制的實現已經沒有了,可是這個 API 以及相應的功能還存在,只不過 API 以及相應的功能是利用了 splice() 這種機制來實現的。
在數據傳輸的過程當中,splice() 機制交替地發送相關的文件描述符的讀寫操做,而且能夠將讀緩衝區從新用於寫操做。它也利用了一種簡單的流控制,經過預先定義的水印( watermark )來阻塞寫請求。有實驗代表,利用這種方法將數據從一個磁盤傳輸到另外一個磁盤會增長 30% 到 70% 的吞吐量,數據傳輸的過程當中, CPU 的負載也會減小一半。
Linux 2.6.17 內核引入了 splice() 系統調用,可是,這個概念在此以前 ] 其實已經存在了很長一段時間了。1988 年,Larry McVoy 提出了這個概念,它被當作是一種改進服務器端系統的 I/O 性能的一種技術,儘管在以後的若干年中常常被說起,可是 splice 系統調用歷來沒有在主流的 Linux 操做系統內核中實現過,一直到 Linux 2.6.17 版本的出現。splice 系統調用須要用到四個參數,其中兩個是文件描述符,一個表示文件長度,還有一個用於控制如何進行數據拷貝。splice 系統調用能夠同步實現,也可使用異步方式來實現。在使用異步方式的時候,用戶應用程序會經過信號 SIGIO 來獲知數據傳輸已經終止。splice() 系統調用的接口以下所示:
long splice(int fdin, int fdout, size_t len, unsigned int flags);
複製代碼
調用 splice() 系統調用會致使操做系統內核從數據源 fdin 移動最多 len 個字節的數據到 fdout 中去,這個數據的移動過程只是通過操做系統內核空間,須要最少的拷貝次數。使用 splice() 系統調用須要這兩個文件描述符中的一個必須是用來表示一個管道設備的。不難看出,這種設計具備侷限性,Linux 的後續版本針對這一問題將會有所改進。參數 flags 用於表示拷貝操做的執行方法,當前的 flags 有以下這些取值:
Splice() 系統調用利用了 Linux 提出的管道緩衝區( pipe buffer )機制,這就是爲何這個系統調用的兩個文件描述符參數中至少有一個必需要指代管道設備的緣由。爲了支持 splice 這種機制,Linux 在用於設備和文件系統的 file_operations 結構中增長了下邊這兩個定義:
ssize_t (*splice_write)(struct inode *pipe, strucuct file *out, size_t len, unsigned int flags);
ssize_t (*splice_read)(struct inode *in, strucuct file *pipe, size_t len, unsigned int flags);
複製代碼
這兩個新的操做能夠根據 flags 的設定在 pipe 和 in 或者 out 之間移動 len 個字節。Linux 文件系統已經實現了具備上述功能而且可使用的操做,並且還實現了一個 generic_splice_sendpage() 函數用於和 socket 之間的接合。