数据包的接收流程

这里来讲下收到数据包后,中断做了什么事情。本文可以看成是对《Understanding Linux Network Internals》的学习笔记。

当一个数据包到达的时候,触发中断(注意,是某个CPU的中断,只有这个CPU会受影响),然后do_IRQ就会被调用。do_IRQ我们之前提过,其先是根据中断号调用handler,然后最后会的触发do_softirq做下半区的处理。

对于handler,主要做了下面的事情(下面会看到,对于NAPI做的事情会稍有不同):
1.将数据帧内容拷贝到分配的sk_buff中
2.初始化sk_buff的一些属性
3.更新设备net_device的private数据
4.设置NET_RX_SOFTIRQ,下半区看到有softirq在pending后其会继续处理

首先说一下,net_device->state如果设置了__LINK_STATE_STAR,表明这个设备是enable了。只有设备enable才能发送或接受数据包。这个状态由net_device的open方法设置,close方法取消。在这个flage设置的情况下,如果想禁止网卡发包可以设置__LINK_STATE_XOFF这个flage。但是没有flag可以禁止收包。netif_running可以用于检查__LINK_STATE_STAR是否设置。

在2.5版本的kernel之前,比如上面第四步中做的事情是通过netif_rx实现的。在2.5后则是引入了NAPI。因此下面这两个都会讲到。NAPI的优点主要是混合了中断和轮询。在老的netif_rx中,一般一个数据包到了后会只会通过中断告知内核,但是当数据包很多的时候内核就会来不及处理响应(其会不停的被新的中断打断正在处理的事情)。对于NAPI来说,如果一个新的数据包到达,然后发现内核还有未处理的数据包的时候,其会将中断模式改为轮询,于是新的数据包到达后就不会打断内核,同时当内核空闲的时候其会去轮询设备,查看是否有要处理的数据包。当压力下来后又可以恢复中断模式。通过这种方法在大量的数据包环境中减少CPU使用率。

首先我们来看下NAPI,在之前文章说过的probe方法中,会初始化net_device中关于NAPI的一些属性:
poll:用于从输入队列获取包的轮询方法。对于NAPI来说其会从一个私有队列中获取输入包,而对于老的netif_rx方法来说则是从softnet_data->input_pkt_queue中获取
poll_list:一个串着等待被轮询的设备的链表的头(指向cpu的softnet_data的poll_list),这些设备目前的中断是关闭的,需要轮询获取数据
quota/weight:poll一次可以从队列中拿走多少个数据包。quota越小的话用在这个设备上的时间也就越少,其它设备就能获得更多的时间
代码如下(在probe中可以找到):

netif_napi_add(netdev, &adapter->napi, e1000e_poll, 64);
void netif_napi_add(struct net_device *dev, struct napi_struct *napi,
            int (*poll)(struct napi_struct *, int), int weight)
{
    INIT_LIST_HEAD(&napi->poll_list);
    hrtimer_init(&napi->timer, CLOCK_MONOTONIC, HRTIMER_MODE_REL_PINNED);
    napi->timer.function = napi_watchdog;
    napi->gro_count = 0; 
    napi->gro_list = NULL;
    napi->skb = NULL;
    napi->poll = poll;
    if (weight > NAPI_POLL_WEIGHT)
        pr_err_once("netif_napi_add() called with weight %d on device %s\n",
                weight, dev->name);
    napi->weight = weight;
    list_add(&napi->dev_list, &dev->napi_list);
    napi->dev = dev; 
#ifdef CONFIG_NETPOLL
    spin_lock_init(&napi->poll_lock);
    napi->poll_owner = -1;
#endif
    set_bit(NAPI_STATE_SCHED, &napi->state);
}
EXPORT_SYMBOL(netif_napi_add);

当某个时候软中断被触发后,net_rx_action对于老的netif_rx会的进行老的一套处理,同时对于NAPI的设备,其会的遍历cpu的softnet_data的poll_list,然后对每个设备调用其poll方法进行处理。但在目前的实现中,不论是老的一套还是新的一套都是基于poll的,这个下面会讲到。

现在分别简单的讲下non-NAPI和NAPI的收包的过程。

non-NAPI:
1.数据包到达网卡
2.网卡触发中断
3.根据中断号调用handler
4.handler会从网卡中拷贝出数据包到sk_buff中,并初始化一些属性
5.handler调用netif_rx,后者会将这个设备对应的backuplog_dev(也是一个net_device结构体)附加到poll_list中,同时将数据包放到softnet_data的input_pkt_queue尾部
6.触发软中断
7.软中断处理函数发现poll_list非空,于是遍历poll_list,对每个设备调用poll方法。在non-NAPI中,poll是内核提供的默认方法,处理逻辑就是从softnet_data的input_pkt_queue取出数据包然后进行处理

NAPI:
1.数据包到达网卡
2.网卡触发中断
3.根据中断号调用handler
4.handler将这个设备放到poll_list中
5.触发软中断
6.软中断处理函数发现poll_list非空,于是遍历poll_list,对每个设备调用poll方法。对于NAPI其poll方法就是驱动自己实现的了

可以看到,对于non-NAPI来说,内核已经让netif_rx的实现和NAPI统一了。另外,在poll_list的设备被poll调用的时候是有quota的,如果处理的数据包达到了quota并且这个设备还有待处理的数据包的时候,其会被移动到poll_list的尾部,这样可以给其他设备处理的时间。

ok,我们继续看poll,现在数据包已经被poll拿到了(可能是来自softnet_data的私有队列,也可能是poll直接从设备拿到),然后poll会开始处理这个包(也就是开始我们的协议栈)。处理的入口是netif_receive_skb。

对于netif_receive_skb,主要做了下面的事情:
1.处理bond的情况(调用skb_bond)
2.初始化skb的h、nh、mac_len字段
3.对于每个protocol的sniffer,拷贝一个skb给他们进行处理
4.判断这个包是否要进入bridge流程。如果要则进入bridge的流程,否则继续
5.对于每个skb->protocol,拷贝一个skb给他们进行处理
6.数据包的收取流程到此结束

现在来总结下几个重要的函数:
netif_rx:老的驱动通过这个函数将数据包放到softnet_data的input_pkt_queue尾部,同时将backuplog_dev放到poll_list中等待下半区处理。
net_rx_action:遍历poll_list中的net_device,调用其poll方法。对于老的驱动poll方法由内核提供,对于NAPI则由驱动自己提供
netif_rx_schedule/__netif_rx_schedule:将net_device放到poll_list中。对于NAPI在中断handler中会调用这个方法,对于non-NAPI则是由netif_rx调用,netif_rx中会的调用这个方法
netif_rx_complete/__netif_rx_complete:poll_list中的net_device在poll处理后,如果没有数据包了则调用这个方法
netif_poll_disable/netif_poll_enable:net_device在被加入到poll_list之前,会检查__LINK_STATE_RX_SCHED这个flag是否设置,如果设置了则认为已经在poll_list中了,则不会再次添加。引入如果想disable一个net_device,可以直接设置则个flag,从此其就不会被放入到poll_list中,于是其收到的包也就一直在设备内存中,不会被内核处理
process_backlog:non-NAPI默认的poll方法
netif_receive_skb:poll方法中,让数据包开始协议栈处理的入口函数。无论是non-NAPI还是NAPI都是通过这个函数传递sk_buff,然后通过协议栈处理这个sk_buff的。

发表评论

电子邮件地址不会被公开。 必填项已用*标注

*