最美情侣中文字幕电影,在线麻豆精品传媒,在线网站高清黄,久久黄色视频

歡迎光臨散文網(wǎng) 會員登陸 & 注冊

解析從Linux零拷貝深入了解Linux-I/O(下)

2023-02-18 17:57 作者:補給站Linux內(nèi)核  | 我要投稿

大文件傳輸場景

零拷貝還是最優(yōu)選嗎

在大文件傳輸?shù)膱鼍跋?,零拷貝技術并不是最優(yōu)選擇;因為在零拷貝的任何一種實現(xiàn)中,都會有「DMA 將數(shù)據(jù)從磁盤拷貝到內(nèi)核緩存區(qū)——Page Cache」這一步,但是,在傳輸大文件(GB 級別的文件)的時候,PageCache 會不起作用,那就白白浪費 DMA 多做的一次數(shù)據(jù)拷貝,造成性能的降低,即使使用了 PageCache 的零拷貝也會損失性能。

這是因為在大文件傳輸場景下,每當用戶訪問這些大文件的時候,內(nèi)核就會把它們載入 PageCache 中,PageCache 空間很快被這些大文件占滿;且由于文件太大,可能某些部分的文件數(shù)據(jù)被再次訪問的概率比較低,這樣就會帶來 2 個問題:

  • PageCache 由于長時間被大文件占據(jù),其他「熱點」的小文件可能就無法充分使用到 PageCache,于是這樣磁盤讀寫的性能就會下降了;

  • PageCache 中的大文件數(shù)據(jù),由于沒有享受到緩存帶來的好處,但卻耗費 DMA 多拷貝到 PageCache 一次。

異步 I/O +direct I/O

那么大文件傳輸場景下我們該選擇什么方案呢?讓我們先來回顧一下我們在文章開頭介紹 DMA 時最早提到過的同步 I/O:

這里的同步?體現(xiàn)在當進程調(diào)用 read 方法讀取文件時,進程實際上會阻塞在 read 方法調(diào)用,因為要等待磁盤數(shù)據(jù)的返回,并且我們當然不希望進程在讀取大文件時被阻塞,對于阻塞的問題,可以用異步 I/O 來解決,即:

它把讀操作分為兩部分:

  • 前半部分,內(nèi)核向磁盤發(fā)起讀請求,但是可以不等待數(shù)據(jù)就位就返回?,于是進程此時可以處理其他任務;

  • 后半部分,當內(nèi)核將磁盤中的數(shù)據(jù)拷貝到進程緩沖區(qū)后,進程將接收到內(nèi)核的通知?,再去處理數(shù)據(jù);

而且,我們可以發(fā)現(xiàn),異步 I/O 并沒有涉及到 PageCache;使用異步 I/O 就意味著要繞開 PageCache,因為填充 PageCache 的過程在內(nèi)核中必須阻塞。

所以異步 I/O 中使用的是direct I/O(對比使用 PageCache 的buffer I/O),這樣才能不阻塞進程,立即返回。

