中断下半部-软中断softirq

本文摘抄于奔跑吧linux内核:基于linux内核源码问题分析

中断上半部:硬件中断处理程序以在关中断的情况下进行(关闭本CPU的所有中断响应,arm是处理器自动完成的)。中断上半部一般完成中断处理的一小部分。eg 响应中断已经被软件接收、硬件中断处理完成时,发送EOI信号给中断控制器

中断下半部——SoftIRQ

软中断是预留给系统中对时间要求最严格和最重要的下半部使用。

对时间要求最严格:应该是被此中断上半部退出的时候都会去尝试执行软中断。因此是下半部里面最快的(软中断,tasklet,工作队列,中断线程化)

软中断类型

/* 通过枚举类型声明软中断,且索引越小,优先级更高 */
enum
{
	HI_SOFTIRQ=0,//优先级为0,是最高优先级的软中断
	TIMER_SOFTIRQ,//定时器的软中断
	NET_TX_SOFTIRQ,//发生网络数据包的软中断
	NET_RX_SOFTIRQ,//收包的软中断
	BLOCK_SOFTIRQ,//块设备的软中断
	BLOCK_IOPOLL_SOFTIRQ,//块设备的软中断
	TASKLET_SOFTIRQ,//专门给tasklet机制准备的软中断,难怪说tasklet基于软中断实现
	SCHED_SOFTIRQ,//进程调度以及负载均衡
	HRTIMER_SOFTIRQ,//高精度定时器
	RCU_SOFTIRQ,    //为RCU服务的软中断 /* Preferable RCU should always be the last softirq */

	NR_SOFTIRQS
};

描述软中断的数据结构。当触发了软中断,就会调用action回调函数处理软中断

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

软中断描述符数组。从软中断注册函数open_softirq看,感觉软中断只有10个,

static struct softirq_action softirq_vec[NR_SOFTIRQS] __cacheline_aligned_in_smp;

结构体irq_cpustat用于描述软中断的状态信息。同时定义了一个数组irq_stat[NR_CPUS],这样每个CPU有一个独立的软中断状态信息

在软中断状态信息irq_cpustat_t成员__softirq_pending用于表示有软中断待处理。从代码看的话,__softirq_pending的低10位就分别对应了10种软中断。例如__softirq_pending的bit 0置位表示有HI_SOFTIRQ软中断待处理。同理,我们想触发软中断也是将对应bit位置上,实现的。

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

irq_cpustat_t irq_stat[NR_CPUS] ____cacheline_aligned;

 软中断注册

/*
nr是软中断的序号*(上面的9个)
*/
void open_softirq(int nr, void (*action)(struct softirq_action *))
{
	softirq_vec[nr].action = action;
}

软中断执行时机:1主动触发软中断 2 硬件中断处理(中断上半部)结束时 3 local_bh_enable

1主动触发软中断(准确是ksoftirq内核线程中)

raise_softirq会关闭本地CPU中断,raise_softirq_irqoff则不会。因此后者可以用在进程上下文。为什么啊?难道前者不可以??不理解。。。

另外的书里面是这样说的:如果中断本来已经禁止了,那么可以调用raise_softirq_irqoff,这样会带来一些优化效果。

我更偏向于这种说法。两者的区别只是重复关开了本cpu的中断。而关中断也执行重复的设置了寄存器。

其实从代码来看,这个主动触发软中断,也并不会马上去执行软中断。只有不在中断上下文时,才会去唤醒软中断线程去执行软中断(其实这个好像也只是将专门负责执行软中断的内核线程设置为run状态,并加入到就绪队列里面,并不会真正马上执行。没有去验证过),否则只是打了一个软中断待执行的标记

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

	local_irq_save(flags);
	raise_softirq_irqoff(nr);
	local_irq_restore(flags);
}
inline void raise_softirq_irqoff(unsigned int nr)
{
    /* 
     __raise_softirq_irqoff(nr)会将本CPU的irq_stat中的__softirq_pending(软中断状态寄存器)
    对应的bit置1
    中断返回时,会检查__softirq_pending,如果不为0,则说明有pending的软中断需要处理
    */
	__raise_softirq_irqoff(nr);
	if (!in_interrupt())
		wakeup_softirqd();
}
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))

