一、背景
随着网络带宽的发展,网速越来越快,之前的中断收包模式已经无法适应目前千兆,万兆的带宽了。如果每个数据包大小等于MTU大小1460字节。当驱动以千兆网速收包时,CPU将每秒被中断91829次。在以MTU收包的情况下都会出现每秒被中断10万次的情况。过多的中断会引起一个问题,CPU一直陷入硬中断而没有时间来处理别的事情了。为了解决这个问题,内核在2.6中引入了NAPI机制。
NAPI就是混合中断和轮询的方式来收包,当有中断来了,驱动关闭中断,通知内核收包,内核软中断轮询当前网卡,在规定时间尽可能多的收包。时间用尽或者没有数据可收,内核再次开启中断,准备下一次收包。
本文将介绍Linux内核中的NAPI:Linux网络设备驱动程序中的一种支持新一代网络适配器的架构。
二、NAPI机制
New API(NAPI)用于支持高速网卡处理网络数据包的一种机制 - 例如在Linux 2.6内核版本中引入的千兆以太网卡,后来又被移植到了2.4.x版本中。
NAPI 是 Linux 上采用的一种提高网络处理效率的技术,它的核心概念就是不采用中断的方式读取数据,而代之以首先采用中断唤醒数据接收的服务程序,然后 POLL 的方法来轮询数据。随着网络的接收速度的增加,NIC 触发的中断能做到不断减少,目前 NAPI 技术已经在网卡驱动层和网络层得到了广泛的应用,驱动层次上已经有 E1000 系列网卡,RTL8139 系列网卡,3c50X 系列等主流的网络适配器都采用了这个技术,而在网络层次上,NAPI 技术已经完全被应用到了著名的netif_rx 函数中间,并且提供了专门的 POLL 方法--process_backlog 来处理轮询的方法;根据实验数据表明采用NAPI技术可以大大改善短长度数据包接收的效率,减少中断触发的时间。
NAPI 对数据包到达的事件的处理采用轮询方法,在数据包达到的时候,NAPI 就会强制执行dev->poll方法。而和不像以前的驱动那样为了减少包到达时间的处理延迟,通常采用中断的方法来进行。
以前的网络设备驱动程序架构已经不能适用于每秒产生数千个中断的高速网络设备,并且它可能导致整个系统处于饥饿状态(译者注:饥饿状态的意思是系统忙于处理中断程序,没有时间执行其他程序)。有些网络设备具有中断合并,或者将多个数据包组合在一起来减少中断请求这种高级功能。
在内核没有使用NAPI来支持这些高级特性之前,这些功能只能全部在设备驱动程序中结合抢占机制(例如基于定时器中断),甚至中断程序范围之外的轮询程序(例如:内核线程,tasklet等)中实现。
正如我们看到的,网络子系统中加入的这个新特性是用于更好的支持中断缓解和数据包限制等功能,更重要的是它允许内核通过round-robin策略(轮询即Round Robin,一种负载均衡策略)将负载分发到不同网络设备上。
NAPI特性的添加不会影响内核的向后兼容性。
2.1 NAPI缺陷
NAPI 存在一些比较严重的缺陷:
1. 对于上层的应用程序而言,系统不能在每个数据包接收到的时候都可以及时地去处理它,而且随着传输速度增加,累计的数据包将会耗费大量的内存,经过实验表明在 Linux 平台上这个问题会比在 FreeBSD 上要严重一些;
2. 另外一个问题是对于大的数据包处理比较困难,原因是大的数据包传送到网络层上的时候耗费的时间比短数据包长很多(即使是采用 DMA 方式),所以正如前面所说的那样,NAPI 技术适用于对高速率的短长度数据包的处理。
2.2 使用 NAPI 先决条件
驱动可以继续使用老的 2.4 内核的网络驱动程序接口,NAPI 的加入并不会导致向前兼容性的丧失,但是 NAPI 的使用至少要得到下面的保证:
1. 设备需要有足够的缓冲区,保存多个数据分组。要使用 DMA 的环形输入队列(也就是 ring_dma,这个在 2.4 驱动中关于 Ethernet 的部分有详细的介绍),或者是有足够的内存空间缓存驱动获得的包。
2. 可以禁用当前设备中断,然而不影响其他的操作。在发送/接收数据包产生中断的时候有能力关断 NIC 中断的事件处理,并且在关断 NIC 以后,并不影响数据包接收到网络设备的环形缓冲区(以下简称 rx-ring)处理队列中。
当前大部分的设备都支持NAPI,但是为了对之前的保持兼容,内核还是对之前中断方式提供了兼容。我们先看下NAPI具体的处理方式。
我们都知道中断分为中断上半部和下半部,上半部完成的任务很是简单,仅仅负责把数据保存下来;而下半部负责具体的处理。为了处理下半部,每个CPU有维护一个softnet_data结构(下文将进行讲解)。我们不对此结构做详细介绍,仅仅描述和NAPI相关的部分。结构中有一个poll_list字段,连接所有的轮询设备。还 维护了两个队列input_pkt_queue和process_queue。这两个用户传统不支持NAPI方式的处理。前者由中断上半部的处理函数把数据包入队,在具体的处理时,使用后者做中转,相当于前者负责接收,后者负责处理。最后是一个napi_struct的backlog,代表一个虚拟设备供轮询使用。在支持NAPI的设备下,每个设备具备一个缓冲队列,存放到来数据。每个设备对应一个napi_struct结构,该结构代表该设备存放在poll_list中被轮询。而设备还需要提供一个poll函数,在设备被轮询到后,会调用poll函数对数据进行处理。基本逻辑就是这样,下文将给出具体流程。
/*
* Incoming packets are placed on per-CPU queues
*/
struct softnet_data {
struct list_head poll_list;
struct sk_buff_head process_queue;
/* stats */
unsigned int processed;
unsigned int time_squeeze;
unsigned int received_rps;
#ifdef CONFIG_RPS
struct softnet_data *rps_ipi_list;
#endif
#ifdef CONFIG_NET_FLOW_LIMIT
struct sd_flow_limit __rcu *flow_limit;
#endif
struct Qdisc *output_queue;
struct Qdisc **output_queue_tailp;
struct sk_buff *completion_queue;
#ifdef CONFIG_XFRM_OFFLOAD
struct sk_buff_head xfrm_backlog;
#endif
/* written and read only by owning cpu: */
struct {
u16 recursion;
u8 more;
} xmit;
#ifdef CONFIG_RPS
/* input_queue_head should be written by cpu owning this struct,
* and only read by other cpus. Worth using a cache line.
*/
unsigned int input_queue_head ____cacheline_aligned_in_smp;
/* Elements below can be accessed between CPUs for RPS/RFS */
call_single_data_t csd ____cacheline_aligned_in_smp;
struct softnet_data *rps_ipi_next;
unsigned int cpu;
unsigned int input_queue_tail;
#endif
unsigned int dropped;
struct sk_buff_head input_pkt_queue;
struct napi_struct backlog;
};
2.3 非NAPI帧的接收
我们将讨论内核在接收一个数据帧后的大致处理流程,不会详细叙述所有细节。
我们认为有必要先了解一下传统的数据包处理流程以便更好的理解NAPI和传统收包方式的区别。
在传统的收包方式中(如下图)数据帧向网络协议栈中传递发生在中断上下文(在接收数据帧时)中调用netif_rx的函数中。 这个函数还有一个变体netif_rx_ni,他被用于中断上下文之外。
netif_rx函数将网卡中收到的数据包(包装在一个socket buffer中)放到系统中的接收队列中(input_pkt_queue),前提是这个接收队列的长度没有大于netdev_max_backlog。这个参数和另外一些参数可以在/proc文件系统中看到(/proc/sys/net/core文件中,可以手动调整这个数值)
netif_rx 函数在中断函数中被调用。
2.3.1 netif_rx - 将网卡中收到的数据包放到系统中的接收队列中
int netif_rx(struct sk_buff *skb)
{
int ret;
...
ret = netif_rx_internal(skb);
...
return ret;
}
static int netif_rx_internal(struct sk_buff *skb)
{
int ret;
net_timestamp_check(netdev_tstamp_prequeue, skb);
trace_netif_rx(skb);
#ifdef CONFIG_RPS
...
#endif
{
unsigned int qtail;
ret = enqueue_to_backlog(skb, get_cpu_light(), &qtail);
put_cpu_light();
}
return ret;
}
2.3.2 enqueue_to_backlog
中间RPS暂时不关心,这里直接调用enqueue_to_backlog放入CPU的全局队列 input_pkt_queue
static int enqueue_to_backlog(struct sk_buff *skb, int cpu,
unsigned int *qtail)
{
struct softnet_data *sd;
unsigned long flags;
unsigned int qlen;
/*获取cpu相关的softnet_data变量*/
sd = &per_cpu(softnet_data, cpu);
/*关中断*/
local_irq_save(flags);
rps_lock(sd);
if (!netif_running(skb->dev))
goto drop;
qlen = skb_queue_len(&sd->input_pkt_queue);
/*如果input_pkt_queue的长度小于最大限制,则符合条件*/
if (qlen <= netdev_max_backlog && !skb_flow_limit(skb, qlen)) {
/*如果input_pkt_queue不为空,说明虚拟设备已经得到调度,此时仅仅把数据加入
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
*/
/*队列为空时,即skb是第一个入队元素,则将state设置为 NAPI_STATE_SCHED(软中断处理函数
rx_net_action会检查此标志),表示软中断可以处理此 backlog */
if (!__test_and_set_bit(NAPI_STATE_SCHED, &sd->backlog.state)) {
/* if返回0的情况下,需要将sd->backlog挂到sd->poll_list上,并激活软中断。
rps_ipi_queued看下面的分析 */
if (!rps_ipi_queued(sd))
____napi_schedule(sd, &sd->backlog);
}
goto enqueue;
}
drop:
sd->dropped++;
rps_unlock(sd);
local_irq_restore(flags);
preempt_check_resched_rt();
atomic_long_inc(&skb->dev->rx_dropped);
kfree_skb(skb);
return NET_RX_DROP;
}
/*
* Check if this softnet_data structure is another cpu one
* If yes, queue it to our IPI list and return 1
* If no, return 0
*/
/*上面注释说的很清楚,在配置RPS情况下,检查sd是当前cpu的还是其他cpu的,
如果是其他cpu的,将sd放在当前cpu的mysd->rps_ipi_list上,并激活当前cpu的软中断,返回1.
在软中断处理函数net_rx_action中,通过ipi中断通知其他cpu来处理放在其他
cpu队列上的skb如果是当前cpu,或者没有配置RPS,则返回0,
在外层函数激活软中断,并将当前cpu的backlog放入sd->poll_list上*/
static int rps_ipi_queued(struct softnet_data *sd)
{
#ifdef CONFIG_RPS
struct softnet_data *mysd = this_cpu_ptr(&softnet_data);
if (sd != mysd) {
sd->rps_ipi_next = mysd->rps_ipi_list;
mysd->rps_ipi_list = sd;
__raise_softirq_irqoff(NET_RX_SOFTIRQ);
return 1;
}
#endif /* CONFIG_RPS */
return 0;
}
2.3.3 ____napi_schedule函数
该函数逻辑也比较简单,主要注意的是设备必须先添加调度然后才能接受数据,添加调度调用了____napi_schedule函数,该函数把设备对应的napi_struct结构插入到softnet_data的poll_list链表尾部,然后唤醒软中断,这样在下次软中断得到处理时,中断下半部就会得到处理。不妨看下源码:
/* Called with irq disabled */
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);
}
input_pkt_queue是softnet_data结构体中的一个成员,定义在netdevice.h文件中。
如果接收到的数据包没有因为input_pkt_queue队列已满而被丢弃,它会被netif_rx_schedule函数调度给软中断NET_RX_SOFTIRQ处理,netif_rx_schedule函数在netif_rx函数内部被调用。
软中断NET_RX_SOFTIRQ的处理逻辑在net_rx_action函数中实现。
此时,我们可以说此函数将数据包从input_pkt_queue队列中传递给了网络协议栈,现在数据包可以被处理了。
2.4 NAPI方式
NAPI的方式相对于非NAPI要简单许多,看下e100网卡的中断处理函数e100_intr,核心部分:
static irqreturn_t e100_intr(int irq, void *dev_id)
{
struct net_device *netdev = dev_id;
struct nic *nic = netdev_priv(netdev);
...
if (likely(napi_schedule_prep(&nic->napi))) {
e100_disable_irq(nic);//屏蔽当前中断
__napi_schedule(&nic->napi);//把设备加入到轮训队列
}
return IRQ_HANDLED;
}
if条件检查当前设备是否 可被调度,主要检查两个方面:
1、是否已经在调度
2、是否禁止了napi pending.
如果符合条件,就关闭当前设备的中断,调用__napi_schedule函数把设备假如到轮训列表,从而开启轮询模式。
总结&#