从网卡收包到上送协议栈有两个模式:
一种是传统的中断模式,即收到一个数据包,执行一次中断处理函数(比如e100_rx),在此函数中分配skb,替换有数据的skb(DMA已经将数据拷贝到初始化的skb),调用netif_rx将有数据的skb放在percpu的队列上(如果开启了RPS,这个队列有可能是本地cpu的,也有可能是其他cpu的),最后激活软中断。之后的软中断处理函数net_rx_action中调用poll函数process_backlog(如果将skb放在其他cpu队列上了,还需要通过ipi激活其他cpu的软中断),处理percpu队列上的数据包,上送协议栈__netif_receive_skb。
中断模式会触发很多中断,影响性能,所以有了napi模式,这种模式下,一次中断可以poll收多个数据包(配额64)。具体的为收到一个中断,执行中断处理函数(比如ixgbe_msix_clean_rings),在此函数中只是激活软中断,并不处理skb,在之后的软中断处理函数net_rx_action中调用驱动注册的poll函数,比如ixgbe_poll,来收包,上送协议栈netif_receive_skb_internal(如果开启了RPS,就会按照non-napi的处理方式,将skb放在percpu的队列上,这个队列有可能是本地cpu的,也有可能是其他cpu的),再经过软中断处理才会将skb上送协议栈__netif_receive_skb。
下面的图片展示了这两种模式的流程,其中蓝色部分为公共流程,红色的为non-NAPI流程,绿色的为NAPI流程。