__raise_softirq_irqoff(nr)会将本CPU的irq_stat中的__softirq_pending(软中断状态寄存器)
    对应的bit置1。 in_interrupt()。如果当前不在硬中断上下文或者软中断上下文(其实就是不在中断上下文),那么就去唤醒内核线程ksoftirqd处理软中断。

1、可以看到ksoftirqd是一个per-CP的,因此每个CPU都有一个单独的ksoftirqd内核线程,用于执行本地CPU的中断。

2、在run_ksoftirqd里面是先屏蔽中断了的。但是__do_softirq实际上执行软中断的过程中又开启了本CPU的硬件中断。不知道为什么这样做。。。

3、最终在__do_softirq中去循环遍历本CPU的软中断状态信息__softirq_pending,然后调用相应的处理函数处理软中断

感觉如果进入了run_softirq中,此时应该是处于进程上下文,又能够被硬件中断或者软中断打断(然后软中断执行的条件保证了,在同一个cpu上是串行执行的)

还有就是在当软中断重复执行10次也仍还存在软中断,那么也会调用wakeup_softirqd。这样能够避免软中断太多,导致长期处于中断上下文,使得普通进程得不到执行??

但是该内核线程虽然运行在进程上下文,但是并不允许睡眠,原因见文末

smpboot_register_percpu_thread为每个cpu都创建了一个内核线程用于执行软中断

DECLARE_PER_CPU(struct task_struct *, ksoftirqd);

static struct smp_hotplug_thread softirq_threads = {
	.store			= &ksoftirqd,
	.thread_should_run	= ksoftirqd_should_run,
	.thread_fn		= run_ksoftirqd,
	.thread_comm		= "ksoftirqd/%u",
};

static __init int spawn_ksoftirqd(void)
{
	register_cpu_notifier(&cpu_nfb);

	BUG_ON(smpboot_register_percpu_thread(&softirq_threads));

	return 0;
}

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();
		rcu_note_context_switch(cpu);
		local_irq_enable();
		cond_resched();//软中断执行完毕之后,主动让出CPU,进行休眠
		return;
	}
	local_irq_enable();
}

软中断处理的内核线程唤醒(如果没有被其他人唤醒。它自己应该也能被调度运行。只不过该内核线程的优先级比较低,主要是为了防止耽误了其他线程的运行)

static void wakeup_softirqd(void)
{
	/* Interrupts are disabled: no need to stop preemption */
	struct task_struct *tsk = __this_cpu_read(ksoftirqd);

	if (tsk && tsk->state != TASK_RUNNING)
		wake_up_process(tsk);
}

 

2 硬件中断处理(中断上半部)结束时

void handle_IRQ(unsigned int irq, struct pt_regs *regs)
{
	struct pt_regs *old_regs = set_irq_regs(regs);

	irq_enter();
	if (unlikely(irq >= nr_irqs)) {
		if (printk_ratelimit())
			printk(KERN_WARNING "Bad IRQ%u\n", irq);
		ack_bad_irq(irq);
	} else {
		generic_handle_irq(irq);
	}
	irq_exit();
	set_irq_regs(old_regs);
}
void irq_exit(void)
{
...................
	/* 不在硬件中断上下文、软中断上下文 同时有软中断待处理 */
	/*
	因此如果在执行软中断的过程中,被硬件中断打断,然后返回时,
	会回到软中断上下文。这些不满足上述条件.不会重新调度新的软中断,
	即不会执行invoke_softirq
	因此软中断在一个CPU上总是串行执行的
	*/
	if (!in_interrupt() && local_softirq_pending())
		invoke_softirq();
.....................
}

