ixgbe 网卡初始化及收发数据概览

本来想继续写 socket 实现,发现网络栈是一个整体,不搞懂网卡与内核的交互,就缺少了最重要的一环。参考内核 4.4, 只梳理了大致框架,忽略很多细节,比如 sriov, xdp


网卡中断逻辑

网卡接到数据后,触发中断,内核回调中断处理程序 ISR. 一般中断都会分成上半部和下半部 (bh), 上半部执行时间短,不允许程序休眠,并且此时中断处于禁止状态。下半部有多种实现,网卡使用软中断,由 ksoftirqd 处理,耗时较长。

在石器时代,网卡中断只由一个 cpu 处理,但是在大数据高吐吞时,就会把某个核(一般是 cpu0) 拖跨,一直频繁的响应中断,严重影响网卡吞吐。所以就有了硬件层面的 RSS 概念,receive side scaling,网卡实现多个队列,每个队列一个中断并且绑定到不同 cpu, 将中断分摊到多个 cpu. 如果硬件不支持硬件队列呢?就有了 RPS 概念,在软件层面模拟一个队列,如果有 RSS,就没必要用 RPS

读取网卡数据有两种方式:中断和轮循。只有中断会使 cpu 负载变得很高,一直忙于响应中断。只用轮循时延得不到保证,会使其它进程恶心。为了高效的使用 cpu,现代操作系统都是采用 中断 + 轮循 的方式,这就是下面会提到的 NAPI

操作系统网络初始化

在网卡正式工作前,先由内核为网络做准备工作。内核调用 net/dev/core.c net_dev_init 初始化网络,主要做如下工作:

  1. dev_proc_init 在操作系统 /proc/net 目录下生成相关文件,/proc/net/dev, /proc/net/ptype, /proc/net/dev_mcast 存放一些统计及状态信息
  2. netdev_kobject_init 创建操作系统目录 /sys/class/net, 当网卡启动后都会在这里进行注册,方便查看
  3. 初始化全局 ptype_all 结构,这里存放不同协义族如何处理接收数据包的回调。打印 /proc/net/ptype 文件可以看到全部,比如 ip_rcv, 这一块很重要
  4. for_each_possible_cpu 初始化每个 cpu 的局部变量,per_cpu 是一个宏,表示他所引用的变量,每个 cpu 都有一个私有的。
    for_each_possible_cpu(i) {
        struct work_struct *flush = per_cpu_ptr(&flush_works, i);
        struct softnet_data *sd = &per_cpu(softnet_data, i);

        INIT_WORK(flush, flush_backlog);
        skb_queue_head_init(&sd->input_pkt_queue);
        skb_queue_head_init(&sd->process_queue);
        INIT_LIST_HEAD(&sd->poll_list);
        sd->output_queue_tailp = &sd->output_queue;
#ifdef CONFIG_RPS
        sd->csd.func = rps_trigger_softirq;
        sd->csd.info = sd;
        sd->cpu = i;
#endif
        sd->backlog.poll = process_backlog;
        sd->backlog.weight = weight_p;
    }

work_struct 每个 cpu 都会有一个,如果网卡消失了,那么回调 flush_backlog 释放属于这个网卡的 skb. 接下来最重要的就是 softnet_data 结构体,非常重要,所有网卡的数据包都是挂载到某个 cpu, 就是放在这个结构体里。如果开启了 RPS,设置软中断回调。最后设置 backlog.poll 回调 process_backlog,这个就是 NAPI Poll

  1. open_softirq 设置软中断传输发送回调分别是 net_tx_action, net_rx_action
  2. cpuhp_setup_state_nocalls 注册通知事件,如果 cpu 坏掉,那么 dev_cpu_dead 将坏掉 cpu 的本地网络包,转移到其它 cpu

网卡初始化

分为两个阶段,在驱动加载时调用 ixgbe_probe 初始化硬件一次,当网卡激活,也就是 if up 时调用 ixgbe_open 初始化与操作系统相关工作。这里以 intel 网卡举例,老的是 igb,新的驱动是 igbxe, 代码路径 drivers/net/ethernet/intel/ixgbe. 有的商用服务器会用 broadcom 网卡,价格便宜一些,驱动原理相近。

