网卡驱动初始化解析

一、环境说明

内核版本: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);
    }
}

至此,网卡已经准备就绪,等待数据包的到来。

 

posted @ 2023-11-05 16:10  划水的猫  阅读(125)  评论(0编辑  收藏  举报