LINUX 網絡子系統中 DMA 機制的實現

我們先從計算機組成原理的層面介紹 DMA,再簡單介紹 Linux 網絡子系統的 DMA 機制是如何的實現的。

一、計算機組成原理中的 DMA

以往的 I/O 設備和主存交換信息都要經過 CPU 的操作。不論是最早的輪詢方式,還是我們學過的中斷方式。雖然中斷方式相比輪詢方式已經節省了大量的 CPU 資源。但是在處理大量的數據時,DMA 相比中斷方式進一步解放了 CPU。

DMA 就是 Direct Memory Access,意思是 I/O 設備直接存儲器訪問,幾乎不消耗 CPU 的資源。在 I/O 設備和主存傳遞數據的時候,CPU 可以處理其他事。

1. I/O 設備與主存信息傳送的控制方式

I/O 設備與主存信息傳送的控制方式分爲程序輪詢、中斷、DMA、RDMA 等。

先用 “圖 1” 大體上說明幾種控制方式的區別,其中黃線代表程序輪詢方式,綠線代表中斷方式,紅線代表 DMA 方式,黑線代表 RDMA 方式,藍線代表公用的線。可以看出 DMA 方式與程序輪詢方式還有中斷方式的區別是傳輸數據跳過了 CPU,直接和主存交流。

“圖 1”中的 “接口” 既包括實現某一功能的硬件電路,也包括相應的控制軟件,如 “DMA 接口” 就是一些實現 DMA 機制的硬件電路和相應的控制軟件。

“DMA 接口” 有時也叫做 “DMA 控制器”(DMAC)。

上週分享 “圖 1” 時,劉老師說在 DMA 方式下, DMA 控制器(即 DMA 接口)也是需要和 CPU 交流的,但是圖中沒有顯示 DMA 控制器與 CPU 交流信息。但是這張圖我是按照哈工大劉宏偉老師的《計算機組成原理》第五章的內容畫出的,應該是不會有問題的。查找了相關資料,覺得兩個劉老師都沒有錯,因爲這張圖強調的是數據的走向,即這裏的線僅是數據線。如果要嚴格一點,把控制線和地址線也畫出來,將是 “圖 2” 這個樣子:

“圖 2”對 “圖 1” 的數據線加粗,新增細實線表示地址線,細虛線表示控制線。可以看出在中斷方式下,無論是傳輸數據、地址還是控制信息,都要經過 CPU,即都要在 CPU 的寄存器中暫存一下,都要浪費 CPU 的資源;但是在 DMA 方式下,傳輸數據和地址時,I/O 設備可以通過 “DMA 接口” 直接與主存交流,只有傳輸控制信息時,才需要用到 CPU。而傳輸控制信息佔用的時間是極小的,可以忽略不計,所以可以認爲 DMA 方式完全沒有佔用 CPU 資源,這等價於 I/O 設備和 CPU 可以實現真正的並行工作,這比中斷方式下的並行程度要更高很多。

2. 三種方式的 CPU 工作效率比較

在 I/O 準備階段,程序輪詢方式的 CPU 一直在查詢等待,而中斷方式的 CPU 可以繼續執行現行程序,但是當 I/O 準備就緒,設備向 CPU 發出中斷請求,CPU 響應以實現數據的傳輸,這個過程會佔用 CPU 一段時間,而且這段時間比使用程序輪詢方式的 CPU 傳輸數據的時間還要長,因爲 CPU 除了傳輸數據還要做一些準備工作,如把 CPU 寄存器中的數據都轉移到棧中。

但是 DMA 方式不一樣,當 I/O 準備就緒,設備向 CPU 發出 DMA 請求,CPU 響應請求,關閉對主存的控制器,只關閉一個或者幾個存取週期,在這一小段時間內,主存和設備完成數據交換。而且在這一小段時間內,CPU 並不是什麼都不能做,雖然 CPU 不能訪問主存,即不能取指令,但是 CPU 的 cache 中已經保存了一些指令,CPU 可以先執行這些指令,只要這些指令不涉及訪存,CPU 和設備還是並行執行。數據傳輸完成後,DMA 接口向 CPU 發出中斷請求,讓 CPU 做後續處理。大家可能會奇怪 DMA 接口爲什麼也能發出中斷請求,其實 DMA 接口內有一箇中斷機構,見 “圖 3”,DMA 技術其實是建立在中斷技術之上的,它包含了中斷技術。

總之,在同樣的時間內,DMA 方式下 CPU 執行現行程序的時間最長,即 CPU 的效率最高。

二、Linux 網絡子系統中 DMA 機制的實現
1. DMA 機制在 TCP/IP 協議模型中的位置

網卡明顯是一個數據流量特別大的地方,所以特別需要 DMA 方式和主存交換數據。