硬件中断处理退出是,irq_exit会检查当前是否有待处理(pendiing)的软中断.

 local_softirq_pending()检查本地CPU的"软中断状态寄存器" __softirq_pending是否有需要处理的软中断

in_interrupt:是否在硬件中断上下文或者软中断上下文。因此如果在执行软中断的过程中,被硬件中断打断,然后返回时,会回到软中断上下文。这些不满足上述条件.不会重新调度新的软中断,    即不会执行invoke_softirq。  因此软中断在一个CPU上总是串行执行的

从__do_softirq中可以看出:

1、在软中断的执行是开中断的 ,因此在软中断执行过程中又可以被硬中断打断。在该函数中循环遍历软中断状态信息的各个bit位。然后去调用相应的软中断处理函数。

2、在函数进来的时候使用__local_bh_disable_ip,去修改preempt_count。禁用了中断下半部,或者说表明在软中断上下文。这样即使开中断被打断,当中断上半部退出irq_exit时,in_interrupt里面去检查preempt_count时为非0,导致无法执行新的软中断。只能继续执行被打断的软中断。软中断在一个cpu上串行执行,是由这两个地方保证的。

另外软中断也不会一直处理。从代码中可以看到如果循环超过10或者是执行时间超过2ms,则会唤醒ksoftirq/n内核线程去处理。因为如果do_softirq里面不做这个限制,一直去检查是否有待执行的软中断并处理,则其他内核线程和用户态的进程得不到处理。

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

	/*
	 * 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;
	//获取本地CPU的"软中断状态寄存器"
	pending = local_softirq_pending();
	account_irq_enter_time(current);

	/*
     增加preempt_count中SOFTIRQ域的计数,表明现在是在软中断上下文 
     屏蔽所有软中断,也证明每个cpu上运行的软中断只有一个    
    */
	__local_bh_disable(_RET_IP_, SOFTIRQ_OFFSET);

	//进入软中断
	lockdep_softirq_enter();

	cpu = smp_processor_id();
restart:
	/* Reset the pending bitmask before enabling irqs */
	set_softirq_pending(0);//清除软中断寄存器,因为后面会依次处理所有的软中断
    /*
     开启硬件中断,执行到这里以后,软中断就能够被硬件中断抢占了
     在这之前硬件中断都是被屏蔽了的
    注意:虽然这里打开了硬件中断,但是上面代码__local_bh_disable将软中断屏蔽了
    因此即使先代码被硬中断打断,在退出硬件中断时,由于in_interrupt不满足,只能回到该软中断
    被打断的地方继续执行
     */
	local_irq_enable();

	h = softirq_vec;

	/* 循环处理所有的软中断.总共9个 */
	do {
		if (pending & 1) {
			unsigned int vec_nr = h - softirq_vec;
			int prev_count = preempt_count();

			kstat_incr_softirqs_this_cpu(vec_nr);

			trace_softirq_entry(vec_nr);
			h->action(h);
			trace_softirq_exit(vec_nr);
            /* 这只能说明软中断处理函数里面preempt_count不对称的情况 */
			if (unlikely(prev_count != preempt_count())) {
				printk(KERN_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);
			}

			rcu_bh_qs(cpu);
		}
		h++;
		pending >>= 1;
	} while (pending);

	/*
	到这里软中断的处理函数以及执行完毕.
	但是由于在软中断执行前,开了本地CPU的中断。
	因此在这段时间可能会发送硬中断以及再次触发软中断.
	重新再去检查一下软中断状态寄存器
	*/
	local_irq_disable();
    /*  
    上面提到由于__local_bh_disable屏蔽了软中断,即使被硬件中断打断,也无法重新进入该函数
    只得回到该函数被打断的地方继续执行。但是由于软中断触发相当于都是修改的per cpu变量。
    因此相当于在这里给了新触发的软中断一个指向的机会
    */
	pending = local_softirq_pending();
	if (pending) {
		/*
		当执行软中断的过程中再次触发软中断。重新执行软中断的条件
		1、软中断处理时间没有超过2ms
		2、当前进程没有调度需求
		3、循环处理的次数不能超过max_restart(10)次
		*/
		if (time_before(jiffies, end) && !need_resched() &&
		    --max_restart)
			goto restart;

		wakeup_softirqd();
	}

	/* 退出软中断上下文 */
	lockdep_softirq_exit();

	account_irq_exit_time(current);
	/*
	清除表示处于软中断上下文的标记
	与之相对的是上面的__local_bh_disable(_RET_IP_, SOFTIRQ_OFFSET)
	*/
	__local_bh_enable(SOFTIRQ_OFFSET);
	WARN_ON_ONCE(in_interrupt());
	tsk_restore_flags(current, old_flags, PF_MEMALLOC);
}

