Linux收包之数据从网卡到协议栈是如何流转的

一、环境说明

内核版本:Linux 3.10

内核源码地址:https://elixir.bootlin.com/linux/v3.10/source (包含各个版本内核源码,且王页可全局搜索函数)

网卡:Intel的igb网卡

网卡驱动源码目录:drivers/net/ethernet/intel/igb/

二、Linux收包依赖组件的初始化

Linux驱动,内核协议栈等等模块在具备接收网卡数据包之前,要做很多的准备工作才行。
比如要提前创建好ksoftirqd内核线程,要注册好各个协议对应的处理函数,网络设备子系统要提前初始化好,网卡要启动好等等。

1.创建软中断ksoftirqd内核进程

每个CPU负责执行一个ksoftirq内核进程,比如ksoftirqd/0运行在CPU 0上,这些内核进程执行不同软中断注册的中断处理函数。
执行硬中断的处理函数的 CPU 核心,也会执行该硬中断后续的软中断处理函数。
ksoftirqd 内核进程通过 spawn_ksoftirqd 函数初始化:

// file: kernel/softirq.c
static struct smp_hotplug_thread softirq_threads = {
    .store            = &ksoftirqd,
    .thread_should_run    = ksoftirqd_should_run,
    .thread_fn        = run_ksoftirqd,
    .thread_comm        = "ksoftirqd/%u",
};

static __init int spawn_ksoftirqd(void)
{
    register_cpu_notifier(&cpu_nfb);

    BUG_ON(smpboot_register_percpu_thread(&softirq_threads));

    return 0;
}
early_initcall(spawn_ksoftirqd);

当ksoftirqd被创建出来以后,它就会进入自己的线程循环函数ksoftirqd_should_run和run_ksoftirqd了。不停地判断有没有软中断需要被处理。

2.网络子系统初始化

网络子系统通过net_dev_init函数进行初始化:

// file: net/core/dev.c
static int __init net_dev_init(void)
{
    ......
    // 为每个CPU都申请一个softnet_data数据结构
    for_each_possible_cpu(i) {
        struct softnet_data *sd = &per_cpu(softnet_data, i);

        memset(sd, 0, sizeof(*sd));
        skb_queue_head_init(&sd->input_pkt_queue);
        skb_queue_head_init(&sd->process_queue);
        sd->completion_queue = NULL;
        INIT_LIST_HEAD(&sd->poll_list);
        sd->output_queue = NULL;
        sd->output_queue_tailp = &sd->output_queue;
#ifdef CONFIG_RPS
        sd->csd.func = rps_trigger_softirq;
        sd->csd.info = sd;
        sd->csd.flags = 0;
        sd->cpu = i;
#endif

        sd->backlog.poll = process_backlog;
        sd->backlog.weight = weight_p;
        sd->backlog.gro_list = NULL;
        sd->backlog.gro_count = 0;
    }

    ......
    // 注册软中断处理函数,用于处理接收和发送的数据包
    open_softirq(NET_TX_SOFTIRQ, net_tx_action);
    open_softirq(NET_RX_SOFTIRQ, net_rx_action);
    ......
}

subsys_initcall(net_dev_init);

内核通过open_softirq函数来注册软中断处理函数:

// file: kernel/softirq.c
void open_softirq(int nr, void (*action)(struct softirq_action *))
{
    softirq_vec[nr].action = action;
}

相关联的softnet_data和napi_struct结构体:

struct softnet_data {
    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      processed;
    unsigned int      time_squeeze;
    unsigned int      cpu_collision;
    unsigned int      received_rps;
    unsigned      dropped; //被丢弃的包的数量
    struct sk_buff_head  input_pkt_queue; //收包队列
    struct napi_struct   backlog; //处理积压队列的napi结构
};

struct napi_struct {
    struct list_head  poll_list; //链表指针,用于挂在softnet_data上
    unsigned long state; //此NAPI设备当前的状态
    int weight; //一个权重值,每次调度NAPI可处理数据包个数的限制
    int (*poll)(structnapi_struct *, int); //poll函数,用于实际来处理数据包
    ……
};

3.协议栈注册

linux通过inet_init将TCP、UDP和ICMP的接收函数注册到了inet_protos数组中,将IP的接收函数注册到了ptype_base哈希表中。

