Linux-4.20.8内核桥收包源码解析(三)----------网卡驱动收包

转制https://blog.csdn.net/Sophisticated_/article/details/87295513

 

每次一个以太网帧到达时,都使用一个IRQ来通知内核。这里暗含着“快”和“慢”的概念。 对低速设备来说,在下一个分组到达之前, IRQ的处理通常已经结束。由于下一个分组也通过IRQ通知,如果前一个分组的IRQ尚未处理完成,则会导致问题,高速设备通常就是这样。现代以太网卡的运作高达10 000 Mbit/s,如果使用旧式方法来驱动此类设备,将造成所谓的“中断风暴”。如果在分组等待处理时接收到新的IRQ,内核不会收到新的信息:在分组进入处理过程之前,内核是可以接收IRQ的,在分组的处理结束后,内核也可以接收IRQ,这些不过是“旧闻”而已。为解决该问题,NAPI使用了IRQ和轮询的组合

假定某个网络适配器此前没有分组到达,但从现在开始,分组将以高频率频繁到达。这就是NAPI设备的情况,如下所述。

第一个分组将导致网络适配器发出IRQ。为防止进一步的分组导致发出更多的IRQ,驱动程序
会关闭该适配器的Rx IRQ。并将该适配器放置到一个轮询表上。
只要适配器上还有分组需要处理,内核就一直对轮询表上的设备进行轮询。
重新启用Rx中断。
如果在新的分组到达时,旧的分组仍然处于处理过程中,工作不会因额外的中断而减速。虽然对设备驱动程序(和一般意义上的内核代码)来说轮询通常是一个很差的方法,但在这里该方法没有什么不利之处:在没有分组还需要处理时,将停止轮询,设备将回复到通常的IRQ驱动的运行方式。在没有中断支持的情况下,轮询空的接收队列将不必要地浪费时间,但NAPI并非如此

NAPI的另一个优点是可以高效地丢弃分组。如果内核确信因为有很多其他工作需要处理,而导致无法处理任何新的分组,那么网络适配器可以直接丢弃分组,无须复制到内核。

只有设备满足如下两个条件时,才能实现NAPI方法。

设备必须能够保留多个接收的分组,例如保存到DMA环形缓冲区中。下文将该缓冲区称为Rx
缓冲区。
该设备必须能够禁用用于分组接收的IRQ。而且,发送分组或其他可能通过IRQ进行的操作,
都仍然必须是启用的
NAPI机制和循环轮询表概览:


如果一个分组到达一个空的Rx缓冲区,则将相应的设备置于轮询表中。由于链表本身的性质,轮询表可以包含多个设备。内核以循环方式处理链表上的所有设备:内核依次轮询各个设备,如果已经花费了一定的时间来处理某个设备,则选择下一个设备进行处理。此外,某个设备都带有一个相对权重,表示与轮询表中其他设备相比,该设备的相对重要性。较快的设备权重较大,较慢的设备权重较小。由于权重指定了在一个轮询的循环中处理多少分组,这确保了内核将更多地注意速度较快的设备

NAPI是中断机制与轮询的混合体,当一批数据包中第一个数据包到达网络设备,会以硬中断的方式通知系统,在硬中断例程,系统将该设备添加到CPU的设备轮询队列,并关闭中断,同时激活数据包输入软中断,由软中断例程遍历轮询队列中的网络设备,从中读取数据包。

相关接口层的初始化:net_dev_init

