Linux零拷貝技術(shù),看完這篇文章就懂了
本文講解 Linux 的零拷貝技術(shù),云計算是一門很龐大的技術(shù)學(xué)科,融合了很多技術(shù),Linux 算是比較基礎(chǔ)的技術(shù),所以,學(xué)好 Linux 對于云計算的學(xué)習(xí)會有比較大的幫助。
本文借鑒并總結(jié)了幾種比較常見的 Linux 下的零拷貝技術(shù),相關(guān)的引用鏈接見文后,大家如果覺得本文總結(jié)得太抽象,可以轉(zhuǎn)到鏈接看詳細(xì)解釋。
為什么需要零拷貝
傳統(tǒng)的 Linux 系統(tǒng)的標(biāo)準(zhǔn) I/O 接口(read、write)是基于數(shù)據(jù)拷貝的,也就是數(shù)據(jù)都是 copy_to_user 或者 copy_from_user,這樣做的好處是,通過中間緩存的機(jī)制,減少磁盤 I/O 的操作,但是壞處也很明顯,大量數(shù)據(jù)的拷貝,用戶態(tài)和內(nèi)核態(tài)的頻繁切換,會消耗大量的 CPU 資源,嚴(yán)重影響數(shù)據(jù)傳輸?shù)男阅埽袛?shù)據(jù)表明,在Linux內(nèi)核協(xié)議棧中,這個拷貝的耗時甚至占到了數(shù)據(jù)包整個處理流程的57.1%。
什么是零拷貝
零拷貝就是這個問題的一個解決方案,通過盡量避免拷貝操作來緩解 CPU 的壓力。Linux 下常見的零拷貝技術(shù)可以分為兩大類:一是針對特定場景,去掉不必要的拷貝;二是去優(yōu)化整個拷貝的過程。由此看來,零拷貝并沒有真正做到“0”拷貝,它更多是一種思想,很多的零拷貝技術(shù)都是基于這個思想去做的優(yōu)化。
零拷貝的幾種方法
原始數(shù)據(jù)拷貝操作
在介紹之前,先看看 Linux 原始的數(shù)據(jù)拷貝操作是怎樣的。如下圖,假如一個應(yīng)用需要從某個磁盤文件中讀取內(nèi)容通過網(wǎng)絡(luò)發(fā)出去,像這樣:
- while((n = read(diskfd, buf, BUF_SIZE)) > 0)
- write(sockfd, buf , n);
那么整個過程就需要經(jīng)歷:1)read 將數(shù)據(jù)從磁盤文件通過 DMA 等方式拷貝到內(nèi)核開辟的緩沖區(qū);2)數(shù)據(jù)從內(nèi)核緩沖區(qū)復(fù)制到用戶態(tài)緩沖區(qū);3)write 將數(shù)據(jù)從用戶態(tài)緩沖區(qū)復(fù)制到內(nèi)核協(xié)議棧開辟的 socket 緩沖區(qū);4)數(shù)據(jù)從 socket 緩沖區(qū)通過 DMA 拷貝到網(wǎng)卡上發(fā)出去。
可見,整個過程發(fā)生了至少四次數(shù)據(jù)拷貝,其中兩次是 DMA 與硬件通訊來完成,CPU 不直接參與,去掉這兩次,仍然有兩次 CPU 數(shù)據(jù)拷貝操作。
方法一:用戶態(tài)直接 I/O
這種方法可以使應(yīng)用程序或者運(yùn)行在用戶態(tài)下的庫函數(shù)直接訪問硬件設(shè)備,數(shù)據(jù)直接跨過內(nèi)核進(jìn)行傳輸,內(nèi)核在整個數(shù)據(jù)傳輸過程除了會進(jìn)行必要的虛擬存儲配置工作之外,不參與其他任何工作,這種方式能夠直接繞過內(nèi)核,極大提高了性能。
缺陷:
1)這種方法只能適用于那些不需要內(nèi)核緩沖區(qū)處理的應(yīng)用程序,這些應(yīng)用程序通常在進(jìn)程地址空間有自己的數(shù)據(jù)緩存機(jī)制,稱為自緩存應(yīng)用程序,如數(shù)據(jù)庫管理系統(tǒng)就是一個代表。
2)這種方法直接操作磁盤 I/O,由于 CPU 和磁盤 I/O 之間的執(zhí)行時間差距,會造成資源的浪費(fèi),解決這個問題需要和異步 I/O 結(jié)合使用。
方法二:mmap
這種方法,使用 mmap 來代替 read,可以減少一次拷貝操作,如下:
- buf = mmap(diskfd, len);
- write(sockfd, buf, len);
應(yīng)用程序調(diào)用 mmap ,磁盤文件中的數(shù)據(jù)通過 DMA 拷貝到內(nèi)核緩沖區(qū),接著操作系統(tǒng)會將這個緩沖區(qū)與應(yīng)用程序共享,這樣就不用往用戶空間拷貝。應(yīng)用程序調(diào)用write ,操作系統(tǒng)直接將數(shù)據(jù)從內(nèi)核緩沖區(qū)拷貝到 socket 緩沖區(qū),最后再通過 DMA 拷貝到網(wǎng)卡發(fā)出去。
缺陷:
1)mmap 隱藏著一個陷阱,當(dāng) mmap 一個文件時,如果這個文件被另一個進(jìn)程所截獲,那么 write 系統(tǒng)調(diào)用會因?yàn)樵L問非法地址被 SIGBUS 信號終止,SIGBUS 默認(rèn)會殺死進(jìn)程并產(chǎn)生一個 coredump,如果服務(wù)器被這樣終止了,那損失就可能不小了。
解決這個問題通常使用文件的租借鎖:首先為文件申請一個租借鎖,當(dāng)其他進(jìn)程想要截斷這個文件時,內(nèi)核會發(fā)送一個實(shí)時的 RT_SIGNAL_LEASE 信號,告訴當(dāng)前進(jìn)程有進(jìn)程在試圖破壞文件,這樣 write 在被 SIGBUS 殺死之前,會被中斷,返回已經(jīng)寫入的字節(jié)數(shù),并設(shè)置 errno 為 success。
通常的做法是在 mmap 之前加鎖,操作完之后解鎖:
方法三:sendfile
從Linux 2.1版內(nèi)核開始,Linux引入了sendfile,也能減少一次拷貝。
- #include<sys/sendfile.h>
- ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);
sendfile 是只發(fā)生在內(nèi)核態(tài)的數(shù)據(jù)傳輸接口,沒有用戶態(tài)的參與,自然避免了用戶態(tài)數(shù)據(jù)拷貝。它指定在 in_fd 和 out_fd 之間傳輸數(shù)據(jù),其中,它規(guī)定 in_fd 指向的文件必須是可以 mmap 的,out_fd 必須指向一個套接字,也就是規(guī)定數(shù)據(jù)只能從文件傳輸?shù)教捉幼?,反之則不行。sendfile 不存在像 mmap 時文件被截獲的情況,它自帶異常處理機(jī)制。
缺陷:
1)只能適用于那些不需要用戶態(tài)處理的應(yīng)用程序。
方法四:DMA 輔助的 sendfile
常規(guī) sendfile 還有一次內(nèi)核態(tài)的拷貝操作,能不能也把這次拷貝給去掉呢?
答案就是這種 DMA 輔助的 sendfile。
這種方法借助硬件的幫助,在數(shù)據(jù)從內(nèi)核緩沖區(qū)到 socket 緩沖區(qū)這一步操作上,并不是拷貝數(shù)據(jù),而是拷貝緩沖區(qū)描述符,待完成后,DMA 引擎直接將數(shù)據(jù)從內(nèi)核緩沖區(qū)拷貝到協(xié)議引擎中去,避免了最后一次拷貝。
缺陷:
1)除了3.4 中的缺陷,還需要硬件以及驅(qū)動程序支持。
2)只適用于將數(shù)據(jù)從文件拷貝到套接字上。
方法五:splice
splice 去掉 sendfile 的使用范圍限制,可以用于任意兩個文件描述符中傳輸數(shù)據(jù)。
- #define _GNU_SOURCE /* See feature_test_macros(7) */
- #include <fcntl.h>
- ssize_t splice(int fd_in, loff_t *off_in, int fd_out, loff_t *off_out, size_t len, unsigned int flags);
但是 splice 也有局限,它使用了 Linux 的管道緩沖機(jī)制,所以,它的兩個文件描述符參數(shù)中至少有一個必須是管道設(shè)備。
splice 提供了一種流控制的機(jī)制,通過預(yù)先定義的水印(watermark)來阻塞寫請求,有實(shí)驗(yàn)表明,利用這種方法將數(shù)據(jù)從一個磁盤傳輸?shù)搅硗庖粋€磁盤會增加 30%-70% 的吞吐量,CPU負(fù)責(zé)也會減少一半。
缺陷:
1)同樣只適用于不需要用戶態(tài)處理的程序
2)傳輸描述符至少有一個是管道設(shè)備。
方法六:寫時復(fù)制
在某些情況下,內(nèi)核緩沖區(qū)可能被多個進(jìn)程所共享,如果某個進(jìn)程想要這個共享區(qū)進(jìn)行 write 操作,由于 write 不提供任何的鎖操作,那么就會對共享區(qū)中的數(shù)據(jù)造成破壞,寫時復(fù)制就是 Linux 引入來保護(hù)數(shù)據(jù)的。
寫時復(fù)制,就是當(dāng)多個進(jìn)程共享同一塊數(shù)據(jù)時,如果其中一個進(jìn)程需要對這份數(shù)據(jù)進(jìn)行修改,那么就需要將其拷貝到自己的進(jìn)程地址空間中,這樣做并不影響其他進(jìn)程對這塊數(shù)據(jù)的操作,每個進(jìn)程要修改的時候才會進(jìn)行拷貝,所以叫寫時拷貝。這種方法在某種程度上能夠降低系統(tǒng)開銷,如果某個進(jìn)程永遠(yuǎn)不會對所訪問的數(shù)據(jù)進(jìn)行更改,那么也就永遠(yuǎn)不需要拷貝。
缺陷:
需要 MMU 的支持,MMU 需要知道進(jìn)程地址空間中哪些頁面是只讀的,當(dāng)需要往這些頁面寫數(shù)據(jù)時,發(fā)出一個異常給操作系統(tǒng)內(nèi)核,內(nèi)核會分配新的存儲空間來供寫入的需求。
方法七:緩沖區(qū)共享
這種方法完全改寫 I/O 操作,因?yàn)閭鹘y(tǒng) I/O 接口都是基于數(shù)據(jù)拷貝的,要避免拷貝,就去掉原先的那套接口,重新改寫,所以這種方法是比較全面的零拷貝技術(shù),目前比較成熟的一個方案是最先在 Solaris 上實(shí)現(xiàn)的 fbuf (Fast Buffer,快速緩沖區(qū))。
Fbuf 的思想是每個進(jìn)程都維護(hù)著一個緩沖區(qū)池,這個緩沖區(qū)池能被同時映射到程序地址空間和內(nèi)核地址空間,內(nèi)核和用戶共享這個緩沖區(qū)池,這樣就避免了拷貝。
缺陷:
1)管理共享緩沖區(qū)池需要應(yīng)用程序、網(wǎng)絡(luò)軟件、以及設(shè)備驅(qū)動程序之間的緊密合作
2)改寫 API ,尚處于試驗(yàn)階段。
高性能網(wǎng)絡(luò) I/O 框架——netmap
Netmap 基于共享內(nèi)存的思想,是一個高性能收發(fā)原始數(shù)據(jù)包的框架,由Luigi Rizzo 等人開發(fā)完成,其包含了內(nèi)核模塊以及用戶態(tài)庫函數(shù)。其目標(biāo)是,不修改現(xiàn)有操作系統(tǒng)軟件以及不需要特殊硬件支持,實(shí)現(xiàn)用戶態(tài)和網(wǎng)卡之間數(shù)據(jù)包的高性能傳遞。
在 Netmap 框架下,內(nèi)核擁有數(shù)據(jù)包池,發(fā)送環(huán)\接收環(huán)上的數(shù)據(jù)包不需要動態(tài)申請,有數(shù)據(jù)到達(dá)網(wǎng)卡時,當(dāng)有數(shù)據(jù)到達(dá)后,直接從數(shù)據(jù)包池中取出一個數(shù)據(jù)包,然后將數(shù)據(jù)放入此數(shù)據(jù)包中,再將數(shù)據(jù)包的描述符放入接收環(huán)中。內(nèi)核中的數(shù)據(jù)包池,通過 mmap 技術(shù)映射到用戶空間。用戶態(tài)程序最終通過 netmap_if 獲取接收發(fā)送環(huán) netmap_ring,進(jìn)行數(shù)據(jù)包的獲取發(fā)送。
總結(jié)
1、零拷貝本質(zhì)上體現(xiàn)了一種優(yōu)化的思想
2、直接 I/O,mmap,sendfile,DMA sendfile,splice,緩沖區(qū)共享,寫時復(fù)制……