主存的內核空間中爲接收和發送數據分別建立了兩個環形緩衝區(Ring Buffer)。分別叫接受環形緩衝區(Receive Ring Buffer)和發送環形緩衝區(Send Ring Buffer),通常也叫 DMA 環形緩衝區。

下圖可以看到 DMA 機制位於 TCP/IP 協議模型中的位置數據鏈路層。

網卡通過 DMA 方式將數據發送到 Receive Ring Buffer, 然後 Receive Ring Buffer 把數據包傳給 IP 協議所在的網絡層,然後再由路由機制傳給 TCP 協議所在的傳輸層,最終傳給用戶進程所在的應用層。下一節在數據鏈路層上分析具體分析網卡是如何處理數據包的。

2. 數據鏈路層上網卡對數據包的處理

DMA 環形緩衝區建立在與處理器共享的內存中。每一個輸入數據包被放置在環形緩衝區中下一個可用緩衝區,然後發出中斷。接着驅動程序將網絡數據包傳給內核的其它部分處理,並在環形緩衝區中放置一個新的 DMA 緩衝區。

驅動程序在初始化時分配 DMA 緩衝區,並使用驅動程序直到停止運行。

準備工作:

系統啓動時網卡(NIC)進行初始化,在內存中騰出空間給 Ring Buffer 。Ring Buffer 隊列每個中的每個元素 Packet Descriptor 指向一個 sk_buff ,狀態均爲 ready

上圖中虛線步驟的解釋:

後續處理:

poll 函數清理 sk_buff,清理 Ring Buffer 上的 Descriptor 將其指向新分配的 sk_buff 並將狀態設置爲 ready。

3. 源碼分析具體網卡(4.19 內核)

Intel 的千兆以太網卡 e1000 使用非常廣泛,我虛擬機上的網卡就是它。

這裏就以該網卡的驅動程序爲例,初步分析它是怎麼建立 DMA 機制的。

源碼目錄及文件:

內核模塊插入函數在 e1000_main.c 文件中,它是加載驅動程序時調用的第一個函數。

/**
* e1000_init_module - Driver Registration Routine
*
* e1000_init_module is the first routine called when the driver is
* loaded. All it does is register with the PCI subsystem.
**/static int __init e1000_init_module(void){int ret;
pr_info("%s - version %s\n", e1000_driver_string, e1000_driver_version);

pr_info("%s\n", e1000_copyright);

ret = pci_register_driver(&e1000_driver);if (copybreak != COPYBREAK_DEFAULT) {if (copybreak == 0)
pr_info("copybreak disabled\n");elsepr_info("copybreak enabled for "   "packets <= %u bytes\n", copybreak);
}return ret;
}

module_init(e1000_init_module);

該函數所做的只是向 PCI 子系統註冊,這樣 CPU 就可以訪問網卡了,因爲 CPU 和網卡是通過 PCI 總線相連的。

具體做法是,在第 230 行,通過 pci_register_driver() 函數將 e1000_driver 這個驅動程序註冊到 PCI 子系統。

e1000_driver 是 struct pci_driver 類型的結構體,

static struct pci_driver e1000_driver = {
.name     = e1000_driver_name,
.id_table = e1000_pci_tbl,
.probe    = e1000_probe,
.remove   = e1000_remove,#ifdef CONFIG_PM/* Power Management Hooks */.suspend  = e1000_suspend,
.resume   = e1000_resume,#endif.shutdown = e1000_shutdown,
.err_handler = &e1000_err_handler
};

