文章目錄
- 其他文章
- 網絡為什麼要分層?
- 發送資料包
- 零拷貝
- 為什麼要有DMA技術
- 傳統的檔案傳輸有多糟糕?
- 如何優化檔案傳輸的性能?
- 如何實作零拷貝
- mmap + write
- sendfile
- PageCache有什麼作用?
- 大檔案傳輸用什麼方式實作?
一台機器将自己想要表達的内容,按照某種約定好的格式發送出去,當另外一台機器收到這些資訊後,也能夠按照約定好的格式解析出來,進而準确、可靠地獲得發送方想要表達的内容。這種約定好的格式就是網絡協定(Networking Protocol)。
網絡為什麼要分層?
我們這裡先建構一個相對簡單的場景,之後幾節内容,我們都要基于這個場景進行講解。
我們假設這裡就涉及三台機器。Linux 伺服器 A 和 Linux 伺服器 B 處于不同的網段,通過中間的 Linux 伺服器作為路由器進行轉發。

說到網絡協定,我們還需要簡要介紹一下兩種網絡協定模型,一種是OSI 的标準七層模型,一種是業界标準的 TCP/IP 模型。它們的對應關系如下圖所示:
為什麼網絡要分層呢?因為網絡環境過于複雜,不是一個能夠集中控制的體系。全球數以億記的伺服器和裝置各有各的體系,但是都可以通過同一套網絡協定棧通過切分成多個層次群組合,來滿足不同伺服器和裝置的通信需求。
我們這裡簡單介紹一下網絡協定的幾個層次。
我們從哪一個層次開始呢?從第三層,網絡層開始,因為這一層有我們熟悉的 IP 位址。也是以,這一層我們也叫 IP 層。
我們通常看到的 IP 位址都是這個樣子的:192.168.1.100/24。斜杠前面是 IP 位址,這個位址被點分隔為四個部分,每個部分 8 位,總共是 32 位。斜線後面 24 的意思是,32 位中,前 24 位是網絡号,後 8 位是主機号。
為什麼要這樣分呢?我們可以想象,雖然全世界組成一張大的網際網路,美國的網站你也能夠通路的,但是這個網絡不是一整個的。你們小區有一個網絡,你們公司也有一個網絡,聯通、移動、電信營運商也各有各的網絡,是以一個大網絡是被分成個小的網絡。
那如何區分這些網絡呢?這就是網絡号的概念。一個網絡裡面會有多個裝置,這些裝置的網絡号一樣,主機号不一樣。不信你可以觀察一下你家裡的手機、電視、電腦。
連接配接到網絡上的每一個裝置都至少有一個 IP 位址,用于定位這個裝置。無論是近在咫尺的你旁邊同學的電腦,還是遠在天邊的電商網站,都可以通過 IP 位址進行定位。是以,IP 位址類似網際網路上的郵寄位址,是有全局定位功能的。
就算你要通路美國的一個位址,也可以從你身邊的網絡出發,通過不斷的打聽道兒,經過多個網絡,最終到達目的位址,和快遞員送包裹的過程差不多。打聽道兒的協定也在第三層,稱為路由協定(Routing protocol),将網絡包從一個網絡轉發給另一個網絡的裝置稱為路由器。
總而言之,第三層幹的事情,就是網絡包從一個起始的 IP 位址,沿着路由協定指的道兒,經過多個網絡,通過多次路由器轉發,到達目标 IP 位址。
從第三層,我們往下看,第二層是資料鍊路層。有時候我們簡稱為二層或者 MAC 層。所謂 MAC,就是每個網卡都有的唯一的硬體位址(不絕對唯一,相對大機率唯一即可)。這雖然也是一個位址,但是這個位址是沒有全局定位功能的。
就像給你送外賣的小哥,不可能根據手機尾号找到你家,但是手機尾号有本地定位功能的,隻不過這個定位主要靠“吼”。外賣小哥到了你的樓層就開始大喊:“尾号 xxxx 的,你外賣到了!”
MAC 位址的定位功能局限在一個網絡裡面,也即同一個網絡号下的 IP 位址之間,可以通過 MAC 進行定位和通信。從 IP 位址擷取 MAC 位址要通過 ARP 協定,是通過在本地發送廣播包,也就是“吼”,獲得的 MAC 位址。
由于同一個網絡内的機器數量有限,通過 MAC 位址的好處就是簡單。比對上 MAC 位址就接收,比對不上就不接收,沒有什麼所謂路由協定這樣複雜的協定。當然壞處就是,MAC 位址的作用範圍不能出本地網絡,是以一旦跨網絡通信,雖然 IP 位址保持不變,但是 MAC 位址每經過一個路由器就要換一次。
我們看前面的圖。伺服器 A 發送網絡包給伺服器 B,原 IP 位址始終是 192.168.1.100,目标 IP 位址始終是 192.168.2.100,但是在網絡 1 裡面,原 MAC 位址是 MAC1,目标 MAC 位址是路由器的 MAC2,路由器轉發之後,原 MAC 位址是路由器的 MAC3,目标 MAC 位址是 MAC4。
是以第二層幹的事情,就是網絡包在本地網絡中的伺服器之間定位及通信的機制。
我們再往下看,第一層,實體層,這一層就是實體裝置。例如連着電腦的網線,我們能連上的 WiFi
從第三層往上看,第四層是傳輸層,這裡面有兩個著名的協定 TCP 和 UDP。尤其是 TCP,更是廣泛使用,在 IP 層的代碼邏輯中,僅僅負責資料從一個 IP 位址發送給另一個 IP 位址,丢包、亂序、重傳、擁塞,這些 IP 層都不管。處理這些問題的代碼邏輯寫在了傳輸層的 TCP 協定裡面。
我們常稱,TCP 是可靠傳輸協定,也是難為它了。因為從第一層到第三層都不可靠,網絡包說丢就丢,是 TCP 這一層通過各種編号、重傳等機制,讓本來不可靠的網絡對于更上層來講,變得“看起來”可靠。哪有什麼應用層歲月靜好,隻不過 TCP 層幫你負重前行。
傳輸層再往上就是應用層,例如咱們在浏覽器裡面輸入的 HTTP,Java 服務端寫的 Servlet,都是這一層的。
二層到四層都是在 Linux 核心裡面處理的,應用層例如浏覽器、Nginx、Tomcat 都是使用者态的。核心裡面對于網絡包的處理是不區分應用的。
從四層再往上,就需要區分網絡包發給哪個應用。在傳輸層的 TCP 和 UDP 協定裡面,都有端口的概念,不同的應用監聽不同的端口。例如,服務端 Nginx 監聽 80、Tomcat 監聽 8080;再如用戶端浏覽器監聽一個随機端口,FTP 用戶端監聽另外一個随機端口。
應用層和核心互通的機制,就是通過 Socket 系統調用。是以經常有人會問,Socket 屬于哪一層,其實它哪一層都不屬于,它屬于作業系統的概念,而非網絡協定分層的概念。隻不過作業系統選擇對于網絡協定的實作模式是,二到四層的處理代碼在核心裡面,七層的處理代碼讓應用自己去做,兩者需要跨核心态和使用者态通信,就需要一個系統調用完成這個銜接,這就是 Socket。
發送資料包
網絡分完層之後,對于資料包的發送,就是層層封裝的過程。
就像下面的圖中展示的一樣,在 Linux 伺服器 B 上部署的服務端 Nginx 和 Tomcat,都是通過 Socket 監聽 80 和 8080 端口。這個時候,核心的資料結構就知道了。如果遇到發送到這兩個端口的,就發送給這兩個程序。
在 Linux 伺服器 A 上的用戶端,打開一個 Firefox 連接配接 Ngnix。也是通過 Socket,用戶端會被配置設定一個随機端口 12345。同理,打開一個 Chrome 連接配接 Tomcat,同樣通過 Socket 配置設定随機端口 12346。
在用戶端浏覽器,我們将請求封裝為 HTTP 協定,通過 Socket 發送到核心。核心的網絡協定棧裡面,在 TCP 層建立用于維護連接配接、序列号、重傳、擁塞控制的資料結構,将 HTTP 包加上 TCP 頭,發送給 IP 層,IP 層加上 IP 頭,發送給 MAC 層,MAC 層加上 MAC 頭,從硬體網卡發出去。
網絡包會先到達網絡 1 的交換機。我們常稱交換機為二層裝置,這是因為,交換機隻會處理到第二層,然後它會将網絡包的 MAC 頭拿下來,發現目标 MAC 是在自己右面的網口,于是就從這個網口發出去。
網絡包會到達中間的 Linux 路由器,它左面的網卡會收到網絡包,發現 MAC 位址比對,就交給 IP 層,在 IP 層根據 IP 頭中的資訊,在路由表中查找。下一跳在哪裡,應該從哪個網口發出去?在這個例子中,最終會從右面的網口發出去。我們常把路由器稱為三層裝置,因為它隻會處理到第三層。
從路由器右面的網口發出去的包會到網絡 2 的交換機,還是會經曆一次二層的處理,轉發到交換機右面的網口。
最終網絡包會被轉發到 Linux 伺服器 B,它發現 MAC 位址比對,就将 MAC 頭取下來,交給上一層。IP 層發現 IP 位址比對,将 IP 頭取下來,交給上一層。TCP 層會根據 TCP 頭中的序列号等資訊,發現它是一個正确的網絡包,就會将網絡包緩存起來,等待應用層的讀取。
應用層通過 Socket 監聽某個端口,因而讀取的時候,核心會根據 TCP 頭中的端口号,将網絡包發給相應的應用。
HTTP 層的頭和正文,是應用層來解析的。通過解析,應用層知道了用戶端的請求,例如購買一個商品,還是請求一個網頁。當應用層處理完 HTTP 的請求,會将結果仍然封裝為 HTTP 的網絡包,通過 Socket 接口,發送給核心。
核心會經過層層封裝,從實體網口發送出去,經過網絡 2 的交換機,Linux 路由器到達網絡 1,經過網絡 1 的交換機,到達 Linux 伺服器 A。在 Linux 伺服器 A 上,經過層層解封裝,通過 socket 接口,根據用戶端的随機端口号,發送給用戶端的應用程式,浏覽器。于是浏覽器就能夠顯示出一個絢麗多彩的頁面了。
即便在如此簡單的一個環境中,網絡包的發送過程,竟然如此的複雜。
零拷貝
為什麼要有DMA技術
在沒有 DMA 技術前,I/O 的過程是這樣的:
- CPU 發出對應的指令給磁盤控制器,然後傳回;
- 磁盤控制器收到指令後,于是就開始準備資料,會把資料放入到磁盤控制器的内部緩沖區中,然後産生一個中斷;
- CPU 收到中斷信号後,停下手頭的工作,接着把磁盤控制器的緩沖區的資料一次一個位元組地讀進自己的寄存器,然後再把寄存器裡的資料寫入到記憶體,而在資料傳輸的期間 CPU 是無法執行其他任務的。
為了友善你了解,我畫了一副圖:
可以看到,整個資料的傳輸過程,都要需要 CPU 親自參與搬運資料的過程,而且這個過程,CPU 是不能做其他事情的。
簡單的搬運幾個字元資料那沒問題,但是如果我們用千兆網卡或者硬碟傳輸大量資料的時候,都用 CPU 來搬運的話,肯定忙不過來。
計算機科學家們發現了事情的嚴重性後,于是就發明了 DMA 技術,也就是直接記憶體通路(Direct Memory Access) 技術。
什麼是 DMA 技術?簡單了解就是,在進行 I/O 裝置和記憶體的資料傳輸的時候,資料搬運的工作全部交給 DMA 控制器,而 CPU 不再參與任何與資料搬運相關的事情,這樣 CPU 就可以去處理别的事務。
那使用 DMA 控制器進行資料傳輸的過程究竟是什麼樣的呢?下面我們來具體看看。
具體過程:
- 使用者程序調用 read 方法,向作業系統發出 I/O 請求,請求讀取資料到自己的記憶體緩沖區中,程序進入阻塞狀态;
- 作業系統收到請求後,進一步将 I/O 請求發送 DMA,然後讓 CPU 執行其他任務;
- DMA 進一步将 I/O 請求發送給磁盤;
- 磁盤收到 DMA 的 I/O 請求,把資料從磁盤讀取到磁盤控制器的緩沖區中,當磁盤控制器的緩沖區被讀滿後,向 DMA 發起中斷信号,告知自己緩沖區已滿;
- DMA 收到磁盤的信号,将磁盤控制器緩沖區中的資料拷貝到核心緩沖區中,此時不占用 CPU,CPU 可以執行其他任務;
- 當 DMA 讀取了足夠多的資料,就會發送中斷信号給 CPU;
- CPU 收到 DMA 的信号,知道資料已經準備好,于是将資料從核心拷貝到使用者空間,系統調用傳回;
可以看到, 整個資料傳輸的過程,CPU 不再參與資料搬運的工作,而是全程由 DMA 完成,但是 CPU 在這個過程中也是必不可少的,因為傳輸什麼資料,從哪裡傳輸到哪裡,都需要 CPU 來告訴 DMA 控制器。
早期 DMA 隻存在在主機闆上,如今由于 I/O 裝置越來越多,資料傳輸的需求也不盡相同,是以每個 I/O 裝置裡面都有自己的 DMA 控制器。
傳統的檔案傳輸有多糟糕?
如果服務端要提供檔案傳輸的功能,我們能想到的最簡單的方式是:将磁盤上的檔案讀取出來,然後通過網絡協定發送給用戶端。
傳統 I/O 的工作方式是,資料讀取和寫入是從使用者空間到核心空間來回複制,而核心空間的資料是通過作業系統層面的 I/O 接口從磁盤讀取或寫入。
代碼通常如下,一般會需要兩個系統調用:
read(file, tmp_buf, len);
write(socket, tmp_buf, len);
代碼很簡單,雖然就兩行代碼,但是這裡面發生了不少的事情。
首先,期間共發生了 4 次使用者态與核心态的上下文切換,因為發生了兩次系統調用,一次是
read()
,一次是
write()
,每次系統調用都得先從使用者态切換到核心态,等核心完成任務後,再從核心态切換回使用者态。
上下文切換到成本并不小,一次切換需要耗時幾十納秒到幾微秒,雖然時間看上去很短,但是在高并發的場景下,這類時間容易被累積和放大,進而影響系統的性能。
其次,還發生了 4 次資料拷貝,其中兩次是 DMA 的拷貝,另外兩次則是通過 CPU 拷貝的,下面說一下這個過程:
- 第一次拷貝,把磁盤上的資料拷貝到作業系統核心的緩沖區裡,這個拷貝的過程是通過 DMA 搬運的。
- 第二次拷貝,把核心緩沖區的資料拷貝到使用者的緩沖區裡,于是我們應用程式就可以使用這部分資料了,這個拷貝到過程是由 CPU 完成的。
- 第三次拷貝,把剛才拷貝到使用者的緩沖區裡的資料,再拷貝到核心的 socket 的緩沖區裡,這個過程依然還是由 CPU 搬運的。
- 第四次拷貝,把核心的 socket 緩沖區裡的資料,拷貝到網卡的緩沖區裡,這個過程又是由 DMA 搬運的。
我們回過頭看這個檔案傳輸的過程,我們隻是搬運一份資料,結果卻搬運了 4 次,過多的資料拷貝無疑會消耗 CPU 資源,大大降低了系統性能。
這種簡單又傳統的檔案傳輸方式,存在備援的上文切換和資料拷貝,在高并發系統裡是非常糟糕的,多了很多不必要的開銷,會嚴重影響系統性能。
是以,要想提高檔案傳輸的性能,就需要減少「使用者态與核心态的上下文切換」和「記憶體拷貝」的次數。
如何優化檔案傳輸的性能?
先來看看,如何減少「使用者态與核心态的上下文切換」的次數呢?
讀取磁盤資料的時候,之是以要發生上下文切換,這是因為使用者空間沒有權限操作磁盤或網卡,核心的權限最高,這些操作裝置的過程都需要交由作業系統核心來完成,是以一般要通過核心去完成某些任務的時候,就需要使用作業系統提供的系統調用函數。
而一次系統調用必然會發生 2 次上下文切換:首先從使用者态切換到核心态,當核心執行完任務後,再切換回使用者态交由程序代碼執行。
是以,要想減少上下文切換到次數,就要減少系統調用的次數。
再來看看,如何減少「資料拷貝」的次數?
在前面我們知道了,傳統的檔案傳輸方式會曆經 4 次資料拷貝,而且這裡面,「從核心的讀緩沖區拷貝到使用者的緩沖區裡,再從使用者的緩沖區裡拷貝到 socket 的緩沖區裡」,這個過程是沒有必要的。
因為檔案傳輸的應用場景中,在使用者空間我們并不會對資料「再加工」,是以資料實際上可以不用搬運到使用者空間,是以使用者的緩沖區是沒有必要存在的。
如何實作零拷貝
零拷貝技術實作的方式通常有 2 種:
- mmap + write
- sendfile
下面就談一談,它們是如何減少「上下文切換」和「資料拷貝」的次數。
mmap + write
在前面我們知道,
read()
系統調用的過程中會把核心緩沖區的資料拷貝到使用者的緩沖區裡,于是為了減少這一步開銷,我們可以用
mmap()
替換
read()
系統調用函數。
buf = mmap(file, len);
write(sockfd, buf, len);
mmap()
系統調用函數會直接把核心緩沖區裡的資料「映射」到使用者空間,這樣,作業系統核心與使用者空間就不需要再進行任何的資料拷貝操作。
具體過程如下:
- 應用程序調用了
後,DMA 會把磁盤的資料拷貝到核心的緩沖區裡。接着,應用程序跟作業系統核心「共享」這個緩沖區;mmap()
- 應用程序再調用
,作業系統直接将核心緩沖區的資料拷貝到 socket 緩沖區中,這一切都發生在核心态,由 CPU 來搬運資料;write()
- 最後,把核心的 socket 緩沖區裡的資料,拷貝到網卡的緩沖區裡,這個過程是由 DMA 搬運的。
我們可以得知,通過使用
mmap()
來代替
read()
, 可以減少一次資料拷貝的過程。
但這還不是最理想的零拷貝,因為仍然需要通過 CPU 把核心緩沖區的資料拷貝到 socket 緩沖區裡,而且仍然需要 4 次上下文切換,因為系統調用還是 2 次。
sendfile
在 Linux 核心版本 2.1 中,提供了一個專門發送檔案的系統調用函數
sendfile()
,函數形式如下:
#include <sys/socket.h>
ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);
它的前兩個參數分别是目的端和源端的檔案描述符,後面兩個參數是源端的偏移量和複制資料的長度,傳回值是實際複制資料的長度。
首先,它可以替代前面的
read()
和
write()
這兩個系統調用,這樣就可以減少一次系統調用,也就減少了 2 次上下文切換的開銷。
其次,該系統調用,可以直接把核心緩沖區裡的資料拷貝到 socket 緩沖區裡,不再拷貝到使用者态,這樣就隻有 2 次上下文切換,和 3 次資料拷貝。如下圖:
但是這還不是真正的零拷貝技術,如果網卡支援 SG-DMA(The Scatter-Gather Direct Memory Access)技術(和普通的 DMA 有所不同),我們可以進一步減少通過 CPU 把核心緩沖區裡的資料拷貝到 socket 緩沖區的過程。
你可以在你的 Linux 系統通過下面這個指令,檢視網卡是否支援 scatter-gather 特性:
$ ethtool -k eth0 | grep scatter-gather
scatter-gather: on
于是,從 Linux 核心
2.4
版本開始起,對于支援網卡支援 SG-DMA 技術的情況下,
sendfile()
系統調用的過程發生了點變化,具體過程如下:
- 第一步,通過 DMA 将磁盤上的資料拷貝到核心緩沖區裡;
- 第二步,緩沖區描述符和資料長度傳到 socket 緩沖區,這樣網卡的 SG-DMA 控制器就可以直接将核心緩存中的資料拷貝到網卡的緩沖區裡,此過程不需要将資料從作業系統核心緩沖區拷貝到 socket 緩沖區中,這樣就減少了一次資料拷貝;
是以,這個過程之中,隻進行了 2 次資料拷貝,如下圖:
這就是所謂的零拷貝(Zero-copy)技術,因為我們沒有在記憶體層面去拷貝資料,也就是說全程沒有通過 CPU 來搬運資料,所有的資料都是通過 DMA 來進行傳輸的。
零拷貝技術的檔案傳輸方式相比傳統檔案傳輸的方式,減少了 2 次上下文切換和資料拷貝次數,隻需要 2 次上下文切換和資料拷貝次數,就可以完成檔案的傳輸,而且 2 次的資料拷貝過程,都不需要通過 CPU,2 次都是由 DMA 來搬運。
是以,總體來看,零拷貝技術可以把檔案傳輸的性能提高至少一倍以上。
PageCache有什麼作用?
回顧前面說道檔案傳輸過程,其中第一步都是先需要先把磁盤檔案資料拷貝「核心緩沖區」裡,這個「核心緩沖區」實際上是磁盤高速緩存(PageCache)。
由于零拷貝使用了 PageCache 技術,可以使得零拷貝進一步提升了性能,我們接下來看看 PageCache 是如何做到這一點的。
讀寫磁盤相比讀寫記憶體的速度慢太多了,是以我們應該想辦法把「讀寫磁盤」替換成「讀寫記憶體」。于是,我們會通過 DMA 把磁盤裡的資料搬運到記憶體裡,這樣就可以用讀記憶體替換讀磁盤。
但是,記憶體空間遠比磁盤要小,記憶體注定隻能拷貝磁盤裡的一小部分資料。
那問題來了,選擇哪些磁盤資料拷貝到記憶體呢?
我們都知道程式運作的時候,具有「局部性」,是以通常,剛被通路的資料在短時間内再次被通路的機率很高,于是我們可以用 PageCache 來緩存最近被通路的資料,當空間不足時淘汰最久未被通路的緩存。
是以,讀磁盤資料的時候,優先在 PageCache 找,如果資料存在則可以直接傳回;如果沒有,則從磁盤中讀取,然後緩存 PageCache 中。
還有一點,讀取磁盤資料的時候,需要找到資料所在的位置,但是對于機械磁盤來說,就是通過磁頭旋轉到資料所在的扇區,再開始「順序」讀取資料,但是旋轉磁頭這個實體動作是非常耗時的,為了降低它的影響,PageCache 使用了「預讀功能」。
比如,假設 read 方法每次隻會讀
32 KB
的位元組,雖然 read 剛開始隻會讀 0 ~ 32 KB 的位元組,但核心會把其後面的 32~64 KB 也讀取到 PageCache,這樣後面讀取 32~64 KB 的成本就很低,如果在 32~64 KB 淘汰出 PageCache 前,程序讀取到它了,收益就非常大。
是以,PageCache 的優點主要是兩個:
- 緩存最近被通路的資料;
- 預讀功能;
這兩個做法,将大大提高讀寫磁盤的性能。
但是,在傳輸大檔案(GB 級别的檔案)的時候,PageCache 會不起作用,那就白白浪費 DMA 多做的一次資料拷貝,造成性能的降低,即使使用了 PageCache 的零拷貝也會損失性能
這是因為如果你有很多 GB 級别檔案需要傳輸,每當使用者通路這些大檔案的時候,核心就會把它們載入 PageCache 中,于是 PageCache 空間很快被這些大檔案占滿。
另外,由于檔案太大,可能某些部分的檔案資料被再次通路的機率比較低,這樣就會帶來 2 個問題:
- PageCache 由于長時間被大檔案占據,其他「熱點」的小檔案可能就無法充分使用到 PageCache,于是這樣磁盤讀寫的性能就會下降了;
- PageCache 中的大檔案資料,由于沒有享受到緩存帶來的好處,但卻耗費 DMA 多拷貝到 PageCache 一次;
是以,針對大檔案的傳輸,不應該使用 PageCache,也就是說不應該使用零拷貝技術,因為可能由于 PageCache 被大檔案占據,而導緻「熱點」小檔案無法利用到 PageCache,這樣在高并發的環境下,會帶來嚴重的性能問題。
大檔案傳輸用什麼方式實作?
那針對大檔案的傳輸,我們應該使用什麼方式呢?
我們先來看看最初的例子,當調用 read 方法讀取檔案時,程序實際上會阻塞在 read 方法調用,因為要等待磁盤資料的傳回,如下圖:
具體過程:
- 當調用 read 方法時,會阻塞着,此時核心會向磁盤發起 I/O 請求,磁盤收到請求後,便會尋址,當磁盤資料準備好後,就會向核心發起 I/O 中斷,告知核心磁盤資料已經準備好;
- 核心收到 I/O 中斷後,就将資料從磁盤控制器緩沖區拷貝到 PageCache 裡;
- 最後,核心再把 PageCache 中的資料拷貝到使用者緩沖區,于是 read 調用就正常傳回了。
對于阻塞的問題,可以用異步 I/O 來解決,它工作方式如下圖:
它把讀操作分為兩部分:
- 前半部分,核心向磁盤發起讀請求,但是可以不等待資料就位就可以傳回,于是程序此時可以處理其他任務;
- 後半部分,當核心将磁盤中的資料拷貝到程序緩沖區後,程序将接收到核心的通知,再去處理資料;
而且,我們可以發現,異步 I/O 并沒有涉及到 PageCache,是以使用異步 I/O 就意味着要繞開 PageCache。
繞開 PageCache 的 I/O 叫直接 I/O,使用 PageCache 的 I/O 則叫緩存 I/O。通常,對于磁盤,異步 I/O 隻支援直接 I/O。
前面也提到,大檔案的傳輸不應該使用 PageCache,因為可能由于 PageCache 被大檔案占據,而導緻「熱點」小檔案無法利用到 PageCache。
于是,在高并發的場景下,針對大檔案的傳輸的方式,應該使用「異步 I/O + 直接 I/O」來替代零拷貝技術。
直接 I/O 應用場景常見的兩種:
- 應用程式已經實作了磁盤資料的緩存,那麼可以不需要 PageCache 再次緩存,減少額外的性能損耗。在 MySQL 資料庫中,可以通過參數設定開啟直接 I/O,預設是不開啟;
- 傳輸大檔案的時候,由于大檔案難以命中 PageCache 緩存,而且會占滿 PageCache 導緻「熱點」檔案無法充分利用緩存,進而增大了性能開銷,是以,這時應該使用直接 I/O。
另外,由于直接 I/O 繞過了 PageCache,就無法享受核心的這兩點的優化:
- 核心的 I/O 排程算法會緩存盡可能多的 I/O 請求在 PageCache 中,最後「合并」成一個更大的 I/O 請求再發給磁盤,這樣做是為了減少磁盤的尋址操作;
- 核心也會「預讀」後續的 I/O 請求放在 PageCache 中,一樣是為了減少對磁盤的操作;
于是,傳輸大檔案的時候,使用「異步 I/O + 直接 I/O」了,就可以無阻塞地讀取檔案了。
是以,傳輸檔案的時候,我們要根據檔案的大小來使用不同的方式:
- 傳輸大檔案的時候,使用「異步 I/O + 直接 I/O」;
- 傳輸小檔案的時候,則使用「零拷貝技術」;
在 Nginx 中,我們可以用如下配置,來根據檔案的大小來使用不同的方式:
location /video/ {
sendfile on;
aio on;
directio 1024m;
}