Linux-软中断概述

Linux软中断

Linux中的中断主要分成了top-half和bottom-half两个部分来进行整个中断的处理。

top-half和bottom-half主要区别在于前者位于硬中断的处理流程中完成追求执行效率越快越好并利用bottom-half来处理剩余逻辑以尽可能多的处理中断事件,后者则是处理较费时或者阻塞的延迟任务等。

本文主要是针对软中断场景进行学习。

在Linux的软中断中,当前有如下的几种软中断的类型来进行调度执行。

在softirq中,主要有如下的几种类型:

优先级名称主要任务
0HI_SOFTIRQ处理tasklet_hi任务
1TIMER_SOFTIRQ处理cpu的计时器相关
2NET_TX_SOFTIRQ处理网络设备发送的数据
3NET_RX_SOFTIRQ处理网络设备接受的数据
4BLOCK_SOFTIRQ处理块设备的中断
5IRQ_POLL_SOFTIRQ用于执行IOPOLL的回调函数
6TASKLET_SOFTIRQ用户处理tasklet任务执行
7SCHED_SOFTIRQ用于进程调度相关
8HRTIMER_SOFTIRQ
9RCU_SOFTIRQ
软中断的注册流程

softirq的注册函数如下:

// 注册的action的类型就是一个可执行的函数
struct softirq_action
{
	void	(*action)(struct softirq_action *);
};

...

// 将软中断的函数直接保存
void open_softirq(int nr, void (*action)(struct softirq_action *))
{
	softirq_vec[nr].action = action;
}

软中断的注册的函数如下:

// tasklet的软中断注册
void __init softirq_init(void)
{
	int cpu;

	for_each_possible_cpu(cpu) {
		per_cpu(tasklet_vec, cpu).tail =
			&per_cpu(tasklet_vec, cpu).head;
		per_cpu(tasklet_hi_vec, cpu).tail =
			&per_cpu(tasklet_hi_vec, cpu).head;
	}

	open_softirq(TASKLET_SOFTIRQ, tasklet_action);
	open_softirq(HI_SOFTIRQ, tasklet_hi_action);
}

// 网络设备的注册的数据接受
static int __init net_dev_init(void)
{
	int i, rc = -ENOMEM;
	...
	if (register_pernet_device(&loopback_net_ops))
		goto out;

	if (register_pernet_device(&default_device_ops))
		goto out;

	open_softirq(NET_TX_SOFTIRQ, net_tx_action);  
	open_softirq(NET_RX_SOFTIRQ, net_rx_action);  

	rc = cpuhp_setup_state_nocalls(CPUHP_NET_DEV_DEAD, "net/dev:dead",
				       NULL, dev_cpu_dead);
	WARN_ON(rc < 0);
	rc = 0;
out:
	return rc;
}

...
// 调度器相关的软中断处理
__init void init_sched_fair_class(void)
{
#ifdef CONFIG_SMP
	open_softirq(SCHED_SOFTIRQ, run_rebalance_domains);

#ifdef CONFIG_NO_HZ_COMMON
	nohz.next_balance = jiffies;
	nohz.next_blocked = jiffies;
	zalloc_cpumask_var(&nohz.idle_cpus_mask, GFP_NOWAIT);
#endif
#endif /* SMP */
}

...
// 时间软中断处理
void __init init_timers(void)
{
	init_timer_cpus();
	posix_cputimers_init_work();
	open_softirq(TIMER_SOFTIRQ, run_timer_softirq);
}

...
// 块设备
static int __init blk_mq_init(void)
{
	int i;

	for_each_possible_cpu(i)
		init_llist_head(&per_cpu(blk_cpu_done, i));
	open_softirq(BLOCK_SOFTIRQ, blk_done_softirq);

	cpuhp_setup_state_nocalls(CPUHP_BLOCK_SOFTIRQ_DEAD,
				  "block/softirq:dead", NULL,
				  blk_softirq_cpu_dead);
	cpuhp_setup_state_multi(CPUHP_BLK_MQ_DEAD, "block/mq:dead", NULL,
				blk_mq_hctx_notify_dead);
	cpuhp_setup_state_multi(CPUHP_AP_BLK_MQ_ONLINE, "block/mq:online",
				blk_mq_hctx_notify_online,
				blk_mq_hctx_notify_offline);
	return 0;
}
subsys_initcall(blk_mq_init);
软中断的触发

