linux内核-软中断与Bottom Half

中断服务一般都是在将中断请求关闭的条件下执行的,以避免嵌套而使控制复杂化。可是,如果关中断的时间持续太长就可能因为CPU不能及时响应其他的中断请求而使中断(请求)丢失,为此,内核允许在将具体的中断服务程序挂入中断请求队列时将SA_INTERRUPT标志置成0,使这个中断服务程序在开中断的条件下执行。然而,实际的情况往往是:若是服务的全过程关中断则扩大打击面,而全称开中断则又造成不安定因素,很难取舍。一般来说,一次中盾服务的过程常常可以分成两部分。开头的部分往往是必须在关中断条件下执行的。这样才能在不受干扰的条件下原子地完成一些关键性操作。同时,这部分操作的时间性又往往很强,必须在中断请求发生后立即或至少是在一定的时间现值中完成,而且相继的多次中断请求也不能合并在一起处理。而后半部分,则通常可以、而且应该在开中断条件下执行,这样才不至于因将中断关闭过久而造成其他中断的丢失。同时,这些操作常常允许延迟到稍后才能执行,而且有可能将多次中断服务中的相关部分合并在一起处理。这里的后半部分就称为bottom half,在内核代码中常常缩写成bh。这个概念在相当程度上来自RISC系统结构。在RISC的CPU中,通常由大量的寄存器。当中断发生时,要将所有这些寄存器的内容压栈,并在返回时加以恢复,为此而付出很高的代价。所以,在RISC结构的系统中往往把中断服务分成两部分。第一部分只保存为数不多的寄存器(内容),并利用这些为数不多的寄存器来完成有限的关键性的操作,称为轻量级中断。而另一部分,那就相当于这里的bh了。虽然i386的结构主要是CISC的,面临的问题不尽相同,但前述的问题已经使bh的必要性在许多情况下变得很明显了。

linux内核为将中断服务非诚两半提供了方便,并设立了相应的机制。在以前的内核中,这个机制就称为bh。但是,在2.4版(确切的说是2.3.43)中有了新的发展和推广。

以前的内核中设置了一个函数指针数组bh_base,其大小为32,数值中的每个指针可以用来指向一个具体的bh函数。同时,又设置了两个32位无符号整数bh_active和bh_mask(2.3.38版本),每个无符号的整数中的32位对应着数组bh_base中的32个元素。

我们可以在中断与bh二者之间建立起一种类比。

  1. 数组bh_base相当于硬件中断机制中的数组irq_desc。不过irq_desc中的每个元素代表着一个中断通道,所以是一个中断服务程序队列。而bh_base中的每个元素却最多只能代表一个bh函数。但是,尽管如此,二者在概念上还是相同的。
  2. 无符号整数bh_active在概念上相当于硬件的中断请求寄存器,而bh_mask则相当于中断屏蔽寄存器。
  3. 需要执行一个bh函数时,就通过一个函数mark_bh将bh_active中的某一位设成1,相当于中断源发生了中断请求,而所设置的具体标志位则类似于中断向量。
  4. 如果相当于中断屏蔽寄存器的bh_mask中相应位也是1,即系统允许这个bh函数,那么就会在每次执行完do_IRQ中的中断服务程序后,以及每次系统调用结束之时,在一个函数do_bottom_half中执行相应的bh函数。而do_bottom_half,则类似于do_IRQ。

为了简化bh函数的设计,在do_bottom_half中也像do_IRQ中一样,把bh函数的执行严格地串行化了。这种串行化有两方面的考虑和措施:

一方面,bh函数的执行不允许嵌套。如果在执行bh函数的过程中发生中断,那么由于每次中断服务以后在do_IRQ中都要检查和处理bh函数的执行,就有可能嵌套。为此,在do_bottom_half中针对同一CPU上的嵌套执行加了锁,这样,如果进入do_bottom_half以后发现已经加了锁,就立即返回。因为这说明CPU在本次中断发生之前已经在这个函数中了。