// file: net/ipv4/af_inet.c
static int __init inet_init(void)
{
    ......
    // 添加基础网络协议到inet_protos数组中
    if (inet_add_protocol(&icmp_protocol, IPPROTO_ICMP) < 0)
        pr_crit("%s: Cannot add ICMP protocol\n", __func__);
    if (inet_add_protocol(&udp_protocol, IPPROTO_UDP) < 0)
        pr_crit("%s: Cannot add UDP protocol\n", __func__);
    if (inet_add_protocol(&tcp_protocol, IPPROTO_TCP) < 0)
        pr_crit("%s: Cannot add TCP protocol\n", __func__);

    ......
    // 将IP的接收函数ip_rcv注册到ptype_base列表里
    dev_add_pack(&ip_packet_type);
    ......
}

fs_initcall(inet_init);

TCP、UDP、ICMP和IP协议的处理函数:

//file: net/ipv4/af_inet.c
static const struct net_protocol tcp_protocol = {
    .early_demux    =    tcp_v4_early_demux,
    .handler    =    tcp_v4_rcv,
    .err_handler    =    tcp_v4_err,
    .no_policy    =    1,
    .netns_ok    =    1,
}
static const struct net_protocol udp_protocol = {
    .handler =    udp_rcv,
    .err_handler =    udp_err,
    .no_policy =    1,
    .netns_ok =    1,
};
static const struct net_protocol icmp_protocol = {
    .handler =    icmp_rcv,
    .err_handler =    icmp_err,
    .no_policy =    1,
    .netns_ok =    1,
};
static struct packet_type ip_packet_type __read_mostly = {
    .type = cpu_to_be16(ETH_P_IP),
    .func = ip_rcv,
};

4.网卡驱动程序初始化以及网卡启动

详细说明见:网卡驱动程序初始化文档

三、驱动程序接收并建立数据包

1.概述

数据包在本层主要处理流程有:
网卡收到数据包,DMA方式写入Ring Buffer,发出硬中断;
内核收到硬中断,NAPI加入本CPU的轮询列表,发出软中断;
内核收到软中断,轮询NAPI并执行poll函数从Ring Buffer取数据;
GRO操作(默认开启),合并多个数据包为一个数据包,如果RPS关闭,则把数据包递交到协议栈;
RPS操作(默认关闭),如果开启,使数据包通过别的(也可能是当前的)CPU递交到协议栈;

流程图:

调用链:

2.网卡收到数据包

网卡收到数据包后,通过DMA写入Ring Buffer(rx_ring)内rx_buffer_info数组的下一个可用元素(igb_rx_buffer)的dma指向的内核内存。当DMA操作完成以后,网卡会像CPU发起一个硬中断,通知CPU有数据到达。
一个网络帧可能占用多个igb_rx_buffer。

3.硬中断处理

假设启动时硬中断类型选择的是MSI-X,那么硬中断注册的函数就igb_msix_ring。

// file: drivers/net/ethernet/intel/igb/igb_main.c
static irqreturn_t igb_msix_ring(int irq, void *data)
{
    struct igb_q_vector *q_vector = data;
    // 更新特定的硬件中的寄存器,记录一下硬件中断频率
    igb_write_itr(q_vector);
    // 调度NAPI
    napi_schedule(&q_vector->napi);
    return IRQ_HANDLED;
}

// file: include/linux/netdevice.h
static inline void napi_schedule(struct napi_struct *n)
{
    if (napi_schedule_prep(n))
        __napi_schedule(n);
}

// file: net/core/dev.c
void __napi_schedule(struct napi_struct *n)
{
    ......
    ____napi_schedule(&__get_cpu_var(softnet_data), n);
    ......
}

// file: net/core/dev.c
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);
}

硬中断主要做了两件事:
把napi_struct结构体的poll_list添加到当前CPU所关联的softnet_data结构体的poll_list链表尾部;
调用__raise_softirq_irqoff函数触发NET_RX_SOFTIRQ软中断,从而内核执行网络子系统初始化时注册的net_rx_action软中断处理函数。

// file: kernel/softirq.c
void __raise_softirq_irqoff(unsigned int nr)
{
    trace_softirq_raise(nr);
    or_softirq_pending(1UL << nr);
}

