在涉及到IO的開發(fā)中,我們經(jīng)??吹搅憧截?zero copy)、內(nèi)存映射(memroy map, 以下簡稱mmap)等技術(shù)被用于提高IO效率,本文將介紹這兩種技術(shù)的基本原理,說明它們是如何提高IO效率的。
相關(guān)概念
Zero copy和mmap涉及到操作系統(tǒng)中的一些基本概念,在了解它們的工作機制前,我們先來復(fù)習(xí)一下這些概念。
虛擬內(nèi)存(virtual memory space)
進程對內(nèi)存的讀寫不是直接使用物理內(nèi)存地址,而是基于虛擬地址。
每個進程運行時,操作系統(tǒng)都會為其創(chuàng)建一個私有的虛擬內(nèi)存,存放進程運行時代碼和數(shù)據(jù)。虛擬內(nèi)存大小取決與操作系統(tǒng)和所在機器的體系結(jié)構(gòu),對于32機器來說,空間大小為4g。
操作系統(tǒng)通過內(nèi)存管理機制,將虛擬內(nèi)存映射到物理內(nèi)存。
虛擬內(nèi)存使得操作系統(tǒng)可以同時支持多個運行進程安全共享物理內(nèi)存,防止進程之間的不安全讀寫。
User space vs kernel space
虛擬內(nèi)存分為兩部分:用戶空間和內(nèi)核空間。用戶空間存放用戶代碼和用戶數(shù)據(jù);內(nèi)核空間存放操作系統(tǒng)代碼。
前面說過,每個進程有自己私有的虛擬內(nèi)存,不同進程的虛擬內(nèi)存中的相同的地址,被映射到物理內(nèi)存中的不同位置。但是內(nèi)核空間是個例外,所有進程是共享內(nèi)核空間的,也就是對不同進程來說,它們內(nèi)核空間內(nèi)的內(nèi)容、地址映射實際上都是相同的。
缺頁中斷(page fault)
操作系統(tǒng)為每個進程的虛擬內(nèi)存和物理內(nèi)存之間建立了一張映射表,需要注意的是,虛擬內(nèi)存中的內(nèi)容只會一部分被裝載到物理內(nèi)存中。
當(dāng)進程訪問的虛擬地址對應(yīng)的內(nèi)容不在物理內(nèi)存時,操作系統(tǒng)會觸發(fā)一個缺頁中斷,將物理內(nèi)存中不用的內(nèi)容暫時置換到磁盤,將需要的內(nèi)容讀取道物理內(nèi)存。通過這種管理模式,我們可以在同時運行多個進程的情況下,讓每個進程覺得自己在獨享整個內(nèi)存空間。
User mode vs kernel mode
操作系統(tǒng)至少為進程提供兩種運行模式:用戶態(tài)(usermode)、內(nèi)核態(tài)(kernel mode)。不同的模式,實際對應(yīng)著不同的運行權(quán)限。
用戶態(tài)的進程,只能訪問用戶空間,不能直接訪問設(shè)備。而內(nèi)核態(tài)的進程,可以訪問用戶空間和內(nèi)核空間,可以訪問硬件設(shè)備。
進程模式切換
一個進程可以通過系統(tǒng)調(diào)用在用戶態(tài)和內(nèi)核態(tài)之間切換,此外,中斷、異常等機制也可以讓進程沖用戶態(tài)切換到內(nèi)核態(tài)。
這里簡單說明下系統(tǒng)調(diào)用的過程;
- 用戶態(tài)進程準備好系統(tǒng)調(diào)用的參數(shù);
- 進程執(zhí)行
trap指令 - cpu自動切換到內(nèi)核態(tài)
- cpu讀取進程事先設(shè)置的系統(tǒng)調(diào)用參數(shù)
- 執(zhí)行指定的系統(tǒng)調(diào)用
- 結(jié)束后,進程重新被設(shè)置為用戶態(tài)
工作過程分析
假定我們現(xiàn)在要實現(xiàn)一個讀取文件,在內(nèi)存中處理,然后將其輸出的需求,我們看看在不同的實現(xiàn)方式中,底層究竟是如何工作的。
普通實現(xiàn)(read + write)
首先,我們使用常規(guī)的文件io操作實現(xiàn)需求。

- 進程通過系統(tǒng)調(diào)用讀取數(shù)據(jù)
- 進程切換到內(nèi)核態(tài),通知設(shè)備進行讀取
- 設(shè)備準備好的數(shù)據(jù)傳送到內(nèi)核空間
- 接收到數(shù)據(jù)后,內(nèi)核態(tài)進程將數(shù)據(jù)從內(nèi)核空間拷貝到用戶空間
- 進程切換回用戶態(tài),繼續(xù)執(zhí)行用戶空間的代碼:處理數(shù)據(jù)
- 進程通過系統(tǒng)調(diào)用輸出數(shù)據(jù)到設(shè)備
- 進程切換到內(nèi)核態(tài),把數(shù)據(jù)從用戶空間拷貝到內(nèi)核空間,通知設(shè)備進行輸出操作
- 設(shè)備完成任務(wù)后,進程再次從內(nèi)核態(tài)切換回用戶態(tài)
在上面的過程中,涉及到4次進程模式切換,兩次內(nèi)存拷貝。這些操作對性能會造成一定影響。
sendfile
接下來,我們通過sendfile調(diào)用,減少上述過程中的內(nèi)存拷貝,實現(xiàn)零拷貝。

通過sendfile,我們看到進程模式切換從4次減少到2次,拷貝從2次減少到1次。
但是sendfile只能完成文件的拷貝操作,無法處理文件內(nèi)容,mmap則可以幫助我們實現(xiàn)零拷貝下的處理。
mmap

通過調(diào)用memory map,我們讓操作系統(tǒng)把文件的內(nèi)容映射到內(nèi)存,對內(nèi)存的讀寫將關(guān)聯(lián)到對應(yīng)的文件。而應(yīng)用通過訪問用戶空間操作這部分內(nèi)存,避免了內(nèi)存拷貝操作。
對于內(nèi)存映射,有些地方容易被誤解,這里說明一下。
內(nèi)存映射是文件到內(nèi)存空間的映射
對于應(yīng)用來說,和文件建立映射關(guān)系的是虛擬地址空間,而不是物理內(nèi)存或者Heap。
當(dāng)我們建立一個2g大小的映射時,并不是在heap,更不是在物理內(nèi)存中分配了這么大的空間,僅僅是在虛擬地址空間中劃出了這么大一個區(qū)域而已。
應(yīng)用訪問內(nèi)存映射區(qū)域時,操作系統(tǒng)會把虛擬的地址映射成真正的物理內(nèi)存地址和底層文件的偏移量。如果應(yīng)用訪問的虛擬地址對應(yīng)的文件內(nèi)容尚未被裝入內(nèi)存,操作系統(tǒng)通過缺頁中斷,將內(nèi)存中的部分內(nèi)容交換出去,騰出空間將文件的內(nèi)容讀取到內(nèi)存。
內(nèi)存映射對性能的提升是有條件的
通過內(nèi)存映射訪問文件,雖然減少了內(nèi)存拷貝,減少了系統(tǒng)調(diào)用引起的進程模式切換,但是過程中需要承擔(dān)缺頁中斷的負擔(dān)。
對于小文件的讀取,或者對于append模式的文件讀寫,內(nèi)存映射的性能未必優(yōu)于普通io操作。只有對大文件的隨機訪問,內(nèi)存映射才可能有明顯優(yōu)勢,不過這仍然需要更具體的分析和進一步的的benchmark測試。