direct I/O 應用場景常見的兩種:

  • 應用程序已經(jīng)實現(xiàn)了磁盤數(shù)據(jù)的緩存,那么可以不需要 PageCache 再次緩存,減少額外的性能損耗。在 MySQL 數(shù)據(jù)庫中,可以通過參數(shù)設置開啟direct I/O,默認是不開啟;

  • 傳輸大文件的時候,由于大文件難以命中 PageCache 緩存,而且會占滿 PageCache 導致「熱點」文件無法充分利用緩存,從而增大了性能開銷,因此,這時應該使用`direct I/O;;

當然,由于direct I/O 繞過了 PageCache,就無法享受內(nèi)核的這兩點的優(yōu)化:

  • 內(nèi)核的 I/O 調(diào)度算法會緩存盡可能多的 I/O 請求在 PageCache 中,最后「合并?」成一個更大的 I/O 請求再發(fā)給磁盤,這樣做是為了減少磁盤的尋址操作;

  • 內(nèi)核也會「預讀?」后續(xù)的 I/O 請求放在 PageCache 中,一樣是為了減少對磁盤的操作;

實際應用中也有類似的配置,在?nginx?中,我們可以用如下配置,來根據(jù)文件的大小來使用不同的方式傳輸:

當文件大小大于 directio 值后,使用「異步 I/O + 直接 I/O」,否則使用「零拷貝技術」。

使用 direct I/O 需要注意的點

首先,貼一下我們的Linus(Linus Torvalds)?O_DIRECT的評價:

"The thing that has always disturbed me about O_DIRECT is that the whole interface is just stupid, and was probably designed by a deranged monkey on some serious mind-controlling substances." —Linus

一般來說能引得Linus開罵的東西,那是一定有很多坑的。

在 Linux 的man page中我們可以看到O_DIRECT?下有一個 Note,還挺長的,這里我就不貼出來了。

總結一下其中需要注意的點如下:


【文章福利】小編推薦自己的Linux內(nèi)核技術交流群:【749907784】整理了一些個人覺得比較好的學習書籍、視頻資料共享在群文件里面,有需要的可以自行添加哦!?。。ê曨l教程、電子書、實戰(zhàn)項目及代碼)??


地址對齊限制

O_DIRECT?會帶來強制的地址對齊限制,這個對齊的大小也跟文件系統(tǒng)/存儲介質(zhì)?相關,并且當前沒有不依賴文件系統(tǒng)自身的接口提供指定文件/文件系統(tǒng)是否有這些限制的信息

  • Linux 2.6 以前 總傳輸大小、用戶的對齊緩沖區(qū)起始地址、文件偏移量必須都是邏輯文件系統(tǒng)的數(shù)據(jù)塊?大小的倍數(shù),這里說的數(shù)據(jù)塊(block)是一個邏輯概念,是文件系統(tǒng)捆綁一定數(shù)量的連續(xù)扇區(qū)而來,因此通常稱為 “文件系統(tǒng)邏輯塊”,可通過以下命令獲?。篵lockdev --getss

  • Linux2.6以后對齊的基數(shù)變?yōu)槲锢砩系拇鎯橘|(zhì)的sector size扇區(qū)大小,對應物理存儲介質(zhì)的最小存儲粒度,可通過以下命令獲?。篵lockdev --getpbsz

帶來這個限制的原因也很簡單,內(nèi)存對齊這件小事通常是內(nèi)核來處理的,而O_DIRECT?繞過了內(nèi)核空間,那么內(nèi)核處理的所有事情都需要用戶自己來處理,這里貼一篇詳細解釋。

O_DIRECT 平臺不兼容

這應該是大部分跨平臺應用需要注意到的點,O_DIRECT?本身就是Linux中才有的東西,在語言層面 / 應用層面需要考慮這里的兼容性保證,比如在Windows下其實也有類似的機制FILE_FLAG_NO_BUFFERIN?用法類似,參考微軟的官方文檔;再比如macOS下的F_NOCACHE?雖然類似O_DIRECT?,但實際使用中也有差距(參考這個issue)。

不要并發(fā)地運行 fork 和 O_DIRECT I/O

如果O_DIRECT I/O中使用到的內(nèi)存buffer是一段私有的映射(虛擬內(nèi)存),如任何使用上文中提到過的mmap并以MAP_PRIVATE?flag 聲明的虛擬內(nèi)存,那么相關的O_DIRECT I/O(不管是異步 I/O / 其它子線程中的 I/O)都必須在調(diào)用fork系統(tǒng)調(diào)用前執(zhí)行完畢;否則會造成數(shù)據(jù)污染或產(chǎn)生未定義的行為(實例可參考這個Page)。

以下情況這個限制不存在:

  • 相關的內(nèi)存buffer是使用shmat分配或是使用mmap以MAP_SHARED?flag 聲明的;

  • 相關的內(nèi)存buffer是使用madvise以MADV_DONTFORK?聲明的(注意這種方式下該內(nèi)存buffer在子進程中不可用)。

避免對同一文件混合使用 O_DIRECT 和普通 I/O

在應用層需要避免對同一文件(尤其是對同一文件的相同偏移區(qū)間內(nèi)?)混合使用O_DIRECT和普通I/O;即使我們的文件系統(tǒng)能夠幫我們處理和保證這里的一致性問題?,總體來說整個I/O吞吐量也會比單獨使用某一種I/O方式要小。

同樣的,應用層也要避免對同一文件混合使用direct I/O和mmap。

NFS 協(xié)議下的 O_DIRECT

雖然NFS文件系統(tǒng)就是為了讓用戶像訪問本地文件一樣去訪問網(wǎng)絡文件,但O_DIRECT在NFS文件系統(tǒng)中的表現(xiàn)和本地文件系統(tǒng)不同,比較老版本的內(nèi)核或是魔改過的內(nèi)核可能并不支持這種組合。

這是因為在NFS協(xié)議中并不支持傳遞flag 參數(shù)?到服務器,所以O_DIRECT I/O實際上只繞過了本地客戶端的Page Cache,但服務端/同步客戶端仍然會對這些I/O進行cache。

當客戶端請求服務端進行I/O同步來保證O_DIRECT的同步語義時,一些服務器的性能表現(xiàn)不佳(尤其是當這些I/O很小時);還有一些服務器干脆設置為欺騙客戶端?,直接返回客戶端「數(shù)據(jù)已寫入存儲介質(zhì)?」,這樣就可以一定程度上避免I/O同步帶來的性能損失,但另一方面,當服務端斷電時就無法保證未完成I/O同步的數(shù)據(jù)的數(shù)據(jù)完整性?了。

Linux的NFS客戶端也沒有上面說過的地址對齊的限制。

在 Golang 中使用 direct I/O

direct io 必須要滿足 3 種對齊規(guī)則:io 偏移扇區(qū)對齊,長度扇區(qū)對齊,內(nèi)存 buffer 地址扇區(qū)對齊;前兩個還比較好滿足,但是分配的內(nèi)存地址僅憑原生的手段是無法直接達成的。

先對比一下 c 語言,libc 庫是調(diào)用 posix_memalign 直接分配出符合要求的內(nèi)存塊,但Golang中要怎么實現(xiàn)呢?

在Golang中,io 的 buffer 其實就是字節(jié)數(shù)組,自然是用 make 來分配,如下:

但buffer中的data字節(jié)數(shù)組首地址并不一定是對齊的。

方法也很簡單,就是先分配一個比預期要大的內(nèi)存塊,然后在這個內(nèi)存塊里找對齊位置?;這是一個任何語言皆通用的方法,在 Go 里也是可用的。

比如,我現(xiàn)在需要一個 4096 大小的內(nèi)存塊,要求地址按照 512 對齊,可以這樣做:

  • 先分配 4096 + 512 大小的內(nèi)存塊,假設得到的內(nèi)存塊首地址是 p1;

  • 然后在 [ p1, p1+512 ] 這個地址范圍找,一定能找到 512 對齊的地址 p2;

  • 返回 p2 ,用戶能正常使用 [ p2, p2 + 4096 ] 這個范圍的內(nèi)存塊而不越界。

以上就是基本原理了?,具體實現(xiàn)如下:

所以,通過以上 AlignedBlock 函數(shù)分配出來的內(nèi)存一定是 512 地址對齊的,唯一的一點點缺點就是在分配較小內(nèi)存塊時對齊的額外開銷顯得比較大。

開源實現(xiàn)

Github 上就有開源的Golang direct I/O實現(xiàn):ncw/directio

使用也很簡單:

  • O_DIRECT 模式打開文件:

  • 讀數(shù)據(jù)


內(nèi)核緩沖區(qū)和用戶緩沖區(qū)之間的傳輸優(yōu)化

到目前為止,我們討論的 zero-copy技術都是基于減少甚至是避免用戶空間和內(nèi)核空間之間的 CPU 數(shù)據(jù)拷貝的,雖然有一些技術非常高效,但是大多都有適用性很窄的問題,比如 sendfile()、splice() 這些,效率很高,但是都只適用于那些用戶進程不需要再處理數(shù)據(jù)?的場景,比如靜態(tài)文件服務器或者是直接轉(zhuǎn)發(fā)數(shù)據(jù)的代理服務器。

前面提到過的虛擬內(nèi)存機制和mmap等都表明,通過在不同的虛擬地址上重新映射頁面可以實現(xiàn)在用戶進程和內(nèi)核之間虛擬復制和共享內(nèi)存;因此如果要在實現(xiàn)在用戶進程內(nèi)處理數(shù)據(jù)(這種場景比直接轉(zhuǎn)發(fā)數(shù)據(jù)更加常見)之后再發(fā)送出去的話,用戶空間和內(nèi)核空間的數(shù)據(jù)傳輸就是不可避免的?,既然避無可避,那就只能選擇優(yōu)化了。

兩種優(yōu)化用戶空間和內(nèi)核空間數(shù)據(jù)傳輸?shù)募夹g:

  • 動態(tài)重映射與寫時拷貝 (Copy-on-Write)

  • 緩沖區(qū)共享 (Buffer Sharing)

寫時拷貝 (Copy-on-Write)

前面提到過過利用內(nèi)存映射(mmap)來減少數(shù)據(jù)在用戶空間和內(nèi)核空間之間的復制,通常用戶進程是對共享的緩沖區(qū)進行同步阻塞讀寫的,這樣不會有線程安全?問題,但是很明顯這種模式下效率并不高,而提升效率的一種方法就是異步地對共享緩沖區(qū)進行讀寫?,而這樣的話就必須引入保護機制來避免數(shù)據(jù)沖突?問題,COW (Copy on Write) 就是這樣的一種技術。

COW 是一種建立在虛擬內(nèi)存重映射技術之上的技術,因此它需要 MMU 的硬件支持,MMU 會記錄當前哪些內(nèi)存頁被標記成只讀,當有進程嘗試往這些內(nèi)存頁中寫數(shù)據(jù)的時候,MMU 就會拋一個異常給操作系統(tǒng)內(nèi)核,內(nèi)核處理該異常時為該進程分配一份物理內(nèi)存并復制數(shù)據(jù)到此內(nèi)存地址,重新向 MMU 發(fā)出執(zhí)行該進程的寫操作。

下圖為COW在Linux中的應用之一: fork / clone,fork出的子進程共享父進程的物理空間,當父子進程有內(nèi)存寫入操作時?,read-only內(nèi)存頁發(fā)生中斷,將觸發(fā)的異常的內(nèi)存頁復制一份?(其余的頁還是共享父進程的)。

局限性

COW 這種零拷貝技術比較適用于那種多讀少寫從而使得 COW 事件發(fā)生較少的場景?,而在其它場景下反而可能造成負優(yōu)化,因為 COW事件所帶來的系統(tǒng)開銷要遠遠高于一次 CPU 拷貝所產(chǎn)生的。

此外,在實際應用的過程中,為了避免頻繁的內(nèi)存映射,可以重復使用同一段內(nèi)存緩沖區(qū),因此,你不需要在只用過一次共享緩沖區(qū)之后就解除掉內(nèi)存頁的映射關系,而是重復循環(huán)使用,從而提升性能。

但這種內(nèi)存頁映射的持久化并不會減少由于頁表往返移動/換頁和 TLB flush所帶來的系統(tǒng)開銷,因為每次接收到 COW 事件之后對內(nèi)存頁而進行加鎖或者解鎖的時候,內(nèi)存頁的只讀標志 (read-ony) 都要被更改為 (write-only)。

COW 的實際應用

Redis 的持久化機制

Redis 作為典型的內(nèi)存型應用,一定是有內(nèi)核緩沖區(qū)和用戶緩沖區(qū)之間的傳輸優(yōu)化的。

Redis 的持久化機制中,如果采用 bgsave 或者 bgrewriteaof 命令,那么會 fork 一個子進程來將數(shù)據(jù)存到磁盤中;總體來說Redis 的讀操作是比寫操作多的(在正確的使用場景下),因此這種情況下使用 COW 可以減少 fork() 操作的阻塞時間。

語言層面的應用

寫時復制的思想在很多語言中也有應用,相比于傳統(tǒng)的深層復制,能帶來很大性能提升;比如 C++ 98 標準下的 std::string 就采用了寫時復制的實現(xiàn):

Golang中的string, slice也使用了類似的思想,在復制 / 切片等操作時都不會改變底層數(shù)組的指向,變量共享同一個底層數(shù)組,僅當進行append / 修改等操作時才可能進行真正的copy(append時如果超過了當前切片的容量,就需要分配新的內(nèi)存)。

緩沖區(qū)共享 (Buffer Sharing)

從前面的介紹可以看出,傳統(tǒng)的 Linux I/O接口,都是基于復制/拷貝的:數(shù)據(jù)需要在操作系統(tǒng)內(nèi)核空間和用戶空間的緩沖區(qū)之間進行拷貝。在進行 I/O 操作之前,用戶進程需要預先分配好一個內(nèi)存緩沖區(qū),使用 read() 系統(tǒng)調(diào)用時,內(nèi)核會將從存儲器或者網(wǎng)卡等設備讀入的數(shù)據(jù)拷貝到這個用戶緩沖區(qū)里;而使用 write() 系統(tǒng)調(diào)用時,則是把用戶內(nèi)存緩沖區(qū)的數(shù)據(jù)拷貝至內(nèi)核緩沖區(qū)。

為了實現(xiàn)這種傳統(tǒng)的 I/O 模式,Linux 必須要在每一個 I/O 操作時都進行內(nèi)存虛擬映射和解除。這種內(nèi)存頁重映射的機制的效率嚴重受限于緩存體系結構、MMU 地址轉(zhuǎn)換速度和 TLB 命中率。如果能夠避免處理 I/O 請求的虛擬地址轉(zhuǎn)換和 TLB 刷新所帶來的開銷,則有可能極大地提升 I/O 性能。而緩沖區(qū)共享就是用來解決上述問題的一種技術(說實話我覺得有些套娃的味道了)。

操作系統(tǒng)內(nèi)核開發(fā)者們實現(xiàn)了一種叫?fbufs?的緩沖區(qū)共享的框架,也即快速緩沖區(qū)( Fast Buffers ),使用一個 fbuf 緩沖區(qū)作為數(shù)據(jù)傳輸?shù)淖钚挝?,使用這種技術需要調(diào)用新的操作系統(tǒng) API,用戶區(qū)和內(nèi)核區(qū)、內(nèi)核區(qū)之間的數(shù)據(jù)都必須嚴格地在 fbufs 這個體系下進行通信。fbufs 為每一個用戶進程分配一個 buffer pool,里面會儲存預分配 (也可以使用的時候再分配) 好的 buffers,這些 buffers 會被同時映射到用戶內(nèi)存空間和內(nèi)核內(nèi)存空間。fbufs 只需通過一次虛擬內(nèi)存映射操作即可創(chuàng)建緩沖區(qū),有效地消除那些由存儲一致性維護所引發(fā)的大多數(shù)性能損耗。

共享緩沖區(qū)技術的實現(xiàn)需要依賴于用戶進程、操作系統(tǒng)內(nèi)核、以及 I/O 子系統(tǒng) (設備驅(qū)動程序,文件系統(tǒng)等)之間協(xié)同工作?。比如,設計得不好的用戶進程容易就會修改已經(jīng)發(fā)送出去的 fbuf 從而污染數(shù)據(jù),更要命的是這種問題很難 debug。雖然這個技術的設計方案非常精彩,但是它的門檻和限制卻不比前面介紹的其他技術少:首先會對操作系統(tǒng) API 造成變動,需要使用新的一些 API 調(diào)用,其次還需要設備驅(qū)動程序配合改動,還有由于是內(nèi)存共享,內(nèi)核需要很小心謹慎地實現(xiàn)對這部分共享的內(nèi)存進行數(shù)據(jù)保護和同步的機制,而這種并發(fā)的同步機制是非常容易出 bug 的從而又增加了內(nèi)核的代碼復雜度,等等。因此這一類的技術還遠遠沒有到發(fā)展成熟和廣泛應用的階段,目前大多數(shù)的實現(xiàn)都還處于實驗階段?。

總結

從早期的I/O到DMA,解決了阻塞CPU的問題;而為了省去I/O過程中不必要的上下文切換和數(shù)據(jù)拷貝過程,零拷貝技術就出現(xiàn)了。

所謂的零拷貝(Zero-copy)技術,就是完完全全不需要在內(nèi)存層面拷貝數(shù)據(jù),省去CPU搬運數(shù)據(jù)的過程。

零拷貝技術的文件傳輸方式相比傳統(tǒng)文件傳輸?shù)姆绞?,減少了 2 次上下文切換和數(shù)據(jù)拷貝次數(shù),只需要 2 次上下文切換和數(shù)據(jù)拷貝次數(shù),就可以完成文件的傳輸,而且 2 次的數(shù)據(jù)拷貝過程,都不需要通過 CPU,2 次都是由 DMA 來搬運?。

總體來看,零拷貝技術至少可以把文件傳輸?shù)男阅芴岣咭槐兑陨?/strong>?,以下是各方案詳細的成本對比:

零拷貝技術是基于 PageCache 的,PageCache 會緩存最近訪問的數(shù)據(jù),提升了訪問緩存數(shù)據(jù)的性能,同時,為了解決機械硬盤尋址慢的問題,它還協(xié)助 I/O 調(diào)度算法實現(xiàn)了 I/O 合并與預讀,這也是順序讀比隨機讀性能好的原因之一;這些優(yōu)勢,進一步提升了零拷貝的性能。

但當面對大文件傳輸時,不能使用零拷貝,因為可能由于 PageCache 被大文件占據(jù),而導致「熱點」小文件無法利用到 PageCache的問題,并且大文件的緩存命中率不高,這時就需要使用「異步 I/O + direct I/O 」的方式;在使用direct I/O時也需要注意許多的坑點?,畢竟連Linus也會被?O_DIRECT?'disturbed' 到。

而在更廣泛的場景下,我們還需要注意到內(nèi)核緩沖區(qū)和用戶緩沖區(qū)之間的傳輸優(yōu)化?,這種方式側重于在用戶進程的緩沖區(qū)和操作系統(tǒng)的頁緩存之間的 CPU 拷貝的優(yōu)化,延續(xù)了以往那種傳統(tǒng)的通信方式,但更靈活。

I/O相關的各類優(yōu)化自然也已經(jīng)深入到了日常我們接觸到的語言、中間件以及數(shù)據(jù)庫的方方面面,通過了解和學習這些技術和思想,也能對日后自己的程序設計以及性能優(yōu)化上有所啟發(fā)。


原文作者:騰訊技術工程



解析從Linux零拷貝深入了解Linux-I/O(下)的評論 (共 條)

使用qq登录你需要登录后才可以评论。
镇江市| 楚雄市| 南郑县| 井研县| 兴隆县| 神农架林区| 西安市| 嘉荫县| 哈密市| 陈巴尔虎旗| 漳浦县| 东乌| 长治县| 美姑县| 彰武县| 鲁甸县| 邯郸县| 东兰县| 桦甸市| 蓝山县| 绵竹市| 惠州市| 潮州市| 电白县| 株洲市| 宁陵县| 长垣县| 休宁县| 丰顺县| 沾化县| 汉中市| 新余市| 栖霞市| 海丰县| 广饶县| 北流市| 宁海县| 武平县| 合山市| 西吉县| 衡阳市|