e1000_driver``` 裏面初始化了設備的名字爲 “e1000”,

還定義了一些操作,如插入新設備、移除設備等,還包括電源管理相關的暫停操作和喚醒操作。下面是 struct pci_driver 一些主要的域。

對該驅動程序稍微瞭解後,先跳過其他部分,直接看 DMA 相關代碼。在 e1000_probe 函數,即 “插入新設備” 函數中,下面這段代碼先對 DMA 緩衝區的大小進行檢查

如果是 64 位 DMA 地址,則把 pci_using_dac 標記爲 1,表示可以使用 64 位硬件,掛起 32 位的硬件;如果是 32 位 DMA 地址,則使用 32 位硬件;若不是 64 位也不是 32 位,則報錯 “沒有可用的 DMA 配置,中止程序”。

/* there is a workaround being applied below that limits
 * 64-bit DMA addresses to 64-bit hardware. There are some
 * 32-bit adapters that Tx hang when given 64-bit DMA addresses
 */pci_using_dac = 0;if ((hw->bus_type == e1000_bus_type_pcix) &&
    !dma_set_mask_and_coherent(&pdev->dev, DMA_BIT_MASK(64))) {
pci_using_dac = 1;
} else {
err = dma_set_mask_and_coherent(&pdev->dev, DMA_BIT_MASK(32));if (err) {
pr_err("No usable DMA config, aborting\n");goto err_dma;
}
}

其中的函數 dma_set_mask_and_coherent() 用於對 dma_mask 和 coherent_dma_mask 賦值。

dma_mask 表示的是該設備通過 DMA 方式可尋址的物理地址範圍,coherent_dma_mask 表示所有設備通過 DMA 方式可尋址的公共的物理地址範圍,

因爲不是所有的硬件設備都能夠支持 64bit 的地址寬度。

/include/linux/dma-mapping.h

/*
* Set both the DMA mask and the coherent DMA mask to the same thing.
* Note that we don't check the return value from dma_set_coherent_mask()
* as the DMA API guarantees that the coherent DMA mask can be set to
* the same or smaller than the streaming DMA mask.
*/static inline int dma_set_mask_and_coherent(struct device *dev, u64 mask){int rc = dma_set_mask(dev, mask);if (rc == 0)
dma_set_coherent_mask(dev, mask);return rc;
}

rc==0 表示該設備的 dma_mask 賦值成功,所以可以接着對 coherent_dma_mask 賦同樣的值。

繼續閱讀 e1000_probe 函數,

if (pci_using_dac) {
netdev->features |= NETIF_F_HIGHDMA;
netdev->vlan_features |= NETIF_F_HIGHDMA;
}

如果 pci_using_dac 標記爲 1,則當前網絡設備的 features 域(表示當前活動的設備功能)和 vlan_features 域(表示 VLAN 設備可繼承的功能)都賦值爲 NETIF_F_HIGHDMA,NETIF_F_HIGHDMA 表示當前設備可以通過 DMA 通道訪問到高地址的內存。

因爲前面分析過,pci_using_dac 標記爲 1 時,當前設備是 64 位的。 e1000_probe 函數完成了對設備的基本初始化,接下來看如何初始化接收環形緩衝區。

/**
 * e1000_setup_rx_resources - allocate Rx resources (Descriptors)
 * @adapter: board private structure
 * @rxdr:    rx descriptor ring (for a specific queue) to setup
 *
 * Returns 0 on success, negative on failure
 **/static int e1000_setup_rx_resources(struct e1000_adapter *adapter,				    struct e1000_rx_ring *rxdr)
{    	'''''''
            
		rxdr->desc = dma_alloc_coherent(&pdev->dev, rxdr->size, &rxdr->dma,
					GFP_KERNEL);    
    	''''''
		memset(rxdr->desc, 0, rxdr->size);

		rxdr->next_to_clean = 0;
		rxdr->next_to_use = 0;
		rxdr->rx_skb_top = NULL;		return 0;
}

這裏 dma_alloc_coherent() 的作用是申請一塊 DMA 可使用的內存,它的返回值是這塊內存的虛擬地址,賦值給 rxdr->desc。其實這個函數還隱式的返回了物理地址,物理地址存在第三個參數中。指針 rxdr 指向的是 struct e1000_rx_ring 這個結構體,該結構體就是接收環形緩衝區。

若成功申請到 DMA 內存,則用 memset() 函數把申請的內存清零,rxdr 的其他域也清零。

對於現在的多核 CPU,每個 CPU 都有自己的接收環形緩衝區,
e1000_setup_all_rx_resources() 中調用 e1000_setup_rx_resources(),初始化所有的接收環形緩衝區。

int e1000_setup_all_rx_resources(struct e1000_adapter *adapter)
{
	int i, err = 0;	for (i = 0; i < adapter->num_rx_queues; i++) {
		err = e1000_setup_rx_resources(adapter, &adapter->rx_ring[i]);		if (err) {
			e_err(probe, "Allocation for Rx Queue %u failed\n", i);			for (i-- ; i >= 0; i--)
				e1000_free_rx_resources(adapter,
							&adapter->rx_ring[i]);			break;
		}
	}	return err;
}

e1000_setup_all_rx_resources() 由 e1000_open() 調用,也就是說只要打開該網絡設備,接收和發送環形緩衝區就會建立好。

int e1000_open(struct net_device *netdev){	struct e1000_adapter *adapter = netdev_priv(netdev);
	struct e1000_hw *hw = &adapter->hw;
	int err;	/* disallow open during test */
	if (test_bit(__E1000_TESTING, &adapter->flags))		return -EBUSY;

	netif_carrier_off(netdev);	/* allocate transmit descriptors */
	err = e1000_setup_all_tx_resources(adapter);	if (err)		goto err_setup_tx;	/* allocate receive descriptors */
	err = e1000_setup_all_rx_resources(adapter);	if (err)		goto err_setup_rx;

DMA 相關內容很多,這次先分享到這裏。

一口 Linux 寫點代碼,寫點人生!

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/j_toeR9K0NZlqnVohSBs9Q