软中断SOFTIRQ

2020-03-24 11:06:03 浏览数 (1)

软中断的引入

软中断的出现和linux系统对中断的划分是分不开的。linux系统将整个中断处理过程分为了两部分,分别为上半部(Top Half)和下半部(Bottom Half),之所以要这样分是因为关闭中断的时间不能过长,也就是在关闭中断期间尽可能少干事,否则影响整个系统的性能。所以linux系统将中断处理分为两部分,在上半部全程关闭中断,下半部打开中断。而在上半部主要干一些和硬件有关的操作,速度快,在下部分做一些耗时的操作。这样一来既能保证系统效率又能处理各种中断。

linux系统针对下半部(Bottom Half)实现了多种机制,如Softirq, Tasklet, workqueue。其中Tasklet是基于Softirq实现的,所以本节先来看Softirq的实现机制。

Softirq的类型

linux系统为不同的方面定义了不同的irq类型,如下:

代码语言:javascript复制
/* PLEASE, avoid to allocate new softirqs, if you need not _really_ high
   frequency threaded job scheduling. For almost all the purposes
   tasklets are more than enough. F.e. all serial device BHs et
   al. should be converted to tasklets, not to softirqs.
 */

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

其中HI_SOFTIRQ和TASKLET_SOFTIRQ用来实现tasklet,NET_TX_SOFTIRQ和NET_RX_SOFTIRQ用于网络的发送和接受操作,TIMER_SOFTIRQ和HRTIMER_SOFTIRQ用于定时器,前者是低精度的定时器,后者是高精度的定时器。BLOCK_SOFTIRQ和BLOCK_IOPOLL_SOFTIRQ用于块设置操作,SCHED_SOFTIRQ用于调度方面的操作,RCU_SOFTIRQ用于rcu方面的操作。这些具体的操作都会在具体的模块中涉及,比如TIMER_SOFTIRQ就会在之前的定时器中提到过。而本文只是描述softirq的工作机制。

注意上述的注释: 大概的意思是不要人为添加softirq类型,目前使用tasklet已经足够满足使用。

Softirq定义

代码语言:javascript复制
static struct softirq_action softirq_vec[NR_SOFTIRQS] __cacheline_aligned_in_smp;

可以看到softirq是静态定义的,linux为每个softirq类型定义了一个softirq_action类型的数组,而__cacheline_aligned_in_smp的意思是在smp系统中保证softirq_vec对齐cacheline。

代码语言:javascript复制
struct softirq_action
{
	void	(*action)(struct softirq_action *);
};

可以看到softirq_action结构之中就只有一个成员action, 该成员代表如果softirq触发,就调用该softirq的回调函数处理中断。

那linux如何判断软中断是否发生呢? linux系统了如下的结构,

代码语言:javascript复制
#define NR_IPI	6

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


#ifndef __ARCH_IRQ_STAT
irq_cpustat_t irq_stat[NR_CPUS] ____cacheline_aligned;
EXPORT_SYMBOL(irq_stat);
#endif

系统通过一个无符号整型变量__softirq_pending的每一位来表示一个softirq类型,同时每个cpu都独享自己的__softirq_pending变量,也就是说该变量是per_cpu变量。同时__softirq_pending变量也是对齐cacheline的。在smp系统中会涉及到ipi中断

同时内核为了方便获得每个cpu的__softirq_pending也提供了一个宏定义,如下:

代码语言:javascript复制
#define __IRQ_STAT(cpu, member)	(irq_stat[cpu].member)

#define local_softirq_pending() 
	__IRQ_STAT(smp_processor_id(), __softirq_pending)

宏定义local_softirq_pending就是获取该cpu的__softirq_pending的值。

Softirq的注册

linux系统通过open_softirq注册softirq的回调函数,如下:

代码语言:javascript复制
void open_softirq(int nr, void (*action)(struct softirq_action *))
{
	softirq_vec[nr].action = action;
}

比如注册TIMER_SOFTIRQ的回调函数如下:

代码语言:javascript复制
open_softirq(TIMER_SOFTIRQ, run_timer_softirq);

Softirq的触发

linux系统通过函数raise_softirq来触发一个软中断,如下:

代码语言:javascript复制
void raise_softirq(unsigned int nr)
{
	unsigned long flags;

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

调用上述的函数之前需要前关掉中断,因为raise_softirq_irqoff函数最终会操作触发软中断类型的bit位, 也就是__softirq_pending变量,该变量本身就是per-cpu变量,所以只需要关掉中断就可以保护该变量的原子性。当然也可以直接调用raise_softirq_irqoff函数,不过事先需要调用者关闭中断。

代码语言:javascript复制
/*
 * This function must run with irqs disabled!
 */
inline void raise_softirq_irqoff(unsigned int nr)
{
	__raise_softirq_irqoff(nr);                          --------------A

	/*
	 * If we're in an interrupt or softirq, we're done
	 * (this also catches softirq-disabled code). We will
	 * actually run the softirq once we return from
	 * the irq or softirq.
	 *
	 * Otherwise we wake up ksoftirqd to make sure we
	 * schedule the softirq soon.
	 */
	if (!in_interrupt())                                 ----------------B
		wakeup_softirqd();
}

A: __raise_softirq_irqoff函数最终会操作触发软中断类型的bit位为1, 通过按位与的操作,将这位置为1。

代码语言:javascript复制
void __raise_softirq_irqoff(unsigned int nr)
{
	trace_softirq_raise(nr);
	or_softirq_pending(1UL << nr);
}
#define or_softirq_pending(x)  (local_softirq_pending() |= (x))

local_softirq_pending在上面已经说过了。

B: 根据注释, 如果当初处于中断上写文中(硬 软 NMI), 直接返回。 如果不是就调用wakeup_softirqd来唤醒本cpu上的内核线程。

处理Softirq

上述说了触发一个Softirq, 那就必须来处理此Softirq, 也就是最终调用到该Softirq的action回调函数中。而Softirq本身的作用就是处理一些Top half没有完成的事,比较耗时的操作交给Softirq来完成。所以接着Top half退出的时候,不就是Softirq被执行调用的过程。

代码语言:javascript复制
/*
 * Exit an interrupt context. Process softirqs if needed and possible:
 */
void irq_exit(void)
{
#ifndef __ARCH_IRQ_EXIT_IRQS_DISABLED
	local_irq_disable();
#else
	WARN_ON_ONCE(!irqs_disabled());
#endif

	account_irq_exit_time(current);
	preempt_count_sub(HARDIRQ_OFFSET);                       //硬件count减1
	if (!in_interrupt() && local_softirq_pending())       
		invoke_softirq();

	tick_irq_exit();
	rcu_irq_exit();
	trace_hardirq_exit(); /* must be last! */
}

进入到invoke_softirq函数成功的条件为: 当前不在中断上下文中,并且有Softirq发生。如果当前有中断嵌套,当irq_exit退出的时候,这时候还会在中断上下文,也是不会处理softirq的。如果当前已经在处理softirq,这时候本地中断是打开的,当再一次softirq发生,也是不会进入到处理函数中。

代码语言:javascript复制
static inline void invoke_softirq(void)
{
	if (!force_irqthreads) {
#ifdef CONFIG_HAVE_IRQ_EXIT_ON_IRQ_STACK
		/*
		 * We can safely execute softirq on the current stack if
		 * it is the irq stack, because it should be near empty
		 * at this stage.
		 */
		__do_softirq();
#else
		/*
		 * Otherwise, irq_exit() is called on the task stack that can
		 * be potentially deep already. So call softirq in its own stack
		 * to prevent from any overrun.
		 */
		do_softirq_own_stack();
#endif
	} else {
		wakeup_softirqd();
	}
}

force_irqthreads是用来判断中断是否强制线程化,目前没有使用到。 在ARM处理器上do_softirq_own_stack函数就是调用到__do_softirq函数。

代码语言:javascript复制
asmlinkage __visible void __do_softirq(void)
{
	unsigned long end = jiffies   MAX_SOFTIRQ_TIME;                                //超时时间2ms
	unsigned long old_flags = current->flags;
	int max_restart = MAX_SOFTIRQ_RESTART;
	struct softirq_action *h;
	bool in_hardirq;
	__u32 pending;
	int softirq_bit;

	pending = local_softirq_pending();                                         //获得当前的pending                                         

	__local_bh_disable_ip(_RET_IP_, SOFTIRQ_OFFSET);                           //disable掉softirq
	in_hardirq = lockdep_softirq_start();

restart:
	/* Reset the pending bitmask before enabling irqs */
	set_softirq_pending(0);                                                    //清空pending状态位

	local_irq_enable();                                                        //打开本地中断

	h = softirq_vec;                     

	while ((softirq_bit = ffs(pending))) {                                    //ffs是从pending中找到第一个bit为1的位置
		unsigned int vec_nr;
		int prev_count;

		h  = softirq_bit - 1;                                             //指针操作,得到找到的pending的softirq_action

		vec_nr = h - softirq_vec;                                         //得到数组的下标,也就是SOFTIRQ类型
		prev_count = preempt_count();                                     //获得到preempt_count的值

		trace_softirq_entry(vec_nr);              
		h->action(h);                                                       //调用到softirq的注册的回调函数
		trace_softirq_exit(vec_nr);
		if (unlikely(prev_count != preempt_count())) {                      //prermpt_count的值发生变化
			pr_err("huh, entered softirq %u %s %p with preempt_count x, exited with x?n",
			       vec_nr, softirq_to_name[vec_nr], h->action,
			       prev_count, preempt_count());
			preempt_count_set(prev_count);
		}
		h  ;                                                  
		pending >>= softirq_bit;
	}

	local_irq_disable();                                                                 //关闭本地中断

	pending = local_softirq_pending();             ------------------A              
	if (pending) {                             
		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);                    //enbale softirq
	WARN_ON_ONCE(in_interrupt());
	tsk_restore_flags(current, old_flags, PF_MEMALLOC);
}

A: 再次检测是否有softirq发生,如果有就需要跳转到restart再次执行上述的操作,而再次执行需要满足三个条件。

1. 执行时间没有超过2ms

2. 没有更高优先级任务需要调度

3. 循环次数没有超过10次

满足上述三个条件就可以进入到restart的执行流程中,否则唤醒softirqd唤醒执行。因为如果SOFTIRQ执行时间过长,会导致一个中断处理流程迟迟无法结束,这意味此前被中断的进程无法执行,影响系统性能。

使能/关闭Softirq

在bottom_half.h中定义了enable/disable Softirq的函数定义,如下:

代码语言:javascript复制
#define SOFTIRQ_DISABLE_OFFSET	(2 * SOFTIRQ_OFFSET)

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

static __always_inline void __local_bh_disable_ip(unsigned long ip, unsigned int cnt)
{
	preempt_count_add(cnt);
	barrier();
}

disabel最终调用到preempt_count_add函数中

代码语言:javascript复制
#define preempt_count_add(val)	__preempt_count_add(val)

static __always_inline void __preempt_count_add(int val)
{
	*preempt_count_ptr()  = val;
}

而最终的含义就是在preempt_count变量的bit9-15位上加1。barrier是内存屏障。

接下来在看enable softirq函数。

代码语言:javascript复制
static inline void local_bh_enable(void)
{
	__local_bh_enable_ip(_THIS_IP_, SOFTIRQ_DISABLE_OFFSET);
}

void __local_bh_enable_ip(unsigned long ip, unsigned int cnt)
{
	WARN_ON_ONCE(in_irq() || irqs_disabled());                    ---------A
	/*
	 * Are softirqs going to be turned on now:
	 */
	if (softirq_count() == SOFTIRQ_DISABLE_OFFSET)
		trace_softirqs_on(ip);
	/*
	 * Keep preemption disabled until we are done with
	 * softirq processing:
	 */
	preempt_count_sub(cnt - 1);                                    ------------B

	if (unlikely(!in_interrupt() && local_softirq_pending())) {
		/*
		 * Run softirq if any pending. And do it in its own stack
		 * as we may be calling this deep in a task call stack already.
		 */
		do_softirq();                                       -------------——C
	}
	preempt_count_dec();                                        -----------------D
	preempt_check_resched();                                     ---------------E
}

A: 如果在硬件中断handler(Top Half)中,调用到enable/disbale Bottom half, 这时候就会有警告,也就是说在硬件中断中不应该调用enable bottom Half的函数,因为bottom half是不可能抢占到Top Half的。

B: 根据注释的意思是保持抢占disbale直到做完softirq的操作,这里没有直接减去cnt,而是减去了cnt-1。如果减去cnt这时候perrmpt_count可能会为0,为0的时候cpu就可以发生抢占,假如这时候有softirq在运行,岂不是此softirq会被抢占到别cpu上。所以需要等C步骤全部执行完毕后,在减去1.

C: 如果当前不再中断上写文,同时有softirq发生,就处理。

D: 这时候已经处理完所有的softirq了,再减去上面的1。

E: 在C步骤中可能会唤醒高优先级的任务,需要在这里处理。

0 人点赞