static int __init net_dev_init(void)
{
    int i, rc = -ENOMEM;

    BUG_ON(!dev_boot_phase);

    // 注册记录相关统计信息的proc文件系统
    // proc/net/dev : 用于显示网络设备收发包统计信息
    // proc/net/softnet_stat : 显示每个CPU的softnet_stat 统计信息
    // proc/net/ptype : 显示注册的协议处理函数
    if (dev_proc_init())
        goto out;
        
    //初始化kobject相关信息,为创建sys文件系统
    if (netdev_kobject_init())
        goto out;

    //初始化 ptype_all链表和ptype_base链表
    INIT_LIST_HEAD(&ptype_all);
    for (i = 0; i < PTYPE_HASH_SIZE; i++)
        INIT_LIST_HEAD(&ptype_base[i]);

    //初始化offload_base链表
    INIT_LIST_HEAD(&offload_base);

    //注册跟网络命名空间相关的信息
    //INIT_LIST_HEAD(&net->dev_base_head); 初始化dev_base_head链表
    //net->dev_name_head 初始化dev_name_head 散列表,用来根据网络设备名获取网络设备
    //net->dev_index_head 初始化dev_index_head 散列表,用来根据设备索引号获取网络设备
    if (register_pernet_subsys(&netdev_net_ops))
        goto out;

    /*
     *    Initialise the packet receive queues.
     */

    //初始化每个CPU的softnet_data ,包括发送数据包的等待释放队列,轮询函数等
    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);

        //非napi使用此input_pkt_queue
        skb_queue_head_init(&sd->input_pkt_queue);
        skb_queue_head_init(&sd->process_queue);
#ifdef CONFIG_XFRM_OFFLOAD
        skb_queue_head_init(&sd->xfrm_backlog);
#endif
        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

        init_gro_hash(&sd->backlog);

        //初始化napi_struct结构
        sd->backlog.poll = process_backlog;
        sd->backlog.weight = weight_p;
    }

    dev_boot_phase = 0;

    /* The loopback device is special if any other network devices
     * is present in a network namespace the loopback device must
     * be present. Since we now dynamically allocate and free the
     * loopback device ensure this invariant is maintained by
     * keeping the loopback device as the first device on the
     * list of network devices.  Ensuring the loopback devices
     * is the first device that appears and the last network device
     * that disappears.
     */
    if (register_pernet_device(&loopback_net_ops))
        goto out;

    if (register_pernet_device(&default_device_ops))
        goto out;

    //注册网络报文输出/输入软中断处理例程
    open_softirq(NET_TX_SOFTIRQ, net_tx_action);
    open_softirq(NET_RX_SOFTIRQ, net_rx_action);

    rc = cpuhp_setup_state_nocalls(CPUHP_NET_DEV_DEAD, "net/dev:dead",
                       NULL, dev_cpu_dead);
    WARN_ON(rc < 0);
    rc = 0;
out:
    return rc;
}

subsys_initcall(net_dev_init);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
softnet_data结构描述了与网络软中断处理相关的报文输入和输出队列,每个CPU有一个单独的softnet_data,因此在操作该结构的成员时不必加锁