// include/linux/interrupt.h
#define or_softirq_pending(x)  (local_softirq_pending() |= (x))

4.软中断处理

处理硬中断的CPU同样也会执行该硬中断触发的软中断注册的处理函数。
ksoftirqd内核进程处理软中断是在run_ksoftirqd函数中:

// file: source/kernel/softirq.c
static void run_ksoftirqd(unsigned int cpu)
{
    local_irq_disable();
    if (local_softirq_pending()) {
        __do_softirq();
        rcu_note_context_switch(cpu);
        local_irq_enable();
        cond_resched();
        return;
    }
    local_irq_enable();
}

在__do_softirq中,判断根据当前CPU的软中断类型,调用其注册的action方法。

// file: kernel/softirq.c
asmlinkage void __do_softirq(void)
{
    ......
    h = softirq_vec; //软中断静态变量数组
    do {
        if (pending & 1) {
            unsigned int vec_nr = h - softirq_vec;
            int prev_count = preempt_count();

            kstat_incr_softirqs_this_cpu(vec_nr);

            trace_softirq_entry(vec_nr);
            h->action(h);
            trace_softirq_exit(vec_nr);
            ......
        }
        h++;
        pending >>= 1;
    } while (pending);
    ......
}

在网络子系统初始化中,为NET_RX_SOFTIRQ注册了处理函数net_rx_action。所以net_rx_action函数就会被执行到了。

net_rx_action的核心逻辑:
获取到当前CPU变量softnet_data,对其poll_list进行遍历,然后执行网卡驱动注册到的poll函数。

// file: net/core/dev.c
static void net_rx_action(struct softirq_action *h)
{
    // 获取到当前CPU变量softnet_data
    struct softnet_data *sd = &__get_cpu_var(softnet_data);
    // time_limit和budget是用来控制net_rx_action函数主动退出的,目的是保证网络包的接收不霸占CPU不放
    unsigned long time_limit = jiffies + 2;
    int budget = netdev_budget;
    void *have;

    local_irq_disable(); //禁用中断

    // 对其poll_list进行遍历
    while (!list_empty(&sd->poll_list)) {
        struct napi_struct *n;
        int work, weight;

        local_irq_enable(); //启用中断

        n = list_first_entry(&sd->poll_list, struct napi_struct, poll_list); //获取队列中的一个NAPI结构
        have = netpoll_poll_lock(n); //加锁

        weight = n->weight; //获取单次读取最大报文个数
        work = 0;
        if (test_bit(NAPI_STATE_SCHED, &n->state)) {
            work = n->poll(n, weight); //执行网卡驱动注册到的poll函数(eg:igb驱动的igb_poll函数),返回处理后的压力值
            trace_napi_poll(n);
        }

        WARN_ON_ONCE(work > weight); //是否超过结构的压力

        budget -= work; //剩余压力值
        local_irq_disable(); //禁用中断
        if (unlikely(work == weight)) { //处理的压力已经达到结构的压力
            if (unlikely(napi_disable_pending(n))) { //将NAPI结构设置为关闭状态
                local_irq_enable();
                napi_complete(n); //NAPI结构从队列中脱链
                local_irq_disable();
            } else {
                ......
                list_move_tail(&n->poll_list, &sd->poll_list); //将NAPI结构链入队列尾部
            }
        }

        netpoll_poll_unlock(have); //解锁
    }
    ......
}

igb驱动程序对应的poll函数为igb_poll,其核心逻辑:
(1)如果内核支持DCA(Direct Cache Access),CPU缓存命中率将会提升;
(2)调用igb_clean_rx_irq循环处理数据包,直到处理完毕或者budget耗尽;
(3)检查clean_complete判断是否所有的工作已经完成?如果是,返回剩下的budget值;否则调用napi_complete_done关闭NAPI,然后通过igb_ring_irq_enable重新打开硬中断,以保证下次中断会重新打开NAPI。

