[轉(zhuǎn)載]https://baijiahao.baidu.com/s?id=1648595456047501430&wfr=spider&for=pc
高頻交易的研究者有時(shí)會(huì)遇到“零拷貝”(zero-copy)技術(shù)這個(gè)名詞,那么什么是“零拷貝”呢?
一般的文件傳輸過(guò)程
考慮這樣一種常用的情形:開(kāi)發(fā)者需要將靜態(tài)內(nèi)容(類似圖片、數(shù)據(jù)表、文件)展示給遠(yuǎn)程的用戶。那么這個(gè)情形就意味著開(kāi)發(fā)者需要先將靜態(tài)內(nèi)容從磁盤(pán)中拷貝出來(lái)放到一個(gè)內(nèi)存buf中,然后將這個(gè)buf通過(guò)socket傳輸給用戶,進(jìn)而用戶或者靜態(tài)內(nèi)容的展示。這看起來(lái)再正常不過(guò)了,但是實(shí)際上這是很低效的流程,我們把上面的這種情形抽象成下面的過(guò)程:
首先調(diào)用read將靜態(tài)內(nèi)容,這里假設(shè)為數(shù)據(jù)文件A,讀取到tmp_buf, 然后調(diào)用write將tmp_buf寫(xiě)入到socket中,如圖:
在這個(gè)過(guò)程中數(shù)據(jù)文件A的經(jīng)歷了4次復(fù)制的過(guò)程:
首先,調(diào)用read時(shí),數(shù)據(jù)文件A拷貝到了kernel模式;之后,CPU控制將kernel模式數(shù)據(jù)復(fù)制到user模式下;調(diào)用write時(shí),先將user模式下的內(nèi)容復(fù)制到到kernel模式下的socket的buffer中;最后將kernel模式下的socket buffer的數(shù)據(jù)復(fù)制到網(wǎng)卡設(shè)備中傳送;
從上面的過(guò)程可以看出,數(shù)據(jù)白白從kernel模式到user模式走了一圈,浪費(fèi)了2次copy(第一次,從kernel模式拷貝到user模式;第二次從user模式再拷貝回kernel模式,即上面4次過(guò)程的第2和3步驟)。而且上面的過(guò)程中kernel和user模式的上下文的切換也是4次。
幸運(yùn)的是,開(kāi)發(fā)者可以用“零拷貝”技術(shù)來(lái)去掉這些無(wú)謂的復(fù)制。應(yīng)用程序用Zero-Copy來(lái)請(qǐng)求kernel直接把disk的data傳輸給socket,而不是通過(guò)應(yīng)用程序傳輸。Zero-Copy大大提高了應(yīng)用程序的性能,并且減少了kernel和user模式上下文的切換。
Linux中的零拷貝
例如,在 Linux 中,減少拷貝次數(shù)的一種方法是調(diào)用 mmap() 來(lái)代替調(diào)用 read,比如:
首先,應(yīng)用程序調(diào)用了 mmap() 之后,數(shù)據(jù)會(huì)先通過(guò) DMA 被復(fù)制到操作系統(tǒng)內(nèi)核的緩沖區(qū)中去。接著,應(yīng)用程序跟操作系統(tǒng)共享這個(gè)緩沖區(qū),這樣,操作系統(tǒng)內(nèi)核和應(yīng)用程序存儲(chǔ)空間就不需要再進(jìn)行任何的數(shù)據(jù)復(fù)制操作。應(yīng)用程序調(diào)用了 write() 之后,操作系統(tǒng)內(nèi)核將數(shù)據(jù)從原來(lái)的內(nèi)核緩沖區(qū)中復(fù)制到與 socket 相關(guān)的內(nèi)核緩沖區(qū)中。接下來(lái),數(shù)據(jù)從內(nèi)核 socket 緩沖區(qū)復(fù)制到協(xié)議引擎中去,這是第三次數(shù)據(jù)拷貝操作。
通過(guò)使用 mmap() 來(lái)代替 read(), 已經(jīng)可以減半操作系統(tǒng)需要進(jìn)行數(shù)據(jù)拷貝的次數(shù)。當(dāng)大量數(shù)據(jù)需要傳輸?shù)臅r(shí)候,這樣做就會(huì)有一個(gè)比較好的效率。但是,這種改進(jìn)也是需要代價(jià)的,使用 mma()p 其實(shí)是存在潛在的問(wèn)題的。當(dāng)對(duì)文件進(jìn)行了內(nèi)存映射,然后調(diào)用 write() 系統(tǒng)調(diào)用,如果此時(shí)其他的進(jìn)程截?cái)嗔诉@個(gè)文件,那么 write() 系統(tǒng)調(diào)用將會(huì)被總線錯(cuò)誤信號(hào) SIGBUS 中斷,因?yàn)榇藭r(shí)正在執(zhí)行的是一個(gè)錯(cuò)誤的存儲(chǔ)訪問(wèn)。這個(gè)信號(hào)將會(huì)導(dǎo)致進(jìn)程被殺死,解決這個(gè)問(wèn)題可以通過(guò)以下這兩種方法:
為 SIGBUS 安裝一個(gè)新的信號(hào)處理器,這樣,write() 系統(tǒng)調(diào)用在它被中斷之前就返回已經(jīng)寫(xiě)入的字節(jié)數(shù)目,errno 會(huì)被設(shè)置成 success。但是這種方法也有其缺點(diǎn),它不能反映出產(chǎn)生這個(gè)問(wèn)題的根源所在,因?yàn)?BIGBUS 信號(hào)只是顯示某進(jìn)程發(fā)生了一些很?chē)?yán)重的錯(cuò)誤。
第二種方法是通過(guò)文件租借鎖來(lái)解決這個(gè)問(wèn)題的,這種方法相對(duì)來(lái)說(shuō)更好一些。我們可以通過(guò)內(nèi)核對(duì)文件加讀或者寫(xiě)的租借鎖,當(dāng)另外一個(gè)進(jìn)程嘗試對(duì)用戶正在進(jìn)行傳輸?shù)奈募M(jìn)行截?cái)嗟臅r(shí)候,內(nèi)核會(huì)發(fā)送給用戶一個(gè)實(shí)時(shí)信號(hào):RT_SIGNAL_LEASE 信號(hào),這個(gè)信號(hào)會(huì)告訴用戶內(nèi)核破壞了用戶加在那個(gè)文件上的寫(xiě)或者讀租借鎖,那么 write() 系統(tǒng)調(diào)用則會(huì)被中斷,并且進(jìn)程會(huì)被 SIGBUS 信號(hào)殺死,返回值則是中斷前寫(xiě)的字節(jié)數(shù),errno 也會(huì)被設(shè)置為 success。文件租借鎖需要在對(duì)文件進(jìn)行內(nèi)存映射之前設(shè)置。
使用 mmap 是 POSIX 兼容的,但是使用 mmap 并不一定能獲得理想的數(shù)據(jù)傳輸性能。數(shù)據(jù)傳輸?shù)倪^(guò)程中仍然需要一次 CPU 復(fù)制操作,而且映射操作也是一個(gè)開(kāi)銷(xiāo)很大的虛擬存儲(chǔ)操作,這種操作需要通過(guò)更改頁(yè)表以及沖刷 TLB (使得 TLB 的內(nèi)容無(wú)效)來(lái)維持存儲(chǔ)的一致性。但是,因?yàn)橛成渫ǔ_m用于較大范圍,所以對(duì)于相同長(zhǎng)度的數(shù)據(jù)來(lái)說(shuō),映射所帶來(lái)的開(kāi)銷(xiāo)遠(yuǎn)遠(yuǎn)低于 CPU 拷貝所帶來(lái)的開(kāi)銷(xiāo)。
sendfile()
為了簡(jiǎn)化用戶接口,同時(shí)還要繼續(xù)保留 mmap()/write() 技術(shù)的優(yōu)點(diǎn):減少 CPU 的復(fù)制次數(shù),Linux 在版本 2.1 中引入了 sendfile() 這個(gè)系統(tǒng)調(diào)用。
sendfile() 不僅減少了數(shù)據(jù)復(fù)制操作,它也減少了上下文切換。首先:sendfile() 系統(tǒng)調(diào)用利用 DMA 引擎將文件中的數(shù)據(jù)復(fù)制到操作系統(tǒng)內(nèi)核緩沖區(qū)中,然后數(shù)據(jù)被復(fù)制到與 socket 相關(guān)的內(nèi)核緩沖區(qū)中去。接下來(lái),DMA 引擎將數(shù)據(jù)從內(nèi)核 socket 緩沖區(qū)中復(fù)制到協(xié)議引擎中去。如果在用戶調(diào)用 sendfile () 系統(tǒng)調(diào)用進(jìn)行數(shù)據(jù)傳輸?shù)倪^(guò)程中有其他進(jìn)程截?cái)嗔嗽撐募敲?sendfile () 系統(tǒng)調(diào)用會(huì)簡(jiǎn)單地返回給用戶應(yīng)用程序中斷前所傳輸?shù)淖止?jié)數(shù),errno 會(huì)被設(shè)置為 success。如果在調(diào)用 sendfile() 之前操作系統(tǒng)對(duì)文件加上了租借鎖,那么 sendfile() 的操作和返回狀態(tài)將會(huì)和 mmap()/write () 一樣。
sendfile() 系統(tǒng)調(diào)用不需要將數(shù)據(jù)拷貝或者映射到應(yīng)用程序地址空間中去,所以 sendfile() 只是適用于應(yīng)用程序地址空間不需要對(duì)所訪問(wèn)數(shù)據(jù)進(jìn)行處理的情況。相對(duì)于 mmap() 方法來(lái)說(shuō),因?yàn)?sendfile 傳輸?shù)臄?shù)據(jù)沒(méi)有越過(guò)用戶應(yīng)用程序 / 操作系統(tǒng)內(nèi)核的邊界線,所以 sendfile () 也極大地減少了存儲(chǔ)管理的開(kāi)銷(xiāo)。但是,sendfile () 也有很多局限性,如下所列:
sendfile() 局限于基于文件服務(wù)的網(wǎng)絡(luò)應(yīng)用程序,比如 web 服務(wù)器。據(jù)說(shuō),在 Linux 內(nèi)核中實(shí)現(xiàn) sendfile() 只是為了在其他平臺(tái)上使用 sendfile() 的 Apache 程序。
由于網(wǎng)絡(luò)傳輸具有異步性,很難在 sendfile () 系統(tǒng)調(diào)用的接收端進(jìn)行配對(duì)的實(shí)現(xiàn)方式,所以數(shù)據(jù)傳輸?shù)慕邮斩艘话銢](méi)有用到這種技術(shù)。
基于性能的考慮來(lái)說(shuō),sendfile () 仍然需要有一次從文件到 socket 緩沖區(qū)的 CPU 復(fù)制操作,這就導(dǎo)致頁(yè)緩存有可能會(huì)被傳輸?shù)臄?shù)據(jù)所污染。
Python對(duì)“零拷貝”的支持
自從Python 3.3中sendfile系統(tǒng)調(diào)用可用作os.sendfile,python 3.5 為基于socket的應(yīng)用帶來(lái)了更高級(jí)的封裝包socket.socket.sendfile。
例如通過(guò)socket.socket.sendfile來(lái)改進(jìn)大尺寸文件的傳輸速度:
在100次大尺寸文件(4GB數(shù)據(jù)文件)傳輸測(cè)試中,和不使用零拷貝技術(shù)的sock.recv相比,使用零拷貝技術(shù)的socket.sendfile可以減少一半的時(shí)間且顯著提高文件傳輸?shù)姆€(wěn)定性(降低傳輸時(shí)間的標(biāo)準(zhǔn)差)。對(duì)于需要大量傳輸數(shù)據(jù)的量化交易應(yīng)用,這一技術(shù)也能改善交易系統(tǒng)的性能。
聯(lián)系客服