傳統IO過程
考慮這樣一個過程:我們從磁盤中讀取一個文件數據,然后將數據通過網絡傳輸到另一個機器。對用戶來說可能就是簡單的理解為兩步操作。
File.read(fileDesc, buf, len);
Socket.send(socket, buf, len);
但是,如果我們看傳輸中涉及的內核部分的內部工作原理,我們將看到
即使是使用DMA傳輸的硬件支持,這種方法也效率很低。首先,內核將使用DMA將磁盤中的數據加載到其自己的內核緩沖區中,除非在先前訪問同一文件之后,該數據仍被緩存在內核緩沖區中。
這樣傳輸不需要太多的CPU工作,CPU只需要進行緩沖區管理和DMA創建和處理。linux 操作系統會根據 read() 系統調用指定的應用程序地址空間的地址,把這塊數據存放到請求這塊數據的應用程序的地址空間中去,在接下來的處理過程中,操作系統需要將數據再一次從用戶應用程序地址空間的緩沖區拷貝到與網絡堆棧相關的內核緩沖區中去,這個過程也是需要占用 CPU 的。
數據拷貝操作結束以后,數據會被打包,然后發送到網絡接口卡上去。在數據傳輸的過程中,應用程序可以先返回進而執行其他的操作。
之后,在調用 write() 系統調用的時候,用戶應用程序緩沖區中的數據內容可以被安全的丟棄或者更改,因為操作系統已經在內核緩沖區中保留了一份數據拷貝,當數據被成功傳送到硬件上之后,這份數據拷貝就可以被丟棄。
所以我們會發現這個過程涉及到了3次上下文切換,和4次數據拷貝的過程:
利用mmap()
在 Linux 中,減少拷貝次數的一種方法是調用 mmap() 來代替調用 read,比如:
tmp_buf = mmap(file, len);
write(socket, tmp_buf, len);
首先,應用程序調用了 mmap() 之后,數據會先通過 DMA 拷貝到操作系統內核的緩沖區中去。接著,應用程序跟操作系統共享這個緩沖區,這樣,操作系統內核和應用程序存儲空間就不需要再進行任何的數據拷貝操作。應用程序調用了 write() 之后,操作系統內核將數據從原來的內核緩沖區中拷貝到與 socket 相關的內核緩沖區中。接下來,數據從內核 socket 緩沖區拷貝到協議引擎中去,這是第三次數據拷貝操作
盡管mmap()可以減少一次 I/O 拷貝,但由于mmap()的實現很復雜,調用mmap()將會帶來額外的開銷,因此在一些情況下,沒有使用mmap()的必要:
訪問小文件時,直接使用read()或write()將更加高效。
單個進程對文件執行順序訪問時(sequential access),使用mmap()幾乎不會帶來性能上的提升。譬如說,使用read()順序讀取文件時,文件系統會使用 read-ahead 的方式提前將文件內容緩存到文件系統的緩沖區,因此使用read()將很大程度上可以命中緩存。
那么,在什么情況下使用mmap()去訪問文件會更高效呢?
對文件執行隨機訪問時,如果使用read()或write(),則意味著較低的 cache 命中率。這種情況下使用mmap()通常將更高效。
多個進程同時訪問同一個文件時(無論是順序訪問還是隨機訪問),如果使用mmap(),那么 OS 緩沖區的文件內容可以在多個進程之間共享,從操作系統角度來看,使用mmap()可以大大節省內存。
sendfile()
為了簡化用戶接口,同時還要繼續保留 mmap()/write() 技術的優點:減少 CPU 的拷貝次數,Linux 在版本 2.1 中引入了 sendfile() 這個系統調用。
sendfile(sockfd, fd, NULL, len);
sendfile() 不僅減少了數據拷貝操作,它也減少了上下文切換。首先:sendfile() 系統調用利用 DMA 引擎將文件中的數據拷貝到操作系統內核緩沖區中,然后數據被拷貝到與 socket 相關的內核緩沖區中去。接下來,DMA 引擎將數據從內核 socket 緩沖區中拷貝到協議引擎中去。
可以看到,與使用read()和write()發送文件相比,使用sendfile()減少了一次 I/O 拷貝和兩次 上下文切換。
sendfile with DMA Gather Copy
為了避免操作系統內核造成的數據副本,需要用到一個支持收集操作的網絡接口,這也就是說,待傳輸的數據可以分散在存儲的不同位置上,而不需要在連續存儲中存放。
這樣一來,從文件中讀出的數據就根本不需要被拷貝到 socket 緩沖區中去,而只是需要將緩沖區描述符傳到網絡協議棧中去,之后其在緩沖區中建立起數據包的相關結構,然后通過 DMA 收集拷貝功能將所有的數據結合成一個網絡數據包。
網卡的 DMA 引擎會在一次操作中從多個位置讀取包頭和數據。Linux 2.4 版本中的 socket 緩沖區就可以滿足這種條件,這也就是用于 Linux 中的眾所周知的零拷貝技術,這種方法不但減少了因為多次上下文切換所帶來開銷,同時也減少了處理器造成的數據副本的個數。
對于用戶應用程序來說,代碼沒有任何改變。首先,sendfile() 系統調用利用 DMA 引擎將文件內容拷貝到內核緩沖區去;然后,將帶有文件位置和長度信息的緩沖區描述符添加到 socket 緩沖區中去,此過程不需要將數據從操作系統內核緩沖區拷貝到 socket 緩沖區中,DMA 引擎會將數據直接從內核緩沖區拷貝到協議引擎中去,這樣就避免了最后一次數據拷貝。