中断上半部
//随意举例一个驱动处理函数
static int vortex_rx(struct net_device *dev)
{
    ...
    //分配skb,包含skb->dev = dev
    skb = netdev_alloc_skb(dev, pkt_len + 5);
    if (skb != NULL) {
        ...
        skb_reserve(skb, 2);    /* Align IP on 16 byte boundaries */
        skb->protocol = eth_type_trans(skb, dev);
        netif_rx(skb);
        dev->stats.rx_packets++;
        ...
    }
    ...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
在进入netif_rx之前,已经设置好了skb->dev

/**
 * eth_type_trans - determine the packet's protocol ID.
 * @skb: received socket data
 * @dev: receiving network device
 *
 * The rule here is that we
 * assume 802.3 if the type field is short enough to be a length.
 * This is normal practice and works for any 'now in use' protocol.
 */
__be16 eth_type_trans(struct sk_buff *skb, struct net_device *dev)
{
    unsigned short _service_access_point;
    const unsigned short *sap;
    const struct ethhdr *eth;

    //记录收包的设备
    skb->dev = dev;
    
    //设置skb->mac_header,即以太网头部的偏移量(skb->mac_header = skb->data - skb->head)
    //只有这里设置了skb->mac_header,才能使用eth_hdr函数(skb->head + skb->mac_header)获取以太网头部
    skb_reset_mac_header(skb);

    //获取以太网头部
    eth = (struct ethhdr *)skb->data;

    //将data指针指向ip层头部(没有vlan的情况下)
    //在有vlan的情况下data指针 是指向vlan tag的tci信息,下节中会深入分析这一做法
    skb_pull_inline(skb, ETH_HLEN);

    //确定目的mac地址是单播还是广播,设置skb->pkt_type的类型
    if (unlikely(is_multicast_ether_addr_64bits(eth->h_dest))) {
        if (ether_addr_equal_64bits(eth->h_dest, dev->broadcast))
            skb->pkt_type = PACKET_BROADCAST;
        else
            skb->pkt_type = PACKET_MULTICAST;
    }
    //目的mac地址不是本机
    else if (unlikely(!ether_addr_equal_64bits(eth->h_dest,
                           dev->dev_addr)))
        skb->pkt_type = PACKET_OTHERHOST;

    /*
     * Some variants of DSA tagging don't have an ethertype field
     * at all, so we check here whether one of those tagging
     * variants has been configured on the receiving interface,__netif_receive_skb
     * and if so, set skb->protocol without looking at the packet.
     */
    if (unlikely(netdev_uses_dsa(dev)))
        return htons(ETH_P_XDSA);

    //当h_proto > 1536
    //ETH_P_IP       0x0800  iP_rcv
    //ETH_P_ARP      0x0806  arp_rcv
    //ETH_P_PPP_SES  0x8864  pppoe_rcv
     if (likely(eth_proto_is_802_3(eth->h_proto)))
        return eth->h_proto;

    /*
     *      This is a magic hack to spot IPX packets. Older Novell breaks
     *      the protocol design and runs IPX over 802.3 without an 802.2 LLC
     *      layer. We look for FFFF which isn't a used 802.2 SSAP/DSAP. This
     *      won't work for fault tolerant netware but does for the rest.
     */
     //IPX数据包没有802.2标准的LLC层,0xFFFF为其标志位
    sap = skb_header_pointer(skb, 0, sizeof(*sap), &_service_access_point);
    if (sap && *sap == 0xFFFF)
        return htons(ETH_P_802_3);

    /*
     *      Real 802.2 LLC
     */
     //802.2标准的LLC协议
    return htons(ETH_P_802_2);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
Ethernat的地址其实就是Mac地址。所以长度是6byte。其中有一位为multicast bit位。格式如下

当unicast/multicast bit位置1时就是Multicast, Mac地址为0xFFFFFFFFFFFF时就是broadcast.

以及 skb的data指针已经指向ip层,设置好了skb->pkt_type,skb->protocol,skb->mac_header(使用eth_hdr函数来获取以太网头部的前提)

接下来会调用netif_rx

int netif_rx(struct sk_buff *skb)
{
    trace_netif_rx_entry(skb);

    return netif_rx_internal(skb);
}

static int netif_rx_internal(struct sk_buff *skb)
{
    int ret;

    net_timestamp_check(netdev_tstamp_prequeue, skb);

    ...
    if (static_key_false(&rps_needed)) {
        ...
    }
    else
    {
        unsigned int qtail;

        ret = enqueue_to_backlog(skb, get_cpu(), &qtail);
        put_cpu();
    }
    return ret;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
netif_rx()调用enqueue_to_backlog()来处理

static int enqueue_to_backlog(struct sk_buff *skb, int cpu, unsigned int *qtail)
{
    struct softnet_data *sd;
    unsigned long flags;
    unsigned int qlen;

    sd = &per_cpu(softnet_data, cpu);

    local_irq_save(flags);
    ...
    //获取input_pkt_queue长度
    qlen = skb_queue_len(&sd->input_pkt_queue);
    if (qlen <= netdev_max_backlog && !skb_flow_limit(skb, qlen)) {
    
        //如果接收队列sd->input_pkt_queue不为空,说明已经有软中断在处理数据包了,
        //则不需要再次触发软中断,直接将数据包添加到接收队列尾部即可
        if (qlen) {
enqueue:
            __skb_queue_tail(&sd->input_pkt_queue, skb);
            input_queue_tail_incr_save(sd, qtail);
            rps_unlock(sd);
            local_irq_restore(flags);
            return NET_RX_SUCCESS;
        }

        /* Schedule NAPI for backlog device
         * We can use non atomic operation since we own the queue lock
         */

        //如果接收队列sd->input_pkt_queue为空,说明当前没有软中断在处理数据包,
        //则把虚拟设备backlog添加到sd->poll_list中以便进行轮询,最后设置NET_RX_SOFTIRQ标志触发软中断。
        if (!__test_and_set_bit(NAPI_STATE_SCHED, &sd->backlog.state)) {
            if (!rps_ipi_queued(sd))
                ____napi_schedule(sd, &sd->backlog);
        }
        goto enqueue;
    }

//如果接收队列sd->input_pkt_queue满了,则直接丢弃数据包
drop:
    sd->dropped++;
    rps_unlock(sd);

    local_irq_restore(flags);

    atomic_long_inc(&skb->dev->rx_dropped);
    kfree_skb(skb);
    return NET_RX_DROP;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
软中断(NET_RX_SOFTIRQ)的处理函数net_rx_action()

中断下半部
net_rx_action()为网络输入软中断的处理例程,当网络设备有数据输入,非NAPI和NAPI的网络设备驱动程序都会激活网络输入软中断进行处理

static __latent_entropy void net_rx_action(struct softirq_action *h)
{

    //获取CPU的softnet_data 
    struct softnet_data *sd = this_cpu_ptr(&softnet_data);

    //一次中断处理的最长时间
    unsigned long time_limit = jiffies +
        usecs_to_jiffies(netdev_budget_usecs);

    //获取本次软中断的接受报文配额
    int budget = netdev_budget;
    LIST_HEAD(list);
    LIST_HEAD(repoll);

    local_irq_disable();
    list_splice_init(&sd->poll_list, &list);
    local_irq_enable();

    //遍历网络设备轮询队列上的网络设备,轮询接受这些网络设备上的报文
    for (;;) {
        struct napi_struct *n;

        if (list_empty(&list)) {
            if (!sd_has_rps_ipi_waiting(sd) && list_empty(&repoll))
                goto out;
            break;
        }

        //获取链表上的第一个napi_struct实例
        n = list_first_entry(&list, struct napi_struct, poll_list);

        //调用napi_struct的poll方法,默认为process_backlog()
        //在之前的net_dev_init中  sd->backlog.poll = process_backlog;
        budget -= napi_poll(n, &repoll);
        ...
    }
    ...
out:
    __kfree_skb_flush();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
当调用napi_struct的poll()来处理数据包时,本地中断是开启的,这意味着新的数据包可以继续添加到输入队列中

接下来会调用napi_struct的poll方法,如果网卡驱动不支持NAPI,则默认的napi_struct->poll()函数为process_backlog

static int process_backlog(struct napi_struct *napi, int quota)
{
    struct softnet_data *sd = container_of(napi, struct softnet_data, backlog);
    bool again = true;
    int work = 0;
    ...
    
    //每次处理的最大数据包数
    napi->weight = dev_rx_weight;
    while (again) {
        struct sk_buff *skb;

        //获取当前待处理报文,获取失败则说明队列为空,
        while ((skb = __skb_dequeue(&sd->process_queue))) {
            rcu_read_lock();

            //当前报文传到上层协议或转发
            __netif_receive_skb(skb);
            
            rcu_read_unlock();
            input_queue_head_incr(sd);

            //统计本次处理的报文数,若达到配额,则结束本次报文输入
            if (++work >= quota)
                return work;

        }

        ...

    return work;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
接下来就调用__netif_receive_skb进入桥进行转发或上交到协议栈处理
--------------------- 
作者:Sophisticated_ 
来源:CSDN 
原文:https://blog.csdn.net/Sophisticated_/article/details/87295513 
版权声明:本文为博主原创文章,转载请附上博文链接!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值