// file: drivers/net/ethernet/intel/igb/igb_main.c
static int igb_poll(struct napi_struct *napi, int budget)
{
    struct igb_q_vector *q_vector = container_of(napi, struct igb_q_vector, napi);
    bool clean_complete = true;

#ifdef CONFIG_IGB_DCA
    if (q_vector->adapter->flags & IGB_FLAG_DCA_ENABLED)
        igb_update_dca(q_vector);
#endif
    
    ......
    if (q_vector->rx.ring)
        clean_complete &= igb_clean_rx_irq(q_vector, budget); //处理数据包

    /* If all work not completed, return budget and keep polling */
    if (!clean_complete)
        return budget;

    /* If not enough Rx work done, exit the polling mode */
    napi_complete(napi);
    igb_ring_irq_enable(q_vector);

    return 0;
}

 igb_clean_rx_irq处理数据包的核心逻辑:

// file: drivers/net/ethernet/intel/igb/igb_main.c
static bool igb_clean_rx_irq(struct igb_q_vector *q_vector, const int budget)
{
    struct igb_ring *rx_ring = q_vector->rx.ring;
    struct sk_buff *skb = rx_ring->skb;
    unsigned int total_bytes = 0, total_packets = 0;
    // ring->count就是desc num,cleaned_count就是hw没有使用、需要clean的desc数目
    // next_to_use的use是指没有填充过数据包即将使用的desc
    // next_to_clean的clean是指已经填充过数据包,并且已经将数据送往协议栈后,需要处理的desc
    u16 cleaned_count = igb_desc_unused(rx_ring);

    do {
        union e1000_adv_rx_desc *rx_desc;

        // 1.如果需要clean的desc超过了阈值,那么先clean一下,返还一些buffer给hw
        if (cleaned_count >= IGB_RX_BUFFER_WRITE) {
            igb_alloc_rx_buffers(rx_ring, cleaned_count);
            cleaned_count = 0;
        }

        // 2.从Ring Buffer中取出下一个可读位置(next_to_clean)的rx_desc,检查它状态是否正常
        rx_desc = IGB_RX_DESC(rx_ring, rx_ring->next_to_clean);
        if (!igb_test_staterr(rx_desc, E1000_RXD_STAT_DD))
            break;

        /* This memory barrier is needed to keep us from reading
         * any other fields out of the rx_desc until we know the
         * RXD_STAT_DD bit is set
         */
        rmb();

        // 3.从RX中取buffer,保存到一个skb_buff中
        skb = igb_fetch_rx_buffer(rx_ring, rx_desc, skb);

        /* exit if we failed to retrieve a buffer */
        if (!skb)
            break;

        cleaned_count++; //每用完一个desc,要cleaned_count++。

        // 4.判断这个buffer是不是一个包的最后一个buffer?如果是,继续处理;如果不是,继续从buffer列表中拿出下一个buffer,加到skb。当数据帧的大小比一个buffer大的时候,会出现这种情况。
        if (igb_is_non_eop(rx_ring, rx_desc))
            continue;

        // 5.检查数据的layout和header信息是正确的
        if (igb_cleanup_headers(rx_ring, rx_desc, skb)) {
            skb = NULL;
            continue;
        }

        // 6.把skb的长度累计到total_bytes,用于统计数据
        total_bytes += skb->len;

        // 7.设置skb的hash,checksum,timestamp,VLAN id,protocol 字段。这些信息是硬件提供的。
        // 如果硬件报告checksum error,csum_error统计就会增加。如果checksum通过了,数据是UDP或者TCP数据,skb就会被标记成CHECKSUM_UNNECESSARY
        igb_process_skb_fields(rx_ring, rx_desc, skb);

        // 8.GRO,合并数据包,将构建好的skb送入协议栈
        napi_gro_receive(&q_vector->napi, skb);

        /* reset skb pointer */
        skb = NULL;

        // 更新处理过的包的统计信息
        total_packets++;
    } while (likely(total_packets < budget)); //循环直至处理的包数量达到 budget

    /* place incomplete frames back on ring for completion */
    rx_ring->skb = skb;

    // 更新统计信息
    u64_stats_update_begin(&rx_ring->rx_syncp);
    rx_ring->rx_stats.packets += total_packets;
    rx_ring->rx_stats.bytes += total_bytes;
    u64_stats_update_end(&rx_ring->rx_syncp);
    q_vector->rx.total_packets += total_packets;
    q_vector->rx.total_bytes += total_bytes;

    if (cleaned_count)
        igb_alloc_rx_buffers(rx_ring, cleaned_count);

    return (total_packets < budget);
}

 5.GRO

