数据包最先当然是由网卡收到(不考虑环回接口这样的虚拟设备),那么之后软件是如何接收该数据,又是如何将数据递交给协议栈的,这篇笔记就来看看linux内核和驱动程序时如何配合完整这个接收过程的。
1. 数据接收模式
当前内核提供了两种数据接收模式:非NAPI方式(老方法)和NAPI(新方法,即New API)。新老方法的接收过程分别如下图所示:
1.1 非NAPI方式
如上图b所示,当网卡收到数据包,产生一个硬中断,驱动程序在硬中断处理过程中,从设备读取数据,构造出SKB,然后直接将该SKB放入当前CPU的网络设备接收队列input_pkt_queue中,该队列随后会由网络接收软中断程序处理,软中断处理程序会将数据包递交给上层协议栈。
图中的“将数据包添加到输入队列”操作是由驱动通过netif_rx()接口实现的,该接口是内核框架提供给驱动使用的。
关于接收队列和网络接收软中断的介绍见下文。
1.2 NAPI方式
如上图a所示,这种方式的第一步并不是将数据从设备中读取出来,而是将网络设备(即net_device)添加到poll_list(一个轮询队列)中,然后激活网络接收软中断,在软中断处理函数中会遍历该poll_list,依次处理该轮询队列上的设备,回调设备提供的数据接收函数完成数据包的接收。
1.3 新老方式对比
老方法完全依赖于中断读取数据包,在高负载场景下,可能会频繁的中断CPU,造成资源浪费。新方法背后的思想也简单:就是中断+轮询。首次数据包到达时,中断CPU,然后驱动程序关掉设备中断,将设备放入poll_list中,只要设备中一直有数据,那么就让该设备一直在poll_list中,网络接收软中断会不断的处理该poll_list,这样可以不断的从设备中读取数据,直到该设备中的数据读取完毕为止。这种模式可以尽可能的减少中断的次数,但是有不会引入太大的时延,所以目前的驱动基本上都采用这种新方法,当然了,作为内核框架是完全兼容这两种方式的,下文会减少这是怎么实现的。
2. 接收队列
从上面的介绍中可以看到,非NAPI方式操作的是input_pkt_queue队列;NAPI方式也会操作poll_list队列,实际上,接收过程涉及如下队列。
2.1 input_pkt_queue
这个队列是每个CPU相关的,即每个CPU都有一个input_pkt_queue,非NAPI接收模式会使用该队列,NAPI方式不使用。该队列中存放的就是数据包SKB,而且队列是由所有设备共享的。
2.2 设备轮询队列poll_list
这个队列同样是CPU相关的,即每个CPU都有一个poll_list,NAPI方式使用该队列,非NAPI方式不使用。注意的是该队列中存放的是设备net_device,这些设备有数据要接收。
注:实际上该队列中挂接的是下文的struct napi_struct,但是由于网络设备和该结构是一一对应的,这么描述也没什么毛病。
2.3 收发队列softnet_data
如下,input_pkt_queue队列和轮询队列poll_list是softnet_data的成员,而后者还包含用于发送和其它方面的一些成员。内核使用该结构来管理所有的收发队列,这里我们先只关注input_pkt_queue和poll_list,其它成员在下文或者其它笔记中会有详细介绍。
/*
* Incoming packets are placed on per-cpu queues so that
* no locking is needed.
*/
//由于struct softnet_data是PER-CPU的,所以对它的访问无需持锁
struct softnet_data
{
//发送过程使用
struct net_device *output_queue;
//对于非NAPI方式的接收,驱动通过轮询或者硬中断(或二者结合)的方式将数据包放入该队列,然后激活
//输入软中断程序,软中断程序会处理该队列中数据包,基于流量控制的排队规则将数据包递交给上层
struct sk_buff_head input_pkt_queue;
//网络设备轮询队列。驱动应该将需要轮询的网络设备的struct napi_struct链接到该队列并激活输入
//软中断程序。输入软中断程序会遍历该队列,调用驱动提供的netpoll()接收完成接收
struct list_head poll_list;
//发送过程使用
struct sk_buff *completion_queue;
//为了将软中断接收处理程序对非NAPI方式和NAPIF方式的处理统一,对于非NAPI接收,在硬中断处理
//后,将backlog结构也加入到poll_list,然后触发软中断接收程序,具体见下面非NAPI方式的接收
struct napi_struct backlog;
};
###2.3.1 收发队列初始化
队列的初始化是在设备接口层初始化过程中完成的,代码片段如下:
static int __init net_dev_init(void)
{
/*
* Initialise the packet receive queues.
*/
//为了高效,每个CPU都有独立收发队列,这样可以减少并发需要的持锁
for_each_possible_cpu(i) {
struct softnet_data *queue;
queue = &per_cpu(softnet_data, i);
//初始化非NAPI接收队列
skb_queue_head_init(&queue->input_pkt_queue);
//初始化完成队列,该队列用于发送过程中存放那些已经发送完毕等待释放的SKB
queue->completion_queue = NULL;
//初始化接收轮询队列
INIT_LIST_HEAD(&queue->poll_list);
//初始化非NAPI对应的backlog,其poll()函数为process_backlog
queue->backlog.poll = process_backlog;
queue->backlog.weight = weight_p;
}
//注册网络接收和发送软中断处理函数
open_softirq(NET_TX_SOFTIRQ, net_tx_action, NULL);
open_softirq(NET_RX_SOFTIRQ, net_rx_action, NULL);
}
可见,整个struct softnet_data结构都是每个CPU一份,这意味其中的收发队列也是每个CPU一个,而且对队列的访问无需持锁。
3. NAPI方式接收数据
首先需要说明的是,驱动程序可以选择不支持这种方式,但是对于高速网络设备,实现它是必要的,如前面所讲,这种方式可以减少中断CPU的次数,对接收效率有很大的提升。NAPI方式的数据包处理方式如下图所示:
3.1 struct napi_struct
为了支持NAPI方式,内核定义了该结构用于控制轮询过程。支持NAPI方式的驱动程序需要在数据到达时,填充其中的信息,将其接入到当前CPU的接收轮询队列poll_list中并激活接收软中断,真正的接收操作在接收软中断中调用poll()完成。
/*
* Structure for NAPI scheduling similar to tasklet but with weighting
*/
struct napi_struct {
/* The poll_list must only be managed by the entity which
* changes the state of the NAPI_STATE_SCHED bit. This means
* whoever atomically sets that bit can add this napi_struct
* to the per-cpu poll_list, and whoever clears that bit
* can remove from the list right before clearing the bit.
*/
//用于将该设备接入CPU的轮询队列poll_list中
struct list_head poll_list;
unsigned long state;
//该设备的配额,一次可以轮询的最大数据包数不能大于等于该值,如果poll()的返回值等于weight
//有特殊含义,见下面net_rx_action()
int weight;
//驱动提供的轮询接口,网络接收软中断会回调该接口进行数据的读取
int (*poll)(struct napi_struct *, int);
};
state目前版本有两个bit可以设置:
值 | 含义 |
---|---|
NAPI_STATE_SCHED | 如果一个设备被加入到轮询队列poll_list,那么会设置该bit,移除时清除 |
NAPI_STATE_DISABLE | 一旦禁用状态被设置,那么该设备将无法被轮询,通常在网络设备DOWN的时候设置该标记 |
3.2 软中断接收net_rx_action()
int netdev_budget __read_mostly = 300;
static void net_rx_action(struct softirq_action *h)
{
//获取待轮询设备队列
struct list_head *list = &__get_cpu_var(softnet_data).poll_list;
//记录开始时间
unsigned long start_time = jiffies;
//本次软中断接收可以接收的最大数据包数目,即配额.
//netdev_budget是一个独立于网络设备的系统参数,默认300,可以通过sysctl修改
int budget = netdev_budget;
void *have;
//由于网卡中断处理函数也会操作poll_list(添加napi_struct),所以虽然该队列每个
//CPU一份,但是还是需要关闭本地CPU的中断
local_irq_disable();
while (!list_empty(list)) {
struct napi_struct *n;
int work, weight;
/* If softirq window is exhuasted then punt.
*
* Note that this is a slight policy change from the
* previous NAPI code, which would allow up to 2
* jiffies to pass before breaking out. The test
* used to be "jiffies - start_time > 1".
*/
//con1:本次软中断的配额已经用尽,所以停止接收,等待下次调度
//con2: 本次软中断执行时间已经超过一个时钟中断间隔,所以停止接收,等待下次调度
//这种设计从包数和时间两个维度控制软中断的执行时长,避免其长时间执行(因为关闭本地CPU中断了)
if (unlikely(budget <= 0 || jiffies != start_time))
goto softnet_break;
//下面的注释解释了为什么这里可以开启中断
local_irq_enable();
/* Even though interrupts have been re-enabled, this
* access is safe because interrupts can only add new
* entries to the tail of this list, and only ->poll()
* calls can remove this head entry from the list.
*/
n = list_entry(list->next, struct napi_struct, poll_list);
//netpoll相关,忽略
have = netpoll_poll_lock(n);
//该网络设备有多少数据需要读取
weight = n->weight;
/* This NAPI_STATE_SCHED test is for avoiding a race
* with netpoll's poll_napi(). Only the entity which
* obtains the lock and sees NAPI_STATE_SCHED set will
* actually make the ->poll() call. Therefore we avoid
* accidently calling ->poll() when NAPI is not scheduled.
*/
//调用驱动提供的poll接口进行数据的接收,返回值work代表实际读取到的数据包数
//如果数据已经全部读取完毕,poll的实现应该将该设备从轮询队列中移除
work = 0;
//只有网络设备设置了NAPI_STATE_SCHED比特位才能被真正调度
if (test_bit(NAPI_STATE_SCHED, &n->state))
work = n->poll(n, weight);
//禁止设备一次读取的数据包数目超过自身的限额
WARN_ON_ONCE(work > weight);
//从总的配额中减去该网络设备消耗的配额
budget -= work;
//下面可能会修改poll_list,所以重新关闭本地CPU中断
local_irq_disable();
/* Drivers must not modify the NAPI state if they
* consume the entire weight. In such cases this code
* still "owns" the NAPI instance and therefore can
* move the instance around on the list at-will.
*/
//如果poll()的返回值和配额相同(work==weight),那么还没有读完,表示需要将该设备移到轮询
//队列末尾等待下一次轮询(感觉这种设计怪怪的,不是非常合理)
if (unlikely(work == weight)) {
//如果设备在这期间已经被DISABLE,那么需要将其从轮询队列中移除
if (unlikely(napi_disable_pending(n)))
__napi_complete(n);
else
list_move_tail(&n->poll_list, list);
}
netpoll_poll_unlock(have);
}
out:
local_irq_enable();
return;
softnet_break:
//增加time_squeeze,如果该值过大,表示每次软中断都没有处理完数据包,
//网卡接收非常忙,所以这里可能是性能瓶颈
__get_cpu_var(netdev_rx_stat).time_squeeze++;
//因为poll_list中还有设备需要接收数据,所以需要再次激活软中断
__raise_softirq_irqoff(NET_RX_SOFTIRQ);
goto out;
}
3.2.1 其它细节
从net_rx_action()的实现中可以清晰的看到,采用NAPI方式接收数据时内核框架做了哪些工作,那么剩下的工作当然应该由驱动程序实现,主要有下面:
- 驱动在将设备加入轮询队列之前,应该调用netif_rx_schedule_prep()判断NAPI结构的状态是否OK(没有被DISABLE),并且还没有被加入到轮询队列(state中没有设置NAPI_STATE_SCHED标记),如果这两个条件都满足,那么设置NAPI_STATE_SCHED标记;然后调用__netif_rx_schedule()将设备加入到轮询队列
- 数据接收完毕后,应该由驱动的poll()函数将设备从轮询队列中移除,这通过netif_rx_complete()完成
- poll()的返回值表示的是本次轮询实际读取的包数
- 内核框架在调用poll()回调后并没有将数据包递交给上层协议栈,所以驱动应该在poll()实现中调用下文会介绍的netif_receive_skb()来完成这个工作
4. 非NAPI方式接收数据
这里我们同样不关注驱动程序具体是如何将数据从硬件中搬移到内存的,而是着眼于驱动拿到SKB后的接收流程。如上文1.1小节介绍,驱动收到数据后调用内核框架提供的netif_rx()将数据包放入input_pkt_queue中待网络接收软中断程序进一步处理。
4.1 netif_rx()
/**
* netif_rx - post buffer to the network code
* @skb: buffer to post
*
* This function receives a packet from a device driver and queues it for
* the upper (protocol) levels to process. It always succeeds. The buffer
* may be dropped during processing for congestion control or by the
* protocol layers.
*
* return values:
* NET_RX_SUCCESS (no congestion)
* NET_RX_DROP (packet was dropped)
*
*/
int netif_rx(struct sk_buff *skb)
{
struct softnet_data *queue;
unsigned long flags;
/* if netpoll wants it, pretend we never saw it */
//如果被netpoll处理了,直接返回DROP,协议栈不处理。
//这里忽略netpoll机制,认为其没有处理
if (netpoll_rx(skb))
return NET_RX_DROP;
//如果驱动没有为数据包设置接收时间戳,在这里设置它
if (!skb->tstamp.tv64)
net_timestamp(skb);
/*
* The code is rearranged so that the path is the most
* short when CPU is congested, but is still operating.
*/
//下面要把SKB放入input_pkt_queue队列,所以要先关闭本地CPU的硬中断,理由同上面poll_list
local_irq_save(flags);
//获取本地CPU的收发队列
queue = &__get_cpu_var(softnet_data);
//网卡设备接收数据包个数统计值+1
__get_cpu_var(netdev_rx_stat).total++;
//判断input_pkt_queue队列中数据包个数是否超过了系统限制netdev_max_backlog
if (queue->input_pkt_queue.qlen <= netdev_max_backlog) {
//如果队列不为空,说明软中断处理程序已经在处理该队列了,
//这时只需将数据包放入input_pkt_queue中即可
if (queue->input_pkt_queue.qlen) {
enqueue:
dev_hold(skb->dev);
__skb_queue_tail(&queue->input_pkt_queue, skb);
local_irq_restore(flags);
return NET_RX_SUCCESS;
}
//如果队列为空,那么需要将backlog调度到poll_list中,如果需要还要激活软中断处理程序,
//这里linux内核巧妙的将NAPI和非NAPI方式的处理流程进行了兼容,见下文的分析
napi_schedule(&queue->backlog);
//激活后把数据包加入到input_pkt_queue中
goto enqueue;
}
//到这里说明input_pkt_queue队列中的数据包超过了系统限制,统计后丢弃该数据包
__get_cpu_var(netdev_rx_stat).dropped++;
local_irq_restore(flags);
kfree_skb(skb);
return NET_RX_DROP;
}
从代码中可以看出,netif_rx()所做的工作就是将数据包放入input_pkt_queue中,如果没有激活接收软中断,那么激活它。
4.2 process_backlog()
无论是NAPI还是非NAPI,最终都要用到网络接收软中断net_rx_action()。为了保持在net_rx_action()中对这两种方式的处理的一致性,内核将非NAPI方式的处理方式也抽象成一种特殊的NAPI,并为其实现了特殊的poll()函数,该poll()函数需要做的事情就是遍历input_pkt_queue队列,将其中的数据包递交给上层协议,这就是struct softnet_data中backlog的设计思想,相关代码如下:
static int __init net_dev_init(void)
{
...
for_each_possible_cpu(i) {
struct softnet_data *queue;
...
//初始化非NAPI对应的backlog,其poll()函数为process_backlog
queue->backlog.poll = process_backlog;
queue->backlog.weight = weight_p;
}
...
}
static int process_backlog(struct napi_struct *napi, int quota)
{
int work = 0;
//获取本CPU的输入队列
struct softnet_data *queue = &__get_cpu_var(softnet_data);
unsigned long start_time = jiffies;
//读取数据量超过配额或者到达1个jiffies后结束
napi->weight = weight_p;
do {
struct sk_buff *skb;
struct net_device *dev;
local_irq_disable();
//取出一个数据包
skb = __skb_dequeue(&queue->input_pkt_queue);
//如果队列已空,则将这个特殊的napi_struct从轮询队列中移除
if (!skb) {
__napi_complete(napi);
local_irq_enable();
break;
}
local_irq_enable();
dev = skb->dev;
//将数据包递交给层三协议
netif_receive_skb(skb);
//设备引用计数-1,可以看到,一旦SKB离开层二,它与设备的绑定关系就被解除了,所以在上层协议中如过
//想要通过skb->dev访问其网络设备是有风险的,因为底层的net_device可能已经被删除了
dev_put(dev);
} while (++work < quota && jiffies == start_time);
//返回本次实际处理的数据包数
return work;
}
5. 递交数据包给上层协议
现在,设备接口层对数据包的接收已经全部介绍完了,设备接口层剩下的工作就是将数据递交给层三协议继续处理了,从process_backlog()中可以看到,这是通过netif_receive_skb()完成的。实际上dev_queue_xmit_nit()也会将数据包传递个层三,但是它只用于AF_PACKET套接字接收本地输出报文时使用,这里暂不研究这种场景。
在看netif_receive_skb()的具体实现之前,先思考一件事情。系统中实际上有很多的协议族,这些协议族主要是层三协议不同,但是它们可以基于共同的物理网络传输数据,所以对于设备接口层,在向上递交数据时,需要知道应该将该数据包转给哪个协议族的层三协议。为了解决这个问题,系统维护一个全局的ptype_base哈希表,所有的层三协议需要将自己能够处理的报文类型提前注册到该哈希表,设备接口层就是基于该哈希表进行数据包的分发。
5.1 报文接收例程
层三协议需要向ptype_base注册的数据结构是struct packet_type,其定义如下:
struct packet_type {
//层三协议数据包类型
__be16 type; /* This is really htons(ether_type). */
//如果层三协议只想接收来自某个网络设备的数据,那么可以指定该成员;如果为NULL,
//则表示接受来自任意网络设备的数据包
struct net_device *dev; /* NULL is wildcarded here */
//层三提供的数据包接收函数,网络设备层处理完毕后调用该函数将数据传给层三协议
int (*func) (struct sk_buff *,
struct net_device *,
struct packet_type *,
struct net_device *);
struct sk_buff *(*gso_segment)(struct sk_buff *skb,
int features);
int (*gso_send_check)(struct sk_buff *skb);
//协议族自行定义的私有数据结构
void *af_packet_priv;
//用于将该结构接入ptype_base哈希表的某个冲突链
struct list_head list;
};
ptype_base哈希表的组织结构如下图所示:
5.2 netif_receive_skb()
/**
* netif_receive_skb - process receive buffer from network
* @skb: buffer to process
*
* netif_receive_skb() is the main receive data processing function.
* It always succeeds. The buffer may be dropped during processing
* for congestion control or by the protocol layers.
*
* This function may only be called from softirq context and interrupts
* should be enabled.
*
* Return values (usually ignored):
* NET_RX_SUCCESS: no congestion
* NET_RX_DROP: packet was dropped
*/
int netif_receive_skb(struct sk_buff *skb)
{
struct packet_type *ptype, *pt_prev;
struct net_device *orig_dev;
int ret = NET_RX_DROP;
__be16 type;
/* if we've gotten here through NAPI, check netpoll */
//netpoll相关,先忽略
if (netpoll_receive_skb(skb))
return NET_RX_DROP;
//如果没有设置接收时间戳,则设置它
if (!skb->tstamp.tv64)
net_timestamp(skb);
//指定接收该报文的网络设备的ID
if (!skb->iif)
skb->iif = skb->dev->ifindex;
//虚拟网络设备Bonding相关的高级话题,先忽略
orig_dev = skb_bond(skb);
if (!orig_dev)
return NET_RX_DROP;
//对于非NAPI方式的接收,这里的统计有bug,会统计两次,因为在netif_rx中已经统计过了
__get_cpu_var(netdev_rx_stat).total++;
//重新设置网络层和传输层的报文头部
skb_reset_network_header(skb);
skb_reset_transport_header(skb);
skb->mac_len = skb->network_header - skb->mac_header;
pt_prev = NULL;
//ptype_all哈希表使用RCU锁保护
rcu_read_lock();
//遍历ptype_all链表,传递一份数据到注册在ptype_all链表上的协议
//这是在网络设备接口层提供的一种勾包方式,主要用于分析,这里先忽略
list_for_each_entry_rcu(ptype, &ptype_all, list) {
if (!ptype->dev || ptype->dev == skb->dev) {
if (pt_prev)
ret = deliver_skb(skb, pt_prev, orig_dev);
pt_prev = ptype;
}
}
//如果网桥已经处理,直接返回,网桥是一种高级话题,这里先忽略,认为它没有处理
skb = handle_bridge(skb, &pt_prev, &ret, orig_dev);
if (!skb)
goto out;
//如果VLAN已经处理,直接返回,VLAN也是一种高级话题,这里先忽略,认为它没有处理
skb = handle_macvlan(skb, &pt_prev, &ret, orig_dev);
if (!skb)
goto out;
//遍历ptype_base哈希表中type所映射的冲突链,调用其提供的func()方法传递给上层处理
//这里之所以看起来这么复杂,完全是为了少调用一次skb_free(),使用了pt_prev指针,网上有许多关于
//该话题的讨论,可以参考:https://blog.csdn.net/plt2007plt/article/details/8876034
type = skb->protocol;
list_for_each_entry_rcu(ptype,
&ptype_base[ntohs(type) & PTYPE_HASH_MASK], list) {
if (ptype->type == type &&
(!ptype->dev || ptype->dev == skb->dev)) {
if (pt_prev)
ret = deliver_skb(skb, pt_prev, orig_dev);
pt_prev = ptype;
}
}
if (pt_prev) {
ret = pt_prev->func(skb, skb->dev, pt_prev, orig_dev);
} else {
kfree_skb(skb);
/* Jamal, now you will not able to escape explaining
* me how you were going to use this. :-)
*/
ret = NET_RX_DROP;
}
out:
rcu_read_unlock();
return ret;
}