目錄
3.4 sendfile + DMA gather copy
1 概述
零拷貝(Zero-copy)技術指在計算機執行操作時,CPU 不需要先將數據從一個內存區域複製到另一個內存區域,從而可以減少上下文切換以及 CPU 的拷貝時間。它的作用是在數據報從網絡設備到用戶程序空間傳遞的過程中,減少數據拷貝次數,減少系統調用,實現 CPU 的零參與,徹底消除 CPU 在這方面的負載。實現零拷貝用到的最主要技術是 DMA 數據傳輸技術和內存區域映射技術。
- 零拷貝機制可以減少數據在內核緩衝區和用戶進程緩衝區之間反覆的 I/O 拷貝操作。
- 零拷貝機制可以減少用戶進程地址空間和內核地址空間之間因爲上下文切換而帶來的 CPU 開銷。
2 Linux I/O讀寫方式
Linux 提供了輪詢、I/O 中斷以及 DMA 傳輸這 3 種磁盤與主存之間的數據傳輸機制,具體實現方式如下:
- 輪詢方式:基於死循環對 I/O 端口進行不斷檢測。
- I/O 中斷方式是指當數據到達時,磁盤主動向 CPU 發起中斷請求,由 CPU 自身負責數據的傳輸過程。
- DMA 傳輸:在 I/O 中斷的基礎上引入了 DMA 磁盤控制器,由 DMA 磁盤控制器負責數據的傳輸,降低了 I/O 中斷操作對 CPU 資源的大量消耗。
2.1 I/O中斷原理
在 DMA 技術出現之前,應用程序與磁盤之間的 I/O 操作都是通過 CPU 的中斷完成的。每次用戶進程讀取磁盤數據時,都需要 CPU 中斷,然後發起 I/O 請求等待數據讀取和拷貝完成,每次的 I/O 中斷都導致 CPU 的上下文切換。
- 用戶進程向 CPU 發起 read 系統調用讀取數據,由用戶態切換爲內核態,然後一直阻塞等待數據的返回。
- CPU 在接收到指令以後對磁盤發起 I/O 請求,將磁盤數據先放入磁盤控制器緩衝區。
- 數據準備完成以後,磁盤向 CPU 發起 I/O 中斷。
- CPU 收到 I/O 中斷以後將磁盤緩衝區中的數據拷貝到內核緩衝區,然後再從內核緩衝區拷貝到用戶緩衝區。
- 用戶進程由內核態切換回用戶態,解除阻塞狀態,然後等待 CPU 的下一個執行時間鍾。
2.2. DMA傳輸原理
整個數據傳輸操作在一個 DMA 控制器的控制下進行的。CPU 除了在數據傳輸開始和結束時做一點處理外(開始和結束時候要做中斷處理),在傳輸過程中 CPU 可以繼續進行其他的工作。這樣在大部分時間裏,CPU 計算和 I/O 操作都處於並行操作,使整個計算機系統的效率大大提高。
有了 DMA 磁盤控制器接管數據讀寫請求以後,CPU 從繁重的 I/O 操作中解脫,數據讀取操作的流程如下:
- 用戶進程向 CPU 發起 read 系統調用讀取數據,由用戶態切換爲內核態,然後一直阻塞等待數據的返回。
- CPU 在接收到指令以後對 DMA 磁盤控制器發起調度指令。
- DMA 磁盤控制器對磁盤發起 I/O 請求,將磁盤數據先放入磁盤控制器緩衝區,CPU 全程不參與此過程。
- 數據讀取完成後,DMA 磁盤控制器會接受到磁盤的通知,將數據從磁盤控制器緩衝區拷貝到內核緩衝區。
- DMA 磁盤控制器向 CPU 發出數據讀完的信號,由 CPU 負責將數據從內核緩衝區拷貝到用戶緩衝區。
- 用戶進程由內核態切換回用戶態,解除阻塞狀態,然後等待 CPU 的下一個執行時間鍾。
2.3 傳統I/O方式
在 Linux 系統中,傳統的訪問方式是通過 write() 和 read() 兩個系統調用實現的,通過 read() 函數讀取文件到到緩存區中,然後通過 write() 方法把緩存中的數據輸出到網絡端口,僞代碼如下:
read(file_fd, tmp_buf, len);
write(socket_fd, tmp_buf, len);
下圖分別對應傳統 I/O 操作的數據讀寫流程,整個過程涉及 2 次 CPU 拷貝、2 次 DMA 拷貝總共 4 次拷貝,以及 4 次上下文切換,下面簡單地闡述一下相關的概念。
- 上下文切換:當用戶程序向內核發起系統調用時,CPU 將用戶進程從用戶態切換到內核態;當系統調用返回時,CPU 將用戶進程從內核態切換回用戶態。
- CPU拷貝:由 CPU 直接處理數據的傳送,數據拷貝時會一直佔用 CPU 的資源。
- DMA拷貝:由 CPU 向DMA磁盤控制器下達指令,讓 DMA 控制器來處理數據的傳送,數據傳送完畢再把信息反饋給 CPU,從而減輕了 CPU 資源的佔有率。
3 零拷貝方式
在 Linux 中零拷貝技術主要有 3 個實現思路:用戶態直接 I/O、減少數據拷貝次數以及寫時複製技術。
- 用戶態直接 I/O:應用程序可以直接訪問硬件存儲,操作系統內核只是輔助數據傳輸。這種方式依舊存在用戶空間和內核空間的上下文切換,硬件上的數據直接拷貝至了用戶空間,不經過內核空間。因此,直接 I/O 不存在內核空間緩衝區和用戶空間緩衝區之間的數據拷貝。
- 減少數據拷貝次數:在數據傳輸過程中,避免數據在用戶空間緩衝區和系統內核空間緩衝區之間的CPU拷貝,以及數據在系統內核空間內的CPU拷貝,這也是當前主流零拷貝技術的實現思路。
- 寫時複製技術:寫時複製指的是當多個進程共享同一塊數據時,如果其中一個進程需要對這份數據進行修改,那麼將其拷貝到自己的進程地址空間中,如果只是數據讀取操作則不需要進行拷貝操作。
3.1 用戶態直接I/O
用戶態直接 I/O 使得應用進程或運行在用戶態(user space)下的庫函數直接訪問硬件設備,數據直接跨過內核進行傳輸,內核在數據傳輸過程除了進行必要的虛擬存儲配置工作之外,不參與任何其他工作,這種方式能夠直接繞過內核,極大提高了性能。
用戶態直接 I/O 只能適用於不需要內核緩衝區處理的應用程序,這些應用程序通常在進程地址空間有自己的數據緩存機制,稱爲自緩存應用程序,如數據庫管理系統就是一個代表。其次,這種零拷貝機制會直接操作磁盤 I/O,由於 CPU 和磁盤 I/O 之間的執行時間差距,會造成大量資源的浪費,解決方案是配合異步 I/O 使用。
3.2 mmap + write
一種零拷貝方式是使用 mmap + write 代替原來的 read + write 方式,減少了 1 次 CPU 拷貝操作。mmap 是 Linux 提供的一種內存映射文件方法,即將一個進程的地址空間中的一段虛擬地址映射到磁盤文件地址,mmap + write 的僞代碼如下:
tmp_buf = mmap(file_fd, len);
write(socket_fd, tmp_buf, len);
使用 mmap 的目的是將內核中讀緩衝區(read buffer)的地址與用戶空間的緩衝區(user buffer)進行映射,從而實現內核緩衝區與應用程序內存的共享,省去了將數據從內核讀緩衝區(read buffer)拷貝到用戶緩衝區(user buffer)的過程,然而內核讀緩衝區(read buffer)仍需將數據到內核寫緩衝區(socket buffer),大致的流程如下圖所示:
基於 mmap + write 系統調用的零拷貝方式,整個拷貝過程會發生 4 次上下文切換,1 次 CPU 拷貝和 2 次 DMA 拷貝,用戶程序讀寫數據的流程如下:
- 用戶進程通過 mmap() 函數向內核(kernel)發起系統調用,上下文從用戶態(user space)切換爲內核態(kernel space)。
- 將用戶進程的內核空間的讀緩衝區(read buffer)與用戶空間的緩存區(user buffer)進行內存地址映射。
- CPU利用DMA控制器將數據從主存或硬盤拷貝到內核空間(kernel space)的讀緩衝區(read buffer)。
- 上下文從內核態(kernel space)切換回用戶態(user space),mmap 系統調用執行返回。
- 用戶進程通過 write() 函數向內核(kernel)發起系統調用,上下文從用戶態(user space)切換爲內核態(kernel space)。
- CPU將讀緩衝區(read buffer)中的數據拷貝到的網絡緩衝區(socket buffer)。
- CPU利用DMA控制器將數據從網絡緩衝區(socket buffer)拷貝到網卡進行數據傳輸。
- 上下文從內核態(kernel space)切換回用戶態(user space),write 系統調用執行返回。
mmap 主要的用處是提高 I/O 性能,特別是針對大文件。對於小文件,內存映射文件反而會導致碎片空間的浪費,因爲內存映射總是要對齊頁邊界,最小單位是 4 KB,一個 5 KB 的文件將會映射佔用 8 KB 內存,也就會浪費 3 KB 內存。
mmap 的拷貝雖然減少了 1 次拷貝,提升了效率,但也存在一些隱藏的問題。當 mmap 一個文件時,如果這個文件被另一個進程所截獲,那麼 write 系統調用會因爲訪問非法地址被 SIGBUS 信號終止,SIGBUS 默認會殺死進程併產生一個 coredump,服務器可能因此被終止。
3.3 sendfile
sendfile 系統調用在 Linux 內核版本 2.1 中被引入,目的是簡化通過網絡在兩個通道之間進行的數據傳輸過程。sendfile 系統調用的引入,不僅減少了 CPU 拷貝的次數,還減少了上下文切換的次數,它的僞代碼如下:
sendfile(socket_fd, file_fd, len);
通過 sendfile 系統調用,數據可以直接在內核空間內部進行 I/O 傳輸,從而省去了數據在用戶空間和內核空間之間的來回拷貝。與 mmap 內存映射方式不同的是, sendfile 調用中 I/O 數據對用戶空間是完全不可見的。也就是說,這是一次完全意義上的數據傳輸過程。
基於 sendfile 系統調用的零拷貝方式,整個拷貝過程會發生 2 次上下文切換,1 次 CPU 拷貝和 2 次 DMA 拷貝,用戶程序讀寫數據的流程如下:
- 用戶進程通過 sendfile() 函數向內核(kernel)發起系統調用,上下文從用戶態(user space)切換爲內核態(kernel space)。
- CPU 利用 DMA 控制器將數據從主存或硬盤拷貝到內核空間(kernel space)的讀緩衝區(read buffer)。
- CPU 將讀緩衝區(read buffer)中的數據拷貝到的網絡緩衝區(socket buffer)。
- CPU 利用 DMA 控制器將數據從網絡緩衝區(socket buffer)拷貝到網卡進行數據傳輸。
- 上下文從內核態(kernel space)切換回用戶態(user space),sendfile 系統調用執行返回。
相比較於 mmap 內存映射的方式,sendfile 少了 2 次上下文切換,但是仍然有 1 次 CPU 拷貝操作。sendfile 存在的問題是用戶程序不能對數據進行修改,而只是單純地完成了一次數據傳輸過程。
3.4 sendfile + DMA gather copy
Linux 2.4 版本的內核對 sendfile 系統調用進行修改,爲 DMA 拷貝引入了 gather 操作。它將內核空間(kernel space)的讀緩衝區(read buffer)中對應的數據描述信息(內存地址、地址偏移量)記錄到相應的網絡緩衝區( socket buffer)中,由 DMA 根據內存地址、地址偏移量將數據批量地從讀緩衝區(read buffer)拷貝到網卡設備中,這樣就省去了內核空間中僅剩的 1 次 CPU 拷貝操作,sendfile 的僞代碼如下:
sendfile(socket_fd, file_fd, len);
在硬件的支持下,sendfile 拷貝方式不再從內核緩衝區的數據拷貝到 socket 緩衝區,取而代之的僅僅是緩衝區文件描述符和數據長度的拷貝,這樣 DMA 引擎直接利用 gather 操作將頁緩存中數據打包發送到網絡中即可,本質就是和虛擬內存映射的思路類似。
基於 sendfile + DMA gather copy 系統調用的零拷貝方式,整個拷貝過程會發生 2 次上下文切換、0 次 CPU 拷貝以及 2 次 DMA 拷貝,用戶程序讀寫數據的流程如下:
- 用戶進程通過 sendfile() 函數向內核(kernel)發起系統調用,上下文從用戶態(user space)切換爲內核態(kernel space)。
- CPU 利用 DMA 控制器將數據從主存或硬盤拷貝到內核空間(kernel space)的讀緩衝區(read buffer)。
- CPU 把讀緩衝區(read buffer)的文件描述符(file descriptor)和數據長度拷貝到網絡緩衝區(socket buffer)。
- 基於已拷貝的文件描述符(file descriptor)和數據長度,CPU 利用 DMA 控制器的 gather/scatter 操作直接批量地將數據從內核的讀緩衝區(read buffer)拷貝到網卡進行數據傳輸。
- 上下文從內核態(kernel space)切換回用戶態(user space),sendfile 系統調用執行返回。
sendfile + DMA gather copy 拷貝方式同樣存在用戶程序不能對數據進行修改的問題,而且本身需要硬件的支持,它只適用於將數據從文件拷貝到 socket 套接字上的傳輸過程。
3.5 splice
sendfile 只適用於將數據從文件拷貝到 socket 套接字上,同時需要硬件的支持,這也限定了它的使用範圍。Linux 在 2.6.17 版本引入 splice 系統調用,不僅不需要硬件支持,還實現了兩個文件描述符之間的數據零拷貝。splice 的僞代碼如下:
splice(fd_in, off_in, fd_out, off_out, len, flags);
splice 系統調用可以在內核空間的讀緩衝區(read buffer)和網絡緩衝區(socket buffer)之間建立管道(pipeline),從而避免了兩者之間的 CPU 拷貝操作。
基於 splice 系統調用的零拷貝方式,整個拷貝過程會發生 2 次上下文切換,0 次 CPU 拷貝以及 2 次 DMA 拷貝,用戶程序讀寫數據的流程如下:
- 用戶進程通過 splice() 函數向內核(kernel)發起系統調用,上下文從用戶態(user space)切換爲內核態(kernel space)。
- CPU 利用 DMA 控制器將數據從主存或硬盤拷貝到內核空間(kernel space)的讀緩衝區(read buffer)。
- CPU 在內核空間的讀緩衝區(read buffer)和網絡緩衝區(socket buffer)之間建立管道(pipeline)。
- CPU 利用 DMA 控制器將數據從網絡緩衝區(socket buffer)拷貝到網卡進行數據傳輸。
- 上下文從內核態(kernel space)切換回用戶態(user space),splice 系統調用執行返回。
splice 拷貝方式也同樣存在用戶程序不能對數據進行修改的問題。除此之外,它使用了 Linux 的管道緩衝機制,可以用於任意兩個文件描述符中傳輸數據,但是它的兩個文件描述符參數中有一個必須是管道設備。
3.6 寫時複製
寫時複製指的是當多個進程共享同一塊數據時,如果其中一個進程需要對這份數據進行修改,那麼就需要將其拷貝到自己的進程地址空間中。這樣做並不影響其他進程對這塊數據的操作,每個進程要修改的時候纔會進行拷貝,所以叫寫時拷貝。這種方法在某種程度上能夠降低系統開銷,如果某個進程永遠不會對所訪問的數據進行更改,那麼也就永遠不需要拷貝。
緩衝區共享方式完全改寫了傳統的 I/O 操作,因爲傳統 I/O 接口都是基於數據拷貝進行的,要避免拷貝就得去掉原先的那套接口並重新改寫,所以這種方法是比較全面的零拷貝技術,目前比較成熟的一個方案是在 Solaris 上實現的 fbuf(Fast Buffer,快速緩衝區)。
fbuf 的思想是每個進程都維護着一個緩衝區池,這個緩衝區池能被同時映射到用戶空間(user space)和內核態(kernel space),內核和用戶共享這個緩衝區池,這樣就避免了一系列的拷貝操作。
緩衝區共享的難度在於管理共享緩衝區池需要應用程序、網絡軟件以及設備驅動程序之間的緊密合作,而且如何改寫 API 目前還處於試驗階段並不成熟。
3.7 Linux零拷貝對比
無論是傳統 I/O 拷貝方式還是引入零拷貝的方式,2 次 DMA Copy 是都少不了的,因爲兩次 DMA 都是依賴硬件完成的。下面從 CPU 拷貝次數、DMA 拷貝次數以及系統調用幾個方面總結一下上述幾種 I/O 拷貝方式的差別。
拷貝方式 | CPU拷貝 | DMA拷貝 | 系統調用 | 上下文切換 |
傳統方式() | 2 | 2 | read/write | 4 |
內存映射() | 1 | 2 | mmap/write | 4 |
sendfile | 1 | 2 | sendfile | 2 |
sendfile + DMA + gather copy | 0 | 2 | sendfile | 2 |
splice | 0 | 2 | sendfile | 2 |
參考: