网卡驱动初始化解析
一、环境说明
内核版本:Linux 3.10
内核源码地址:https://elixir.bootlin.com/linux/v3.10/source (包含各个版本内核源码,且王页可全局搜索函数)
网卡:Intel的igb网卡
网卡驱动源码目录:drivers/net/ethernet/intel/igb/
二、网卡驱动的加载
网卡需要有驱动才能工作,驱动是加载到内核中的模块,负责衔接网卡和内核。
当相应的网卡收到数据包时,网络模块会调用相应的驱动程序处理数据。
网卡驱动程序 igb 向 Linux 内核通过 module_init 宏注册一个初始化函数 igb_init_module,当驱动加载的时候,该函数被内核调用。
// file: drivers/net/ethernet/intel/igb/igb_main.c static struct pci_driver igb_driver = { .name = igb_driver_name, .id_table = igb_pci_tbl, //所支持的设备列表 .probe = igb_probe, //探测函数 .remove = igb_remove, ...... }; static int __init igb_init_module(void) { ...... ret = pci_register_driver(&igb_driver); return ret; } module_init(igb_init_module);
调用链:igb_init_module() -> pci_register_driver() -> __pci_register_driver() -> driver_register()
通过调用 driver_register 函数,把网卡的驱动(driver)加载到内核 PCI 子系统。
三、网卡驱动的初始化
一个驱动程序可以支持一个或多个设备,而一个设备只会绑定一个驱动程序。
驱动程序将其支持的所有设备保存在一个列表 struct pci_device_id 中。
igb 驱动程序所支持的 PCI 设备列表部分如下:
// file: drivers/net/ethernet/intel/igb/igb_main.c static DEFINE_PCI_DEVICE_TABLE(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 }, { PCI_VDEVICE(INTEL, E1000_DEV_ID_I210_FIBER), board_82575 }, { PCI_VDEVICE(INTEL, E1000_DEV_ID_I210_SERDES), board_82575 }, { PCI_VDEVICE(INTEL, E1000_DEV_ID_I210_SGMII), board_82575 }, { PCI_VDEVICE(INTEL, E1000_DEV_ID_I350_COPPER), board_82575 }, { PCI_VDEVICE(INTEL, E1000_DEV_ID_I350_FIBER), board_82575 }, { PCI_VDEVICE(INTEL, E1000_DEV_ID_I350_SERDES), board_82575 }, { PCI_VDEVICE(INTEL, E1000_DEV_ID_I350_SGMII), board_82575 }, { PCI_VDEVICE(INTEL, E1000_DEV_ID_82580_COPPER), board_82575 }, { PCI_VDEVICE(INTEL, E1000_DEV_ID_82580_FIBER), board_82575 }, { PCI_VDEVICE(INTEL, E1000_DEV_ID_82580_QUAD_FIBER), board_82575 }, { PCI_VDEVICE(INTEL, E1000_DEV_ID_82580_SERDES), board_82575 }, { PCI_VDEVICE(INTEL, E1000_DEV_ID_82580_SGMII), board_82575 }, { PCI_VDEVICE(INTEL, E1000_DEV_ID_82580_COPPER_DUAL), board_82575 }, { PCI_VDEVICE(INTEL, E1000_DEV_ID_DH89XXCC_SGMII), board_82575 }, { PCI_VDEVICE(INTEL, E1000_DEV_ID_DH89XXCC_SERDES), board_82575 }, { PCI_VDEVICE(INTEL, E1000_DEV_ID_DH89XXCC_BACKPLANE), board_82575 }, { PCI_VDEVICE(INTEL, E1000_DEV_ID_DH89XXCC_SFP), board_82575 }, { PCI_VDEVICE(INTEL, E1000_DEV_ID_82576), board_82575 }, { PCI_VDEVICE(INTEL, E1000_DEV_ID_82576_NS), board_82575 }, { PCI_VDEVICE(INTEL, E1000_DEV_ID_82576_NS_SERDES), board_82575 }, { PCI_VDEVICE(INTEL, E1000_DEV_ID_82576_FIBER), board_82575 }, { PCI_VDEVICE(INTEL, E1000_DEV_ID_82576_SERDES), board_82575 }, { PCI_VDEVICE(INTEL, E1000_DEV_ID_82576_SERDES_QUAD), board_82575 }, { PCI_VDEVICE(INTEL, E1000_DEV_ID_82576_QUAD_COPPER_ET2), board_82575 }, { PCI_VDEVICE(INTEL, E1000_DEV_ID_82576_QUAD_COPPER), board_82575 }, { PCI_VDEVICE(INTEL, E1000_DEV_ID_82575EB_COPPER), board_82575 }, { PCI_VDEVICE(INTEL, E1000_DEV_ID_82575EB_FIBER_SERDES), board_82575 }, { PCI_VDEVICE(INTEL, E1000_DEV_ID_82575GB_QUAD_COPPER), board_82575 }, /* required last entry */ {0, } };
内核通过设备 ID 与驱动支持的设备列表匹配,选择合适的驱动控制网卡,然后调用之前注册到内核 PCI 子系统的探测函数(probe)完成初始化。
例如 igb 驱动程序的 igb_probe 函数,其处理流程包括:
- 设置 DMA 寻址限制和缓存一致性;
- 申请内核内存;
- struct net_device 结构体的创建、初始化和注册;
- 注册 struct net_device_ops(里面有 igb_open)到 net_device;
- 注册驱动支持的 ethtool 调用函数;
- 注册 poll 函数到 NAPI 子系统;
igb 驱动程序中 igb_probe 函数的部分代码如下:
// file: drivers/net/ethernet/intel/igb/igb_main.c static int igb_probe(struct pci_dev *pdev, const struct pci_device_id *ent) { /* 申请 DMA 内存空间和 I/O 端口 */ err = dma_set_mask(&pdev->dev, DMA_BIT_MASK(64)); /* 获取 PCIe 设备的 Resource(Memory BAR、I/O BAR 和 MSI-X BAR),并通过这些 BARs 完成一系列访问和初始化 */ err = pci_request_selected_regions(pdev, pci_select_bars(pdev, IORESOURCE_MEM), igb_driver_name); /* 网络设备 */ netdev = alloc_etherdev_mq(sizeof(struct igb_adapter), IGB_MAX_TX_QUEUES); /* net_device_ops 结构体,代表一个网络设备 */ netdev->netdev_ops = &igb_netdev_ops; /* 注册驱动支持的 ethtool 调用函数 */ igb_set_ethtool_ops(netdev); /* 函数里面注册了 poll 函数 */ err = igb_sw_init(adapter); }
1、注册 net_device_ops 到 net_device
net_device_ops 结构体包含了指向打开设备、发送数据和设置 MAC 地址等操作函数的指针
// file: drivers/net/ethernet/intel/igb/igb_main.c static const struct net_device_ops igb_netdev_ops = { .ndo_open = igb_open, //设备启动函数 .ndo_stop = igb_close, .ndo_start_xmit = igb_xmit_frame, .ndo_get_stats64 = igb_get_stats64, .ndo_set_rx_mode = igb_set_rx_mode, .ndo_set_mac_address = igb_set_mac, .ndo_change_mtu = igb_change_mtu, .ndo_do_ioctl = igb_ioctl, .ndo_tx_timeout = igb_tx_timeout, .ndo_validate_addr = eth_validate_addr, .ndo_vlan_rx_add_vid = igb_vlan_rx_add_vid, .ndo_vlan_rx_kill_vid = igb_vlan_rx_kill_vid, .ndo_set_vf_mac = igb_ndo_set_vf_mac, .ndo_set_vf_vlan = igb_ndo_set_vf_vlan, .ndo_set_vf_tx_rate = igb_ndo_set_vf_bw, .ndo_set_vf_spoofchk = igb_ndo_set_vf_spoofchk, .ndo_get_vf_config = igb_ndo_get_vf_config, #ifdef CONFIG_NET_POLL_CONTROLLER .ndo_poll_controller = igb_netpoll, #endif .ndo_fix_features = igb_fix_features, .ndo_set_features = igb_set_features, }; static int igb_probe(struct pci_dev *pdev, const struct pci_device_id *ent) { ...... netdev->netdev_ops = &igb_netdev_ops; ...... }
2、注册 poll 函数到 NAPI 子系统
网卡驱动程序都会实现 poll 函数,igb 驱动程序实现的 poll 函数是 igb_poll 函数
// file: drivers/net/ethernet/intel/igb/igb_main.c static int igb_alloc_q_vector(struct igb_adapter *adapter, int v_count, int v_idx, int txr_count, int txr_idx, int rxr_count, int rxr_idx) { ...... /* allocate q_vector and rings */ q_vector = kzalloc(size, GFP_KERNEL); /* initialize NAPI */ netif_napi_add(adapter->netdev, &q_vector->napi, igb_poll, 64); ...... }
调用链:igb_sw_init() -> igb_init_interrupt_scheme() -> igb_alloc_q_vectors() -> igb_alloc_q_vector() -> netif_napi_add()
通过调用 netif_napi_add 函数,将 igb_poll 函数注册到 NAPI 子系统。
四、网卡设备的启用
当网络设备被启用时 net_device_ops 中的 ndo_open 所指向的函数将会被调用。完成以下处理:
- 分配多 TX/RX 队列的内核内存空间;
- 给网卡配置 RX/TX 队列,给 RX 申请 DMA 空间;
- 注册硬中断处理函数;
- 打开 NAPI;
- 打开网卡硬中断;
igb 驱动程序中 ndo_open 指向的是 igb_open,部分代码如下:
// file: drivers/net/ethernet/intel/igb/igb_main.c static int igb_open(struct net_device *netdev) { return __igb_open(netdev, false); } static int __igb_open(struct net_device *netdev, bool resuming) { /* 分配多 TX 队列的内存空间 */ err = igb_setup_all_tx_resources(adapter); /* 分配多 RX 队列的内存空间 */ err = igb_setup_all_rx_resources(adapter); /* 给网卡配置 RX/TX 队列,给 RX 申请 DMA 空间 */ igb_configure(adapter); /* 注册中断处理函数 */ err = igb_request_irq(adapter); /* 打开 NAPI */ for (i = 0; i < adapter->num_q_vectors; i++) napi_enable(&(adapter->q_vector[i]->napi)); /* 打开硬中断 */ igb_irq_enable(adapter); /* 启动所有 TX 队列 */ netif_tx_start_all_queues(netdev); }
1、分配 TX/RX 多队列(Ring Buffer)内存空间
目前大部分网络都采用基于环形缓冲区的队列来进行 DMA 的收发数据包。
igb_open 代码中 igb_setup_all_rx_resources 会循环调用 igb_setup_rx_resources 函数 num_rx_queues 次,每次申请一个 Ring Buffer 内存空间,并且通过 DMA 申请连续内核空间用来存放 Ring Buffer 对应的网络数据。
// file: drivers/net/ethernet/intel/igb/igb_main.c static int igb_setup_all_rx_resources(struct igb_adapter *adapter) { for (i = 0; i < adapter->num_rx_queues; i++) { err = igb_setup_rx_resources(adapter->rx_ring[i]); } } int igb_setup_rx_resources(struct igb_ring *rx_ring) { struct device *dev = rx_ring->dev; int size; /* Ring Buffer 的元素是 struct igb_rx_buffer */ size = sizeof(struct igb_rx_buffer) * rx_ring->count; /* 申请 Ring Buffer 内存空间 */ rx_ring->rx_buffer_info = vzalloc(size); /* Round up to nearest 4K */ rx_ring->size = rx_ring->count * sizeof(union e1000_adv_rx_desc); rx_ring->size = ALIGN(rx_ring->size, 4096); /* 通过 DMA 申请连续内核空间,数量与 Ring Buffer 长度一致 */ rx_ring->desc = dma_alloc_coherent(dev, rx_ring->size, &rx_ring->dma, GFP_KERNEL); /* 复位 */ rx_ring->next_to_alloc = 0; rx_ring->next_to_clean = 0; rx_ring->next_to_use = 0; } // file: drivers/net/ethernet/intel/igb/igb.h struct igb_rx_buffer { dma_addr_t dma; /* DMA 内核空间地址 */ struct page *page; unsigned int page_offset; };
2、网卡配置 TX/RX 队列
创建完 RX 和 TX 队列后,需要把他们关联到网卡硬件,关联方式是通过把 RX/TX 的首元素写入网卡寄存器等操作,最后需要申请 RX 队列内「长度 - 1」个 igb_rx_buffer 元素的 DMA 地址(总线地址)空间,便于网卡收到数据好有地方存。
// file: drivers/net/ethernet/intel/igb/igb_main.c static void igb_configure(struct igb_adapter *adapter) { struct net_device *netdev = adapter->netdev; int i; /* 给网卡配置 TX/RX 队列,收发数据均从一个元素开始 */ igb_configure_tx(adapter); igb_configure_rx(adapter); /* 清空网卡内的 RX FIFO */ igb_rx_fifo_flush_82575(&adapter->hw); /* 给每个 RX 队列分配 DMA 空间,便于网卡硬件接收数据写入其中 */ for (i = 0; i < adapter->num_rx_queues; i++) { struct igb_ring *ring = adapter->rx_ring[i]; igb_alloc_rx_buffers(ring, igb_desc_unused(ring)); } }
3、注册中断函数
通常设备可以采用不同的中断方式:MSI-X、MSI 和 legacy 模式的中断方式。
MSI-X 中断是较好的方法,特别是对于支持多 RX 队列的网卡,每个 RX 队列都有其分配的特定硬中断号,可以绑定固定的 CPU 处理。
根据设备所支持的中断方式,驱动程序采用最合适的中断方式注册处理函数。
在 igb 驱动中,igb_msix_ring、igb_intr_msi 和 igb_intr 分别是 MSI-X、MSI 和 legacy 模式的中断处理函数。
igb 按照 MSI-X -> MSI -> legacy 的顺序尝试注册中断处理函数。
// file: drivers/net/ethernet/intel/igb/igb_main.c static int igb_request_irq(struct igb_adapter *adapter) { struct net_device *netdev = adapter->netdev; struct pci_dev *pdev = adapter->pdev; int err = 0; /* MSI-X */ if (adapter->msix_entries) { err = igb_request_msix(adapter); if (!err) goto request_done; /* fall back to MSI */ } /* MSI */ if (adapter->flags & IGB_FLAG_HAS_MSI) { err = request_irq(pdev->irq, igb_intr_msi, 0, netdev->name, adapter); if (!err) goto request_done; /* fall back to legacy interrupts */ } /* legacy interrupts */ err = request_irq(pdev->irq, igb_intr, IRQF_SHARED, netdev->name, adapter); }
多数情况下网卡驱动会选择 MSI-X 中断方式,调用 igb_request_msix 函数,然后注册 igb_msix_ring 函数为中断处理函数。
// file: drivers/net/ethernet/intel/igb/igb_main.c static int igb_request_msix(struct igb_adapter *adapter) { ...... for (i = 0; i < adapter->num_q_vectors; i++) { /* 注册 igb_msix_ring 硬中断函数 */ err = request_irq(adapter->msix_entries[vector].vector, igb_msix_ring, 0, q_vector->name, q_vector); } }
当 NIC 收到数据后发出一个硬件中断信号时,上面注册的中断函数将会执行。
4、打开 NAPI
NAPI 的核心概念是不采用频繁硬中断的方式读取数据,而是首先采用硬中断唤醒 NAPI 子系统,然后触发软中断,网络子系统处理软中断,然后循环调用 poll_list 中的 NAPI 实例的 poll 函数来循环接收数据包,这样可以防止高频硬中断影响系统的运行效率。
当然,NAPI 也有缺陷,系统不能及时接收每一个包,而是多个包一起处理,进而增加了部分数据包的延时。
前面驱动程序介绍了如何将 poll 函数注册到 NAPI 子系统,但是 NAPI 通常会等到设备被打开之后才会开始工作。
打开 NAPI 比较简单。在 igb 驱动中,调用 napi_enable 实现。
// file: drivers/net/ethernet/intel/igb/igb_main.c static int __igb_open(struct net_device *netdev, bool resuming) { ...... /* 打开 NAPI */ for (i = 0; i < adapter->num_q_vectors; i++) napi_enable(&(adapter->q_vector[i]->napi)); ...... }
5、打开硬中断
打开 NIC 硬中断,等待数据包的到来。
打开中断是一个硬件操作,igb 驱动通过函数 igb_irq_enable 写寄存器实现。
// file: drivers/net/ethernet/intel/igb/igb_main.c static void igb_irq_enable(struct igb_adapter *adapter) { struct e1000_hw *hw = &adapter->hw; if (adapter->msix_entries) { u32 ims = E1000_IMS_LSC | E1000_IMS_DOUTSYNC | E1000_IMS_DRSTA; u32 regval = rd32(E1000_EIAC); wr32(E1000_EIAC, regval | adapter->eims_enable_mask); regval = rd32(E1000_EIAM); wr32(E1000_EIAM, regval | adapter->eims_enable_mask); wr32(E1000_EIMS, adapter->eims_enable_mask); if (adapter->vfs_allocated_count) { wr32(E1000_MBVFIMR, 0xFF); ims |= E1000_IMS_VMMB; } wr32(E1000_IMS, ims); } else { wr32(E1000_IMS, IMS_ENABLE_MASK | E1000_IMS_DRSTA); wr32(E1000_IAM, IMS_ENABLE_MASK | E1000_IMS_DRSTA); } }
至此,网卡已经准备就绪,等待数据包的到来。