在每次处理完硬件中断(do_IRQ)之后,都会通过irq_exit()函数来检查是否触发软中断处理流程。

/**
 * irq_exit - Exit an interrupt context, update RCU and lockdep
 *
 * Also processes softirqs if needed and possible.
 */
void irq_exit(void)
{
	__irq_exit_rcu();
	rcu_irq_exit(); 
	 /* must be last! */
	lockdep_hardirq_exit();
}

...
  
static inline void __irq_exit_rcu(void)
{
#ifndef __ARCH_IRQ_EXIT_IRQS_DISABLED
	local_irq_disable();
#else
	lockdep_assert_irqs_disabled();
#endif
	account_hardirq_exit(current);
	preempt_count_sub(HARDIRQ_OFFSET);
	if (!in_interrupt() && local_softirq_pending()) // 不是在硬中断或者软中断并且检查到当前有待处理的软中断
		invoke_softirq();  

	tick_irq_exit();
}

当满足条件调用invoke_softirq就可以触发软中断执行。

tatic inline void invoke_softirq(void)
{
	if (ksoftirqd_running(local_softirq_pending()))  // 检查当时是否有正在执行的守护程序
		return;

	if (!force_irqthreads() || !__this_cpu_read(ksoftirqd)) {
#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();  // 否则唤醒当前ksoftirq线程来执行
	}
}

函数__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();

	softirq_handle_begin();
	in_hardirq = lockdep_softirq_start();
	account_softirq_enter(current);

restart:
	/* Reset the pending bitmask before enabling irqs */
	set_softirq_pending(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);  // 调用软中断对应的action 即open_softirq注册的回调action
		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;
	}

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

	local_irq_disable();

	pending = local_softirq_pending();
	if (pending) {
		if (time_before(jiffies, end) && !need_resched() &&
		    --max_restart)  // 检查是否需要调度 没有超时  没有超过十次运行限制 则继续执行
			goto restart;

		wakeup_softirqd();  // 否则唤醒后台softirq线程来进行后台处理
	}

	account_softirq_exit(current);  
	lockdep_softirq_end(in_hardirq);
	softirq_handle_end();
	current_restore_flags(old_flags, PF_MEMALLOC);
}

如果超了限制会唤醒ksoftirq后台守护线程来进行执行。

static void wakeup_softirqd(void)
{
	/* Interrupts are disabled: no need to stop preemption */
	struct task_struct *tsk = __this_cpu_read(ksoftirqd); // 获取当前cpu的后台守护线程

	if (tsk)
		wake_up_process(tsk);  //唤醒该线程执行
}

softireqd后台守护线程在系统初始化的时候就会注册成功。

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)
{
	cpuhp_setup_state_nocalls(CPUHP_SOFTIRQ_DEAD, "softirq:dead", NULL,
				  takeover_tasklets);
	BUG_ON(smpboot_register_percpu_thread(&softirq_threads));

	return 0;
}
early_initcall(spawn_ksoftirqd);

...

static void run_ksoftirqd(unsigned int cpu)
{
	ksoftirqd_run_begin();
	if (local_softirq_pending()) {
		/*
		 * We can safely run softirq on inline stack, as we are not deep
		 * in the task stack here.
		 */
		__do_softirq();   // 也是执行的__do_softirq来执行响应软中断时间
		ksoftirqd_run_end();
		cond_resched();
		return;
	}
	ksoftirqd_run_end();
}

至此,软中断的初步的执行流程就是如此。

总结

本文简单的概述了有关Linux软中断的处理的基本流程,软中断包括了网络数据的发送与接受,tasklet机制的执行等几种类型的响应,在执行完成硬件中断之后在执行到irq_exit来检查当时是否有软中断待执行,如果有则直接执行,如果执行的超过限定时候或者次数则调用softirqd的守护进程来进行剩余的软中断的响应处理。由于本人才疏学浅,如有错误请批评指正。

http://unicornx.github.io/2016/02/12/20160212-lk-drv-tasklet/

https://chasinglulu.github.io/2019/07/16/%E4%B8%AD%E6%96%AD%E5%BB%B6%E8%BF%9F%E5%A4%84%E7%90%86%E6%9C%BA%E5%88%B6%E3%80%8Cinterrupt-delay-processing%E3%80%8D/

https://zhuanlan.zhihu.com/p/80371745

https://zhuanlan.zhihu.com/p/447363709

https://developer.aliyun.com/article/458892


版权声明:本文为qq_33339479原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。