软中断SOFTIRQ

软中断的引入

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

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

Softirq的类型

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

/* 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定义

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。

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

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

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

#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也提供了一个宏定义,如下:

#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的回调函数,如下:

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

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

open_softirq(TIMER_SOFTIRQ, run_timer_softirq);

Softirq的触发

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

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函数,不过事先需要调用者关闭中断。

/*
 * 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。

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被执行调用的过程。

/*
 * 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发生,也是不会进入到处理函数中。

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函数。

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

	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的函数定义,如下:

#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函数中

#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函数。

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 条评论
登录 后参与评论

相关文章

  • Linux音频驱动-OSS和ALSA声音系统简介及其比较

    昨天想在Ubuntu上用一下HTK工具包来绘制语音信号的频谱图和提取MFCC的结果,但由于前段时间把Ubuntu升级到13.04,系统的声卡驱动是ALSA(Ad...

    DragonKingZhu
  • 根据crash学习用户空间程序内存布局

    在32位机器上,总共有4G大小的虚拟地址空间,其中0-3G是给应用程序使用,3-4G是给内核使用。

    DragonKingZhu
  • spin_lock的变体

    当处理器上当前进程A需要对共享变量a操作,所以在操作前通过spin_lock获取锁进入临界区,如上图标号1。当进程A进入临界区后,进程A所在的处理器发生了一个外...

    DragonKingZhu
  • Mono 4.0 Mac上运行asp.net mvc 5.2.3

    Mono 4.0 已经发布,二进制包已经准备好,具体的发布说明参见:http://www.mono-project.com/docs/about-mono/re...

    张善友
  • 10.17 VR扫描:华为发布Mate10系列手机;TPCAST无线套件售价涨至2199元

    VRPinea
  • PCIE4.0 测试初探

    规范中明确标明了TX详细测试步骤和连接方法,以及要求的最低带宽 -- 25GHz:

    鲜枣课堂
  • Java内存管理与垃圾回收

    根据《Java虚拟机规范(第2版)》的规定,Java虚拟机所管理的内存将会包括以下几个运行时数据区域,如下图所示:

    IT大飞说
  • 3种可供选择的SD-WAN架构

    如果企业正在考虑向广域网中引入SD-WAN技术,那么首先应该掌握不同的SD-WAN架构优缺点。近年来,随着SD-WAN的热潮不断兴起,目前市场上先后有数十种产品...

    SDNLAB
  • 044.集群存储-StorageClass

    StorageClass作为对存储资源的抽象定义,对用户设置的PVC申请屏蔽后端存储的细节,一方面减少了用户对于存储资源细节的关注,另一方面减轻了管理员手工管理...

    木二
  • 腾讯WeTest- QA 最佳实践|互联网大厂如何提升软件质量?

    ? ? 2019 软件测试与质量保障行业有哪些新的变化、趋势或危机? 测试行业最顶级技术大咖们目前在关注哪些技术方向? 企业业务面临哪些测试难题?如何提升质量...

    WeTest质量开放平台团队

扫码关注云+社区

领取腾讯云代金券