代码有些涉及硬件知识,不影响分析的情况下忽略。这里涉及几个重要结构体,网卡本身是一个 pci 设备,所以自然是 pci_dev, 并且有 pci_dev_id. 所有网卡在内核都用 net_device 来抽象表示。

ixgbe_probe

函数声明如下,传入 pci_dev 设备

int ixgbe_probe(struct pci_dev *pdev, const struct pci_device_id *ent)
  1. 激活网卡内存,配置设备 DMA, 在内核虚拟地址空间申请地址,保留网卡 io, memory 地址
  2. 最重要的是调用 alloc_etherdev_mq 在内核分配空间,生成 net_device 结构并初始化。这里重点关注 indices 用来初始化 RSS 队列,默认 64 个队列。
  3. ioremap 将网卡 pci 地址重新映射,赋值给 hw_addr, io_addr
  4. ixgbe_set_ethtool_ops 设置 ethtool 操作接口,这样将网卡操作抽象出来。
  5. ixgbe_sw_init 初始化网卡私有数据,这里看到设置 RSS 队列个数逻辑 min_t(int, ixgbe_max_rss_indices(adapter), num_online_cpus()),取硬件队列和 cpu 的最小值。
  6. ixgbe_enable_sriov 打开 sriov 硬件虚拟化
  7. eth_platform_get_mac_address 设置网卡硬件 mac 地址和 mac filter
  8. ixgbe_init_interrupt_scheme 申请中断,优先判断硬件是否支持 MSI-X, 检查中断向量 num_q_vector 个数,取 cpu 核数和硬件队列最小值,分配中断向量空间 msix_entries. 查看测试机,由于 cpu 总核数 40,所以 num_q_vector 也是 40. 如果不支持 MSI-X 那么设置 num_q_vectors 为 1.
  9. ixgbe_alloc_q_vectors 在内核申请空间,可以看到队列结构体 ixgbe_ring 是和 ixgbe_q_vector 一起分配的,将 vector 中断和队列绑定,rxtx 两个队列同时绑定到一个 vector 中断,理想情况下一对队列拥有一个中断,如果中断不够用,那么会有多个共用。这里还涉及到 cpu 亲缘性和 numa. 最后 netif_napi_add 给每个中断设置 NAPI_POLL 回调。
  10. ixgbe_cache_ring_rss 将队列 ixgbe_ring 与硬件 RSS 关联对应
  11. register_netdev 该网卡设备注册到名字空间

ixgbe_probe 主要功能就是生成 net_device 结构, 读取硬件 mac 地址,将网卡注册到系统中。由于支持 msi-x 根据 cpu 个数配置硬件中断和队列,设置 ixgbe_poll 回调。

ixgbe_open

第二个初始化的函数,在网卡激活时调用。其本功能很简单,申请所有 rx, tx 队列资源,申请中断,配置 msi-x.

  1. ixgbe_setup_all_tx_resources 分配发送队列,每个队列申请自己的 DMA 地址,总长度 sizeof(struct ixgbe_tx_buffer) * tx_ring->count, 其中 count 就是大家常说的网卡队列 ring buffer 个数,默认 512,一般都要调大,防止丢包。
  2. ixgbe_setup_all_rx_resources 同理,分配接收队列,我的测试机是 40 个列队。
  3. ixgbe_configure 设置网卡虚拟化,接收模式,flow director等等,最后调用 ixgbe_configure_tx, ixgbe_configure_rx 网卡硬件配置接收发送队列。
  4. ixgbe_request_irq 向操作系统申请 irq 硬中断,当前网卡支持 msi-x,最终调用 ixgbe_request_msix_irqs 申请中断向量,并配置中断回调 ixgbe_msix_clean_rings

网卡硬中断处理流程

