Linux内核深度解析之中断、异常和系统调用——中断下半部之软中断

软中断

软中断(softirq)是中断处理程序在开启中断的情况下执行的部分,可以被硬中断抢占。

内核定义了一张软中断向量表,每种软中断有一个唯一的编号,对应一个softirq_action实例,softirq_action实例的成员action是处理函数。

kernel/softirq.c
static struct softirq_action softirq_vec[NR_SOFTIRQS] __cacheline_aligned_in_smp;		/* 软中断向量表 */

include/linux/interrupt.h
struct softirq_action
{
	void	(*action)(struct softirq_action *);		/* 软中断处理函数 */
};

1. 软中断的种类

目前内核定义了10种软中断,定义如下:

include/linux/interrupt.h
enum
{
	HI_SOFTIRQ=0,		/* 高优先级的小任务 */
	TIMER_SOFTIRQ,		/* 定时器软中断 */
	NET_TX_SOFTIRQ,		/* 网络栈发送报文的软中断 */
	NET_RX_SOFTIRQ,		/* 网络栈接收报文的软中断 */
	BLOCK_SOFTIRQ,		/* 块设备软中断 */
	IRQ_POLL_SOFTIRQ,	/* 支持IO轮询的块设备软中断 */
	TASKLET_SOFTIRQ,	/* 低优先级的小任务 */
	SCHED_SOFTIRQ,		/* 调度软中断,用于在处理器之间的负载均衡 */
	HRTIMER_SOFTIRQ, 	/* 高精度定时器 */
	RCU_SOFTIRQ,    	/* RCU软中断 */

	NR_SOFTIRQS
};

软中断的编号越小优先级越高。

2. 注册软中断的处理函数

函数open_softirq()用来注册软中断的处理函数,在软中断向量表中为指定的软终端编号设置处理函数。

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

同一种软中断的处理函数可以在多个处理器上同时执行,处理函数必须是可以重入的,需要使用锁保护临界区。

3. 触发软中断

函数raise_softirq用来触发软中断,参数是软中断编号。

void raise_softirq(unsigned int nr);

在已经禁止中断的情况下可以调用函数raise_softirq_irqoff来触发中断。

void raise_softirq_irqoff(unsigned int nr);

函数raise_softirq在当前处理器的待处理软中断位图中为指定的软中断编号设置对应的位,如下所示:

raise_softirq()  ->  raise_softirq_irqoff()  ->  __raise_softirq_irqoff()

kernel/softirq.c
void __raise_softirq_irqoff(unsigned int nr)
{
	trace_softirq_raise(nr);
	or_softirq_pending(1UL << nr);
}

把宏or_softirq_pending展开以后是:

irq_stat[smp_processor_id()].__softirq_pending |= (1UL << nr);

4. 执行软中断

内核执行软中断的地方如下:

(1)在中断处理程序的后半部分执行软中断,对执行时间有限制:不能超过2毫秒,并且最多执行10次;

(2)每个处理器有一个软中断线程,调度策略是SCHED_NORMAL,优先级是120;

(3)开启软中断的函数local_bd_enable()。

(1)中断处理程序执行软中断

在中断处理程序的后半部分,调用函数irq_exit()以退出中断上下文,处理软中断,其代码如下:

kernel/softirq.c
void irq_exit(void)
{
    ...
	preempt_count_sub(HARDIRQ_OFFSET);
	if (!in_interrupt() && local_softirq_pending())		/* 如果正在处理的硬中断没有抢占正在执行的软中断,没有禁止 */ 
		invoke_softirq();		/* 软中断并且当前处理器待处理软中断位图不是空的,那么调用函数invoke_softirq来处理软中断 */
								/* 如果in_interrupt()为真,表示在不可屏蔽中断、硬中断或软中断上下文,或者禁止软中断 */
    ...
}

函数invoke_softirq()来处理软中断,其主要代码如下:

kernel/softirq.c
tatic inline void invoke_softirq(void)
{
	if (ksoftirqd_running(local_softirq_pending()))		/* 如果软中断线程处于就绪状态或运行状态,那么让软中断线程执行软中断 */
		return;

	if (!force_irqthreads) {		/* 如果没有强制中断线程化,那么调用函数__do_softirq()执行软中断 */
		__do_softirq();
	} else {		/* 如果强制中断线程化,那么唤醒软中断线程执行软中断 */
		wakeup_softirqd();
	}
}

函数__do_softirq是执行软中断的核心函数,其主要代码如下:

kernel/softirq.c
#define MAX_SOFTIRQ_TIME  msecs_to_jiffies(2)
#define MAX_SOFTIRQ_RESTART 10
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 s 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 swap
	 */
	current->flags &= ~PF_MEMALLOC;

	pending = local_softirq_pending();		/* 把局部变量pending设置为当前处理器的待处理软中断位图 */
	account_irq_enter_time(current);

	__local_bh_disable_ip(_RET_IP_, SOFTIRQ_OFFSET);		/* 把抢占计数器的软中断计数加1 */
	in_hardirq = lockdep_softirq_start();

