零拷貝技術(shù):如何減少 CPU 開銷提升 I/O 性能?
0.引言
在高并發(fā)場景下,傳統(tǒng)的I/O操作需要進行多次數(shù)據(jù)拷貝,很容易成為性能瓶頸。而零拷貝技術(shù)(Zero-Copy)通過消除冗余拷貝,讓系統(tǒng)輕松實現(xiàn)百萬級吞吐(如Kafka)。本文將從內(nèi)核原理、API實現(xiàn)到實戰(zhàn)應用,徹底解析這項關(guān)鍵技術(shù)。
1.零拷貝原理
零拷貝是一種高效的數(shù)據(jù)傳輸技術(shù),其主要目的在于減少數(shù)據(jù)的拷貝次數(shù)來提高性能,接下來我們來看其實現(xiàn)原理。
要理解零拷貝的原理首先要理解傳統(tǒng)的數(shù)據(jù)傳輸過程,也就是我們之前說的read和write流程,我們來梳理一下(以讀取磁盤數(shù)據(jù)通過網(wǎng)絡發(fā)送為例),用戶調(diào)用read系統(tǒng)調(diào)用來請求文件數(shù)據(jù),此時CPU需要切換到內(nèi)核態(tài),然后通過DMA將磁盤數(shù)據(jù)拷貝到內(nèi)核空間中,然后再將內(nèi)核緩沖區(qū)數(shù)據(jù)拷貝到用戶空間緩沖區(qū)中,之后CPU切換回用戶態(tài);對于寫來說,用戶調(diào)用write系統(tǒng)調(diào)用,從用戶態(tài)切換到內(nèi)核態(tài),然后將數(shù)據(jù)從用戶緩沖求拷貝到Socket關(guān)聯(lián)的緩沖區(qū),然后數(shù)據(jù)由DMA傳送只網(wǎng)卡緩沖區(qū),然后返回,從內(nèi)核態(tài)切換至用戶態(tài)。
圖片
有了對傳統(tǒng)數(shù)據(jù)傳輸流程的介紹,我們接下來來看零拷貝的相同操作的流程,用戶進程發(fā)起sendfile系統(tǒng)調(diào)用,提高DMA拷貝將數(shù)據(jù)拷貝到內(nèi)核緩沖區(qū),然后把內(nèi)核緩沖區(qū)數(shù)據(jù)拷貝到網(wǎng)卡,把文件描述信息拷貝到socket緩沖區(qū),然后切換回用戶態(tài)。

有了上面兩個流程的對比,我們可以理解,其實零拷貝(指的是消除了內(nèi)核到用戶空間的數(shù)據(jù)拷貝)就是減少了數(shù)據(jù)的拷貝和用戶-內(nèi)核空間的切換。
2.零拷貝接口實現(xiàn)(代碼均基于Linux 5.10)
2.1 sendfile
2.1.1 函數(shù)定義
sendfile用于在兩個文件描述符之間傳遞數(shù)據(jù)。
#include <sys/sendfile.h>
//out_fd:輸出文件描述符,通常為套接字描述符。
//in_fd:輸入文件描述符,必須是一個支持 mmap() 操作的文件描述符。
//offset:指定從文件的哪個偏移量開始讀取數(shù)據(jù),如果為 NULL,則從當前文件指針位置開始。
//count:要傳輸?shù)臄?shù)據(jù)長度。
//sendfile() 成功返回實際傳輸?shù)淖止?jié)數(shù),失敗返回 -1。
ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);2.1.2 函數(shù)源碼解析
sendfile主要函數(shù)為do_sendfile,而do_sendfile中主要函數(shù)為
splice_direct_to_actor,接下來我們對其進行流程梳理如下,其通過內(nèi)部管道消除了向用戶空間的拷貝。
圖片
2.2 mmap
2.2.1 函數(shù)定義
mmap用于申請一段內(nèi)存空間,將內(nèi)核緩沖區(qū)數(shù)據(jù)映射到用戶空間。
#include <sys/mman.h>
//addr:指定映射的起始地址,通常設(shè)為 NULL,由系統(tǒng)自動分配。
//length:映射的長度。
//prot:映射區(qū)域的保護方式,如 PROT_READ(可讀)、PROT_WRITE(可寫)等。
//flags:映射的標志,如 MAP_SHARED(共享映射)、MAP_PRIVATE(私有映射)等。
//fd:要映射的文件描述符。
//offset:映射的起始偏移量。
void *mmap(void *addr, size_t length, int prot, int flags,
int fd, off_t offset);
int munmap(void *addr, size_t length);
圖片
2.2.2 函數(shù)源碼解析
mmap中主要調(diào)用為ksys_mmap_pgoff,最終會落到do_mmap函數(shù),其整體流程如下,通過多次檢測和惰性分配以及權(quán)限分離來實現(xiàn):
圖片
2.3 splice
2.3.1 函數(shù)定義
splice用于在兩個文件描述符之間移動數(shù)據(jù)。
#include <fcntl.h>
//fd_in:輸入文件描述符。
//off_in:輸入文件的偏移量指針。
//fd_out:輸出文件描述符。
//off_out:輸出文件的偏移量指針。
//len:要傳輸?shù)臄?shù)據(jù)長度。
//flags:傳輸標志,如 SPLICE_F_MOVE(嘗試移動數(shù)據(jù))、SPLICE_F_NONBLOCK(非阻塞操作)等。
ssize_t splice(int fd_in, loff_t *off_in, int fd_out,
loff_t *off_out, size_t len, unsigned int flags);2.3.2 源碼分析
其實現(xiàn)也是依賴管道緩沖區(qū),通過內(nèi)核區(qū)創(chuàng)建和轉(zhuǎn)移所有權(quán)避免了向用戶空間拷貝。
2.4 tee
2.4.1 函數(shù)定義
tee用于在兩個管道描述符之間復制數(shù)據(jù),和splice差異存在于一個是復制一個是移動,且tee只支持管道。
#include <fcntl.h>
//out_fd : 待寫入內(nèi)容的文件描述符
//in_fd : 待讀出內(nèi)容的文件描述符
//len : 需要復制的字節(jié)數(shù)
//flags : 選項
//返回值:成功:返回在兩個文件描述符之間復制的字節(jié)數(shù);沒有數(shù)據(jù):返回0
ssize_t tee(int fd_in, int fd_out, size_t len, unsigned int flags);2.4.2 實現(xiàn)分析
和splice類似,只不過一個是移交所有權(quán),一個是增加引用。
3.實際應用以及分析
在實際場景中,我們可以使用sendfile發(fā)送靜態(tài)文件,比如nginx就支持這種配置;可以通過mmap來映射日志文件數(shù)據(jù)到用戶空間,寫完后通過內(nèi)核將數(shù)據(jù)刷到磁盤,提升寫入性能。比較優(yōu)秀的使用者還有kafka,其也是通過零拷貝提升其數(shù)據(jù)傳輸能力。
4.總結(jié)
本文介紹了零拷貝的原理、相關(guān)接口實現(xiàn)和一些實際例子,下一屆將繼續(xù)IO系列,IO性能的性能的衡量和監(jiān)控。