另一方面,是在多CPU系统中,在同一时间内最多只允许一个CPU执行bh函数,以防有两个甚至更多个CPU同时来执行bh函数而互相干扰。为此在do_bottom_half中针对不同CPU同时执行bh函数也加了锁。这样,如果进入do_bottom_half以后发现这个锁已经锁上,就说明已经由CPU在执行bh函数,所以也立即返回。

这两条措施,特别是第二条措施,保证了从单CPU结构到多CPU SMP结构的平稳过渡。可是,在当时的linux内核可以在多CPU SMP结构上稳定运行以后,就慢慢发现这样的处理对于多CPU SMP结构的性能有不利的影响。原因就在于上述的第二条措施使bh函数的执行完全串行化了。当系统中有很多bh函数需要执行时,虽然系统中有多个CPU存在,却只有一个CPU这么一个独木桥。跟do_IRQ做一比较就可以发现,在do_IRQ中的串行化只是针对一个具体中断通道到的,而bh函数的串行化却是全局性的,所以是防卫过当了。既然如此,就应该考虑放宽上述的第二条措施。但是,如果放宽了这一条,就要对bh函数本身的设计和实现有更高的要求(例如对使用全局变量的互斥),而原来已经存在的bh函数显然不符合这些要求。所以,比较好的办法是保留bh。另外再增设一种或几种机制。并把它们纳入一个统一的框架中。这就是2.4版中的软中断(softirq)机制。

从字面上说softirq就是软中断,可是软中断这个词(尤其是在中文里)已经被用作信号(signal)的代名词,因为信号实际上就是以软件手段实现的中断机制。但是,另一方面,把类似于bh的机制称为软中断又确实很贴切。这一方面反映了上述bh函数与中断之间的类比,另一方面也反映了这是一种在时间要求上更为软性的中断请求。实际上,这里所体现的是层次的不同。如果说硬中断通常是外部设备对CPU的中断,那么softirq通常是硬中断服务程序对内核的中断,而信号则是由内核(或其他进程)对某个进程的中断。后面这二者都是由软件产生的软中断。所以,对软中断这次的含义要根据上下文加以区分。

下面,我们以bh函数为主线,通过阅读代码来叙述2.4版内核的软中断(softirq)机制。

系统在初始化时通过函数softirq_init对内核的软中断机制进行初始化。其代码如下:

void __init softirq_init()
{
	int i;

	for (i=0; i<32; i++)
		tasklet_init(bh_task_vec+i, bh_action, i);

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

软中断本身是一种机制,同时也使一个框架。在这个框架里有bh机制,这是一种特殊的软中断,也可以说是设计最保守的,但却是最简单、最安全的软中断。除此之外,还有其他的软中断,定义如下:

enum
{
	HI_SOFTIRQ=0,
	NET_TX_SOFTIRQ,
	NET_RX_SOFTIRQ,
	TASKLET_SOFTIRQ
};

这里最值得注意的是TASKLET_SOFTIRQ,代表着一种称为tasklet的机制。也许采用tasklet这个词的原意在于表示这是一片小小的任务,但是这个词容易使人联想到task即进程而引起误会,其实这二者毫无关系。显然,NET_TX_SOFTIRQ和NET_RX_SOFTIRQ两种软中断是专为网络操作而设的,所以在softirq_init中只对TASKLET_SOFTIRQ和HI_SOFTIRQ两种软中断进行初始化。

先看bh机制的初始化。内核中为bh机制设置了一个结构数组bh_task_vec,这是tasklet_struct数据结构的数组。这种数据结构的定义也在interrupt.h中:

/* Tasklets --- multithreaded analogue of BHs.

   Main feature differing them of generic softirqs: tasklet
   is running only on one CPU simultaneously.

   Main feature differing them of BHs: different tasklets
   may be run simultaneously on different CPUs.

   Properties:
   * If tasklet_schedule() is called, then tasklet is guaranteed
     to be executed on some cpu at least once after this.
   * If the tasklet is already scheduled, but its excecution is still not
     started, it will be executed only once.
   * If this tasklet is already running on another CPU (or schedule is called
     from tasklet itself), it is rescheduled for later.
   * Tasklet is strictly serialized wrt itself, but not
     wrt another tasklets. If client needs some intertask synchronization,
     he makes it with spinlocks.
 */

struct tasklet_struct
{
	struct tasklet_struct *next;
	unsigned long state;
	atomic_t count;
	void (*func)(unsigned long);
	unsigned long data;
};

代码的作者加了详细的注释,说tasklet是多序(不是多进程或多线程!)的bh函数。为什么这么说呢?因为对tasklet的串行化不像对bh函数那样严格,所以允许在不同的CPU上同时执行tasklet,但必须是不同的tasklet。一个tasklet_struct数据结构就代表这一个tasklet,结构中的函数指针func指向其服务程序。那么,为什么bh机制中要使用这种数据结构呢?这是因为bh函数的执行(并不是bh函数本身)就是作为一个tasklet来实现的,在此基础上再加上更严格的限制,就成了bh。

函数tasklet_init的代码在kernel/softirq.c中:

softirq_init=>tasklet_init

void tasklet_init(struct tasklet_struct *t,
		  void (*func)(unsigned long), unsigned long data)
{
	t->func = func;
	t->data = data;
	t->state = 0;
	atomic_set(&t->count, 0);
}

在softirq_init中,对于bh的32个tasklet_struct结构调用tasklet_init以后,它们的函数指针func全都指向bh_action。

对其他软中断的初始化是通过open_softirq完成的,其代码如下:

softirq_init=>open_softirq


void open_softirq(int nr, void (*action)(struct softirq_action*), void *data)
{
	unsigned long flags;
	int i;

	spin_lock_irqsave(&softirq_mask_lock, flags);
	softirq_vec[nr].data = data;
	softirq_vec[nr].action = action;

	for (i=0; i<NR_CPUS; i++)
		softirq_mask(i) |= (1<<nr);
	spin_unlock_irqrestore(&softirq_mask_lock, flags);
}

内核中为软中断设置了一个以软中断号为下标的数组softirq_vec,类似于中断机制的irq_desc。

static struct softirq_action softirq_vec[32] __cacheline_aligned;

这是一个softirq_action数据结构的数组,其定义为:

/* softirq mask and active fields moved to irq_cpustat_t in
 * asm/hardirq.h to get better cache usage.  KAO
 */

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

数组softirq_vec是个全局变量,系统中的各个CPU所看到的是同一个数组。但是,每个CPU各有其自己的软中断控制、状况结构,所以这些数据结构形成一个以CPU编号为下标的数组irq_stat。这个数组也是全局变量,但是各个CPU可以按其自身编号的访问相应的数据结构。我们把有关的定义列出于下,供读者自己阅读:


/* entry.S is sensitive to the offsets of these fields */
typedef struct {
	unsigned int __softirq_active;
	unsigned int __softirq_mask;
	unsigned int __local_irq_count;
	unsigned int __local_bh_count;
	unsigned int __syscall_count;
	unsigned int __nmi_count;	/* arch dependent */
} ____cacheline_aligned irq_cpustat_t;


irq_cpustat_t irq_stat[NR_CPUS];


#ifdef CONFIG_SMP
#define __IRQ_STAT(cpu, member)	(irq_stat[cpu].member)
#else
#define __IRQ_STAT(cpu, member)	((void)(cpu), irq_stat[0].member)
#endif	

  /* arch independent irq_stat fields */
#define softirq_active(cpu)	__IRQ_STAT((cpu), __softirq_active)
#define softirq_mask(cpu)	__IRQ_STAT((cpu), __softirq_mask)

数据结构中__softirq_active相当于软中断请求寄存器,__softirq_mask则相当于软中断屏蔽寄存器。函数open_softirq除把函数指针action填入softirq_vec中的相应元素外,还把所有CPU的中断屏蔽寄存器中的相应位设置成1,使这个软中断在每个CPU上都可以执行。从softirq_init中调用open_softirq把TASKLET_SOFTIRQ和HI_SOFTIRQ两个软中断的处理程序分别设置成tasklet_action和tasklet_hi_action。

内核中还有另一个以CPU编号为下标的数组tasklet_hi_vec,这是tasklet_head结构数组,每个tasklet_head结构就是一个tasklet_struct结构的队列头。


struct tasklet_head
{
	struct tasklet_struct *list;
} __attribute__ ((__aligned__(SMP_CACHE_BYTES)));

struct tasklet_head tasklet_hi_vec[NR_CPUS] __cacheline_aligned;

回到bh机制这个话题上。通过softirq_init只是使相应tasklet_struct结构中的函数指针func只想了bh_action,也就是建立了bh的执行机制,而具体的bh函数还没有与之挂钩,就好像具体的中断服务程序尚未挂入中断服务队列一样。具体bh函数是通过init_bh设置的。下面是取自sched_init中的一个片段:


	init_bh(TIMER_BH, timer_bh);
	init_bh(TQUEUE_BH, tqueue_bh);
	init_bh(IMMEDIATE_BH, immediate_bh);

以用于时钟中断的bh函数timer_bh为例,其bh向量或bh编号为TIMER_BH。目前内核中已经定义的编号如下:

/* Who gets which entry in bh_base.  Things which will occur most often
   should come first */
   
enum {
	TIMER_BH = 0,
	TQUEUE_BH,
	DIGI_BH,
	SERIAL_BH,
	RISCOM8_BH,
	SPECIALIX_BH,
	AURORA_BH,
	ESP_BH,
	SCSI_BH,
	IMMEDIATE_BH,
	CYCLADES_BH,
	CM206_BH,
	JS_BH,
	MACSERIAL_BH,
	ISICOM_BH
};

再看init_bh的代码:

void init_bh(int nr, void (*routine)(void))
{
	bh_base[nr] = routine;
	mb();
}

显然,这里的数组bh_base就是前述的函数指针数组。这里调用的函数mb与CPU中执行指令的流水线有关,而这并不是我们现在所关心的。

需要执行一个特定的bh函数时,可以通过一个inline函数mark_bh提出请求。读者在时钟中断博客中可以看到在do_timer中通过mark_bh(TIMER_BH);提出对timer_bh的执行请求。函数mark_bh的代码如下:

static inline void mark_bh(int nr)
{
	tasklet_hi_schedule(bh_task_vec+nr);
}

如前所述,内核中为bh函数的执行设立了一个tasklet_struct结构数组bh_task_vec,这里以bh函数的编号为下标就可以找到相应的数据结构,并用其调用tasklet_hi_schedule,其代码也在同一个文件中。读者应该还记得,在bh_task_vec的每个tasklet_struct结构中,函数指针func都指向bh_action。

mark_bh=>tasklet_hi_schedule

static inline void tasklet_hi_schedule(struct tasklet_struct *t)
{
	if (!test_and_set_bit(TASKLET_STATE_SCHED, &t->state)) {
		int cpu = smp_processor_id();
		unsigned long flags;

		local_irq_save(flags);
		t->next = tasklet_hi_vec[cpu].list;
		tasklet_hi_vec[cpu].list = t;
		__cpu_raise_softirq(cpu, HI_SOFTIRQ);
		local_irq_restore(flags);
	}
}

这里的smp_processor_id返回当前进程所在CPU的编号,然后以此为下标从tasklet_hi_vec中找到该CPU的队列头,把参数t所指的tasklet_struct数据结构链入这个队列。由此可见,对执行bh函数的要求是在哪一个CPU上提出的,就把它调度在哪一个CPU上执行,函数名中的schedule就是这个意思,而与进程调度毫无关系。另一方面,一个tasklet_struct代表着对bh函数的一次执行,在同一时间内只能把它链入一个队列中,而不可能同时出现在多个队列中。对于同一个tasklet_struct数据结构,如果已经对其调用了tasklet_hi_schedule,而尚未得到执行,就不允许再将其链入队列,所以在数据结构中设置了一个标志位TASKLET_STATE_SCHED来保证这一点。最后,还要通过__cpu_raise_softirq正式发出软中断请求。

mark_bh=>tasklet_hi_schedule=>__cpu_raise_softirq


static inline void __cpu_raise_softirq(int cpu, int nr)
{
	softirq_active(cpu) |= (1<<nr);
}

读者在前面已经看到过softirq_active的定义,它对给定CPU的软中断控制、状况结构操作,将其中softirq_active字段内的相应标志位设成1.

内核每当在do_IRQ中执行一个通道中的中断服务程序以后,以及每当从系统调用返回时,都要检查是否有软中断请求在等待执行。下面是do_IRQ中的一个片段:

	if (softirq_active(cpu) & softirq_mask(cpu))
		do_softirq();

另一段代码取自arch/i386/entry.S,这是在从系统调用返回时执行的:

ENTRY(ret_from_sys_call)
#ifdef CONFIG_SMP
	movl processor(%ebx),%eax
	shll $CONFIG_X86_L1_CACHE_SHIFT,%eax
	movl SYMBOL_NAME(irq_stat)(,%eax),%ecx		# softirq_active
	testl SYMBOL_NAME(irq_stat)+4(,%eax),%ecx	# softirq_mask
#else
	movl SYMBOL_NAME(irq_stat),%ecx		# softirq_active
	testl SYMBOL_NAME(irq_stat)+4,%ecx	# softirq_mask
#endif
	jne   handle_softirq




handle_softirq:
	call SYMBOL_NAME(do_softirq)
	jmp ret_from_intr

注意,这里的processor表示task_struct数据结构中该字段的位移,所以207行是从当前进程的task_struct数据结构中取当前CPU的编号。而SYMBOL_NAME(irq_stat)(,%eax)则相当于irq_stat[cpu],并且是其中第一个字段;相应地,SYMBOL_NAME(irq_stat)+4(,%eax)相当于这个数据结构中的第二个字段,并且第一个字段必须是32位。读者不妨回过去看一下irq_cpustat_t的定义,在那里有个注释,说entry.S中的代码对这个数据结构中的字段位置敏感,就是这个意思。所以,这些汇编代码实际上与上面do_IRQ中的两行C代码时一样的。

检测到软中断请求以后,就要通过do_softirq加以执行了。其代码如下:


asmlinkage void do_softirq()
{
	int cpu = smp_processor_id();
	__u32 active, mask;

	if (in_interrupt())
		return;

	local_bh_disable();

	local_irq_disable();
	mask = softirq_mask(cpu);
	active = softirq_active(cpu) & mask;

	if (active) {
		struct softirq_action *h;

restart:
		/* Reset active bitmask before enabling irqs */
		softirq_active(cpu) &= ~active;

		local_irq_enable();

		h = softirq_vec;
		mask &= ~active;

		do {
			if (active & 1)
				h->action(h);
			h++;
			active >>= 1;
		} while (active);

		local_irq_disable();

		active = softirq_active(cpu);
		if ((active &= mask) != 0)
			goto retry;
	}

	local_bh_enable();

	/* Leave with locally disabled hard irqs. It is critical to close
	 * window for infinite recursion, while we help local bh count,
	 * it protected us. Now we are defenceless.
	 */
	return;

retry:
	goto restart;
}

软中断服务程序既不允许在一个硬中断服务程序内部执行,也不允许在一个软中断服务程序内部执行,所以要通过一个宏操作in_interrupt加以检测,这是在include/asm-i386/hardirq.h中定义的:

/*
 * Are we in an interrupt context? Either doing bottom half
 * or hardware interrupt processing?
 */
#define in_interrupt() ({ int __cpu = smp_processor_id(); \
	(local_irq_count(__cpu) + local_bh_count(__cpu) != 0); })