软中断流程分为两步,首先激活软中断,然后在某个时刻执行软中断处理函数
- 激活软中断有以下三个地方
a. 非网络软中断激活方式
raise_softirq
raise_softirq_irqoff(nr);
__raise_softirq_irqoff(unsigned int nr)
or_softirq_pending(1UL << nr);
b. NAPI模式下激活软中断方式,一般在驱动的中断处理函数中调用
napi_schedule
__napi_schedule(n);
____napi_schedule(this_cpu_ptr(&softnet_data), n);
list_add_tail(&napi->poll_list, &sd->poll_list);
__raise_softirq_irqoff(NET_RX_SOFTIRQ);
or_softirq_pending(1UL << nr);
c. non-NAPI模式下激活软中断方式,在netif_rx->enqueue_to_backlog时调用
enqueue_to_backlog
sd = &per_cpu(softnet_data, cpu);
____napi_schedule(sd, &sd->backlog);
list_add_tail(&napi->poll_list, &sd->poll_list);
__raise_softirq_irqoff(NET_RX_SOFTIRQ);
or_softirq_pending(1UL << nr);
- 执行软中断的有以下三个地方:
a. 硬件中断代码返回的时候
irq_exit
if (!in_interrupt() && local_softirq_pending())
invoke_softirq
__do_softirq
b. ksoftirqd内核服务线程运行的时候
__do_softirq
invoke_softirq
raise_softirq_irqoff
wakeup_softirqd
run_ksoftirqd
if (local_softirq_pending()) {
__do_softirq
c. netif_rx_ni
netif_rx_ni 会先将做和netif_rx一样的操作后,如果有软中断激活,则执行软中断
netif_rx_ni
if (local_softirq_pending())
do_softirq();
do_softirq_own_stack();
if (local_softirq_pending())
__do_softirq
软中断相关初始化
kernel启动时,软中断相关初始化
static int __init net_dev_init(void)
{
...
/*
* Initialise the packet receive queues.
*/
初始化percpu的结构softnet_data
for_each_possible_cpu(i) {
struct softnet_data *sd = &per_cpu(softnet_data, i);
skb_queue_head_init(&sd->input_pkt_queue);
skb_queue_head_init(&sd->process_queue);
INIT_LIST_HEAD(&sd->poll_list);
sd->output_queue_tailp = &sd->output_queue;
#ifdef CONFIG_RPS
sd->csd.func = rps_trigger_softirq; //激活其他cpu软中断
sd->csd.info = sd;
sd->cpu = i;
#endif
backlog借用napi的结构,实现non-NAPI的处理。
process_backlog就是NAPI下的poll函数
sd->backlog.poll = process_backlog;
sd->backlog.weight = weight_p;
}
...
注册和网络相关的两个软中断处理函数
open_softirq(NET_TX_SOFTIRQ, net_tx_action);
open_softirq(NET_RX_SOFTIRQ, net_rx_action);
...
}
支持以下软中断类型
enum
{
HI_SOFTIRQ=0,
TIMER_SOFTIRQ,
NET_TX_SOFTIRQ,
NET_RX_SOFTIRQ,
BLOCK_SOFTIRQ,
BLOCK_IOPOLL_SOFTIRQ,
TASKLET_SOFTIRQ,
SCHED_SOFTIRQ,
HRTIMER_SOFTIRQ,
RCU_SOFTIRQ, /* Preferable RCU should always be the last softirq */
NR_SOFTIRQS
};
注册软中断处理函数
void open_softirq(int nr, void (*action)(struct softirq_action *))
{
softirq_vec[nr].action = action;
}
non-NAPI处理流程
- 激活软中断
网卡收到数据包后,通过中断通知cpu,cpu调用网卡驱动注册的中断处理函数,比如dm9000_interrupt,调用netif_rx将skb放入percpu队列,激活软中断。细节请看下面代码分析
static irqreturn_t dm9000_interrupt(int irq, void *dev_id)
/* Received the coming packet */
if (int_status & ISR_PRS)
dm9000_rx(dev);
//分配 skb
skb = netdev_alloc_skb(dev, RxLen + 4)
//将数据存入 skb
rdptr = (u8 *) skb_put(skb, RxLen - 4);
(db->inblk)(db->io_data, rdptr, RxLen);
//调用netif_rx处理skb
netif_rx(skb);
int netif_rx(struct sk_buff *skb)
{
//static tracepoint
trace_netif_rx_entry(skb);
return netif_rx_internal(skb);
}
获取合适的cpu,调用 enqueue_to_backlog 将skb放入percpu的队列中
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
如果内核配置选项配置了 RPS,并且使能了rps(echo f >
/sys/class/net/eth0/queues/rx-0/rps_cpus),则通过get_rps_cpu获取合适的cpu(有
可能是本地cpu也有可能是remote cpu),否则使用本地cpu
if (static_key_false(&rps_needed)) {
struct rps_dev_flow voidflow, *rflow = &voidflow;
int cpu;
preempt_disable();
rcu_read_lock();
cpu = get_rps_cpu(skb->dev, skb, &rflow);
if (cpu < 0)
cpu = smp_processor_id();
ret = enqueue_to_backlog(skb, cpu, &rflow->last_qtail);
rcu_read_unlock();
preempt_enable();
} else
#endif
{
unsigned int qtail;
没有配置rps,则获取当地cpu
ret = enqueue_to_backlog(skb, get_cpu(), &qtail);
put_cpu();
}
return ret;
}
将skb放在指定cpu的softnet_data->input_pkt_queue队列中,
如果是队列上第一个包还需要激活软中断
/*
* enqueue_to_backlog is called to queue an skb to a per CPU backlog
* queue (may be a remote CPU 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;
获取percpu的sd
sd = &per_cpu(softnet_data, cpu);
local_irq_save(flags);
rps_lock(sd);
if (!netif_running(skb->dev))
goto drop;
如果队列中skb个数小于netdev_max_backlog(默认值1000,可以通过sysctl修改netdev_max_backlog值),
并且 skb_flow_limit (为了防止large flow占用太多cpu,small flow得不到处理。代码实现没看明白)返回false,则skb可以继续入队,否则drop skb
qlen = skb_queue_len(&sd->input_pkt_queue);
if (qlen <= netdev_max_backlog &&