概述
第一次聽說這個概念是在看kafka原理的時候,因為當時很好奇為什么kafka一個基於磁盤存儲的MQ會那么快,當時找到的答案是kafka采用磁盤順序讀寫和零拷貝技術,從而使得kafka的吞吐量非常大。本文就介紹一下操作系統中的零拷貝技術原理,之后會介紹kafka是如何使用操作系統的零拷貝技術實現高性能的。
為什么使用零拷貝
為了說明零拷貝的好處,這里先舉兩個例子,這兩個例子都沒有使用零拷貝技術,通過這兩個例子,大家應該明白為什么要使用零拷貝。
示例一:傳統文件訪問
看下圖
圖片來源:零拷貝(zero-copy)
上圖過程解析如下:
- 進程向操作系統發起read系統調用,進行上下文切換,切換到內核態,需要將磁盤數據讀入內存,自己進入阻塞狀態
- 操作系統向磁盤發起請求,磁盤將數據導入到磁盤驅動緩沖區,無需使用CPU,當驅動器緩沖區滿了之后,向操作系統發起中斷請求,告訴操作系統自己的緩沖區滿了
- 操作系統將驅動器緩沖區的數據拷貝到內核緩沖區,此步驟為DMA copy
- 如果內核中的數據少於用戶請求數據,重復步驟2和步驟3,直到數據達到要求為止
- 將數據從內核緩沖區拷貝到用戶緩沖區,此過程為CPU Copy,同時從系統調用中返回,進行上下文調用,切換到用戶態
從上面解析過程可知,傳統讀操作,總共需要2次上下文切換,3次Copy,圖中顯示的是兩次,一次為DMA Copy,一次為CPU Copy,其實還有一次從磁盤拷貝到驅動器緩沖區,可能是因為這個是無法避免,所以我看很多文章沒有提這個。
上面提到一個新的概念DMA Copy,在我的一篇文章有提到,操作系統訪問磁盤有三種方式,其中之一就是DMA,下面就介紹一下DMA工作過程。
- 進程向操作系統發起read系統調用,進行上下文切換,切換到內核態,需要將磁盤數據讀入內存,自己進入阻塞狀態
- CPU收到之后,將請求交給DMA,自己去忙其他事情
- DMA向磁盤發起請求
- 磁盤將數據讀到磁盤驅動器緩沖區中,當緩沖區數據滿了之后,向操作系統發起中斷請求
- DMA將緩沖區的數據復制到內核緩沖區中
- 如果數據少於用戶請求數據,重復不走4和步驟5,直到數據達到要求為止,此時DMA向操作系統發起中斷請求
- CPU將內核緩沖區的數據拷貝到用戶緩沖區
從以上解析過程可以發現,DMA其實就是CPU的一個代理,與磁盤進行交互,因為磁盤速度太慢,CPU直接和磁盤交互太浪費CPU時間,所以搞了一個小弟(DMA),讓這個小弟替自己干活。
小結
以上過程比較浪費的一點是從內核緩沖區拷貝到用戶緩沖區,這個過程基本什么事情都沒有干,只是一個拷貝,而且需要CPU參與,如果數據量非常大,這個過程是非常浪費時間的,而且在內存中保存兩份數據,也浪費空間,而零拷貝要的事情是什么呢?就是能不能把這個過程給避免了,在內存中只保留一份數據,讓用戶空間和內核空間共享,但是從磁盤緩沖區拷貝內存這個步驟是無法避免的,所以零拷貝並不是真的是0次拷貝,而是盡量減小拷貝的次數,算是一種優化拷貝過程的方法。
上面介紹的傳統的讀請求,其實寫請求也是一樣,先從用戶緩沖區拷貝到內核緩沖區,之后再寫到磁盤上,也是兩次復制,如果讓用戶緩沖區和內核緩沖區共享就可以減少一次復制,實際上kafka也是這樣做的,調用的是操作系統提供的系統調用mmap,mmap可以讓磁盤文件直接映射到內存,並且用戶空間和內核空間共享同一個緩沖區,在向kafka中寫數據的時候就是這個過程。
示例二:發送數據到網絡
圖片來源:Zero-Copy in Linux
上圖過程解析如下:
- 將磁盤數據通過DMA拷貝到內核緩沖區(為了簡寫,就不寫磁盤驅動器緩沖區了)
- CPU將內核緩沖區數據拷貝到用戶緩沖區
- CPU將用戶緩沖區數據拷貝到socket緩沖區
- DMA將socket緩沖區數據拷貝到網卡
以上過程總共發生了四次上下文切換,四次拷貝,四次上下文切換如下
- 從用戶空間發起read系統調用,去磁盤獲取數據,從用戶態切換到內核態,發生第一次上下文切換
- 當數據准備好之后,從內核態把數據拷貝到用戶態,發生第二次上下文切換
- 把數據從用戶緩沖區拷貝到內核socket緩沖區,發生第三次上下文切換
- 發送完之后,重進進入用戶態正常執行,發生第四次上下文切換
小結
大家可能發現,在四次拷貝過程中,第二次和第三次好像什么也沒有做,就是轉了一個圈,那能不能把第二次和第三次給優化掉,直接從內核緩沖區拷貝到網卡呢?答案是可以的,使用操作系統提供的接口就可以實現,比如linux的sendfile,實際上在消費者從kafka中消費數據的時候就是使用sendfile優化的。
零拷貝的幾種方法
下面會介紹幾種常見的零拷貝技術,以及他們的特點。
第一種方法:mmap
英文Memory Mapped Files,簡稱mmap,從英文名稱就可以看出叫做內存映射文件,就是把磁盤上的一個文件通過DMA拷貝到內存,然后對內存文件的操作就像直接操作磁盤文件一樣,由於文件在內存中以頁的方式存儲,當有些頁被修改之后,需要把臟頁刷新回磁盤,這時不需要像傳統的方式發起write系統調用,這時可以直接將內存中的臟頁刷新回磁盤,無需CPU參與,無需上下文切換。
mmap具體流程
- 進程啟動映射過程,並在虛擬地址空間創建映射區域
- 調用系統調用函數mmap,實現虛擬地址空間和物理地址空間的映射
- 進程發起對映射空間的訪問,引發缺頁中斷,將磁盤上的數據拷貝到內存中
以上過程中只在第三步進程需要使用這一頁的數據,比如進行read或者write操作時,才會把磁盤上的數據通過DMA將磁盤數據拷貝到內核緩沖區。在第二步處於用戶空間的進程直接將虛擬地址空間映射到內核緩沖區,無需將處於內核的數據拷貝到用戶進程。除此之外,處於內核空間的緩沖區可以被映射到多個進程,也就是說多個進程可以通過共享內存,而無需在內存中保存多份重復數據,多個進程共享數據的過程如下。
- 進程A讀取某一頁的數據,發現內存中沒有,引發缺頁中斷,DMA將磁盤數據拷貝到內存
- 進程B也需要同一頁的數據,引發缺頁中斷,這時並不會再去磁盤讀取,而是直接將虛擬地址空間映射到進程A剛剛訪問物理地址空間上
是不是覺得mmap很牛批,但mmap也不是萬能的,即便是少了一次復制過程,如果對磁盤進行隨機讀寫的話,速度也是慢的一匹,順序讀寫還可以,而且寫到mmap的數據並沒有刷到磁盤,在程序主動調用flush的時候,操作系統才會把緩沖區的數據刷到磁盤,Kafka提供了一個參數——producer.type來控制是不是主動flush;如果Kafka寫入到mmap之后就立即flush然后再返回Producer叫同步(sync);寫入mmap之后立即返回Producer不調用flush叫異步(async)。
第二種方法:sendfile
針對上面的示例二,通過sendfile系統調用可以解決這個問題,在linux 2.1時,采用如下做法:
圖片來源:Linux Zero-copy(零拷貝)
從上圖可以看出,這個優化只是把從內核空間拷貝到用戶空間的過程給去掉了,但是在內核內部,依然需要從內核緩沖區拷貝到socket緩沖區,而且這個拷貝是CPU Copy,這個過程也是其實也是不需要的,又沒有做什么別的工作,純粹就是拷貝。
從linux 2.4之后,上面那個CPU Copy就給干掉了,如下圖:
從圖中可以看出,從內核緩沖區向socket緩沖區依然有復制過程,但是只是復制了文件描述符(定義:內核(kernel)利用文件描述符(file descriptor)來訪問文件。文件描述符是非負整數。打開現存文件或新建文件時,內核會返回一個文件描述符。讀寫文件也需要使用文件描述符來指定待讀寫的文件。),數據其實是直接從內核緩沖區拷貝到網卡的。
第三種方法:splice
上面的sendfile已經可以實現0次CPU 拷貝了,但是在linux2.6的時候又新增了一個新的系統調用splice,為什么要新增splice呢?下面是linus大佬的解釋:
- the pipe _is_ the buffer. The reason sendfile() sucks is that sendfile cannot work with <n> different buffer representations. sendfile() only works with _one_ buffer representation, namely the "page cache of the file". By using the page cache directly, sendfile() doesn't need any extra buffering, but that's also why sendfile() fundamentally _cannot_ work with anything else. You cannot do "sendfile" between two sockets to forward data from one place to another, for example. You cannot do sendfile from a streaming device.
上面這段話的大致意思是sendfile()非常的爛(sucks),因為sendfile只能將數據從文件拷貝到別的緩沖區,比如把文件拷貝到socket緩沖區,而不能從一個socket緩沖區拷貝到另一個socket緩沖區或者別的場景。
splice和sendfile不同,他可以適用於任意兩個文件描述符之間互相通信。通過管道實現,兩個緩沖區中間連接一個管道,但是並不是把第一個緩沖區的數據拷貝到pipe,而是將指針(引用)拷貝進去。
總結
零拷貝其實最主要的優化就是CPU Copy減少為0,因為拷貝過程非常浪費CPU時間,所以盡量把這個時間減小,引入DMA作為CPU的一個代理,也是為了讓CPU可以做別的事情,因為像磁盤這樣的設備性能太差,如果讓CPU直接訪問,那就太浪費了。
前幾篇文章一直在介紹進程,零拷貝之前一直沒有搞清楚是什么,最近正好在學習操作系統,所以就順便學了一下,下一篇介紹操作系統文件管理。
參考: