【轉(zhuǎn)自】:https://www.ibm.com/developerworks/cn/linux/l-cn-zerocopy2/index.html
Linux 中的直接 I/O
如果應用程序可以直接訪問網(wǎng)絡接口存儲么介,那么在應用程序訪問數(shù)據(jù)之前存儲總線就不需要被遍歷旧蛾,數(shù)據(jù)傳輸所引起的開銷將會是最小的。應用程序或者運行在用戶模式下的庫函數(shù)可以直接訪問硬件設(shè)備的存儲同波,操作系統(tǒng)內(nèi)核除了進行必要的虛擬存儲配置工作之外,不參與數(shù)據(jù)傳輸過程中的其它任何事情。直接 I/O 使得數(shù)據(jù)可以直接在應用程序和外圍設(shè)備之間進行傳輸,完全不需要操作系統(tǒng)內(nèi)核頁緩存的支持赡模。關(guān)于直接 I/O 技術(shù)的具體實現(xiàn)細節(jié)可以參看 developerWorks 上的另一篇文章”Linux 中直接 I/O 機制的介紹” ,本文不做過多描述师抄。
圖 1. 使用直接 I/O 的數(shù)據(jù)傳輸
針對數(shù)據(jù)傳輸不需要經(jīng)過應用程序地址空間的零拷貝技術(shù)
利用 mmap()
在 Linux 中漓柑,減少拷貝次數(shù)的一種方法是調(diào)用 mmap() 來代替調(diào)用 read,比如:
`tmp_buf = mmap(file, len);`
`write(socket, tmp_buf, len);`
首先,應用程序調(diào)用了 mmap() 之后欺缘,數(shù)據(jù)會先通過 DMA 拷貝到操作系統(tǒng)內(nèi)核的緩沖區(qū)中去栋豫。接著,應用程序跟操作系統(tǒng)共享這個緩沖區(qū)谚殊,這樣丧鸯,操作系統(tǒng)內(nèi)核和應用程序存儲空間就不需要再進行任何的數(shù)據(jù)拷貝操作。應用程序調(diào)用了 write() 之后嫩絮,操作系統(tǒng)內(nèi)核將數(shù)據(jù)從原來的內(nèi)核緩沖區(qū)中拷貝到與 socket 相關(guān)的內(nèi)核緩沖區(qū)中丛肢。接下來,數(shù)據(jù)從內(nèi)核 socket 緩沖區(qū)拷貝到協(xié)議引擎中去剿干,這是第三次數(shù)據(jù)拷貝操作蜂怎。
圖 2. 利用 mmap() 代替 read()
通過使用 mmap() 來代替 read(), 已經(jīng)可以減半操作系統(tǒng)需要進行數(shù)據(jù)拷貝的次數(shù)。當大量數(shù)據(jù)需要傳輸?shù)臅r候置尔,這樣做就會有一個比較好的效率杠步。但是,這種改進也是需要代價的榜轿,使用 mma()p 其實是存在潛在的問題的幽歼。當對文件進行了內(nèi)存映射,然后調(diào)用 write() 系統(tǒng)調(diào)用谬盐,如果此時其他的進程截斷了這個文件甸私,那么 write() 系統(tǒng)調(diào)用將會被總線錯誤信號 SIGBUS 中斷,因為此時正在執(zhí)行的是一個錯誤的存儲訪問飞傀。這個信號將會導致進程被殺死皇型,解決這個問題可以通過以下這兩種方法:
- 為 SIGBUS 安裝一個新的信號處理器,這樣砸烦,write() 系統(tǒng)調(diào)用在它被中斷之前就返回已經(jīng)寫入的字節(jié)數(shù)目弃鸦,errno 會被設(shè)置成 success。但是這種方法也有其缺點幢痘,它不能反映出產(chǎn)生這個問題的根源所在唬格,因為 BIGBUS 信號只是顯示某進程發(fā)生了一些很嚴重的錯誤。
- 第二種方法是通過文件租借鎖來解決這個問題的雪隧,這種方法相對來說更好一些西轩。我們可以通過內(nèi)核對文件加讀或者寫的租借鎖员舵,當另外一個進程嘗試對用戶正在進行傳輸?shù)奈募M行截斷的時候脑沿,內(nèi)核會發(fā)送給用戶一個實時信號:RT_SIGNAL_LEASE 信號,這個信號會告訴用戶內(nèi)核破壞了用戶加在那個文件上的寫或者讀租借鎖马僻,那么 write() 系統(tǒng)調(diào)用則會被中斷庄拇,并且進程會被 SIGBUS 信號殺死,返回值則是中斷前寫的字節(jié)數(shù),errno 也會被設(shè)置為 success措近。文件租借鎖需要在對文件進行內(nèi)存映射之前設(shè)置溶弟。
使用 mmap 是 POSIX 兼容的,但是使用 mmap 并不一定能獲得理想的數(shù)據(jù)傳輸性能北滥。數(shù)據(jù)傳輸?shù)倪^程中仍然需要一次 CPU 拷貝操作荧缘,而且映射操作也是一個開銷很大的虛擬存儲操作灭美,這種操作需要通過更改頁表以及沖刷 TLB (使得 TLB 的內(nèi)容無效)來維持存儲的一致性。但是擒权,因為映射通常適用于較大范圍,所以對于相同長度的數(shù)據(jù)來說阁谆,映射所帶來的開銷遠遠低于 CPU 拷貝所帶來的開銷碳抄。
sendfile()
為了簡化用戶接口,同時還要繼續(xù)保留 mmap()/write() 技術(shù)的優(yōu)點:減少 CPU 的拷貝次數(shù)场绿,Linux 在版本 2.1 中引入了 sendfile() 這個系統(tǒng)調(diào)用剖效。
sendfile() 不僅減少了數(shù)據(jù)拷貝操作,它也減少了上下文切換焰盗。首先:sendfile() 系統(tǒng)調(diào)用利用 DMA 引擎將文件中的數(shù)據(jù)拷貝到操作系統(tǒng)內(nèi)核緩沖區(qū)中璧尸,然后數(shù)據(jù)被拷貝到與 socket 相關(guān)的內(nèi)核緩沖區(qū)中去。接下來姨谷,DMA 引擎將數(shù)據(jù)從內(nèi)核 socket 緩沖區(qū)中拷貝到協(xié)議引擎中去逗宁。如果在用戶調(diào)用 sendfile () 系統(tǒng)調(diào)用進行數(shù)據(jù)傳輸?shù)倪^程中有其他進程截斷了該文件,那么 sendfile () 系統(tǒng)調(diào)用會簡單地返回給用戶應用程序中斷前所傳輸?shù)淖止?jié)數(shù)梦湘,errno 會被設(shè)置為 success瞎颗。如果在調(diào)用 sendfile() 之前操作系統(tǒng)對文件加上了租借鎖,那么 sendfile() 的操作和返回狀態(tài)將會和 mmap()/write () 一樣捌议。
圖 3. 利用 sendfile () 進行數(shù)據(jù)傳輸
sendfile() 系統(tǒng)調(diào)用不需要將數(shù)據(jù)拷貝或者映射到應用程序地址空間中去哼拔,所以 sendfile() 只是適用于應用程序地址空間不需要對所訪問數(shù)據(jù)進行處理的情況。相對于 mmap() 方法來說瓣颅,因為 sendfile 傳輸?shù)臄?shù)據(jù)沒有越過用戶應用程序 / 操作系統(tǒng)內(nèi)核的邊界線倦逐,所以 sendfile () 也極大地減少了存儲管理的開銷。但是宫补,sendfile () 也有很多局限性檬姥,如下所列:
- sendfile() 局限于基于文件服務的網(wǎng)絡應用程序,比如 web 服務器粉怕。據(jù)說健民,在 Linux 內(nèi)核中實現(xiàn) sendfile() 只是為了在其他平臺上使用 sendfile() 的 Apache 程序。
- 由于網(wǎng)絡傳輸具有異步性贫贝,很難在 sendfile () 系統(tǒng)調(diào)用的接收端進行配對的實現(xiàn)方式秉犹,所以數(shù)據(jù)傳輸?shù)慕邮斩艘话銢]有用到這種技術(shù)蛉谜。
- 基于性能的考慮來說,sendfile () 仍然需要有一次從文件到 socket 緩沖區(qū)的 CPU 拷貝操作崇堵,這就導致頁緩存有可能會被傳輸?shù)臄?shù)據(jù)所污染型诚。
帶有 DMA 收集拷貝功能的 sendfile()
上小節(jié)介紹的 sendfile() 技術(shù)在進行數(shù)據(jù)傳輸仍然還需要一次多余的數(shù)據(jù)拷貝操作,通過引入一點硬件上的幫助鸳劳,這僅有的一次數(shù)據(jù)拷貝操作也可以避免狰贯。為了避免操作系統(tǒng)內(nèi)核造成的數(shù)據(jù)副本,需要用到一個支持收集操作的網(wǎng)絡接口赏廓,這也就是說暮现,待傳輸?shù)臄?shù)據(jù)可以分散在存儲的不同位置上,而不需要在連續(xù)存儲中存放楚昭。這樣一來栖袋,從文件中讀出的數(shù)據(jù)就根本不需要被拷貝到 socket 緩沖區(qū)中去,而只是需要將緩沖區(qū)描述符傳到網(wǎng)絡協(xié)議棧中去抚太,之后其在緩沖區(qū)中建立起數(shù)據(jù)包的相關(guān)結(jié)構(gòu)塘幅,然后通過 DMA 收集拷貝功能將所有的數(shù)據(jù)結(jié)合成一個網(wǎng)絡數(shù)據(jù)包。網(wǎng)卡的 DMA 引擎會在一次操作中從多個位置讀取包頭和數(shù)據(jù)尿贫。Linux 2.4 版本中的 socket 緩沖區(qū)就可以滿足這種條件电媳,這也就是用于 Linux 中的眾所周知的零拷貝技術(shù),這種方法不但減少了因為多次上下文切換所帶來開銷庆亡,同時也減少了處理器造成的數(shù)據(jù)副本的個數(shù)匾乓。對于用戶應用程序來說,代碼沒有任何改變又谋。首先拼缝,sendfile() 系統(tǒng)調(diào)用利用 DMA 引擎將文件內(nèi)容拷貝到內(nèi)核緩沖區(qū)去;然后彰亥,將帶有文件位置和長度信息的緩沖區(qū)描述符添加到 socket 緩沖區(qū)中去咧七,此過程不需要將數(shù)據(jù)從操作系統(tǒng)內(nèi)核緩沖區(qū)拷貝到 socket 緩沖區(qū)中,DMA 引擎會將數(shù)據(jù)直接從內(nèi)核緩沖區(qū)拷貝到協(xié)議引擎中去任斋,這樣就避免了最后一次數(shù)據(jù)拷貝继阻。
圖 4. 帶有 DMA 收集拷貝功能的 sendfile
通過這種方法,CPU 在數(shù)據(jù)傳輸?shù)倪^程中不但避免了數(shù)據(jù)拷貝操作废酷,理論上瘟檩,CPU 也永遠不會跟傳輸?shù)臄?shù)據(jù)有任何關(guān)聯(lián),這對于 CPU 的性能來說起到了積極的作用:首先澈蟆,高速緩沖存儲器沒有受到污染墨辛;其次,高速緩沖存儲器的一致性不需要維護丰介,高速緩沖存儲器在 DMA 進行數(shù)據(jù)傳輸前或者傳輸后不需要被刷新背蟆。然而實際上,后者實現(xiàn)起來非常困難哮幢。源緩沖區(qū)有可能是頁緩存的一部分带膀,這也就是說一般的讀操作可以訪問它,而且該訪問也可以是通過傳統(tǒng)方式進行的橙垢。只要存儲區(qū)域可以被 CPU 訪問到垛叨,那么高速緩沖存儲器的一致性就需要通過 DMA 傳輸之前沖刷新高速緩沖存儲器來維護。而且柜某,這種數(shù)據(jù)收集拷貝功能的實現(xiàn)是需要硬件以及設(shè)備驅(qū)動程序支持的嗽元。
splice()
splice() 是 Linux 中與 mmap() 和 sendfile() 類似的一種方法。它也可以用于用戶應用程序地址空間和操作系統(tǒng)地址空間之間的數(shù)據(jù)傳輸喂击。splice() 適用于可以確定數(shù)據(jù)傳輸路徑的用戶應用程序剂癌,它不需要利用用戶地址空間的緩沖區(qū)進行顯式的數(shù)據(jù)傳輸操作。那么翰绊,當數(shù)據(jù)只是從一個地方傳送到另一個地方佩谷,過程中所傳輸?shù)臄?shù)據(jù)不需要經(jīng)過用戶應用程序的處理的時候,spice() 就成為了一種比較好的選擇监嗜。splice() 可以在操作系統(tǒng)地址空間中整塊地移動數(shù)據(jù)谐檀,從而減少大多數(shù)數(shù)據(jù)拷貝操作。而且裁奇,splice() 進行數(shù)據(jù)傳輸可以通過異步的方式來進行桐猬,用戶應用程序可以先從系統(tǒng)調(diào)用返回,而操作系統(tǒng)內(nèi)核進程會控制數(shù)據(jù)傳輸過程繼續(xù)進行下去刽肠。splice() 可以被看成是類似于基于流的管道的實現(xiàn)溃肪,管道可以使得兩個文件描述符相互連接,splice 的調(diào)用者則可以控制兩個設(shè)備(或者協(xié)議棧)在操作系統(tǒng)內(nèi)核中的相互連接音五。
splice() 系統(tǒng)調(diào)用和 sendfile() 非常類似乍惊,用戶應用程序必須擁有兩個已經(jīng)打開的文件描述符,一個用于表示輸入設(shè)備放仗,一個用于表示輸出設(shè)備润绎。與 sendfile() 不同的是,splice() 允許任意兩個文件之間互相連接诞挨,而并不只是文件到 socket 進行數(shù)據(jù)傳輸莉撇。對于從一個文件描述符發(fā)送數(shù)據(jù)到 socket 這種特例來說,一直都是使用 sendfile() 這個系統(tǒng)調(diào)用惶傻,而 splice 一直以來就只是一種機制棍郎,它并不僅限于 sendfile() 的功能。也就是說银室,sendfile() 只是 splice() 的一個子集涂佃,在 Linux 2.6.23 中励翼,sendfile() 這種機制的實現(xiàn)已經(jīng)沒有了,但是這個 API 以及相應的功能還存在辜荠,只不過 API 以及相應的功能是利用了 splice() 這種機制來實現(xiàn)的汽抚。
在數(shù)據(jù)傳輸?shù)倪^程中,splice() 機制交替地發(fā)送相關(guān)的文件描述符的讀寫操作造烁,并且可以將讀緩沖區(qū)重新用于寫操作午笛。它也利用了一種簡單的流控制,通過預先定義的水右┗恰( watermark )來阻塞寫請求告组。有實驗表明癌佩,利用這種方法將數(shù)據(jù)從一個磁盤傳輸?shù)搅硪粋€磁盤會增加 30% 到 70% 的吞吐量,數(shù)據(jù)傳輸?shù)倪^程中驼卖, CPU 的負載也會減少一半氨肌。
Linux 2.6.17 內(nèi)核引入了 splice() 系統(tǒng)調(diào)用酌畜,但是,這個概念在此之前 ] 其實已經(jīng)存在了很長一段時間了桥胞。1988 年贩虾,Larry McVoy 提出了這個概念,它被看成是一種改進服務器端系統(tǒng)的 I/O 性能的一種技術(shù)伊群,盡管在之后的若干年中經(jīng)常被提及策精,但是 splice 系統(tǒng)調(diào)用從來沒有在主流的 Linux 操作系統(tǒng)內(nèi)核中實現(xiàn)過,一直到 Linux 2.6.17 版本的出現(xiàn)丸卷。splice 系統(tǒng)調(diào)用需要用到四個參數(shù)谜嫉,其中兩個是文件描述符,一個表示文件長度哆档,還有一個用于控制如何進行數(shù)據(jù)拷貝僧鲁。splice 系統(tǒng)調(diào)用可以同步實現(xiàn)象泵,也可以使用異步方式來實現(xiàn)寞秃。在使用異步方式的時候,用戶應用程序會通過信號 SIGIO 來獲知數(shù)據(jù)傳輸已經(jīng)終止春寿。splice() 系統(tǒng)調(diào)用的接口如下所示:
long splice(int fdin, int fdout, size_t len, unsigned int flags);
調(diào)用 splice() 系統(tǒng)調(diào)用會導致操作系統(tǒng)內(nèi)核從數(shù)據(jù)源 fdin 移動最多 len 個字節(jié)的數(shù)據(jù)到 fdout 中去忽孽,這個數(shù)據(jù)的移動過程只是經(jīng)過操作系統(tǒng)內(nèi)核空間兄一,需要最少的拷貝次數(shù)出革。使用 splice() 系統(tǒng)調(diào)用需要這兩個文件描述符中的一個必須是用來表示一個管道設(shè)備的。不難看出耳璧,這種設(shè)計具有局限性展箱,Linux 的后續(xù)版本針對這一問題將會有所改進混驰。參數(shù) flags 用于表示拷貝操作的執(zhí)行方法,當前的 flags 有如下這些取值:
- SPLICE_F_NONBLOCK:splice 操作不會被阻塞竞慢。然而筹煮,如果文件描述符沒有被設(shè)置為不可被阻塞方式的 I/O 居夹,那么調(diào)用 splice 有可能仍然被阻塞。
- SPLICE_F_MORE:告知操作系統(tǒng)內(nèi)核下一個 splice 系統(tǒng)調(diào)用將會有更多的數(shù)據(jù)傳來檬洞。
- SPLICE_F_MOVE:如果輸出是文件沟饥,這個值則會使得操作系統(tǒng)內(nèi)核嘗試從輸入管道緩沖區(qū)直接將數(shù)據(jù)讀入到輸出地址空間贤旷,這個數(shù)據(jù)傳輸過程沒有任何數(shù)據(jù)拷貝操作發(fā)生幼驶。
Splice() 系統(tǒng)調(diào)用利用了 Linux 提出的管道緩沖區(qū)( pipe buffer )機制,這就是為什么這個系統(tǒng)調(diào)用的兩個文件描述符參數(shù)中至少有一個必須要指代管道設(shè)備的原因购桑。為了支持 splice 這種機制氏淑,Linux 在用于設(shè)備和文件系統(tǒng)的 file_operations 結(jié)構(gòu)中增加了下邊這兩個定義:
`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);`
這兩個新的操作可以根據(jù) flags 的設(shè)定在 pipe 和 in 或者 out 之間移動 len 個字節(jié)假残。Linux 文件系統(tǒng)已經(jīng)實現(xiàn)了具有上述功能并且可以使用的操作守问,而且還實現(xiàn)了一個 generic_splice_sendpage() 函數(shù)用于和 socket 之間的接合。
對應用程序地址空間和內(nèi)核之間的數(shù)據(jù)傳輸進行優(yōu)化的零拷貝技術(shù)
前面提到的幾種零拷貝技術(shù)都是通過盡量避免用戶應用程序和操作系統(tǒng)內(nèi)核緩沖區(qū)之間的數(shù)據(jù)拷貝來實現(xiàn)的穆端,使用上面那些零拷貝技術(shù)的應用程序通常都要局限于某些特殊的情況:要么不能在操作系統(tǒng)內(nèi)核中處理數(shù)據(jù)体啰,要么不能在用戶地址空間中處理數(shù)據(jù)嗽仪。而這一小節(jié)提出的零拷貝技術(shù)保留了傳統(tǒng)在用戶應用程序地址空間和操作系統(tǒng)內(nèi)核地址空間之間傳遞數(shù)據(jù)的技術(shù)闻坚,但卻在傳輸上進行優(yōu)化。我們知道跨蟹,數(shù)據(jù)在系統(tǒng)軟件和硬件之間的傳遞可以通過 DMA 傳輸來提高效率橘沥,但是對于用戶應用程序和操作系統(tǒng)之間進行數(shù)據(jù)傳輸這種情況來說座咆,并沒有類似的工具可以使用。本節(jié)介紹的技術(shù)就是針對這種情況提出來的堤舒。
利用寫時復制
在某些情況下植酥,Linux 操作系統(tǒng)內(nèi)核中的頁緩存可能會被多個應用程序所共享弦牡,操作系統(tǒng)有可能會將用戶應用程序地址空間緩沖區(qū)中的頁面映射到操作系統(tǒng)內(nèi)核地址空間中去驾锰。如果某個應用程序想要對這共享的數(shù)據(jù)調(diào)用 write() 系統(tǒng)調(diào)用走越,那么它就可能破壞內(nèi)核緩沖區(qū)中的共享數(shù)據(jù)旨指,傳統(tǒng)的 write() 系統(tǒng)調(diào)用并沒有提供任何顯示的加鎖操作谆构,Linux 中引入了寫時復制這樣一種技術(shù)用來保護數(shù)據(jù)。
什么是寫時復制
寫時復制是計算機編程中的一種優(yōu)化策略呵晨,它的基本思想是這樣的:如果有多個應用程序需要同時訪問同一塊數(shù)據(jù)摸屠,那么可以為這些應用程序分配指向這塊數(shù)據(jù)的指針粱哼,在每一個應用程序看來揭措,它們都擁有這塊數(shù)據(jù)的一份數(shù)據(jù)拷貝,當其中一個應用程序需要對自己的這份數(shù)據(jù)拷貝進行修改的時候需纳,就需要將數(shù)據(jù)真正地拷貝到該應用程序的地址空間中去不翩,也就是說口蝠,該應用程序擁有了一份真正的私有數(shù)據(jù)拷貝,這樣做是為了避免該應用程序?qū)@塊數(shù)據(jù)做的更改被其他應用程序看到傲霸。這個過程對于應用程序來說是透明的昙啄,如果應用程序永遠不會對所訪問的這塊數(shù)據(jù)進行任何更改寸五,那么就永遠不需要將數(shù)據(jù)拷貝到應用程序自己的地址空間中去梳杏。這也是寫時復制的最主要的優(yōu)點十性。
寫時復制的實現(xiàn)需要 MMU 的支持,MMU 需要知曉進程地址空間中哪些特殊的頁面是只讀的楷掉,當需要往這些頁面中寫數(shù)據(jù)的時候靖诗,MMU 就會發(fā)出一個異常給操作系統(tǒng)內(nèi)核支示,操作系統(tǒng)內(nèi)核就會分配新的物理存儲空間颂鸿,即將被寫入數(shù)據(jù)的頁面需要與新的物理存儲位置相對應。
寫時復制的最大好處就是可以節(jié)約內(nèi)存浓冒。不過對于操作系統(tǒng)內(nèi)核來說尖坤,寫時復制增加了其處理過程的復雜性慢味。
數(shù)據(jù)傳輸?shù)膶崿F(xiàn)及其局限性
數(shù)據(jù)發(fā)送端
對于數(shù)據(jù)傳輸?shù)陌l(fā)送端來說纯路,實現(xiàn)相對來說是比較簡單的,對與應用程序緩沖區(qū)相關(guān)的物理頁面進行加鎖顶岸,并將這些頁面映射到操作系統(tǒng)內(nèi)核的地址空間辖佣,并標識為“ write only ”凌简。當系統(tǒng)調(diào)用返回的時候恃逻,用戶應用程序和網(wǎng)絡堆棧就都可以讀取該緩沖區(qū)中的數(shù)據(jù)寇损。在操作系統(tǒng)已經(jīng)傳送完所有的數(shù)據(jù)之后矛市,應用程序就可以對這些數(shù)據(jù)進行寫操作诲祸。如果應用程序嘗試在數(shù)據(jù)傳輸完成之前對數(shù)據(jù)進行寫操作救氯,那么就會產(chǎn)生異常着憨,這個時候操作系統(tǒng)就會將數(shù)據(jù)拷貝到應用程序自己的緩沖區(qū)中去,并且重置應用程序端的映射漆改。數(shù)據(jù)傳輸完成之后挫剑,對加鎖的頁面進行解鎖操作樊破,并重置 COW 標識。
數(shù)據(jù)接收端
對于數(shù)據(jù)接收端來說羽氮,該技術(shù)的實現(xiàn)則需要處理復雜得多的情況档押。如果 read() 系統(tǒng)調(diào)用是在數(shù)據(jù)包到達之前發(fā)出的令宿,并且應用程序是被阻塞的粒没,那么 read() 系統(tǒng)調(diào)用就會告知操作系統(tǒng)接收到的數(shù)據(jù)包中的數(shù)據(jù)應該存放到什么地方去簇爆。在這種情況下入蛆,根本沒有必要進行頁面重映射哨毁,網(wǎng)絡接口卡可以提供足夠的支持讓數(shù)據(jù)直接存入用戶應用程序的緩沖區(qū)中去。如果數(shù)據(jù)接收是異步的想幻,在 read() 系統(tǒng)調(diào)用發(fā)出之前脏毯,操作系統(tǒng)不知道該把數(shù)據(jù)寫到哪里抄沮,因為它不知道用戶應用程序緩沖區(qū)的位置叛买,所以操作系統(tǒng)內(nèi)核必須要先把數(shù)據(jù)存放到自己的緩沖區(qū)中去。
局限性
寫時復制技術(shù)有可能會導致操作系統(tǒng)的處理開銷很大.所有相關(guān)的緩沖區(qū)都必須要進行頁對齊處理刻伊,并且使用的 MMU 頁面一定要是整數(shù)個的捶箱。對于發(fā)送端來說丁屎,這不會造成什么問題晨川。但是對于接收端來說删豺,它需要有能力處理更加復雜的情況呀页。首先蓬蝶,數(shù)據(jù)包的尺寸大小要合適疾党,大小需要恰到好處能夠覆蓋一整頁的數(shù)據(jù)惨奕,這就限制了那些 MTU 大小大于系統(tǒng)內(nèi)存頁的網(wǎng)絡梨撞,比如 FDDI 和 ATM卧波。其次港粱,為了在沒有任何中斷的情況下將頁面重映射到數(shù)據(jù)包的流旦签,數(shù)據(jù)包中的數(shù)據(jù)部分必須占用整數(shù)個頁面宁炫。對于異步接收數(shù)據(jù)的情況來說羔巢,為了將數(shù)據(jù)高效地移動到用戶地址空間中去竿秆,可以使用這樣一種方法:利用網(wǎng)絡接口卡的支持幽钢,傳來的數(shù)據(jù)包可以被分割成包頭和數(shù)據(jù)兩部分搅吁,數(shù)據(jù)被存放在一個單獨的緩沖區(qū)內(nèi)谎懦,虛擬存儲系統(tǒng)然后就會將數(shù)據(jù)映射到用戶地址空間緩沖區(qū)去溃斋。使用這種方法需要滿足兩個先決條件梗劫,也就是上面提到過的:一是應用程序緩沖區(qū)必須是頁對齊的梳侨,并且在虛擬存儲上是連續(xù)的走哺;二是傳來的數(shù)據(jù)有一頁大小的時候才可以對數(shù)據(jù)包進行分割丙躏。事實上晒旅,這兩個先決條件是很難滿足的。如果應用程序緩沖區(qū)不是頁對齊的谈秫,或者數(shù)據(jù)包的大小超過一個頁拟烫,那么數(shù)據(jù)就需要被拷貝构灸。對于數(shù)據(jù)發(fā)送端來說喜颁,就算數(shù)據(jù)在傳輸?shù)倪^程中對于應用程序來說是寫保護的半开,應用程序仍然需要避免使用這些忙緩沖區(qū)寂拆,這是因為寫時拷貝操作所帶來的開銷是很大的。如果沒有端到端這一級別的通知鬓长,那么應用程序很難會知道某緩沖區(qū)是否已經(jīng)被釋放還是仍然在被占用尝江。
這種零拷貝技術(shù)比較適用于那種寫時復制事件發(fā)生比較少的情況涉波,因為寫時復制事件所產(chǎn)生的開銷要遠遠高于一次 CPU 拷貝所產(chǎn)生的開銷。實際情況中炭序,大多數(shù)應用程序通常都會多次重復使用相同的緩沖區(qū)啤覆,所以,一次使用完數(shù)據(jù)之后惭聂,不要從操作系統(tǒng)地址空間解除頁面的映射窗声,這樣會提高效率”棵伲考慮到同樣的頁面可能會被再次訪問,所以保留頁面的映射可以節(jié)省管理開銷侨歉,但是屋摇,這種映射保留不會減少由于頁表往返移動和 TLB 沖刷所帶來的開銷,這是因為每次頁面由于寫時復制而進行加鎖或者解鎖的時候幽邓,頁面的只讀標志都要被更改。
緩沖區(qū)共享
還有另外一種利用預先映射機制的共享緩沖區(qū)的方法也可以在應用程序地址空間和操作系統(tǒng)內(nèi)核之間快速傳輸數(shù)據(jù)火脉。采用緩沖區(qū)共享這種思想的架構(gòu)最先在 Solaris 上實現(xiàn)牵舵,該架構(gòu)使用了“ fbufs ”這個概念柒啤。這種方法需要修改 API。應用程序地址空間和操作系統(tǒng)內(nèi)核地址空間之間的數(shù)據(jù)傳遞需要嚴格按照 fbufs 體系結(jié)構(gòu)來實現(xiàn)畸颅,操作系統(tǒng)內(nèi)核之間的通信也是嚴格按照 fbufs 體系結(jié)構(gòu)來完成的担巩。每一個應用程序都有一個緩沖區(qū)池,這個緩沖區(qū)池被同時映射到用戶地址空間和內(nèi)核地址空間没炒,也可以在必要的時候才創(chuàng)建它們涛癌。通過完成一次虛擬存儲操作來創(chuàng)建緩沖區(qū),fbufs 可以有效地減少由存儲一致性維護所引起的大多數(shù)性能問題送火。該技術(shù)在 Linux 中還停留在實驗階段拳话。
為什么要擴展 Linux I/O API
傳統(tǒng)的 Linux 輸入輸出接口,比如讀和寫系統(tǒng)調(diào)用种吸,都是基于拷貝的弃衍,也就是說,數(shù)據(jù)需要在操作系統(tǒng)內(nèi)核和應用程序定義的緩沖區(qū)之間進行拷貝坚俗。對于讀系統(tǒng)調(diào)用來說镜盯,用戶應用程序呈現(xiàn)給操作系統(tǒng)內(nèi)核一個預先分配好的緩沖區(qū),內(nèi)核必須把讀進來的數(shù)據(jù)放到這個緩沖區(qū)內(nèi)猖败。對于寫系統(tǒng)調(diào)用來說速缆,只要系統(tǒng)調(diào)用返回,用戶應用程序就可以自由重新利用數(shù)據(jù)緩沖區(qū)恩闻。
為了支持上面這種機制激涤,Linux 需要能夠為每一個操作都進行建立和刪除虛擬存儲映射。這種頁面重映射的機制依賴于機器配置判呕、cache 體系結(jié)構(gòu)倦踢、TLB 未命中處理所帶來的開銷以及處理器是單處理器還是多處理器等多種因素。如果能夠避免處理 I/O 請求的時候虛擬存儲 / TLB 操作所產(chǎn)生的開銷侠草,則會極大地提高 I/O 的性能辱挥。fbufs 就是這樣一種機制。使用 fbufs 體系結(jié)構(gòu)就可以避免虛擬存儲操作边涕。由數(shù)據(jù)顯示晤碘,fbufs 這種結(jié)構(gòu)在 DECStation? 5000/200 這個單處理器工作站上會取得比上面提到的頁面重映射方法好得多的性能。如果要使用 fbufs 這種體系結(jié)構(gòu)功蜓,必須要擴展 Linux API园爷,從而實現(xiàn)一種有效而且全面的零拷貝技術(shù)。
快速緩沖區(qū)( Fast Buffers )原理介紹
I/O 數(shù)據(jù)存放在一些被稱作 fbufs 的緩沖區(qū)內(nèi)式撼,每一個這樣的緩沖區(qū)都包含一個或者多個連續(xù)的虛擬存儲頁童社。應用程序訪問 fbuf 是通過保護域來實現(xiàn)的,有如下這兩種方式:
- 如果應用程序分配了 fbuf著隆,那么應用程序就有訪問該 fbuf 的權(quán)限
- 如果應用程序通過 IPC 接收到了 fbuf扰楼,那么應用程序?qū)@個 fbuf 也有訪問的權(quán)限
對于第一種情況來說呀癣,這個保護域被稱作是 fbuf 的“ originator ”;對于后一種情況來說弦赖,這個保護域被稱作是 fbuf 的“ receiver ”项栏。
傳統(tǒng)的 Linux I/O 接口支持數(shù)據(jù)在應用程序地址空間和操作系統(tǒng)內(nèi)核之間交換,這種交換操作導致所有的數(shù)據(jù)都需要進行拷貝蹬竖。如果采用 fbufs 這種方法沼沈,需要交換的是包含數(shù)據(jù)的緩沖區(qū),這樣就消除了多余的拷貝操作币厕。應用程序?qū)?fbuf 傳遞給操作系統(tǒng)內(nèi)核列另,這樣就能減少傳統(tǒng)的 write 系統(tǒng)調(diào)用所產(chǎn)生的數(shù)據(jù)拷貝開銷。同樣的劈榨,應用程序通過 fbuf 來接收數(shù)據(jù)访递,這樣也可以減少傳統(tǒng) read 系統(tǒng)調(diào)用所產(chǎn)生的數(shù)據(jù)拷貝開銷。如下圖所示:
圖 5. Linux I/O API
I/O 子系統(tǒng)或者應用程序都可以通過 fbufs 管理器來分配 fbufs同辣。一旦分配了 fbufs拷姿,這些 fbufs 就可以從程序傳遞到 I/O 子系統(tǒng),或者從 I/O 子系統(tǒng)傳遞到程序旱函。使用完后响巢,這些 fbufs 會被釋放回 fbufs 緩沖區(qū)池。
fbufs 在實現(xiàn)上有如下這些特性棒妨,如圖 9 所示:
- fbuf 需要從 fbufs 緩沖區(qū)池里分配踪古。每一個 fbuf 都存在一個所屬對象,要么是應用程序券腔,要么是操作系統(tǒng)內(nèi)核伏穆。fbuf 可以在應用程序和操作系統(tǒng)之間進行傳遞,fbuf 使用完之后需要被釋放回特定的 fbufs 緩沖區(qū)池纷纫,在 fbuf 傳遞的過程中它們需要攜帶關(guān)于 fbufs 緩沖區(qū)池的相關(guān)信息枕扫。
- 每一個 fbufs 緩沖區(qū)池都會和一個應用程序相關(guān)聯(lián),一個應用程序最多只能與一個 fbufs 緩沖區(qū)池相關(guān)聯(lián)辱魁。應用程序只有資格訪問它自己的緩沖區(qū)池烟瞧。
- fbufs 不需要虛擬地址重映射,這是因為對于每個應用程序來說染簇,它們可以重新使用相同的緩沖區(qū)集合参滴。這樣,虛擬存儲轉(zhuǎn)換的信息就可以被緩存起來锻弓,虛擬存儲子系統(tǒng)方面的開銷就可以消除砾赔。
- I/O 子系統(tǒng)(設(shè)備驅(qū)動程序,文件系統(tǒng)等)可以分配 fbufs,并將到達的數(shù)據(jù)直接放到這些 fbuf 里邊过蹂。這樣十绑,緩沖區(qū)之間的拷貝操作就可以避免聚至。
圖 6. fbufs 體系結(jié)構(gòu)
前面提到酷勺,這種方法需要修改 API,如果要使用 fbufs 體系結(jié)構(gòu)扳躬,應用程序和 Linux 操作系統(tǒng)內(nèi)核驅(qū)動程序都需要使用新的 API脆诉,如果應用程序要發(fā)送數(shù)據(jù),那么它就要從緩沖區(qū)池里獲取一個 fbuf贷币,將數(shù)據(jù)填充進去击胜,然后通過文件描述符將數(shù)據(jù)發(fā)送出去。接收到的 fbufs 可以被應用程序保留一段時間役纹,之后偶摔,應用程序可以使用它繼續(xù)發(fā)送其他的數(shù)據(jù),或者還給緩沖區(qū)池促脉。但是辰斋,在某些情況下,需要對數(shù)據(jù)包內(nèi)的數(shù)據(jù)進行重新組裝瘸味,那么通過 fbuf 接收到數(shù)據(jù)的應用程序就需要將數(shù)據(jù)拷貝到另外一個緩沖區(qū)內(nèi)宫仗。再者,應用程序不能對當前正在被內(nèi)核處理的數(shù)據(jù)進行修改旁仿,基于這一點藕夫,fbufs 體系結(jié)構(gòu)引入了強制鎖的概念以保證其實現(xiàn)。對于應用程序來說枯冈,如果 fbufs 已經(jīng)被發(fā)送給操作系統(tǒng)內(nèi)核毅贮,那么應用程序就不會再處理這些 fbufs。
fbufs 存在的一些問題
管理共享緩沖區(qū)池需要應用程序尘奏、網(wǎng)絡軟件滩褥、以及設(shè)備驅(qū)動程序之間的緊密合作。對于數(shù)據(jù)接收端來說罪既,網(wǎng)絡硬件必須要能夠?qū)⒌竭_的數(shù)據(jù)包利用 DMA 傳輸?shù)接山邮斩朔峙涞恼_的存儲緩沖區(qū)池中去铸题。而且,應用程序稍微不注意就會更改之前發(fā)到共享存儲中的數(shù)據(jù)的內(nèi)容琢感,從而導致數(shù)據(jù)被破壞丢间,但是這種問題在應用程序端是很難調(diào)試的。同時驹针,共享存儲這種模型很難與其他類型的存儲對象關(guān)聯(lián)使用烘挫,但是應用程序、網(wǎng)絡軟件以及設(shè)備驅(qū)動程序之間的緊密合作是需要其他存儲管理器的支持的。對于共享緩沖區(qū)這種技術(shù)來說饮六,雖然這種技術(shù)看起來前景光明其垄,但是這種技術(shù)不但需要對 API 進行更改,而且需要對驅(qū)動程序也進行更改卤橄,并且這種技術(shù)本身也存在一些未解決的問題绿满,這就使得這種技術(shù)目前還只是出于試驗階段。在測試系統(tǒng)中窟扑,這種技術(shù)在性能上有很大的改進喇颁,不過這種新的架構(gòu)的整體安裝目前看起來還是不可行的。這種預先分配共享緩沖區(qū)的機制有時也因為粒度問題需要將數(shù)據(jù)拷貝到另外一個緩沖區(qū)中去嚎货。
總結(jié)
本系列文章介紹了 Linux 中的零拷貝技術(shù)橘霎,本文是其中的第二部分。本文對第一部分文章中提出的 Linux 操作系統(tǒng)上出現(xiàn)的幾種零拷貝技術(shù)進行了更詳細的介紹殖属,主要描述了它們各自的優(yōu)點姐叁,缺點以及適用場景。對于網(wǎng)絡數(shù)據(jù)傳輸來說洗显,零拷貝技術(shù)的應用受到了很多體系結(jié)構(gòu)方面因素的阻礙外潜,包括虛擬存儲體系結(jié)構(gòu)以及網(wǎng)絡協(xié)議體系結(jié)構(gòu)等。所以墙懂,零拷貝技術(shù)仍然只是在某些很特殊的情況中才可以應用橡卤,比如文件服務或者使用某種特殊的協(xié)議進行高帶寬的通信等。但是损搬,零拷貝技術(shù)在磁盤操作中的應用的可行性就高得多了碧库,這很可能是因為磁盤操作具有同步的特點,以及數(shù)據(jù)傳輸單元是按照頁的粒度來進行的巧勤。
針對 Linux 操作系統(tǒng)平臺提出并實現(xiàn)了很多種零拷貝技術(shù)嵌灰,但是并不是所有這些零拷貝技術(shù)都被廣泛應用于現(xiàn)實中的操作系統(tǒng)中的。比如颅悉,fbufs 體系結(jié)構(gòu)沽瞭,它在很多方面看起來都很吸引人,但是使用它需要更改 API 以及驅(qū)動程序剩瓶,它還存在其他一些實現(xiàn)上的困難驹溃,這就使得 fbufs 還只是停留在實驗的階段。動態(tài)地址重映射技術(shù)只是需要對操作系統(tǒng)做少量修改延曙,雖然不需要修改用戶軟件豌鹤,但是當前的虛擬存儲體系結(jié)構(gòu)并不能很好地支持頻繁的虛擬地址重映射操作。而且為了保證存儲的一致性枝缔,重映射之后還必須對 TLB 和一級緩存進行刷新布疙。事實上,利用地址重映射實現(xiàn)的零拷貝技術(shù)適用的范圍是很小的,這是因為虛擬存儲操作所帶來的開銷往往要比 CPU 拷貝所產(chǎn)生的開銷還要大灵临。此外截型,為了完全消除 CPU 訪問存儲,通常都需要額外的硬件來支持儒溉,而這種硬件的支持并不是很普及宦焦,同時也是非常昂貴的。
本系列文章的目的是想幫助讀者理清這些出現(xiàn)在 Linux 操作系統(tǒng)中的零拷貝技術(shù)都是從何種角度來幫助改善數(shù)據(jù)傳輸過程中遇到的性能問題的睁搭。關(guān)于各種零拷貝技術(shù)的具體實現(xiàn)細節(jié)赶诊,本系列文章沒有做詳細描述笼平。同時意敛,零拷貝技術(shù)一直是在不斷地發(fā)展和完善當中的乾戏,本系列文章并沒有涵蓋 Linux 上出現(xiàn)的所有零拷貝技術(shù)。