目錄
文章目錄
前文列表
網卡的工作原理
Linux 操作系統的功能可以概括為進程管理、內存管理、文件系統管理、設備管理和計算機網絡等幾部分。所有的操作系統執行最終都可以映射到對物理設備的操作。除去對 CPU、內存等處理機設備的操作之外,操作系統對其他外部設備的操作都通過專門的驅動程序完成。操作系統的每種外設在內核中都必須有對應的設備驅動程序對其進行處理。所以分析網卡的工作原理即是分析網卡的驅動程序。
網絡是獨立的一個模塊。為了屏蔽物理網絡設備的多樣性,Linux 內核協議棧實現中,對底層物理設備進行了抽象並定義了一個統一的概念,稱之為 Socket 接口。所有對網絡硬件的訪問都是通過接口完成的,接口提供了一個抽象而統一的操作集合來處理基本數據報文的發送和接收。一個網絡接口就被看作是一個發送和接收數據包的實體。
對於每個網絡接口,都用一個 net_device 的數據結構來表示。net_device 中有很多提供系統訪問和協議層調用的設備方法,包括提供設備初始化和往系統注冊用的 init 函數,打開和關閉網絡設備的 open 和 stop 函數,處理數據包發送的函數 hard_start_xmit,以及中斷處理函數。
所有被發送和接收的數據報文都用 sk_buff 結構表示。要發送數據時,內核網絡協議棧將根據系統路由表選擇相應的網絡接口進行數據傳輸;當接收數據包時,通過驅動程序注冊的中斷服務程序進行數據的接口處理。
網卡與網卡適配器
我們知道計算機的輸入輸出系統由外部硬件設備(e.g. 網卡)及其與主機之間的控制部件(Controller)所構成,其中控制部件常被稱為設備控制器、設備適配器、設備驅動或 I/O 接口,主要負責控制並實現主機與外設之間的數據傳輸。
首先明確一下術語,在本文中,網卡指物理網絡設備卡、網卡適配器指網卡設備控制器,即安裝在操作系統上的網絡設備驅動。
網絡設備驅動在 Linux 內核中是以內核模塊的形式存在的。所以對於網卡驅動的初始化,同樣需要提供一個內核模塊初始化函數來完成的,初始化網絡設備的硬件寄存器、配置 DMA 以及初始化相關內核變量等。
設備初始化函數在內核模塊被加載時調用,包括:
- 初始化 PHY 模塊,包括設置雙工/半雙工運行模式、設備運行速率和自協商模式等。
- 初始化 MAC 模塊,包括設置設備接口模式等。
- 初始化 DMA 模塊,包括建立 BD(Buffer descriptor)表、設置 BD 屬性以及給 BD 分配緩存等。
網卡的組成
網卡工作在物理層和數據鏈路層,主要由 PHY/MAC 芯片、Tx/Rx FIFO、DMA 等組成,其中網線通過變壓器接 PHY 芯片、PHY 芯片通過 MII 接 MAC 芯片、MAC 芯片接 PCI 總線。
- PHY 芯片主要負責:CSMA/CD、模數轉換、編解碼、串並轉換。
- MAC 芯片主要負責:
- 比特流和數據幀的轉換(7 字節的前導碼 Preamble 和 1 字節的幀首定界符 SFD)
- CRC 校驗
- Packet Filtering(L2 Filtering、VLAN Filtering、Manageability/Host Filtering)
- Tx/Rx FIFO:Tx 表示發送(Transport),Rx 是接收(Receive)。
- DMA(Direct Memory Access):直接存儲器存取 I/O 模塊。
CPU 與網卡的協同
以往,從網卡的 I/O 區域,包括 I/O 寄存器或 I/O 內存中讀取數據,這都要 CPU 親自去讀,然后把數據放到 RAM 中,也就占用了 CPU 的運算資源。直到出現了 DMA 技術,其基本思想是外設和 RAM 之間開辟直接的數據傳輸通路。一般情況下,總線所有的工作周期(總線周期)都用於 CPU 執行程序。DMA 控制就是當外設完成數據 I/O 的准備工作之后,會占用總線的一個工作周期,和 RAM 直接交換數據。這個周期之后,CPU 又繼續控制總線執行原程序。如此反復的,直到整個數據塊的數據全部傳輸完畢,從而解放了 CPU。
- 首先,內核在 RAM 中為收發數據建立一個環形的緩沖隊列,通常叫 DMA 環形緩沖區,又叫 BD(Buffer descriptor)表。
- 內核將這個緩沖區通過 DMA 映射,把這個隊列交給網卡;
- 網卡收到數據,就直接放進這個環形緩沖區,也就是直接放進 RAM 了;
- 然后,網卡驅動向系統產生一個中斷,內核收到這個中斷,就取消 DMA 映射,這樣,內核就直接從主內存中讀取數據;
網絡設配器的收包流程
- 網卡驅動申請 Rx descriptor ring,本質是一致性 DMA 內存,保存了若干的 descriptor。將 Rx descriptor ring 的總線地址寫入網卡寄存器 RDBA。
- 網卡驅動為每個 descriptor 分配 skb_buff 數據緩存區,本質上是在內存中分配的一片緩沖區用來接收數據幀。將數據緩存區的總線地址保存到 descriptor。
- 網卡接收到高低電信號。
- PHY 芯片首先進行數模轉換,即將電信號轉換為比特流。
- MAC 芯片再將比特流轉換為數據幀(Frame)。
- 網卡驅動將數據幀寫入 Rx FIFO。
- 網卡驅動找到 Rx descriptor ring 中下一個將要使用的 descriptor。
- 網卡驅動使用 DMA 通過 PCI 總線將 Rx FIFO 中的數據包復制到 descriptor 保存的總線地址指向的數據緩存區中。其實就是復制到 skb_buff 中。
- 因為是 DMA 寫入,所以內核並沒有監控數據幀的寫入情況。所以在復制完后,需要由網卡驅動啟動硬中斷通知 CPU 數據緩存區中已經有新的數據幀了。每一個硬件中斷會對應一個中斷號,CPU 執行硬下述中斷函數。實際上,硬中斷的中斷處理程序,最終是通過調用網卡驅動程序來完成的。硬中斷觸發的驅動程序首先會暫時禁用網卡硬中斷,意思是告訴網卡再來新的數據就先不要觸發硬中斷了,只需要把數據幀通過 DMA 拷入主存即可。
- NAPI(以 e1000 網卡為例):
e1000_intr() -> __napi_schedule() -> __raise_softirq_irqoff(NET_RX_SOFTIRQ)
- 非 NAPI(以 dm9000 網卡為例):
dm9000_interrupt() -> dm9000_rx() -> netif_rx() -> napi_schedule() -> __napi_schedule() -> __raise_softirq_irqoff(NET_RX_SOFTIRQ)
- NAPI(以 e1000 網卡為例):
- 硬中斷后繼續啟動軟中斷,啟用軟中斷目的是將數據幀的后續處理流程交給軟中斷處理程序異步的慢慢處理。此時網卡驅動就退出硬件中斷了,其他外設可以繼續調用操作系統的硬件中斷。但網絡 I/O 相關的硬中斷,需要等到軟中斷處理完成並再次開啟硬中斷后,才能被再次觸發。ksoftirqd 執行軟中斷函數
net_rx_action()
:- NAPI(以 e1000 網卡為例),觸發
napi()
系統調用,napi()
逐一消耗 Rx Ring Buffer 指向的 skb_buff 中的數據包:net_rx_action() -> e1000_clean() -> e1000_clean_rx_irq() -> e1000_receive_skb() -> netif_receive_skb()
- 非 NAPI(以 dm9000 網卡為例):
net_rx_action() -> process_backlog() -> netif_receive_skb()
- NAPI(以 e1000 網卡為例),觸發
- 網卡驅動通過
netif_receive_skb()
將 sk_buff 上送到協議棧。 - 重新開啟網絡 I/O 硬件中斷,有新的數據幀到來時可以繼續觸發網絡 I/O 硬件中斷,繼續通知 CPU 來消耗數據幀。
傳統方式和 NAPI 方式
值得注意的是,傳統收包是每個報文都觸發中斷,如果中斷太頻繁,CPU 就總是處理中斷,其他任務無法得到調度,於是 NAPI(New API)收包方式出現了,其思路是采用「中斷+輪詢」的方式收包以提高吞吐。NAPI 收包需要網卡驅動支持,例如 Intel e1000 系列網卡。下圖為傳統方式和 NAPI 方式收包流程差異:
中斷方式與輪詢方式
Linux 內核在接收數據時有兩種方式可供選擇,一種是中斷方式,另外一種是輪詢方式。
從本質上來講,中斷,是一種電信號,當設備有某種事件發生的時候,它就會產生中斷,通過總線把電信號發送給中斷控制器,如果中斷的線是激活的,中斷控制器就把電信號發送給處理器的某個特定引腳。處理器於是立即停止自己正在做的事,跳到內存中內核設置的中斷處理程序的入口點,進行中斷處理。
使用中斷方式,首先在使用該驅動之前,需要將該中斷對應的中斷類型號和中斷處理程序注冊進去。網卡驅動在初始化時會將具體的 xx_open 函數掛接在驅動的 open 接口上。網卡的中斷一般會分為兩種,一種是發送中斷,另一種是接收中斷。Linux 內核需要分別對這兩種中斷類型號進行注冊。對於中斷方式來說,由於每收到一個包都會產生一個中斷,而處理器會迅速跳到中斷服務程序中去處理收包,因此中斷接收方式的實時性高,但如果遇到數據包流量很大的情況時,過多的中斷會增加系統的負荷。
- 發送中斷處理程序(xx_isr_tx)的工作主要是監控數據發送狀態、更新數據發送統計等。
- 接收中斷處理程序(xx_isr_rx)的工作主要是接收數據並傳遞給協議層、監控數據接收狀態、更新數據接收統計等。
如果采用輪詢方式,就不需要使能網卡的中斷狀態,也不需要注冊中斷處理程序。操作系統會專門開啟一個任務去定時檢查 BD 表,如果發現當前指針指向的 BD 非空閑,則將該 BD 對應的數據取出來,並恢復 BD 的空閑狀態。由於是采用任務定時檢查的原理,從而輪詢接收方式的實時性較差,但它沒有中斷那種系統上下文切換的開銷,因此輪詢方式在處理大流量數據包時會顯得更加高效。
網絡設配器的發包過程
NOTE:發包過程只作為簡單介紹。
- 網卡驅動創建 Tx descriptor ring,將 Tx descriptor ring 的總線地址寫入網卡寄存器 TDBA。
- 協議棧通過
dev_queue_xmit()
將 sk_buffer 下送到網卡驅動。 - 網卡驅動將 sk_buff 放入 Tx descriptor ring,更新網卡寄存器 TDT。
- DMA 感知到 TDT 的改變后,找到 Tx descriptor ring 中下一個將要使用的 descriptor。
- DMA 通過 PCI 總線將 descriptor 的數據緩存區復制到 Tx FIFO。
- 復制完后,通過 MAC 芯片將數據包發送出去。
- 發送完后,網卡更新網卡寄存器 TDH,啟動硬中斷通知 CPU 釋放數據緩存區中的數據包。
sk_buff(Socket Buffer)
Linux 內核中,用 sk_buff(skb)來描述一個緩存,所謂分配緩存空間,就是建立一定數量的 sk_buff。sk_buff 是 Linux 內核網絡協議棧實現中最重要的結構體,它是網絡數據報文在內核中的表現形式。用戶態應用程序(應用層)可以通過系統調用接口訪問 BSD Socket 層,傳遞給 Socket 的數據首先會保存在 sk_buff 對應的緩沖區中,sk_buff 的結構定義在 include/linux/skbuff.h 文件中。它保存數據報文的結構為一個雙向鏈表,如下所示:
當數據被儲存到了 sk_buff 緩存區中,網卡驅動的發送函數 hard_start_xmit 也隨之被調用,流程圖如下所示:
- 首先創建一個 Socket,然后調用 write() 之類的寫函數通過 Socket 訪問網卡驅動,同時將數據保存在 sk_buff 緩沖區。
- Socket 調用發送函數 hard_start_xmit。hard_start_xmit 函數在初始化過程中會被掛接成類似於 xx_tx 的某個具體的發送函數,xx_tx 主要實現如下步驟:
- 從 Tx BD 表中取出一個空閑的 BD。
- 根據 sk_buff 中保存的數據修改 BD 的屬性,一個是數據長度,另一個是數據報文緩存指針。值得注意的是,數據報文緩存指針對應的必須是物理地址,這是因為 DMA 在獲取 BD 中對應的數據時只能識別物理地址。
- 修改該 BD 的狀態為就緒態,DMA 模塊將自動發送處於就緒態 BD 中所對應的數據。
- 移動發送 BD 表的指針指向下一個 BD。
- DMA 開始將處於就緒態 BD 緩存內的數據發送至網絡,當發送完成后自動恢復該 BD 為空閑態。
當網卡接收到數據時,DMA 會自動將數據保存起來並通知 CPU 處理,CPU 通過中斷或輪詢的方式發現有數據接收進來后,再將數據保存到 sk_buff 緩沖區中,並通過 Socket 接口讀出來。流程圖如下所示:
- 網卡接收到數據后,DMA 搜索 Rx BD 表,取出空閑的 BD,並將數據自動保存到該 BD 的緩存中,修改 BD 為就緒態,並同時觸發中斷(該步驟可選)。
- 處理器可以通過中斷或者輪詢的方式檢查接收 BD 表的狀態,無論采用哪種方式,它們都需要實現以下步驟。
- 從接收 BD 表中取出一個空閑的 BD。
- 如果當前 BD 為就緒態,檢查當前 BD 的數據狀態,更新數據接收統計。
- 從 BD 中取出數據保存在 sk_buff 的緩沖區中。
- 更新 BD 的狀態為空閑態。
- 移動接收 BD 表的指針指向下一個 BD。
- 用戶調用 read 之類的讀函數,從 sk_buff 緩沖區中讀出數據,同時釋放該緩沖區。
DMA 與 Buffer descriptor
網卡驅動會在 RAM 中建立並為例兩個環形隊列,稱為 BD(Buffer descriptor)表,一個收(Rx)、一個發(Tx),每一個表項稱為 descriptor(描述符)。descriptor 所存放的內容是由 CPU 決定的,一般會存放 descriptor 所指代的 Data buffer(實際的數據存儲空間)的指針、數據長度以及一些標志位。
Rx/Tx 的 BD 表首地址分別存放於 CPU 的寄存器中,這樣 CPU 就可以通過 BD 表項中的指針,索引到實際 Data buffer 的數據存儲空間。每使用一次 DMA 傳輸數據,DB 表項就會下移一個。所以,DMA 並不是直接操作 Data Buffer 的,而是通過 descriptor 索引真實數據再執行傳輸。
Linux 內核通過調用 dma_map_single(struct device *dev,void *buffer,size_t size,enum dma_data_direction direction)
來建立 DMA 映射關系。
struct device *dev
:描述一個設備;buffer
:把哪個地址映射給設備,也就是某一個skb。要映射全部,做一個雙向鏈表的循環即可;size
:緩存大小;direction
:映射方向,即誰傳給誰。一般來說,是雙向映射,數據得以在設備和內存之間雙向流動;
對於 PCI 設備而言,通過函數 pci_map_single 把 buffer 交給設備,設備可以直接從里邊讀/取數據。
參考文檔
https://blog.csdn.net/zhangtaoym/article/details/75948505
https://blog.csdn.net/jiangganwu/article/details/83037139
https://blog.csdn.net/kklvsports/article/details/74452953
https://wenku.baidu.com/view/1d8f60bc1a37f111f1855bed.html
https://blog.csdn.net/sdulibh/article/details/46843011
https://blog.csdn.net/YuZhiHui_No1/article/details/38666589
https://blog.csdn.net/YuZhiHui_No1/column/info/linux-skb