显然,这个测试防止了软中断服务程序的嵌套,这就是前面讲的第一条串行化措施。与local_bh_disable有关的定义在include/asm-i386/softirq.h中:

#define cpu_bh_disable(cpu)	do { local_bh_count(cpu)++; barrier(); } while (0)
#define cpu_bh_enable(cpu)	do { barrier(); local_bh_count(cpu)--; } while (0)

#define local_bh_disable()	cpu_bh_disable(smp_processor_id())
#define local_bh_enable()	cpu_bh_enable(smp_processor_id())

从do_softirq的代码中可以看出,使CPU不能执行软中断服务程序的关卡只有一个,那就是in_interrupt,所以对软中断服务程序的执行并没有采取前述的第二条串行化措施。这就是说,不同的CPU可以同时进入对软中断服务程序的执行(见78行),分别执行各自所请求的软中断服务。从这个意义上,软中断服务程序的执行时并发的、多序的。但是,这些软中断服务程序的设计和实现必须十分小心,不能让它互相干扰(例如通过共享的全局变量)。至于do_softirq中其他的代码,则读者不会感到困难,我们就不多说了。

在我们这个情景中,如前所述,执行的服务程序为bh_action,其代码如下:

/* BHs are serialized by spinlock global_bh_lock.

   It is still possible to make synchronize_bh() as
   spin_unlock_wait(&global_bh_lock). This operation is not used
   by kernel now, so that this lock is not made private only
   due to wait_on_irq().

   It can be removed only after auditing all the BHs.
 */
