NAPI真的是kernel开发者词穷想的名字吧,你看看kernel里面各种名字,不知道为啥就不能起个好听点的。
言归正传,wiki:https://en.wikipedia.org/wiki/New_API 给出的解释是NAPI是一种用于网络设备的中断缓解的技术。
听起来比较抽象,想象下双十一当天的淘宝服务器,有撑破天际的网络数据从全世界各个角落汇聚到该服务器的每个网卡,也就意味着无时无刻该网卡都有大量数据进来,IRQ不断的产生, CPU正常调度不断被IRQ打断,网卡多累,CPU的效率多低。我这里把他称为纯中断方式。所以NAPI的核心想法就是减轻网卡的负担,不让网卡每次都响应中断,响应一次中断缓一缓,让子弹飞一会,数据攒一会,再响应中断, 收割积攒的数据,这样就使得效率高很多。
谈到NAPI,很多人的理解就仅仅是poll方式,好像我用了NAPI就是等于用了poll方式接收数据,其实不然。
NAPI的精髓是在poll方式 / 纯中断方式之间自由灵活的游走切换。
As a compromise, the Linux kernel uses the interrupt-driven mode by
default and only switches to polling mode when the flow of incoming
packets exceeds a certain threshold, known as the "weight" of the
network interface. --wiki
进一步说是,数据量不大的时候用纯中断,数据量很大的时候用poll的方式。那么什么时候switch,如何switch呢,且听我慢慢道来。
要搞懂NAPI,就要从它背后的逻辑和机制看起。要看懂此文,需要了解linux的中断处理的大致机制
I.
首先从三个重要的API开始:
netif_napi_add --driver告诉内核要使用napi的机制,初始化响应参数,注册poll的回调函数
napi_schedule --driver告诉内核开始调度napi的机制,稍后poll回调函数会被调用
napi_complete --driver告诉内核其工作不饱满即中断不多,数据量不大,改变napi的状态机,后续将采用纯中断方式响应数据
net_rx_action --内核初始化注册的软中断,注册poll的回调函数会被其调用
使用起来好像不是特别复杂。
II.
再来,我们看下几个重要的数据结构和原理。
softnet_data --这是一个PER_CPU的queue,更准确地说是一个和每个CPU绑定,属于该CPU的data queue,incoming packets are placed on per-CPU queues. 注意它的成员poll_list
struct softnet_data {
struct Qdisc *output_queue;
struct Qdisc **output_queue_tailp;
struct list_head poll_list;//// napi->poll_list结构挂入这个list,包括NAPI接口的driver以及非NAPI接口的driver都可以统一加入到这个poll_list
struct sk_buff *completion_queue;
...省略
};
napi_struct -- napi的关键结构,它也有一个poll_list,用于挂在softnet_data的poll_list上,是softnet_data poll list上的最小调度单位。还有一个weight需要着重说一下,这是每次调用poll回调函数时分配的最大skb数量,或者说从DMA buffer里面可以收割的最大的skb的数量。也就是前面wiki说的threshold。默认64,千兆网卡。netif_napi_add的时候注册。
struct napi_struct {
struct list_head poll_list; /* 用于加入处于轮询状态的设备队列 */
unsigned long state; /* 设备的状态 */
int weight; /* 每次处理的该设备的最大skb数量 */
int (*poll) (struct napi_struct *, int); /* 此设备的轮询方法 */
#ifdef CONFIG_NETPOLL
...省略
#endif
unsigned int gro_count;
struct net_device *dev;
struct list_head dev_list;
struct sk_buff *gro_list;
struct sk_buff *skb;
};
画个图简单说:
所以一个napi_struct在softnet_data的poll list会发生的故事只可能有:
a. enqueue, 入队列
b. dequeue, 出队列
c. reorder, 重新调整队列顺序
III.
那么napi_struct什么时候enqueue挂在softnet_data的poll list,以及什么时候reorder移动它,或者dequeue从list里面去掉,不妨先了解下整个NAPI工作的流程
图侵删
请注意上面的图不是软件流程图,只是大概的因果关系,具体怎么回事等我细细道来,
1. HW ISR,具体来说算是DMA中断,告诉CPU搬砖完成,这样会通过触发中断最终触发driver注册到内核的中断函数,例如注册的PCI的中断.
2. 在这个IRQ函数中,通常driver会disable和clear IRQ,比如关PCI中断,然后调用napi_schedule,它所作的事情就是enqueue,然后mask一个NET_RX_SOFTIRQ event
3.接下来不久,当中断上半部结束,开始中断下半部的时候,会在__do_softirq中发现先前mask的NET_RX_SOFTIRQ有效,net_rx_action作为其callback会被调用。在net_rx_action会调用driver注册的poll函数。net_rx_action这个最重要的函数还是要看下的。
static void net_rx_action(struct softirq_action *h) //from kernel 3.13
{
struct softnet_data *sd = &__get_cpu_var(softnet_data);
unsigned long time_limit = jiffies + 2;
int budget = netdev_budget;//300
void *have;
local_irq_disable();
while (!list_empty(&sd->poll_list)) {
struct napi_struct *n;
int work, weight;
/* If softirq window is exhuasted then punt.
* Allow this to run for 2 jiffies since which will allow
* an average latency of 1.5/HZ.
*/
if (unlikely(budget <= 0 || time_after_eq(jiffies, time_limit)))
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_first_entry(&sd->poll_list, struct napi_struct, poll_list);
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
* accidentally calling ->poll() when NAPI is not scheduled.
*/
work = 0;
if (test_bit(NAPI_STATE_SCHED, &n->state)) {
work = n->poll(n, weight);
trace_napi_poll(n);
}
WARN_ON_ONCE(work > weight);
budget -= work;
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.
*/
if (unlikely(work == weight)) {
if (unlikely(napi_disable_pending(n))) {
local_irq_enable();
napi_complete(n);
local_irq_disable();
} else {
if (n->gro_list) {
/* flush too old packets
* If HZ < 1000, flush all packets.
*/
local_irq_enable();
napi_gro_flush(n, HZ >= 1000);
local_irq_disable();
}
list_move_tail(&n->poll_list, &sd->poll_list);
}
}
netpoll_poll_unlock(have);
}
out:
net_rps_action_and_irq_enable(sd);
#ifdef CONFIG_NET_DMA
/*
* There may not be any more sk_buffs coming right now, so push
* any pending DMA copies to hardware
*/
dma_issue_pending_all();
#endif
return;
softnet_break:
sd->time_squeeze++;
__raise_softirq_irqoff(NET_RX_SOFTIRQ);
goto out;
}
一条条说:
1) 每次NET_RX这个软中断也会分配一个budget,默认300,含义见下条。
int budget = netdev_budget;//300
2)每次都从list头取个napi_struct,然后调poll回调函数。结合之前的napi的budget,我们大概明白,poll回调函数会消耗软中断的budget,如果这个poll list的poll消耗完了软中断的所有budget(300),则循环退出,按照每次poll消耗都是64来算也就是5次。这个时候poll list还有待poll的napi struct。另一个退出条件是超时2 jiffies。这个两种条件一种是poll list上的poll收割的数据都是大包,所以时间没超时但是总的budget用完。另一种情况就是都是数量庞大的小包,但是都没超budget,最后超时。然后不管哪种情况,都会mask一个NET_RX_SOFTIRQ event。留待下次net_rx_action处理。
if (unlikely(budget <= 0 || time_after_eq(jiffies, time_limit)))
goto softnet_break; ...省略
n = list_first_entry(&sd->poll_list, struct napi_struct, poll_list); ... 省略
work = n->poll(n, weight); ... 省略
budget -= work;... 省略
__raise_softirq_irqoff(NET_RX_SOFTIRQ);
3)Driver的poll函数通常每家实现不一样,大概逻辑就是通常会从dma ring里面把数据拿出来,最多拿napi的注册weight(默认64)的skb。
如果ring里面的数据不够weight,注意了,这里很关键,就会认为,数据已经拿完,执行napi_complete, 将该napi_struct从list里面拿掉,并且开启中断,比如开启PCI,进入纯中断模式;这之后有可能有新的中断触发,于是新的napi_struct在HW ISR中(抢占软中断)被加入到poll list,而且那个时候for循环还没有退出。
如果ring里面数据太多,那就只能拿到weight这么多,注意了,这里不会将napi_struct从list上拿掉,也不会关中断,poll函数返回时会在net_rx_action中返回消耗的budget,如果消耗到最大值weight(64),说明还有数据要取,这样会放到poll list的尾部(假设不考虑GRO的情况)。留待下次到队头再次poll。这时候是switch到了poll模式
work = n->poll(n, weight); ... 省略
if (unlikely(work == weight)) {
if (unlikely(napi_disable_pending(n))) {
... 省略
} else {
if (n->gro_list) {
... 省略
}
list_move_tail(&n->poll_list, &sd->poll_list);
}
只有在poll回调函数里面认为已收割数据不超过weight,中断才打开,才会有新的中断进来,才会有新的napi_struct加进list(queue)。
只要poll回调函数里面的中断没有打开,比如PCI,都属于poll模式,在net_rx_action的budget(300)和2 jiffies时间用完时,退出for循环, 不一定是poll模式,比如一直都是小包的且很频繁。也有可能会退出。
图侵删
4. 非正常退出时,list上一定还是有napi_struct的,那么非正常退出之后又发生了什么?无论是哪种非正常退出(netdev_budget 用完 or 超时2 jiffies),新的NET_RX_SOFTIRQ mask会置起来,那么然后什么时候处理呢?
这里要对内核软中断机制有一定的了解。简单说就是net_rx_action退出之后返回到__do_softirq, 这里面还会check是否有pending的中断。如果有并且没有超时(2ms),也没有超过最大restart次数(10次),也不需要resched,那么会重新调用net_rx_action,重复之前的过程;
但是,如果不满足上述条件,将会退出软中断过程,
1)将net_rx_action的执行放在ksoftirqd
2)由于其他设备中断触发软中断后执行__do_softirq,间接执行net_rx_action
3)内核进程调用local_bh_enable中也会调用__do_softirq
pending=local_softirq_pending();
if(pending){
if(time_before(jiffies,end)&&!need_resched()&&--max_restart)
goto restart;
}
所以补全之前的图:
IV.
关于NAPI的Tunning,可以看到主要有的参数有netdev_budget, napi_struct的budget,以及超时的2 jiffies。时间是hard-coded,除非你想改内核代码,前两个都可以在不同阶段tunning。最灵活的netdev_budget,可以run time改动,比如
echo 600 > /proc/sys/net/core/netdev_budget
或者
sudo sysctl -w net.core.netdev_budget=600(更改 /etc/sysctl.conf当然也可以)
关于NAPI的debug或者说网络性能的debug,还有一个有意思统计值,就是softnet_data中的time_squeeze, 它记录了非正常退出的次数,对理解网络瓶颈有很大意义。
sd->time_squeeze++;
具体来说time_squeeze可以通过以下路径找到
cat /proc/net/softnet_stat
打印出来的结果是一系列的值,对于kernel 3.13,来自net-procfs.c,softnet_seq_show。time_squeeze就是第三个值,请记住结果中的每一行代表一个CPU。
seq_printf(seq,
"%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x\n",
sd->processed, sd->dropped, sd->time_squeeze,0,
0,0,0,0,/*was fastroute*/
sd->cpu_collision, sd->received_rps, flow_limit_count);
V.
最后我们不妨来考虑下面几种情况,到底是poll模式还是中断模式:
a. 中断频繁,数据量大
b. 中断不频繁,数据量大
c. 中断频繁,数据量小
d. 中断不频繁,数据量小
d. 中断不频繁,大包小包都有,就是数据量波动
欢迎讨论:bmebob_zhao@163.com
以上.
网友评论