3 local_bh_enable()

可以看到local_bh_enble实现里面,如果不在中断上下文,并且有待处理的软中断,也会去执行软中断do_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());
................................
	preempt_count_sub(cnt - 1);

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

因此硬中断会抢占软中断,软中断又会抢占进程和线程

之前在想spin_lock_irq会屏蔽软中断不?现在看来是不能屏蔽软中断的。因为软中断有两个途径执行。屏蔽中断只是使得软中断无法从中断上半部退出时被执行

执行被local_bh_disable()和local_bh_enable()包围的代码区域时,由于softirq是被屏蔽的,因而在这段时间里也是不能睡眠的。难道是因为,睡眠切到了其他进程,然后直到切回来这段时间,都无法响应软中断??

从local_bh_disable/local_bh_enable的代码实现,并不是因为进程切换走到切回来这段时间无法响应软中断,才不能切换。上述的两个函数起始只是针对thread_info->preempt_count进行修改。thread_info是每个进程或者内核线程独有的。被切走的线程preempt_count被local_bh_disable修改了,并不代表。即将运行的进程的preempt_count也处于中断上下文(in_interrupt()非0),无法执行软中断。

《linux内核设计与实现》关于内核抢占有这样一种说法,preempt_count计数器初值为0,获取锁+1,释放锁-1。当计数为0说明内核可执行抢占,即调度。反之不为0,说明当前进程或者线程仍持有锁,进行调度是不安全的。

为什么在ksoftirqd线程执行期间也不允许睡眠?因为进入ksoftirqd之后,softirq也是被屏蔽的,相当于是执行了local_bh_disable()。因此进入softirq是在softirq上下文,关闭softirq抢占也是在softirq上下文

另外本CPU的软中断状态信息__softirq_pending这里面待执行软中断标记又是何时被打上的呢?在中断处理程序中触发软中断是最常见的形式。

中断上半部_这个我好像学过的博客-CSDN博客_irq_svc文末写了一个例子

/* 定义 taselet */
struct tasklet_struct testtasklet;
/* tasklet 处理函数 */
void testtasklet_func(unsigned long data)
{
/* tasklet 具体处理内容 */
}
/* 中断处理函数 */
irqreturn_t test_handler(int irq, void *dev_id)
{
......
/* 调度 tasklet */
tasklet_schedule(&testtasklet);
......
}
/* 驱动入口函数 */
static int __init xxxx_init(void)
{
......
/* 初始化 tasklet */
tasklet_init(&testtasklet, testtasklet_func, data);
/* 注册中断处理函数 */
request_irq(xxx_irq, test_handler, 0, "xxx", &xxx_dev);
......
}

其实就是我们通过request_irq(xxx_irq, test_handler, 0, "xxx", &xxx_dev);注册的回调函数去设置软中断的标记的。这个注册的回调函数test_handler其实是属于中断上半部的流程。 

可以看到test_handler里面调用了tasklet_schedule,而tasklet_schedule里面就去使用raise_softirq_irqoff主动触发了软中断。相当于就是中断上半部就会设置软中断的状态信息

void __tasklet_schedule(struct tasklet_struct *t)
{
	unsigned long flags;
...............
	raise_softirq_irqoff(TASKLET_SOFTIRQ);
.................
}

你可能感兴趣的:(linux)