这个图非常简单,大致流程如下:

               ┌────────────────┐   4.Call      ┌───────────────┐   5.Raise     ┌─────────────────┐
               │                │    Back       │               │   softirq     │                 │
               │      CPU       │─────────────▶ │   NIC DRIVER  │──────────────▶│    KSOFTSWAPD   │
               │                │               │               │               │                 │
               └────────────────┘               └───────────────┘               └─────────────────┘
                        ▲                                                                │         
             3.Raise    │                                                                │         
               IRQ      │                                                                │         
                        │                                                                │         
                        │                                                                │         
               ┌────────────────┐                                                        ▼         
               │                │                                                                  
               │                │                                                                  
               │                │                                                                  
               │      NIC       │                                                                  
  1. packet    │                │                                                                  
   ──────────▶ │                │                                                                  
               │                │                                                                  
               │                │           ┌────┬────┬────┬────┬────┬────┬────┐                   
               │   ┌────────┐   │ 2. DMA    │    │    │    │    │    │    │    │                   
               │   │  DMA   │ ──┼─────────▶ │    │    │    │    │    │    │    │                   
               │   └────────┘   │           │    │    │    │    │    │    │    │                   
               └────────────────┘           └────┴────┴────┴────┴────┴────┴────┘                   
                                                                                                   
                                                       Ring buffer                                 
                                                                                                                                                                                                  
  1. 网卡接收数据包,如果没有开启混杂模式,那么 filter 过滤掉不属于自己 mac 的包。
  2. DMA 控制器将数据包写到内存。
  3. 网卡通知 cpu 产生硬中断,触发 callback ixgbe_msix_clean_rings, 此时 q_vector 网卡硬中断处于关闭状态。
static inline void ____napi_schedule(struct softnet_data *sd,
                     struct napi_struct *napi)
{
    list_add_tail(&napi->poll_list, &sd->poll_list);
    __raise_softirq_irqoff(NET_RX_SOFTIRQ);
}

将等待轮循列表添加到 cpu 私有变量 softnet_data 里,然后触发软中断 NET_RX_SOFTIRQ

  1. 每个 cpu 都有一个软中断守护进程 KSOFTIRQD, 由他负责回调处理。

其实这里有个疑问,ring buffer 每个队列有一个,那数据包到来后如何选择写到哪个呢?另外,直到此时网卡硬中断还处于关闭状态。

软中断处理数据

在操作系统初始化得知,软中断回调 net_rx_action 来完成数据包的轮循,但不是死循环,有两个退出条件:处理数据超过 budget 配额或是超时一定时间,代码写写 300个和 2000us. 如果有未处理数据,再次触发软中断,等待调度。最后打开硬中断。

这个逻辑在函数 ixgbe_poll 里,当前 cpu 不止一个队列,所以会将配额尽可能得分配。对于发送,接收队列分别调用 ixgbe_clean_tx_irq, ixgbe_clean_rx_irq 处理。

ixgbe_clean_rx_irq: 每个 ixgbe_ring 保存一个环形数组,ring buffer,读出 skb 数据后调用 ixgbe_rx_skb 开始处理,如果开启了 GRO 那么合并小包,然后再查看是否有 RPS 逻辑,如果有就走,没有就调用 __netif_receive_skb 根据上文提到的 ptype_all,来选择是 ip_rcv, 还是 ipv6_rcv 再继续上层逻辑。调用 ip_rcv 之后会回调 NF_INET_PRE_ROUTING hook, 也就是大家都熟悉的 iptables pre_routing 链。

ixgbe_clean_tx_irq: 每次限制发送 q_vector->tx.work_limit 个报文,底层网卡逻辑暂时不看了。

关于接收数据时 RPS

ixgbe_rx_skb 处理数据包时,先判断是否开启 RPS, 如果 get_rps_cpu 返回 cpu, 那么将 skb 数据包入队列 softnet_data.input_pkt_queue, 最后触发软中断 net_rx_action,那么这个软中断回调是哪个函数呢?是 process_backlog,在 net_dev_init 里指定。最终还是走到 __netif_receive_skb 逻辑。

其间队列可能满,会丢弃包,通过 netdev_max_backlog 来调节 backlog 队列大小。

你可能感兴趣的:(ixgbe 网卡初始化及收发数据概览)