设备模块中数据包接收的两个队列
驱动层程序通过netif_rx或者netif_rx_ni将接收的数据包传入到设备层,设备模块分成两个阶段处理数据包。第一阶段将数据包添加到接收队列(input_pkt_queue)末尾,接收处理完成;第二阶段将接收队列的数据包移动到处理队列(process_queue)中。两个阶段的操作都是链表操作,不涉及到skb数据包的拷贝。
在第一阶段中,仅是添加到接收队列即返回,以便驱动程序可以接收下一个数据包。第二阶段在中断下半部中执行,首先处理process_queue队列中已有的数据包,之后将第一阶段添加到input_pkt_queue队列中的数据包,拼接到process_queue队列中,等待下次处理。
队列初始化
网络设备模块在初始化时,为每一个CPU初始化接收和处理队列。
DEFINE_PER_CPU_ALIGNED(struct softnet_data, softnet_data);
数据包接收队列
函数enqueue_to_backlog负责将数据包添加到接收队列中。首先取出对应cpu的input_pkt_queue队列长度,如果其值已经超过netdev_max_backlog(默认为1000,可通过proc文件/proc/sys/net/core/netdev_max_backlog修改)的最大值,此数据包skb将会被丢弃。否则添加到接收队列的末尾。
由于每个CPU具有单独的input_pkt_queue队列,在对其操作的时候,我们只需要关闭当前CPU的中断即可。
static int enqueue_to_backlog(struct sk_buff *skb, int cpu, unsigned int *qtail)
{
sd = &per_cpu(softnet_data, cpu);
local_irq_save(flags);
qlen = skb_queue_len(&sd->input_pkt_queue);
if (qlen <= netdev_max_backlog && !skb_flow_limit(skb, qlen)) {
__skb_queue_tail(&sd->input_pkt_queue, skb);
local_irq_restore(flags);
return NET_RX_SUCCESS;
}
}
数据包处理队列
内核在下半部中调用process_backlog函数处理数据包。由其实现可见,对于process_queue队列中数据包,调用网络核心接收函数__netif_receive_skb处理,知道配额quota用完为止。之后,判读如果接收队列input_pkt_queue不为空,将接收队列拼接到处理队列上。接收队列清空。继续处理添加到process_queue队列的数据包。
在处理接收队列input_pkt_queue时,关闭本地CPU的中断,确保队列操作的完整性。
static int process_backlog(struct napi_struct *napi, int quota)
{
while (again) {
while ((skb = __skb_dequeue(&sd->process_queue))) {
__netif_receive_skb(skb);
if (++work >= quota) return work;
}
local_irq_disable();
if (skb_queue_empty(&sd->input_pkt_queue)) {
again = false;
} else {
skb_queue_splice_tail_init(&sd->input_pkt_queue,
&sd->process_queue);
}
local_irq_enable();
}
}
由函数process_backlog可见,process_queue处理队列的存在,使在处理数据包时不必关闭本地CPU的中断。而对input_pkt_queue队列的处理又做到了精简,提高了效率,关闭中断的时间缩短了。
内核版本
Linux-4.15
推荐阅读