GRO(Generic Receive Offloading)是LGO的一种软件实现,从而能让所有NIC都支持这个功能。
通过合并(足够类似)的包来减少传送给网络协议栈的包数,有助于减少CPU的使用量。
GRO使协议层只需处理一个header,而将包含大量数据的整个大包送到用户程序。
napi_gro_receive就是实现GRO机制的函数:
在dev_gro_receive函数中,具体地执行多个数据包合并操作;当合并完成后会调用napi_skb_finish函数。

// file: net/core/dev.c
gro_result_t napi_gro_receive(struct napi_struct *napi, struct sk_buff *skb)
{
    skb_gro_reset_offset(skb);

    return napi_skb_finish(dev_gro_receive(napi, skb), skb);
}

static gro_result_t napi_skb_finish(gro_result_t ret, struct sk_buff *skb)
{
    switch (ret) {
    case GRO_NORMAL:
        if (netif_receive_skb(skb))
            ret = GRO_DROP;
        break;

    ......
}

四、查找数据包类型且调用其处理函数

netif_receive_skb代表的是设备驱动程序和L3协议处理例程间的边界,它有三个主要任务:
把帧的副本传给每个协议分流器(如果正在运行的话);
把帧的副本传给skb->protocol所关联的L3协议处理函数;
负责此层必须处理的一些功能,例如桥接;

// file: net/core/dev.c
int netif_receive_skb(struct sk_buff *skb)
{
    ...... //RPS处理逻辑
    return __netif_receive_skb(skb);
}
static int __netif_receive_skb(struct sk_buff *skb)
{
    ......
    ret = __netif_receive_skb_core(skb, false);
    return ret;
}

最终通过__netif_receive_skb_core函数调用deliver_skb将skb交到网络协议栈继续处理。

static int __netif_receive_skb_core(struct sk_buff *skb, bool pfmemalloc)
{
    ......
    orig_dev = skb->dev; //数据包绑定主设备,返回原来的接收设备指针

    skb_reset_network_header(skb); //数据包链路层头部指针指向数据块开始地址
    if (!skb_transport_header_was_set(skb))
        skb_reset_transport_header(skb); //数据包传输层头部指针指向数据块开始地址
    skb_reset_mac_len(skb);

    ......

    list_for_each_entry_rcu(ptype, &ptype_all, list) { //在队列中查找设备相同的结构(tcpdump就是从这个入口获取包的)
        if (!ptype->dev || ptype->dev == skb->dev) {
            if (pt_prev) //找到了数据包类型结构
                ret = deliver_skb(skb, pt_prev, orig_dev); //传递数据包
            pt_prev = ptype; //记录找到的数据包类型结构
        }
    }
 
    ...... //与数据包分类器/网桥相关

    type = skb->protocol; //取得数据包的协议类型
    list_for_each_entry_rcu(ptype, &ptype_base[ntohs(type) & PTYPE_HASH_MASK], list) { //在队列数组中查找设备相同的数据包类型结构
        if (ptype->type == type && //对比协议类型和设备
            (ptype->dev == null_or_dev || ptype->dev == skb->dev ||
             ptype->dev == orig_dev)) { //如果没有指定设备或者指定了设备并且相同
            if (pt_prev) /找到了数据包类型结构
                ret = deliver_skb(skb, pt_prev, orig_dev); //传递数据包
            pt_prev = ptype; //记录找到的数据包类型结构
        }
    }

    if (pt_prev) { //如果找到了数据包类型结构
        if (unlikely(skb_orphan_frags(skb, GFP_ATOMIC)))
            goto drop;
        else
            ret = pt_prev->func(skb, skb->dev, pt_prev, orig_dev); //调用它的处理函数
    } else {
drop:
        atomic_long_inc(&skb->dev->rx_dropped);
        kfree_skb(skb);
        ret = NET_RX_DROP;
    }
    ......
}

static inline int deliver_skb(struct sk_buff *skb, struct packet_type *pt_prev, struct net_device *orig_dev)
{
    ......
    return pt_prev->func(skb, skb->dev, pt_prev, orig_dev); //调用协议的处理函数(对于ip包来讲,就会进入到ip_rcv)
}

至此,数据包被送到了网络协议栈中。下一步就是L3层协议处理函数进行处理。

posted @ 2023-12-06 22:35  划水的猫  阅读(389)  评论(0编辑  收藏  举报