1 软中断概述

软中断是实现中断下半部的一种手段,与2.5以前版本的下半段机制不同。软中断可以同时运行在不同的CPU上。

1.1 软中断的表示

内核中用结构体softirq_action表示一个软中断。软中断是一组静态定义的接口,有32个。但是内核(2.6.34)中只实现了10个。可用的软中断的个数用NR_SOFTIRQ表示,NR_SOFTIRQ=10,软中断的类型用一个枚举体表示。这里需要注意的是,32个软中断体现在位掩码是unsigned int 类型。

static struct softirq_action softirq_vec[NR_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
};
struct softirq_action
{
	void	(*action)(struct softirq_action *);
};

2 软中断相关的数据结构

2.1 thread_info的preempt_count字段

preempt_count 是一个32位的int型,共分为5个字段
Linux内核软中断-LMLPHP
宏in_interrupt检测软中断计数器 硬中断计数器 和 NMI掩码,只要这三个字段任意一个字段不为0,就表示进程处于中断上下文。

#define in_irq()		(hardirq_count())
#define in_softirq()		(softirq_count())
#define in_interrupt()		(irq_count())

2.2 pending位掩码

每个CPU上都有一个irq_stat结构,irq_stat中的__softirq_pending是一个32位的掩码,为1表示该软中断已经激活,正等待处理。为0表示软中断被禁止。在do_irq中被使用。
内核使用local_softirq_pending得到当前CPU上的位掩码

#define local_softirq_pending()	percpu_read(irq_stat.__softirq_pending)
#define set_softirq_pending(x)	percpu_write(irq_stat.__softirq_pending, (x))
#define or_softirq_pending(x)	percpu_or(irq_stat.__softirq_pending, (x))

irq_cpustat_t irq_stat[NR_CPUS]
typedef struct {
        ...
	unsigned int __softirq_pending;
        ...
}irq_cpustat_t;

2.3 软中断栈

进程的内核栈的大小根据编译时选项不同,可以是4K或者8K。如果是8K堆栈,中断,异常和软中断(softirq) 共享这个堆栈。如果选择4K堆栈,则内核堆栈 硬中断堆栈 软中断堆栈各自使用一个4K空间。关于软中断堆栈,后面在软中断处理时再详细说明。

#ifdef CONFIG_4KSTACKS

static DEFINE_PER_CPU_PAGE_ALIGNED(union irq_ctx, softirq_stack);

union irq_ctx {
	struct thread_info      tinfo;
	u32                     stack[THREAD_SIZE/sizeof(u32)];
} __attribute__((aligned(PAGE_SIZE)));

3 软中断的初始化

内核使用open_softirq初始化一个软中断,nr是代表软中断类型的常量,action指向一个软中断处理函数

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

4 软中断的触发(raise softirq)

触发就是将位掩码pending的相应位 置1的过程。内核使用raise_softirq完成触发软中断,nr是要触发的软中断类型。值的注意的是,中断的触发 发生关闭硬中断的情况下。

触发软中断的过程中,如果该进程未处于中断上下文,说明当前进程处于进程上下文中,那么我们直接调用wakeup_softirqd调度ksoftirqd即可。

反之,如果当前处于中断上下文中或软中断被禁止使用,那么就不必调度内核线程,在中断处理后期irq_exit中,会调用invoke_softirq()处理软中断。
实际的工作是交给or_softirq_pending(1UL << (nr)); 完成的,该函数通过位操作将指定为和pending相加。

Linux内核软中断-LMLPHP

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

	/*
	 * 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())
		wakeup_softirqd();
}

#define __raise_softirq_irqoff(nr) do { or_softirq_pending(1UL << (nr)); }

5. 软中断的处理

5.1 处理软中断的时机

1 在do_irq的末期(irq_exit)

如果当前进程没有处于中断上下文中并且本地CPU上还有没有处理的软中断,那么就调用invoke_softirq()处理软中断。

#ifdef __ARCH_IRQ_EXIT_IRQS_DISABLED
# define invoke_softirq()	__do_softirq()
#else
# define invoke_softirq()	do_softirq()
#endif
void irq_exit(void)
{
	account_system_vtime(current);
	trace_hardirq_exit();
	sub_preempt_count(IRQ_EXIT_OFFSET);
	if (!in_interrupt() && local_softirq_pending())
		invoke_softirq();

	rcu_irq_exit();
#ifdef CONFIG_NO_HZ
	/* Make sure that timer wheel updates are propagated */
	if (idle_cpu(smp_processor_id()) && !in_interrupt() && !need_resched())
		tick_nohz_stop_sched_tick(0);