restart:
	/* Reset the pending bitmask before enabling irqs */
	set_softirq_pending(0);		/* 把当前处理器的待处理软中断位图重新设置为0 */

	local_irq_enable();		/* 开启硬中断 */

	h = softirq_vec;

	while ((softirq_bit = ffs(pending))) {		/* 从低位向高位扫描待处理软中断位图,针对每个设置了对应位的软中断 */
		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();
	if (pending) {		/* 如果软中断的处理函数又触发软中断 */
		if (time_before(jiffies, end) && !need_resched() &&
		    --max_restart)		/* 如果软中断的执行时间小于2毫秒,不需要重现调度进程,并且软中断的执行次数没超过10 */
			goto restart;		/* 那么跳转到restart继续执行软中断 */

		wakeup_softirqd();		/* 唤醒软中断线程执行软中断 */
	}

	lockdep_softirq_end(in_hardirq);
	account_irq_exit_time(current);
	__local_bh_enable(SOFTIRQ_OFFSET);		/* 把抢占计数器的软中断计数减1 */
	WARN_ON_ONCE(in_interrupt());
	current_restore_flags(old_flags, PF_MEMALLOC);
}

(2)软中断线程

每个处理器有一个软中断线程,名称是“ksoftirqd/”后面跟着处理器编号,调度策略是SCHED_NORMAL,优先级是120。

软中断线程的核心函数是run_ksoftirqd(),其代码如下:

tatic 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();
		return;
	}
	local_irq_enable();		/* 开启硬中断 */
}

(3)开启软中断时执行软中断

当进程调用函数local_bd_enable()开启软中断的时候,如果是开启最外层的软中断,并且当前处理器的待处理软中断位图不是空的,那么执行软中断。

local_bh_enable()  ->  __local_bd_enable_ip()

kernel/softirq.c
void __local_bh_enable_ip(unsigned long ip, unsigned int cnt)
{
	...
	preempt_count_sub(cnt - 1);

	if (unlikely(!in_interrupt() && local_softirq_pending())) {
		do_softirq();
	}

	preempt_count_dec();
    ...
}

5. 抢占计数器

每个进程的thread_info结构体中有一个抢占计数器:int interrupt_count,它用来表示当前进程能不能被抢占。

抢占是指当进程在内核模式下运行的时候可以被其他进程抢占,如果优先级更高的进程处于就绪状态,强行剥夺当前进程的处理器使用权。

内核按照各种场景对抢占计数器的位进行了划分:

其中0~7位表示抢占计数;

第8~15位是软中断计数;

第16~19位是硬中断计数;

第20为是不可屏蔽中断计数。

include/linux/preempt.h
/*
 *         PREEMPT_MASK:	0x000000ff
 *         SOFTIRQ_MASK:	0x0000ff00
 *         HARDIRQ_MASK:	0x000f0000
 *             NMI_MASK:	0x00100000
 * PREEMPT_NEED_RESCHED:	0x80000000
 */
#define PREEMPT_BITS	8
#define SOFTIRQ_BITS	8
#define HARDIRQ_BITS	4
#define NMI_BITS	1

各种场景分别利用各自的位禁止或开启抢占:

(1)普通场景(PREEMPT_MASK):对应函数preempt_disable()和preempt_enable();

(2)软中段场景(SOFTIRQ_MASK):对应函数local_bd_disable()和local_bh_enable();

(3)硬中断场景(HARDIRQ_MASK):对应函数__irq_enter()和__irq_exit()

(4)不可屏蔽中断场景(NMI_MASK):对应函数nmi_enter()和nmi_exit()

反过来也可以通过抢占计数器的值判断当前处在什么场景:

include/linux/preempt.h
#define in_irq()		(hardirq_count())
#define in_softirq()		(softirq_count())
#define in_interrupt()		(irq_count())
#define in_serving_softirq()	(softirq_count() & SOFTIRQ_OFFSET)
#define in_nmi()		(preempt_count() & NMI_MASK)
#define in_task()		(!(preempt_count() & \
				   (NMI_MASK | HARDIRQ_MASK | SOFTIRQ_OFFSET)))

#define hardirq_count()	(preempt_count() & HARDIRQ_MASK)
#define softirq_count()	(preempt_count() & SOFTIRQ_MASK)
#define irq_count()	(preempt_count() & (HARDIRQ_MASK | SOFTIRQ_MASK \
				 | NMI_MASK))

in_irq()表示硬中断场景,也就是正在执行硬中断。

in_softirq()表示软中断场景,包括禁止软中断和正在执行软中断。

in_interrupt()表示正在执行不可屏蔽中断、硬中断或软中断、或者禁止软中断。

in_servicing_softirq()表示正在执行软中断。

in_nmi()表示不可屏蔽中断场景。

in_task()表示普通场景,也就是进程上下文。

6.禁止/开启软中断

禁止软中断的函数是local_bd_disable(),注意:这个函数只能禁止本处理器的软中断,不能禁止其他处理器的软中断。该函数把抢占计数器的软中断计数加2其他代码如下:

include/linux/bottom_half.h
static __always_inline void __local_bh_disable_ip(unsigned long ip, unsigned int cnt)
{
	preempt_count_add(cnt);
	barrier();
}

static inline void local_bh_disable(void)
{
	__local_bh_disable_ip(_THIS_IP_, SOFTIRQ_DISABLE_OFFSET);
}

include/linux/preempt.h
#define SOFTIRQ_DISABLE_OFFSET	(2 * SOFTIRQ_OFFSET)

开启软中断的函数是local_bd_enable(),该函数把抢占计数器的软中断计数减2。

为什么禁止软中断的函数local_bd_disable()把抢占计数器的软中断计数加2,而不是加1呢?目的是区分禁止软中断和正在执行软中断这两种情况。__do_softirq()把抢占计数器的软中断计数加1,。如果软中断计数是奇数,可以确定正在执行软中断。

猜你喜欢

转载自blog.csdn.net/linuxweiyh/article/details/106955074