在分析softirq框架时,我们按照数据结构+算法拆开来分析(转载请标明出处)

一、抛开代码细节,聊聊软中断关键的数据结构

在上一章介绍中,我们看到了程序员是如何通过上下部机制来压榨CPU的剩余价值。

里面提到了上下部分机制的理解——上半部记录,下半部处理,软中断设计思路就按照这个中心思想来

我们抛开代码细节逻辑,先看看软中断需要哪些信息才能正常工作

1.之前提到了上下半部工作机制,上半部主要工作就是记录

那么我们首先需要一个数据结构来保存这个信息,上半部记录到我们需要处理什么,下半部拿到记录信息,逐个处理待处理的任务,这个记录的数据结构就是嵌入到irq_stat中的__softirq_pending变量,__softirq_pending每一位表示一类软中断,irq_stat是一个每CPU变量。相关代码如下

DEFINE_PER_CPU_ALIGNED(irq_cpustat_t, irq_stat)
typedef struct {
	unsigned int __softirq_pending;
	unsigned int ipi_irqsp[NR_IPI];
} ____cacheline_aligned irq_cpustat_t;

2.在__softirq_pending中纪录下来需要处理那些软中断后,具体要做哪些工作,也需要一个数据结构来保存,softirq使用了一个函数指针数组softirq_vec来保存具体要做哪些事情

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

enum
{
	HI_SOFTIRQ=0,
	TIMER_SOFTIRQ,
	NET_TX_SOFTIRQ,
	NET_RX_SOFTIRQ,
	BLOCK_SOFTIRQ,
	IRQ_POLL_SOFTIRQ,
	TASKLET_SOFTIRQ,
	SCHED_SOFTIRQ,
	HRTIMER_SOFTIRQ,
	RCU_SOFTIRQ,    /* Preferable RCU should always be the last softirq */

	NR_SOFTIRQS
};
static struct softirq_action softirq_vec[NR_SOFTIRQS] __cacheline_aligned_in_smp;

 3.在处理软中断与硬件中断类似,在软中断处理过程中,虽然可以响应硬件中断,但是不应该被抢占的,而且有时候我们需要禁用软中断,所以也需要一个值表示是否在软中断处理过程中,以及软中断禁用深度。

我们使用preempt_count[15:8](8-16位)标志来表示这些信息。

第8位标志是否在软中断处理过程中

9-15位表示软中断禁用深度, local_bh_disable() [15:9]位对应的值+1,local_bh_enable() [15:9]位对应的值-1

*	PREEMPT_MASK:	0x000000ff
*	SOFTIRQ_MASK:	0x0000ff00
*	HARDIRQ_MASK:	0x000f0000
*	NMI_MASK:	0x00f00000
* 	PREEMPT_NEED_RESCHED:	0x80000000

二、softirq主要逻辑(算法)

第一节分析了softirq中关键的一些数据数据结构,这一节分析一下softirq主要的一些算法逻辑

1.软中断注册

软中断注册其实就是填充softirq_vec这个函数指针数组的过程,之前介绍数据结构的时候提到过,__softirq_pending中的每一位,都表示一类软中断,软中断注册其实就是针对每一类软中断,都将它的处理函数挂到softirq_vec这个函数指针数组中

具体代码如下

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

例如注册HI_SOFTIRQ,注册完后效果如下

open_softirq(HI_SOFTIRQ, tasklet_hi_action);

 全部注册完以后,效果如下

 2.软中断触发

软中断触发过程,其实就是标记__softirq_pending记录过程,触发软中断,就是讲__softirq_pending中对应位置1,这样在软中断处理的时候,发现某一位被置1的话,就回调用注册的对应处理函数进行处理,关键代码如下

void raise_softirq(unsigned int nr)
{
	unsigned long flags;

	local_irq_save(flags);
	raise_softirq_irqoff(nr);
	local_irq_restore(flags);
}

void __raise_softirq_irqoff(unsigned int nr)
{
	lockdep_assert_irqs_disabled();
	trace_softirq_raise(nr);
	or_softirq_pending(1UL << nr); //设置对应软中断位
}

3.软中断处理

上面两节我们已经介绍了软中断注册和触发,那软中断什么时候处理呢?

主要有如下几个地方

  • 硬件代码返回(irq_exit)
  • ksoftirqd内核线程中(每个CPU都对应一个ksoftirqd 内核线程来处理当前CPU的softirq)
  • 网络子系统显示调用的地方
  • 使能下半部机制中local_bh_enable()

软中断处理函数的核心逻辑在__do_softirq中,下面就详细介绍一下__do_softirq

