Linux 网卡驱动的工作原理

2014-04-03 linux kernel network

如题所示,介绍网卡是如何接收数据,然后又是如何交给上层处理。因为鄙人的网卡驱动用的是 e1000e,所以就以此为例了。

Just enjoy it.

简介

网络设备有很多,但它们的工作原理基本相同,可以分为两个层次,分别为 MAC (Media Access Control) 对应于 OSI 的数据链路层,PHY (Physical Layer) 对应于物理层。

同时为了减小 CPU 的压力,底层很多采用 DMA 方式。

查看驱动

对于网卡驱动,通常是以内核模块的方式提供,本机网卡对应的内核驱动可以通过如下方式查看。

----- 可以查看Ethernet、Wireless字样,以及Kernel driver in use:(也就是所使用的驱动)
# lspci -vvv | less
... ...
00:19.0 Ethernet controller: Intel Corporation Ethernet Connection I218-LM (rev 04)
        Subsystem: Dell Device 05cb
        Control: I/O+ Mem+ BusMaster+ SpecCycle- MemWINV- VGASnoop- ParErr- Stepping- SERR- FastB2B- DisINTx+
        Status: Cap+ 66MHz- UDF- FastB2B- ParErr- DEVSEL=fast >TAbort- <TAbort- <MAbort- >SERR- <PERR- INTx-
        Latency: 0
        Interrupt: pin A routed to IRQ 46
        Region 0: Memory at f7e00000 (32-bit, non-prefetchable) [size=128K]
        Region 1: Memory at f7e3c000 (32-bit, non-prefetchable) [size=4K]
        Region 2: I/O ports at f080 [size=32]
        Capabilities: [c8] Power Management version 2
                Flags: PMEClk- DSI+ D1- D2- AuxCurrent=0mA PME(D0+,D1-,D2-,D3hot+,D3cold+)
                Status: D0 NoSoftRst- PME-Enable- DSel=0 DScale=1 PME-
        Capabilities: [d0] MSI: Enable+ Count=1/1 Maskable- 64bit+
                Address: 00000000fee0f00c  Data: 41a4
        Capabilities: [e0] PCI Advanced Features
                AFCap: TP+ FLR+
                AFCtrl: FLR-
                AFStatus: TP-
        Kernel driver in use: e1000e
... ...

----- 查看驱动相关的信息
# modinfo e1000e
filename:       .../kernel/drivers/net/ethernet/intel/e1000e/e1000e.ko
version:        3.2.5-k
license:        GPL
description:    Intel(R) PRO/1000 Network Driver
author:         Intel Corporation, <linux.nics@intel.com>
... ...

----- 查看对应的中断信息
$ cat /proc/interrupts
           CPU0       CPU1       CPU2       CPU3
... ...
 46:        853          0        930          0   PCI-MSI-edge      eth0
... ...

如上可以看到硬件的信息,以及所使用的驱动信息。

源码解析

接下来查看下 e1000e 相关的驱动。

网络初始化

网络设备相关的驱动在内核的 drivers/net 目录下,如上的 e1000e 驱动在 net/ethernet/intel/e1000e 目录下,接下来我们就以此为例。

首先是模块初始化函数,也就是 module_init() 宏指定的相关函数。

char e1000e_driver_name[] = "e1000e";
static struct pci_driver e1000_driver = {
    .name     = e1000e_driver_name,             // 驱动名称
    .id_table = e1000_pci_tbl,             // 指定了那些设备可以使用该驱动
    .probe    = e1000_probe,
    .remove   = e1000_remove,
    .driver   = {
        .pm = &e1000_pm_ops,
    },
    .shutdown = e1000_shutdown,
    .err_handler = &e1000_err_handler
};

static int __init e1000_init_module(void)
{
    int ret;

    pr_info("Intel(R) PRO/1000 Network Driver - %s\n",
        e1000e_driver_version);
    pr_info("Copyright(c) 1999 - 2014 Intel Corporation.\n");
    ret = pci_register_driver(&e1000_driver);

    return ret;
}
module_init(e1000_init_module);

