接下來,我們將以 Intel 的 igb 網卡驅動程式為例,解析網卡驅動的工作原理。今天我們先探討乙太網路卡驅動如何發現網卡,並完成 net_device
的建立與初始化。
首先,我們需要了解作業系統如何得知某張 PCIe 網卡應該使用哪個網卡驅動。根據 PCI(e) 設備的運作原理,PCI(e) 設備的 Configuration Space Header 保存了設備的基本資料,因此,當作業系統開機時,系統可以從這個 header 中取得 PCIe 網卡的 Vendor ID 及 Device ID。
// drivers/net/ethernet/intel/igb/igb_main.c
static const struct pci_device_id igb_pci_tbl[] = {
{ PCI_VDEVICE(INTEL, E1000_DEV_ID_I354_BACKPLANE_1GBPS) },
{ PCI_VDEVICE(INTEL, E1000_DEV_ID_I354_SGMII) },
{ PCI_VDEVICE(INTEL, E1000_DEV_ID_I354_BACKPLANE_2_5GBPS) },
{ PCI_VDEVICE(INTEL, E1000_DEV_ID_I211_COPPER), board_82575 },
{ PCI_VDEVICE(INTEL, E1000_DEV_ID_I210_COPPER), board_82575 },
...
}
在驅動程式內部,會維護一個靜態列表 igb_pci_tbl
,用來記錄該網卡驅動所支援的 PCIe 設備 (Vendor ID 和 Device ID)。
module_init(igb_init_module);
static int __init igb_init_module(void)
{
...
ret = pci_register_driver(&igb_driver);
...
}
static struct pci_driver igb_driver = {
.name = igb_driver_name,
.id_table = igb_pci_tbl, /* 支援列表 */
.probe = igb_probe,
.remove = igb_remove,
#ifdef CONFIG_PM
.driver.pm = &igb_pm_ops,
#endif
.shutdown = igb_shutdown,
.sriov_configure = igb_pci_sriov_configure,
.err_handler = &igb_err_handler
};
驅動載入時,會呼叫 pci_register_driver
函數向 kernel 的 PCI 子系統註冊,並提供所支援的 PCIe 設備列表 (igb_driver.id_table = igb_pci_tbl)。
當 PCI 子系統發現符合條件的網卡時,會呼叫該驅動程式的 pci_driver.probe
函數,也就是 igb_probe
,請求驅動程式初始化該 PCIe 設備。
igb_probe
的初始化內容十分複雜,包含設定 MAC address、初始化網卡設備的記憶體空間等,本文將挑選部分內容進行說明。
在 igb_probe
中,首先會呼叫 alloc_etherdev_mq
,為 net_device
分配記憶體空間:
static int igb_probe(struct pci_dev *pdev, const struct pci_device_id *ent) {
struct net_device *netdev;
struct igb_adapter *adapter;
struct e1000_hw *hw;
...
netdev = alloc_etherdev_mq(sizeof(struct igb_adapter),
IGB_MAX_TX_QUEUES);
...
adapter = netdev_priv(netdev);
adapter->netdev = netdev;
...
netdev->netdev_ops = &igb_netdev_ops;
igb_set_ethtool_ops(netdev);
...
strcpy(netdev->name, "eth%d");
err = register_netdev(netdev);
...
}
// drivers/net/ethernet/intel/igb/igb.h
struct igb_adapter {
unsigned long active_vlans[BITS_TO_LONGS(VLAN_N_VID)];
struct net_device *netdev;
...
}
alloc_etherdev_mq
負責分配 net_device
的記憶體空間。對於乙太網路卡,除了 net_device
本身的資訊之外,還需要額外保存物理網卡的資訊,因此,igb 驅動定義了一個 igb_adapter
結構來保存這些額外資訊。alloc_etherdev_mq
函數除了分配 net_device
的記憶體空間,還會分配 igb_adapter
的記憶體空間。
接著,驅動程式會填充 netdev_ops
以支援網路子系統所需的操作,同時透過 igb_set_ethtool_ops
填充 ethtool_ops
結構。我們平常使用 ethtool
操作乙太網路卡時,實際上就是在呼叫網卡驅動程式實作的 ethtool_ops
。
trcpy(netdev->name, "eth%d")
設定了網卡的名稱,因此,我們常見的網卡名稱會是 ethXXX
這樣的格式。最後,驅動會呼叫 register_netdev
,向網路子系統註冊該網路設備。
接下來,我們探討 igb_probe
中與記憶體映射相關的部分:
/* igb_probe */
err = pci_enable_device_mem(pdev);
err = pci_request_mem_regions(pdev, igb_driver_name);
驅動會呼叫 pci_enable_device_mem
,如下所示:
/**
* pci_enable_device_mem - Initialize a device for use with Memory space
* @dev: PCI device to be initialized
*
* Initialize device before it's used by a driver. Ask low-level code
* to enable Memory resources. Wake up the device if it was suspended.
* Beware, this function can fail.
*/
pci_enable_device_mem(pdev);
- pci_enable_device_flags(dev, IORESOURCE_MEM);
-- pci_write_config_word(dev, PCI_COMMAND, cmd |= PCI_COMMAND_IO);
根據註解,在建立記憶體映射之前,應先呼叫這個函數。該函數會對 PCI configuration space header 中的 command register 寫入 PCI_COMMAND_IO
(0x1)。根據 PCI 規範,這表示允許設備回應 Memory Space 存取操作。
接下來,驅動會呼叫 pci_request_mem_regions(pdev, igb_driver_name)
來標記記憶體映射區域:
pci_request_mem_regions(pdev, igb_driver_name)
- pci_request_selected_regions(pdev, pci_select_bars(pdev, IORESOURCE_MEM), name);
-- __pci_request_selected_regions(pdev, bars, res_name, 0); /* res_name = igb_driver_name */
--- __pci_request_region(pdev, i, res_name, excl)
---- __request_mem_region(pci_resource_start(pdev, bar),
pci_resource_len(pdev, bar), res_name,
exclusive)
----- __request_region_locked(res, parent, start, n, name, flags);
------ __request_resource(parent, res)
這個操作會將所有映射到記憶體的區塊標記成該驅動程式專用,以防止其他 kernel module 讀寫該區域。
/* igb_probe */
adapter->io_addr = pci_iomap(pdev, 0, 0);
netdev->mem_start = pci_resource_start(pdev, 0);
netdev->mem_end = pci_resource_end(pdev, 0);
最後,驅動會呼叫 pci_iomap
來將 Bar 0 映射到的物理記憶體空間,重新映射到虛擬記憶體空間中。
void __iomem *pci_iomap(struct pci_dev *dev, int bar, unsigned long maxlen)
{
return pci_iomap_range(dev, bar, 0, maxlen);
}
void __iomem *pci_iomap_range(struct pci_dev *dev,
int bar,
unsigned long offset,
unsigned long maxlen)
{
resource_size_t start = pci_resource_start(dev, bar);
resource_size_t len = pci_resource_len(dev, bar);
unsigned long flags = pci_resource_flags(dev, bar);
...
if (flags & IORESOURCE_IO)
return __pci_ioport_map(dev, start, len);
if (flags & IORESOURCE_MEM)
return ioremap(start, len);
...
}
pci_iomap
函數會根據 BAR 的類型 (IO Space mapping 或 Memory Space mapping) 進行處理。對於 MMIO,會呼叫 ioremap
完成虛擬記憶體映射。