spinlock_t global_bh_lock = SPIN_LOCK_UNLOCKED;

static void bh_action(unsigned long nr)
{
	int cpu = smp_processor_id();

	if (!spin_trylock(&global_bh_lock))
		goto resched;

	if (!hardirq_trylock(cpu))
		goto resched_unlock;

	if (bh_base[nr])
		bh_base[nr]();

	hardirq_endlock(cpu);
	spin_unlock(&global_bh_lock);
	return;

resched_unlock:
	spin_unlock(&global_bh_lock);
resched:
	mark_bh(nr);
}

这里对具体bh函数的执行(见257行)又设置了两到关卡。一道是hardirq_trylock,其定义为:

#define hardirq_trylock(cpu)	(local_irq_count(cpu) == 0)

与前面的in_interrupt比较下就可看出,这还是防止从一个硬中断服务程序内部调用bh_action。而另一道关卡spin_trylock就不同了,定义如下:

#define spin_trylock(lock)	(!test_and_set_bit(0,(lock)))

这把锁即使全局变量global_bh_lock,只要有一个CPU在253行至260行之间运行,别的CPU就不能进入这个区间了,所以在任何时间最多只有一个CPU在执行bh函数。这就是前述的第二条串行化措施。至于根据bh函数编号执行相应的函数,那就很简单了。在我们这个情景中,具体的bh函数是timer_bh,我们将在时钟中断博客中阅读这个函数的代码。