如上,实际的初始化,只是通过 pci_register_driver() 注册的 PCI 驱动。当然其它的驱动也类似,同样会在加载的时候都会通过该函数注册,然后,在该函数中会通过驱动对应的 probe() 函数探测设备,如果找到则加载驱动,注册添加一个网络设备。

现在已经定义一个 PCI 驱动,但是哪些 PCI 设备可以使用此驱动? 实际上与 e1000_driver.id_table 中的定义相关,对于 e1000e 也就是通过 struct pci_device_id e1000_pci_tbl[] 定义。

接下来看看是如何匹配的。

首先介绍下 PCI 的相关内容,PCI 有 3 种地址空间:IO 空间、内存地址空间、配置空间。一个 PCI 配置空间至少有 256 字节,如下:

PCI 配置空间格式

id_tablestruct pci_device_id 类型的一个数组,每个元素就对应一条使用的 PCI 硬件信息,如果符合就可以使用这个驱动,例如我的网卡,其类型为:

pci -nn | grep -E 'Ethernet controller.*Intel'
00:19.0 Ethernet controller [0200]: Intel Corporation Ethernet Connection I218-LM [8086:155a] (rev 04)

刚好可以匹配上如下的记录。

#define E1000_DEV_ID_PCH_LPTLP_I218_LM      0x155A

static const struct pci_device_id e1000_pci_tbl[] = {
    ... ...
    { PCI_VDEVICE(INTEL, E1000_DEV_ID_PCH_LPTLP_I218_LM), board_pch_lpt },
    ... ...
};

在 pci_driver 中的 probe 成员(也就是 e1000_probe() 函数)用来对设置进行一系列的初始化操作。在驱动和设备的初始化阶段,包括在总线上驱动注册初始化;驱动探测设备注册初始化;开启设备,初始化接收缓存。

网络设备通过 struct net_device 标示,包括硬件网络设备接口,如以太网;软件网络设备接口,如 loopback 。通过 dev_base 头指针将设备链接起来集体管理,每个节点代表一个网络设备接口。

另外,也个比较重要的是中断相关内容,也就是在 e1000_open() 函数中调用 e1000_request_irq() 函数。

static int e1000_request_irq(struct e1000_adapter *adapter)
{
    ... ...
    /* E1000E_INT_MODE_MSIX模式
     * # cat /proc/interrupts | grep eth0
     *   86          0          0          0  IR-PCI-MSI-edge      eth0-rx-0
     *    0          0          0          0  IR-PCI-MSI-edge      eth0-tx-0
     *   10          0          0          0  IR-PCI-MSI-edge      eth0
     */
    if (adapter->flags & FLAG_MSI_ENABLED) { // 如果是MSI
        err = request_irq(adapter->pdev->irq, e1000_intr_msi, 0,
                  netdev->name, netdev);
        if (!err)
            return err;

        /* fall back to legacy interrupt */
        e1000e_reset_interrupt_capability(adapter);
        adapter->int_mode = E1000E_INT_MODE_LEGACY;
    }

    /* E1000E_INT_MODE_MSI模式
     * # cat /proc/interrupts | grep eth0
     * 853          0        930          0   PCI-MSI-edge      eth0
     */
    err = request_irq(adapter->pdev->irq, e1000_intr, IRQF_SHARED,
              netdev->name, netdev);
    ... ...
}

如上,在注册完设备之后,会通过 request_irq() 申请中断,不同的网卡类型会注册不同的中断处理函数,下面以 e1000_intr() 为例。

NAPI (New API)

报文接收是整个协议栈的入口,负责从网卡中把报文接收并送往内核协议栈相应协议处理模块处理。一般有中断和轮询两种方式,最开始,网络流量小,采用中断方式。

