Linux内核网络收包角度—浅入中断(2)

语言: CN / TW / HK

上文L inux内核网络收包角度——浅入中断(1) 主要从针对e1000网卡收包开始,分析了硬中断注册、处理过程,网卡收包后硬中断的过程非常简洁,处理过程比较简单, 要是将网卡的napi->poll_list加入到CPU的softnet_data->poll_list里,然后发起软中断,就结束了,下面的就交给软中断进行数据包的接收处理,所以收包任务最大程度地交给软中断处理,最大程度简化硬中断处理。 总结以上描述中,上篇博文的具体细节步骤:

  • 内核启动网卡,为网卡分配(Ring Buffer,内核通过网卡驱动将DMA内存地址信息写入网卡寄存器,使得网卡获得DMA内存信息),并通过request_irq 内核 APi注册硬中断服务例程e1000_intr

  • 网卡收到数据包,通过DMA将数据包写入内存

  • 网卡触发硬中断,通过CPU接收数据

  • CPU中断当前的进程,跳转到异常向量表的中断异常向量处

  • 保存现场

  • 调用irq_handler

  • 对于ARM64,irq_handler将调用gic_handle_irq

  • gic_handle_irq首先读取中断寄存器得到硬件中断号,调用handle_domain_irq函数

  • handle_domain_irq ->irq_enter进入中断上下文

  • handle_domain_irq ->irq_find_mapping通过硬件中断号获取IRQ Number

  • handle_domain_irq->generic_handle_irq进入中断通用层处理

  • generic_handle_irq ->irq_to_desc通过IRQ Number获取对应的中断描述符

  • generic_handle_irq ->generic_handle_irq_desc->__handle_irq_event_percpu:遍历中断描述符中的action链表,依次执行每个action中回调函数action->handler,对应e1000网卡驱动,执行e1000_intr中断服务例程

  • e1000_intr->ew32(IMC, ~0)禁止网卡中断,避免频繁硬中断,降低内核的工作效率

  • e1000_intr-> napi_schedule激活NAPI,   napi_struct.poll_list 挂在 softnet_data.poll_list 上,方便后面软中断调用 napi_struct.poll 获取网卡数据。然后设置NET_RX_SOFTIRQ 软中断标识位。

  • handle_domain_irq->irq_exit():退出中断上下文,触发软中断执行收包流程。

  • 恢复现场

上文L inux内核网络收包角度——浅入中断(1) 重点在于通过网卡收包分析了硬中断, 本文从网络收包角度分析和学习软中 断过程。

软中断的种类、定义(Kernel 4.15)

enum
{
 HI_SOFTIRQ=0,
 TIMER_SOFTIRQ,
 NET_TX_SOFTIRQ,
 NET_RX_SOFTIRQ,
 BLOCK_SOFTIRQ,
 IRQ_POLL_SOFTIRQ,
 TASKLET_SOFTIRQ,
 SCHED_SOFTIRQ,
 HRTIMER_SOFTIRQ, /* Unused, but kept as tools rely on the
       numbering. Sigh! */
 RCU_SOFTIRQ,    /* Preferable RCU should always be the last softirq */

 NR_SOFTIRQS
}

以上是内核默认定义的几种软中断,硬中断是在CPU每个指令周期后,会去判断是否有硬中断产生,根据硬中断号找到对应的IRQ Number,执行对应的中断服务例程,而软中断会有内核线程,轮询一组标志位,如果标志位有值,那去这个标志位对应的软中断向量表,找到中断处理函数执行。

软中断的注册

全局的软中断向量数组,即软中断描述符表

static struct softirq_action softirq_vec[NR_SOFTIRQS] __cacheline_aligned_in_smp;

const char * const softirq_to_name[NR_SOFTIRQS] = {
"HI", "TIMER", "NET_TX", "NET_RX", "BLOCK", "IRQ_POLL",
"TASKLET", "SCHED", "HRTIMER", "RCU"
}

数组中的内容就是软中断触发时的钩子函数:

struct softirq_action
{

void (*action)(struct softirq_action *);
}

如网络收发包的软中断处理函数在网络子系统初始化时注册的:

static int __init net_dev_init(void)
{
......
open_softirq(NET_TX_SOFTIRQ, net_tx_action);
open_softirq(NET_RX_SOFTIRQ, net_rx_action);
......
}

注册软中断回调函数:

void open_softirq(int nr, void (*action)(struct softirq_action *))
{
softirq_vec[nr].action = action;
}

软中断在内核中的整体如下所示,pending为软中断标志位

其中标志位在内核中使用irq_cpustat来表示:

typedef struct {
unsigned int __softirq_pending;
#ifdef CONFIG_SMP
unsigned int ipi_irqs[NR_IPI];
#endif
} ____cacheline_aligned irq_cpustat_t;
}

其中__softirq_pending为软中断标志位,该类型为int类型,最多支持32位软中断,ipi_rrq位cpu与cpu之间的中断。

在Linux内核中,定义NR_CPUS个该结构,即每个CPU有一个32bit的位图,来维护本cpu上的软中断是否激活

irq_cpustat_t irq_stat[NR_CPUS] ____cacheline_aligned;

软中断的激活

在上文 L inux内核网络收包角度——浅入中断(1) 中,硬中断的中断服务例程e1000_intr执行:

e1000_intr-> __napi_schedule:

void __napi_schedule(struct napi_struct *n)
{
unsigned long flags;

local_irq_save(flags);//禁用中断
____napi_schedule(this_cpu_ptr(&softnet_data), n);
local_irq_restore(flags);//恢复中断
}

__napi_schedule(this_cpu_ptr(&softnet_data),n):

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);
}

最终调用__raise_soft_irqoff设置软中断标志位,nr为NET_RX_SOFTIRQ

void __raise_softirq_irqoff(unsigned int nr)
{
trace_softirq_raise(nr);
or_softirq_pending(1UL << nr);
}

可以看到在执行 __raise_soft_irqoff函数前,在 __napi_schedule中,先是通过local_irq_save(flags)禁用了中断,因为置位位图是一个竞争操作,所有硬中断都可以做,所以要保证关中断的情况下完成,等pending置位结束后调用local_irq_restore(flags)恢复中断。

内核线程ksoftirqd是如何监测到软中断发生的:

static void run_ksoftirqd(unsigned int cpu)
{
local_irq_disable();
if (local_softirq_pending()) {
/*
* We can safely run softirq on inline stack, as we are not deep
* in the task stack here.
*/

__do_softirq();
local_irq_enable();
cond_resched_rcu_qs();
return;
}
local_irq_enable();
}

上面函数先关中断查看本cpu的pending置位情况,如果有则进行进一步软中断处理。

软中断的入口函数__do_softirq:

asmlinkage __visible void __softirq_entry __do_softirq(void)
{
......
pending = local_softirq_pending(); //将32位的pending集合取出来
account_irq_enter_time(current);

__local_bh_disable_ip(_RET_IP_, SOFTIRQ_OFFSET);
in_hardirq = lockdep_softirq_start();

restart:

set_softirq_pending(0);//pending清空

local_irq_enable(); //使能中断

h = softirq_vec; //软中断处理向量表,里面是软中断处理函数指针

while ((softirq_bit = ffs(pending))) { //获取最高位置1的位数
unsigned int vec_nr;
int prev_count;

h += softirq_bit - 1;

vec_nr = h - softirq_vec;//指针直接相减,得到软件中断号
prev_count = preempt_count();

kstat_incr_softirqs_this_cpu(vec_nr);//对当前处理的软中断进行统计基数

trace_softirq_entry(vec_nr);
h->action(h); //回调中断处理函数
trace_softirq_exit(vec_nr);
if (unlikely(prev_count != preempt_count())) {
pr_err("huh, entered softirq %u %s %p with preempt_count %08x, exited with %08x?\n",
vec_nr, softirq_to_name[vec_nr], h->action,
prev_count, preempt_count());
preempt_count_set(prev_count);
}
h++;
pending >>= softirq_bit;
}

rcu_bh_qs();
local_irq_disable();//关中断

pending = local_softirq_pending(); //重新获取pending集合(开中断到关中断的过程中可能有新的硬中断到来,这时候需要重新处理一遍)
if (pending) { //如果pengding中又有软中断置位
if (time_before(jiffies, end) && !need_resched() &&
--max_restart)
goto restart;

wakeup_softirqd(); //唤醒软中断处理线程处理
}

lockdep_softirq_end(in_hardirq);
account_irq_exit_time(current);
__local_bh_enable(SOFTIRQ_OFFSET);
WARN_ON_ONCE(in_interrupt());
current_restore_flags(old_flags, PF_MEMALLOC);
}

对于数据包接收h->action()将执行:网络子系统注册的的回调函数net_rx_action:

static int __init net_dev_init(void)
{
......
open_softirq(NET_TX_SOFTIRQ, net_tx_action);
open_softirq(NET_RX_SOFTIRQ, net_rx_action);
......
}

关于net_rx_action涉及到软中断处理中进一步使用NAPI机制收包,在接下来的文章中进行分析和学习。

参考:

http://zhuanlan.zhihu.com/p/363225092

http://zhuanlan.zhihu.com/p/363225717

http://www.i4k.xyz/article/weixin_38878510/109024909

END