#endif
	preempt_enable_no_resched();
}

#ifdef __ARCH_IRQ_EXIT_IRQS_DISABLED
# define invoke_softirq()	__do_softirq()
#else
# define invoke_softirq()	do_softirq()
#endif

程序5-1 irq_exit

2 当软中断被重复触发超过10次时,内核会调用wakeup_softirqd()唤醒内核线程ksoftirqd去处理软中断。

5.2 软中断的处理

1 do_softirq

根据内核堆栈大小,有两种do_softirq,一种是通用do_irq,另一种是架构相关的do_irq(arch/x86/kernel/irq_32.c)。

通用的do_irq的工作流程
1 判断当前是否处于硬中断上下文中或者软中断被禁用,如果是那么直接返回
2 保存Eflags 然后关闭本地硬件中断
3 获取本地CPU的位掩码pending 如果有待处理的软中断就调用__do_irq
4 从__do_irq返回恢复Eflags

asmlinkage void do_softirq(void)
{
        //用来保存位掩码的局部变量
	__u32 pending;
        //保存Eflags寄存器的局部变量
	unsigned long flags;
        //如果do_softirq在中断上下文中被调用 或 软中断被禁止使用 那么不处理软中断
        //直接返回
	if (in_interrupt())
		return;
        //将Eflags保存到flags中 然后关硬件中断
	local_irq_save(flags);
        //获取本地CPU上的位掩码
	pending = local_softirq_pending();

	if (pending)
		__do_softirq();
        //将flags
	local_irq_restore(flags);
}

x86_32架构下使用4K软中断堆栈的do_softirq处理流程
1 类似于通用的do_softirq,如果在中断上下文中或者软中断被禁止使用就立即返回。然后关外部中断
2 如果本地CPU上存在待处理的软中断就开始对软中断堆栈的处理,关键是令isp指向软中断堆栈的栈底。然后在软中断栈上调用call_on_stack。call_on_stack是一段内联汇编,其主要目的是完成从内核栈到软中断栈的切换。先将esp保存到ebx中,使用call指令跳转到__do_softirq子例程,子例程返回时再恢复esp。

asmlinkage void do_softirq(void)
{
	unsigned long flags;
	struct thread_info *curctx;
	union irq_ctx *irqctx;
	u32 *isp;

	if (in_interrupt())
		return;

	local_irq_save(flags);

	if (local_softirq_pending()) {
                //curctx指向当前进程的thread_info结构
		curctx = current_thread_info();
                //irqctx包含一个软中断堆和thread_info结构
		irqctx = __get_cpu_var(softirq_ctx);
                //触发硬中断和软中断是同一个进程所以将threadinfo的task指针统一
		irqctx->tinfo.task = curctx->task;
                //从内核堆栈切换到软中断堆栈 需要保存内核堆栈的栈指针寄存器内容
		irqctx->tinfo.previous_esp = current_stack_pointer;

		/* build the stack frame on the softirq stack */
                //isp指向软中断栈底
		isp = (u32 *) ((char *)irqctx + sizeof(*irqctx));
                //在软中断堆栈上调用__do_softirq
		call_on_stack(__do_softirq, isp);
	}
	local_irq_restore(flags);
}

static void call_on_stack(void *func, void *stack)
{
        //call *%%edi 间接绝对近调用 偏移地址存储在edi寄存器中
        //指令执行时 先将eip入栈 然后将edi-->eip
        //先交换ebx and esp
        //然后调用__do_softirq
        //然后将ebx-->esp 恢复xchgl之前的esp
        //输出约束将ebx --> stack
	asm volatile("xchgl	%%ebx,%%esp	\n"
		     "call	*%%edi		\n"
		     "movl	%%ebx,%%esp	\n"
		     : "=b" (stack)
		     : "0" (stack),
		       "D"(func)
		     : "memory", "cc", "edx", "ecx", "eax");
}

2 __do_softirq

软中断的处理实际上是由 __do_softirq完成,整体的思路是遍历pending,如果某一位不为空表示本地CPU上有待处理的软中断,然出调用软中断的处理函数。
开始处理软中断前,内核要调用__local_bh_disable(通过将preempt_count的软中断计数器加1)关闭下半部。如前所说,处理软中断的时机不止一个,内核要保证在本地CPU上软中断的处理是串行的。
另外在处理软中断的循环结束时,内核还要检测是否有重复触发的软中断。先调用local_softirq_pending()获取位掩码pending,然后根据pending继续处理软中断,不过这种重复处理不能超过10次(MAX_SOFTIRQ_RESTART),一旦超过10次,内核就会唤醒ksoftirqd