当流量增大时,有此引起的中断将会影响到系统的整体效率。例如,我们使用标准的 100M 网卡,可能实际达到的接收速率为 80MBits/s,而此时数据包平均长度为 1500Bytes,则每秒产生的中断数目为:

80M bits/s / (8 Bits/Byte * 1500 Byte) = 6667 个中断 /s

当每秒数千个中断请求时,那么很大一部分时间会消耗在中断上下文中;因此,当流量较大时,最好采用轮询的方式。而轮询,在请求较少时仍需要定时捞取数据,从而也会导致资源浪费。

而 NAPI 就是为了解决此问题。

除了中断次数之外,当系统压力很大,不得不丢数据报文时,最好的方式是在最底层直接丢弃。采用 NAPI 后,可以直接在网络驱动中丢弃,而不需要再通知到内核。

在网卡中断中,首先处理的是关闭中断,因为我们已经知道了现在有很多的报文需要去处理,关闭中断,从而防止其它的中断请求再次到来。接着就是通知 Network Subsystem 来处理现在已经接收的报文。

结构体

//----- 每个CPU会分配一个结构体
DEFINE_PER_CPU_ALIGNED(struct softnet_data, softnet_data);

struct softnet_data {                         // 保存接收报文,每个CPU一个队列
    struct Qdisc        *output_queue;        // 输出帧的控制
    struct Qdisc        **output_queue_tailp;
    struct list_head    poll_list;            // 有输入帧待处理的设备链表
    struct sk_buff      *completion_queue;    // 已经成功被传递出的帧的链表
    struct sk_buff_head process_queue;

    unsigned int        dropped;
    struct sk_buff_head input_pkt_queue;      // 接收到数据会分配一个skb,并保存在该链表中,注意只针对非NAPI
                                              // 而NAPI有自己的私有队列
    struct napi_struct  backlog;              // 用来兼容非NAPI的驱动
};

struct napi_struct {
    struct list_head    poll_list;            // 等待被执行的设备链表,链表的头就是softnet_data.poll_list
    unsigned long       state;
    int                 weight;               // 标示设备的权重
    unsigned int        gro_count;
    int (*poll)(struct napi_struct *, int);   // NAPI都有poll虚函数,而非NAPI没有,初始化会赋值process_backlog
    struct net_device   *dev;                 // 指向具体的网络设备
    struct sk_buff      *gro_list;
    struct sk_buff      *skb;
    struct list_head    dev_list;             // 指向设备的NAPI链表
    struct hlist_node   napi_hash_node;
    unsigned int        napi_id;
};

上述结构体中有 Qdisc (Queueing Discipline),即排队规则,也就是我们经常说的 QoS 。

与设备关联

在网卡驱动中,同时会创建轮询函数,一般在网卡初始化的时候完成,通过 nefif_napi_add() 函数添加。

void netif_napi_add(struct net_device *dev, struct napi_struct *napi,
            int (*poll)(struct napi_struct *, int), int weight);

也就是用于将轮询函数与实际的网络设备 struct net_device 关联起来。上述函数的入参中包括了一个权重,该值通常是一个经验数据,一般 10Mb 的网卡设置为 16,而更快的网卡则设置为 64 。

通知软中断

通知是通过 napi_schedule() 函数进行,有如下的两种方式;其中 napi_schedule_prep() 是为了判定现在是否已经进入了轮询模式。

//----- 将网卡预定为轮询模式
void napi_schedule(struct napi_struct *n);

//----- 或者
if (napi_schedule_prep(n))        // 返回0表示已经在做poll操作了
    __napi_schedule(n);

接下来查看下源码的具体实现。