作为对比,我们列出另一个软中断服务程序tasklet_action的代码,对着可以把它与bh_action比较,看看有哪些重要的区别。这个函数的代码定义如下:

struct tasklet_head tasklet_vec[NR_CPUS] __cacheline_aligned;

static void tasklet_action(struct softirq_action *a)
{
	int cpu = smp_processor_id();
	struct tasklet_struct *list;

	local_irq_disable();
	list = tasklet_vec[cpu].list;
	tasklet_vec[cpu].list = NULL;
	local_irq_enable();

	while (list != NULL) {
		struct tasklet_struct *t = list;

		list = list->next;

		if (tasklet_trylock(t)) {
			if (atomic_read(&t->count) == 0) {
				clear_bit(TASKLET_STATE_SCHED, &t->state);

				t->func(t->data);
				/*
				 * talklet_trylock() uses test_and_set_bit that imply
				 * an mb when it returns zero, thus we need the explicit
				 * mb only here: while closing the critical section.
				 */
#ifdef CONFIG_SMP
				smp_mb__before_clear_bit();
#endif
				tasklet_unlock(t);
				continue;
			}
			tasklet_unlock(t);
		}
		local_irq_disable();
		t->next = tasklet_vec[cpu].list;
		tasklet_vec[cpu].list = t;
		__cpu_raise_softirq(cpu, TASKLET_SOFTIRQ);
		local_irq_enable();
	}
}

最后,软中断服务程序,报货bh函数,与常规中断服务程序的分离并不是强制性的,要根据设备驱动的具体情况(也许还有设计人员的水平)来决定。

おすすめ

転載: blog.csdn.net/guoguangwu/article/details/121133026