asmlinkage __visible void __softirq_entry __do_softirq(void)
{
	unsigned long end = jiffies + MAX_SOFTIRQ_TIME;
	unsigned long old_flags = current->flags;
	int max_restart = MAX_SOFTIRQ_RESTART;
	struct softirq_action *h;
	bool in_hardirq;
	__u32 pending;
	int softirq_bit;

	/*
	 * Mask out PF_MEMALLOC as the current task context is borrowed for the
	 * softirq. A softirq handled, such as network RX, might set PF_MEMALLOC
	 * again if the socket is related to swapping.
	 */
	current->flags &= ~PF_MEMALLOC;

	pending = local_softirq_pending(); //获取当前CPU的__softirq_pending值

	softirq_handle_begin();         //标记preempt_count第8位,表示在软中断上下文中
	in_hardirq = lockdep_softirq_start();
	account_softirq_enter(current);    //记录软中断处理开始的时间

restart:
	/* Reset the pending bitmask before enabling irqs */
	set_softirq_pending(0); //清空__softirq_pending值,接下来正式开始处理软中断

	local_irq_enable();  //使能local_irq,这样就可以处理硬件中断,也就是软中断设置的初衷

	h = softirq_vec;    //指向软中断处理函数指针数组softirq_vec

	while ((softirq_bit = ffs(pending))) { //遍历pending值,逐个处理软中断
		unsigned int vec_nr;
		int prev_count;

		h += softirq_bit - 1;    //h指向对应软中断处理函

		vec_nr = h - softirq_vec;
		prev_count = preempt_count();    //获取当前preempt_count值,之前提到过过,这值可以显示各类中断使能状态等,详情见第一节

		kstat_incr_softirqs_this_cpu(vec_nr);

		trace_softirq_entry(vec_nr);    //可以通过ftrace追踪软中断处理
		h->action(h);                    //指向对应的软中断处理函数
		trace_softirq_exit(vec_nr);
		if (unlikely(prev_count != preempt_count())) {    //判断一下preempt_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;
	}

	if (!IS_ENABLED(CONFIG_PREEMPT_RT) &&
	    __this_cpu_read(ksoftirqd) == current)
		rcu_softirq_qs();

	local_irq_disable();

	pending = local_softirq_pending();    //重新获取__softirq_pending值,之前提到过,软中断处理过程中,硬件中断时打开的,所有有可能在处理过程中,会有新的softirq被标记
	if (pending) {//如果真的有新的软中断到来
		if (time_before(jiffies, end) && !need_resched() &&
		    --max_restart)    //这里会判断本轮软中断处理时间是否超过2s,是否超过10次,避免软中断频繁触发,导致长时间占用CPU
			goto restart;

		wakeup_softirqd(); //如果本轮软中断处理超过2s或者超过10次,就先退出软中断处理,让出CPU来,唤醒ksoftirqd task,稍后处理这些新的软中断
	}

	account_softirq_exit(current);
	lockdep_softirq_end(in_hardirq);
	softirq_handle_end();        //清空preempt_count第8位,表示目前不在处理软中断上下文中
	current_restore_flags(old_flags, PF_MEMALLOC);
}

三、softirq关键变量或函数说明

关键信息说明
__softirq_pending值pre_cpu变量,表示目前有哪些软中断待处理,这个值在上半部分标记
softirq_vec[]函数指针函数指针数组,指向各个软中断处理函数
preempt_count值

记录当期hardirq/softirq/NMI irq/抢占新等

使用[15:8]记录softirq信息,第8位表示是否在软中断处理过程中,9-15位表示软中断禁用深度

open_softirq 函数将软中断处理函数填充到softirq_vec函数指针数组中
raise_softirq 函数标记对应的__softirq_pending值,供软中断处理函数处理
__do_softirq函数软中断处理的核心函数

__local_bh_enable(SOFTIRQ_OFFSET)

__local_bh_disable(SOFTIRQ_OFFSET)

禁用/使能软中断函数,实际就是设置preempt_count的值
local_softirq_pending 函数返回当前cpu的__softirq_pending值,表示有那些软中断待处理
ksoftirqd进程

每个cpu都有一个对应的ksoftirqd进程,用于处理软中断

一般软中断在硬件中断返回时就会处理掉,但有时候软中断处理时间过长,避免软中断长时间占用CPU,也先让出CPU ,并唤醒ksoftirqd来处理软中断

in_softirq()函数返回是否在软中断上下文中,就是检查preempt_count对应8-15位是否为0

Logo

更多推荐