static inline bool napi_schedule_prep(struct napi_struct *n)
{
    return !napi_disable_pending(n) &&
        !test_and_set_bit(NAPI_STATE_SCHED, &n->state);
}
static inline void napi_schedule(struct napi_struct *n)
{
    if (napi_schedule_prep(n))
        __napi_schedule(n);
}
static inline void ____napi_schedule(struct softnet_data *sd,
                     struct napi_struct *napi)
{
    // 把自己挂到per cpu的softnet_data上,触发NET_RX_SOFTIRQ软中断
    list_add_tail(&napi->poll_list, &sd->poll_list);
    __raise_softirq_irqoff(NET_RX_SOFTIRQ);
}
void __napi_schedule(struct napi_struct *n)
{
    unsigned long flags;

    local_irq_save(flags);
    ____napi_schedule(this_cpu_ptr(&softnet_data), n);
    local_irq_restore(flags);
}

可以看到 napi_schedule() 基本操作是添加到链表中,然后触发软中断。

软中断

软中断包括了读写中断,均在 net_dev_init() 函数中初始化。

static int __init net_dev_init(void)
{
    ... ...
    open_softirq(NET_TX_SOFTIRQ, net_tx_action);
    open_softirq(NET_RX_SOFTIRQ, net_rx_action);
    ... ...
}
subsys_initcall(net_dev_init);

轮询函数

驱动中创建轮询函数,它的工作是从网卡获取数据包并将其送入到网络子系统,函数声明如下。

int (*poll)(struct napi_struct *napi, int weight);

该函数在将网卡切换为轮询模式之后,用 poll() 方法处理接收队列中的数据包,如队列为空,则重新切换为中断模式。在切换回中断模式前,需要先通过 netif_rx_completer() 关闭轮询模式,然后开启网卡接收中断。

//----- 退出轮询模式
void __napi_complete(struct napi_struct *n)
{
    list_del(&n->poll_list);
    smp_mb__before_atomic();
    clear_bit(NAPI_STATE_SCHED, &n->state);
}
void napi_complete(struct napi_struct *n)
{
    unsigned long flags;

    /*
     * don't let napi dequeue from the cpu poll list
     * just in case its running on a different cpu
     */
    if (unlikely(test_bit(NAPI_STATE_NPSVC, &n->state)))
        return;

    napi_gro_flush(n, false);
    local_irq_save(flags);
    __napi_complete(n);
    local_irq_restore(flags);
}

关于驱动需要做的修改,可以参考 Driver porting: Network drivers,这篇是 2.6 时的文章,稍微有点老。

接收报文

在 e1000e 驱动中,只有 NAPI,也就是在收到中断后,然后调用 NAPI 轮询。

static irqreturn_t e1000_intr(int __always_unused irq, void *data)
{
    ... ...
    if (napi_schedule_prep(&adapter->napi)) {
        adapter->total_tx_bytes = 0;
        adapter->total_tx_packets = 0;
        adapter->total_rx_bytes = 0;
        adapter->total_rx_packets = 0;
        __napi_schedule(&adapter->napi);
    }

    return IRQ_HANDLED;
}

static int e1000e_poll(struct napi_struct *napi, int weight)
{
    ... ...
    if (!adapter->msix_entries ||
        (adapter->rx_ring->ims_val & adapter->tx_ring->ims_val))
        tx_cleaned = e1000_clean_tx_irq(adapter->tx_ring);        // 查看并回收tx slot

    adapter->clean_rx(adapter->rx_ring, &work_done, weight);

    if (!tx_cleaned)
        work_done = weight;

    /* If weight not fully consumed, exit the polling mode */
    if (work_done < weight) {
        if (adapter->itr_setting & 3)
            e1000_set_itr(adapter);
        napi_complete(napi);
        if (!test_bit(__E1000_DOWN, &adapter->state)) {
            if (adapter->msix_entries)
                ew32(IMS, adapter->rx_ring->ims_val);
            else
                e1000_irq_enable(adapter);
        }
    }

    return work_done;
}

对于接收过程,我们仅大致疏理一下其执行过程。

napi_gro_receive()
 |-skb_gro_reset_offset()
 |-dev_gro_receive()
 |-napi_skb_finish()
   |-netif_receive_skb_internal()
     |-__netif_receive_skb()            # 进入上层的接收函数