#define MAX_SOFTIRQ_RESTART 10

asmlinkage void __do_softirq(void)
{
        // softirq_action表示一个软中断
	struct softirq_action *h;
        // 局部变量pending 保存待处理软中断位图
	__u32 pending;
        // 软中断的重启次数
	int max_restart = MAX_SOFTIRQ_RESTART;
	int cpu;
        //获取本地CPU上所有待处理的软中断
	pending = local_softirq_pending();
	account_system_vtime(current);

        //关闭下半部中断
	__local_bh_disable((unsigned long)__builtin_return_address(0));
	lockdep_softirq_enter();

	cpu = smp_processor_id();
restart:
	/* Reset the pending bitmask before enabling irqs */
        //pending已经保存了所有带出软中断的状态 所以将pending bitmask clear
	set_softirq_pending(0);
        // 开中断
	local_irq_enable();
        //h指向第一类软中断
	h = softirq_vec;
	do {
                //先处理第一类软中断
		if (pending & 1) {
			int prev_count = preempt_count();
			kstat_incr_softirqs_this_cpu(h - softirq_vec);

			trace_softirq_entry(h, softirq_vec);
                        //调用软中断处理程序
			h->action(h);
			trace_softirq_exit(h, softirq_vec);
			if (unlikely(prev_count != preempt_count())) {
				printk(KERN_ERR "huh, entered softirq %td %s %p"
				       "with preempt_count %08x,"
				       " exited with %08x?\n", h - softirq_vec,
				       softirq_to_name[h - softirq_vec],
				       h->action, prev_count, preempt_count());
				preempt_count() = prev_count;
			}

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

        //关闭本地CPU上的硬中断
	local_irq_disable();
        //获取位掩码
	pending = local_softirq_pending();
	if (pending && --max_restart)
		goto restart;

	if (pending)
		wakeup_softirqd();

	lockdep_softirq_exit();

	account_system_vtime(current);
   //将软中断计数器加1,激活软中断
	_local_bh_enable();
}

6 ksoftirqd内核线程

6.1 ksoftirqd

在内核处理类似NET_RX_SOFTIRQ的软中断时,如果有大量等待处理的数据包。就会不断的调用
__raise_softirq_irqoff(NET_RX_SOFTIRQ)重复触发软中断NET_RX_SOFTIRQ。这样做会导致用户进程的” 饥饿问题 “ (长时间无法获得CPU)。
针对这种问题内核使用内核线程ksoftirqd去处理自行触发次数超过10次的软中断。

6.2 ksoftirqd的实现

static int ksoftirqd(void * __bind_cpu)
{
        //ksoftirqd的优先级最低(nice = 19)
	set_user_nice(current, 19);
        //将ksoftirqd设置为不可冻结
	current->flags |= PF_NOFREEZE;
        //设置ksoftirqd为可中断状态
    	set_current_state(TASK_INTERRUPTIBLE);

	while (!kthread_should_stop()) {
                //如果没有待处理的软中断则 调度别的进程
		if (!local_softirq_pending())
			schedule();

		__set_current_state(TASK_RUNNING);

		while (local_softirq_pending()) {
			/* Preempt disable stops cpu going offline.
			   If already offline, we'll be on wrong CPU:
			   don't process */
                        //关闭内核抢占
			preempt_disable();
                        //处理软中断
			do_softirq();
                        //开启内核抢占
			preempt_enable();
                        //cond_resched()的目的是提高系统实时性, 主动放弃cpu供优先级更高的任务使用
			cond_resched();
		}

		set_current_state(TASK_INTERRUPTIBLE);
	}
	__set_current_state(TASK_RUNNING);
	return 0;
}

程序6-1 ksoftirqd主功能函数

out:
	local_irq_enable();
	return;

softnet_break:
	__get_cpu_var(netdev_rx_stat).time_squeeze++;
	__raise_softirq_irqoff(NET_RX_SOFTIRQ);
	goto out;

程序6-2 net_rx_action

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

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

程序6-3 wakeup_softirqd

还未解决的问题

set_current_state和 __set_current_state的区别
PF_NOFREEZE
cond_resched()
为什么在do_softirq开始处理软中断时要关闭硬件中断

参考

05-04 17:46