Linux内核:中断、软中断、tasklet

在之前我所写的Linux驱动程序中,会经常使用到中断机制,像CC1100高频驱动、倒车雷达驱动等等。但所用到的中断机制都基本上是用到中断的顶半部,即:编写中断处理函数,通过request_irq函数申请中断,这样当中断来临的时候,就会自动执行中断处理程序里面的内容。之所以没有使用到中断的底半部,是因为我们这些驱动程序中,中断处理函数一般都能被很快执行完,同时也不会存在有任何休眠的动作,因此使用中断的顶半部对于我们这些驱动程序来说,反而相对简单一些。因此这也就得出,并不是任何中断程序都一定会使用到中断的底半部。


中断顶半部

对于中断的顶半部,我想大部分的关于Linux驱动的书上都会有详细的讲解,并且这一块理解和实践起来都比较容易,但这里我需要讲解的是关于共享中断的这一部分,因为这一块可能对于一些初学者会有一点难度。

共享中断是指多个设备共享一根中断线(中断线在这里可以理解为中断号,也就是说多个设备共享一个中断号),为什么会有这种情况发生,因为在Linux内核中,中断线的数目是有限的,如果每一个设备都使用一根中断线的话,中断线肯定是不够的,所以聪明的Linux内核设计师们就提出了共享中断这一理念。这里理念的主要目的就是可以在一根中断线上搭载多个中断设备。那么好了,现在问题也来了,既然都在同一个中断线上,如果中断来了的话,要如何判断该中断来自于哪一个设备呢?其实对于Linux内核来说,要判断其来自哪一个设备,其需要做两步工作。当一个中断来临时,Linux内核会遍历该中断线上所有注册了的中断处理程序,在该中断处理程序中,就会迅速判断到底是来自于哪一个硬件设备。而在中断程序中如何来判断呢?这就需要相应产生中断的硬件设备来支持了。例如可能中断处理程序会检查一下该处理程序对应的硬件设备的某一寄存器的状态来判断是否该设备发生了中断,如果是该设备发出的中断,就执行接下来的处理函数。如果不是,就立即返回(应该返回IRQ_RETVAL(IRQ_NONE))。

首先我们来看一下在申请共享中断的过程与一般申请中断有哪些不同。

我们知道申请注册中断的函数是:

request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags,
	    const char *name, void *dev)
{
	return request_threaded_irq(irq, handler, NULL, flags, name, dev);
}

  1. 如果我们要申请共享中断函数的话,flag标志位必须还要指定一个IRQF_SHARED(即flag再“ | ”上一个IRQF_SHARED),注意,该中断线的每一个中断设备在申请中断的时候都必须要加上该标志位。
  2. 对于每一个注册的中断处理程序来说,最后一个参数dev必须是唯一的(这是共享中断所明确要求的)。为了确保dev参数值是唯一的。可以将dev参数的值设为指向申请中断函数的设备结构体指针即可(我们的CC1101和倒车雷达驱动都是这么干的)。而且由于中断处理函数可能会用到设备结构体的数据,因此这是一个一箭双雕的方法。对于共享中断处理程序,dev的参数值不能为NULL。

中断上下文

在这里顺便提一下中断上下文,当我们执行一个中断处理函数时,内核就会处于中断上下文(Interrupt Context)中。与进程上下文不同,中断上下文与进程并没有什么关系。与current宏也没有任何关系,尽管此时若使用的current标志位的话,其任然是指向被中断的进程。由于中断上下文不依赖与进程,因此中断上下文不能休眠,不能在中断上下文中调用某些可能引起休眠的函数。

由于中断上下文可以打断其他正在执行的代码,因此,中断上下文在执行时间上由严格的时间限制。中断上下文中的代码需要尽可能简洁,尽量不要使用循环或者是耗时比较长的函数来处理中断任务。这是由于中断上下文已经打断了其他正在执行的代码,甚至可能是其他的中断处理程序,因此中断处理程序应该快速地执行完,否则可能会使其他被打断的程序长时间等待而造成系统性能下降甚至崩溃。当然,在中断上下文中处理复杂耗时的任务也在所难免,但最好将这部分任务放在中断的底半部(主要因为中断的底半部,可以被其他甚至是同类型的中断打断,并且中断底半部函数是异步执行。)。这样既可以很快地执行完中断处理程序(尽快回复被中断的代码),又可以在中断程序中完成很复杂的任务。后面的软中断或者是tasklet都属于中断的上下文中。

在Linux2.6内核中,中断处理程序拥有自己的栈,每一个处理器一个,大小为一页(4KB),尽管中断栈并不算大,但平均可用栈空间要比Linux内核的其他程序大得多。因为中断程序把这一页据为己有。在我们编写中断处理程序时,并不需要关心如何设置中断栈或内核栈的大小,总之,尽量节约中断栈的空间就行了。


中断的底半部

下面我们来开始讲解中断底半部,如果用一个词来形容底半部的功能,就是“延迟执行”,为什么要这样说呢,后面分析过后就会深刻理解这一点了。在中断的上半部,即中断处理程序结束前,当前的中断线在所有的处理器上都会被屏蔽,如果在申请中断线时使用了IRQF_DISABLED,那么情况会更加糟糕,在中断处理程序执行时会禁止所有的本地中断。因此尽可能地缩短中断被屏蔽的时间对系统的响应能力和性能都至关重要。因此,要将耗时较长的任务放到底半部延迟执行。因为底半部并不禁止其他中断上半部的执行(哪怕是自己的中断处理函数)。对于中断底半部的实现方式一共有三种;

  1. 采用软中断的方式
  2. 采用tasklet微线程
  3. 采用工作队列
下面我们分别介绍这三种实现中断底半部的方式


软中断
软中断使内核可以执行延迟任务,由于其运作方式与之前的中断类似,并且完全由软件实现,因此称之为软中断。在讲解软中断之前,我们先来看一下软中断的一个模型图:


我们可以看到,所有的中断线上的中断都需要经过do_IRQ函数,执行该函数,需要一个irq的参数,也就是中断号。然后do_IRQ函数便会轮询得执行该中断线上的所以中断处理程序,该实现方式主要是在do_IRQ函数中调用handle_IRQ_event函数实现。handle_IRQ_event会执行irq所在的中断线上的所有的中断处理函数。好了,现在如果我们执行完了handle_IRQ_event函数后,我们会在do_IRQ中调用irq_exit函数,该函数才是负责调用和处理待决的软中断。首先我们来看一下这个函数的源代码:
void irq_exit(void)
{
	account_system_vtime(current);
	trace_hardirq_exit();
	sub_preempt_count(IRQ_EXIT_OFFSET);
	if (!in_interrupt() && local_softirq_pending())
 //判断是否有软中断被请求,主要是看是否有执行raise_softirq函数,
		invoke_softirq(); //用于唤醒软中断,即会激活do_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();
}
一旦我们激活软中断,调用do_softirq函数,就说明程序已经进入软中断环境了。与do_IRQ所处的中断的顶半部不同,处于软中断环境中,是可以被其他中断程序打断的,甚至是处于同一中断线的中断,也因为此,所以软中断可以执行一些稍微时间长一点的任务,也不会迟滞系统对中断的反应时间。
下面我将详细介绍有关do_softirq函数的实现原理和过程,因为只有了解该过程,才能充分理解软中断的工作原理。
软中断由一个softirq_action结构体表示,在该结构体中只定义了一个函数指针
struct softirq_action
{
	void	(*action)(struct softirq_action *); //函数指针名为action,其中参数类型为一个
};
其实这个函数指针就是指向该软中断的处理函数。

当我们需要使用软中断时,首先要想Linux内核注册软中断,注册的内容包括软中断的类型和软中断处理函数指针。所以这也就知道了为什么说软中断由一个softirq_action结构体表示。因为软中断最为核心的地方就是软中断处理函数。
在驱动程序中,如果我们要想注册软中断,我们需要使用open_softirq函数
void open_softirq(int nr, void (*action)(struct softirq_action *))
{
	softirq_vec[nr].action = action; //指定软中断处理函数指针。
}

我们可以发现,这里出现了一个softirq_vec数组,该数组类型当然是softirq_action类型的了,数组的下标表示了不同的软中断类型。下标越小,软中断的优先级越高。下面是不同的下标表示不同的类型的软中断,其用一个枚举来表示。

enum
{
	HI_SOFTIRQ=0, //优先级最高的软中断,用于tasklet
	TIMER_SOFTIRQ,
	NET_TX_SOFTIRQ, //发送网络数据的软中断
	NET_RX_SOFTIRQ,
	BLOCK_SOFTIRQ,
	BLOCK_IOPOLL_SOFTIRQ,
	TASKLET_SOFTIRQ, //tasklet软中断
	SCHED_SOFTIRQ,
	HRTIMER_SOFTIRQ,
	RCU_SOFTIRQ,    /* Preferable RCU should always be the last softirq */

	NR_SOFTIRQS   //该枚举值就是当前Linux内核允许注册的最大软中断数
};
这里需要注意的是softirq_vec对于整个Linux内核是全局的,所以任何一个软中断的处理程序都是全局唯一的,即softirq_vec[nr].action = action中一旦指定了其软中断处理程序的方法,则这个类型的软中断的处理程序就确定了。
当我们编写了含有softirq_action参数的软中断处理程序,并且通过open_softirq函数注册完之后(open_softirq函数的功能其实很简单,就是根据nr指定的软中断类型定位当前驱动的softirq_action数组的相应元素,然后将action指定的软中断处理函数的指针赋给softirq_vec[nr].action,注意这里的softirq_vec是全局的。),我们就可以使用软中断了,一般怎么使用软中断呢?
前面我们知道,如果在还没有开启Ksoftirq线程的情况下,我们会在中断顶半部的do_IRQ中调用irq_exit函数,而该函数会唤醒软中断,即会执行do_softirq函数,所以归根结底,要想执行软中断,必须得要执行do_IRQ函数,但是是不是只要我们在调用do_IRQ函数之前,注册了软中断函数,我们的软中断函数就会被执行了呢?答案当然是否定的。因为如果这样的话,那每一次中断结束时,所有的软中断处理程序都得执行了,不管这个软中断处理程序是否跟这个中断线有关。所以呀,要想do_softirq函数只执行我们想要的软中断处理程序的话,应该需要用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函数真正调用的是raise_softirq_irqoff(nr)函数,同时从这里我们也知道了,一个软中断类型,只能对应一个软中断处理程序,而若要使用tasklet类型的软中断的话,就必须要执行raise_softirq_irqoff(TASKLET_SOFTIRQ)函数。
相应类型的软中断被挂起之后,下面我们就来看看Linux是如何执行它的吧,通过上面我给出的那个软中断的模型图,我们可以知道执行软中断的函数是do_softirq函数,这个函数是执行一切软中断处理程序的根本,也就是说任何一个软中断处理程序都应该经过do_softirq函数调用的,好了我们现在来看看do_softirq的庐山真面目吧
asmlinkage void do_softirq(void)
{
	__u32 pending;
	unsigned long flags;

	if (in_interrupt())
		return;

	local_irq_save(flags);

	pending = local_softirq_pending(); //再获取pending标志位,是否有软中断处理程序被raise

	if (pending)
		__do_softirq(); //如果有,则执行_do_softirq函数

	local_irq_restore(flags);
}
所以do_softirq函数真正起作用的是_do_softirq函数。
asmlinkage void __do_softirq(void)
{
	struct softirq_action *h;
	__u32 pending;
	int max_restart = MAX_SOFTIRQ_RESTART;
	int cpu;

	pending = local_softirq_pending();
	account_system_vtime(current);

	__local_bh_disable((unsigned long)__builtin_return_address(0),
				SOFTIRQ_OFFSET);
	lockdep_softirq_enter();

	cpu = smp_processor_id();
restart:
	/* Reset the pending bitmask before enabling irqs */
	set_softirq_pending(0);

	local_irq_enable();

	h = softirq_vec;

	do {
		if (pending & 1) {    //将pending不同的为与1相"&",来确定哪种类型的软中断被挂起了
			unsigned int vec_nr = h - softirq_vec; //获取softirq_vec数组的下标值,该下标值也就确定了软中断属于什么类型了
			int prev_count = preempt_count();

			kstat_incr_softirqs_this_cpu(vec_nr);

			trace_softirq_entry(vec_nr);
			h->action(h);  //在这里执行软中断的处理函数action
			trace_softirq_exit(vec_nr);  //执行完该软中断处理程序之后,就应该将挂起的标志位重新置0,将相应的_softirq_pending
			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() = prev_count;
			}

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

	local_irq_disable();

	pending = local_softirq_pending();
	if (pending && --max_restart)
		goto restart;

	if (pending)  //重新获取的pending,如果其不为0,说明又有新的软中断处理程序被挂起,如果待处理的软中断程序过多,就应该开启Ksoftirq线程。从而达到延时目的
		wakeup_softirqd(); //开启Ksoftirq线程(软中断处理线程),即将Ksoftirq线程加入至可运行队列

	lockdep_softirq_exit();

	account_system_vtime(current);
	__local_bh_enable(SOFTIRQ_OFFSET);
}
关于Ksoftirq线程后面会详细讲解。到此软中断的工作原理就全部讲解完了,理解了软中断的机制,再去理解tasklet就容易很多了。


tasklet微线程

软中断是将操作推迟到将来某一个时刻执行的最有效的方法。由于该延迟机制处理复杂,多个处理器可以同时并且独立得处理(即do_softirq函数可以被多个CPU同时执行),并且一个软中断的处理程序可以在多个CPU上同时执行,因此处理程序必须要被设计为完全可重入和线程安全的。此外临界区必须用自旋锁保护。由于软中断因为这些原因就显得太过于麻烦,因此引入tasklet机制,就变得很有必要了。tasklet是基于软中断实现的,在我们上面讲软中断的时候知道,tasklet确切的说应该是软中断的一个类型,所以根据软中断的性质,一个软中断类型对应一个软中断处理程序action。同理,也可以推出tasklet也会对应于一个唯一的action。可能讲到这里会有读者觉得,既然一个tasklet类型的软中断只对应一个软中断处理程序,那么我可能在一个驱动程序中使用多个tasklet怎么办?或者是有多个驱动程序里面都要使用tasklet又怎么办?要回答这个问题,我们就要了解tasklet另一个重要的性质。那就是,每一个CPU都会有自己独立的tasklet队列,虽然一个tasklet类型的软中断只对应一个action处理程序,但是我们可以在该处理程序中轮询执行一个tasklet队列,队列里面的每一个tasklet_struct都会对应一个tasklet处理函数,这样当我们的驱动程序中需要使用到tasklet的时候,只要往这个tasklet队列加入我们自定义的tasklet_struct对象就可以了。同时,由于每一个CPU都会有一个tasklet队列,并且每一个CPU只会执行自己tasklet队列里面的tasklet_struct对象,因此tasklet并不需要自旋锁的保护(当然这只能是对同一个tasklet而言,如果多个不同的tasklet需要使用同一资源的话,仍需要自旋锁的保护,后面了解了tasklet机制之后就会明白这一点),因此这样就降低了对tasklet处理函数的要求。
对于一个tasklet对象是通过一个tasklet_struct结构体来描述的,该结构体定义在include\linux\interrupt.h文件中
struct tasklet_struct
{
	struct tasklet_struct *next; //链接下一个tasklet_struct对象,以构成一个tasklet队列
	unsigned long state;  //该tasklet的运行状态标志位
	atomic_t count;     //该tasklet被引用的次数标志位,当count为0时,表示已激活可用
	void (*func)(unsigned long); //该tasklet的处理函数指针,也是tasklet的核心所在
	unsigned long data;   //给上面的处理函数传的参数。
};
下面就给出一个tasklet的队列示意图
理解这个tasklet队列非常有用,这样我们就可以充分理解tasklet的工作机制了。从上面这个队列,我们可以看到这个队列的头是一个名叫tasklet_vec的tasklet_head结构体,我们来看看tasklet_head结构体体
struct tasklet_head
{
	struct tasklet_struct *head;
	struct tasklet_struct **tail;
};
可以看到,里面有两个元素,第一个是一个tasklet_struct结构体指针,在tasklet_vec中,这个用来指向与其最近的一个tasklet_struct结构体。另外一个是一个tasklet_struct类型的指针的指针,干嘛用的呢,从上图我们可以看到,它指向的是tasklet队列最后一个tasklet_struct的next指针的地址。这样一个tasklet_head就可以维护一个tasklet队列了。这里需要注意的是,当这个tasklet列表没有一个tasklet_struct元素时,它的指向是这样的:
通过这两个示意图,我们也知道了如何往该队列增加tasklet_struct对象了,那就是将最后一个tasklet_sturct的next指针指向新加的tasklet_struct对象,同时将列表头的tail指针的指针指向新加的tasklet_struct结构体的next指针的地址。下面我们看看具体添加的代码:
tasklet_struct * t;
* _get_cpu_var(tasklet_vec).tail = t;
_get_cpu_var(tasklet_vec).tail = &(t->next);



初学者在第一次看到段代码时,一般都看不太懂,即便是已经看懂了我上面话的tasklet队列示意图。其实这段代码的目的非常简单,就是将新的tasklet_struct指针t加入tasklet队列,需要注意的是_get_cpu_var(变量名)的作用是获取独属于该CPU的tasklet_vec变量。要看懂这两行代码需要有较强的C语言功底,尤其是要对指针的指针理解深刻。
首先我们来看第一行代码:
*_get_cpu_var(tasklet_vec).tail = t;
将t所指向的地址赋给指针的指针tail,我们知道 *tail 表示的是其指向指针所指向的地址(在这里有*tail == head 或者是 *tail == tasklet_struct ->next),而对于tail我们知道,其有两种情况,第一种是指向最后一个tasklet_struct对象的next指针的地址,因此如果我们执行了 *_get_cpu_var(tasklet_vec).tail = t;就表示了最后一个tasklet_struct对象的next的指向也发生变化了,即next指向了新的tasklet_struct;如果是第二种情况的话,即tail指向的是head指针的地址,那么也可以知道执行 *_get_cpu_var(tasklet_vec).tail = t;之后,head便会指向新的tasklet_struct结构体t。
第二行代码:
_get_cpu_var(tasklet_vec).tail = &(t->next);
这行代码很简单,就是将tail指针指向新的tasklet_struct的next地址。

了解了上面的添加tasklet_struct对象之后,下面我们就可以分析tasklet的工作调用机制了。我们知道每一个软中断类型都会对应一个action(软中断处理程序),所以tasklet类型的软中断同样也有其唯一对应的action(一般其他类型软中断的action都是由用户自己编写,但是tasklet不一样,Linux设计师已经帮我们实现了。所以也是因为这样,tasklet被广泛应用于驱动程序中。)
static void tasklet_action(struct softirq_action *a)
{
	struct tasklet_struct *list;

	local_irq_disable();  //禁止本地中断
	list = __this_cpu_read(tasklet_vec.head); //获取本地中断的tasklet_vec.head指针的指向
	__this_cpu_write(tasklet_vec.head, NULL);  //将tasklet_vec.head赋值为null
	__this_cpu_write(tasklet_vec.tail, &__get_cpu_var(tasklet_vec).head);  //将tasklet_vec.tail赋值为head的地址
	local_irq_enable();

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

		list = list->next;

		if (tasklet_trylock(t)) { //主要是判断该tasklet是否处于run状态,如果处于run状态的话,就从新将其放入tasklet_vec队列中
			if (!atomic_read(&t->count)) {
				if (!test_and_clear_bit(TASKLET_STATE_SCHED, &t->state))
					BUG();
				t->func(t->data); //执行tasklet的处理函数
				tasklet_unlock(t);
				continue;
			}
			tasklet_unlock(t);
		}

		local_irq_disable();
		t->next = NULL;
		*__this_cpu_read(tasklet_vec.tail) = t;  //如果tasklet正在被其他CPU运行,那么就将该tasklet重新装入队列现在再来看这两行代码就应该熟悉了吧
		__this_cpu_write(tasklet_vec.tail, &(t->next));
		__raise_softirq_irqoff(TASKLET_SOFTIRQ); //将tasklet挂起,等待下一次调用do_softirq函数的时候,这些加入tasklet队列的tasklet_struct对象就会被执行。
		local_irq_enable();
	}
}

接下来我们再来看tasklet一个非常重要的函数,就是tasklet_schedule,这个函数通常用于中断处理程序中,用于将tasklet_struct加入所在CPU的tasklet队列,同时将tasklet软中断挂起。因为我们知道,在中断的上半部中的irq_exit函数中,会激活do_softirq函数,所以在中断处理程序中使用tasklet_schedule函数就显得特别必要。下面我们来看一下tasklet_schedule函数的源码:
static inline void tasklet_schedule(struct tasklet_struct *t)
{
	if (!test_and_set_bit(TASKLET_STATE_SCHED, &t->state))
		__tasklet_schedule(t); //调用_tasklet_schedule函数
}
这里要说明一下test_and_set_bit(TASKLET_STATE_SCHED, &t->state)函数,这个函数的目的是首先判断t->state是否是TASKLET_STATE_SCHED,如果是就返回TASKLET_STATE_SCHED,如果不是则将t->state设置为TASKLET_STATE_SCHED,同时返回t->state原来的值。所以这里要执行if后面的代码,就必须要t->state原来的值为0,即该tasklet是一个全新的tasklet,没有被用过。我们再来看看_tasklet_schedule函数:
void __tasklet_schedule(struct tasklet_struct *t)
{
	unsigned long flags;

	local_irq_save(flags); //禁止本地中断,因为tasklet_vec是本地CPU的公共资源,在一个程序正在使用时,肯定不能被其他程序同时使用,这样被导致安全问题。
	t->next = NULL;
	*__this_cpu_read(tasklet_vec.tail) = t;
	__this_cpu_write(tasklet_vec.tail, &(t->next));  //这两行代码很熟悉吧
	raise_softirq_irqoff(TASKLET_SOFTIRQ);  //后面当然也很熟悉
	local_irq_restore(flags); //恢复本地中断
}

在我们驱动程序中若要使用tasklet,首先我们还必须要创建一个tasklet_struct对象,通常创建tasklet_struct对象一共有两种方式;
一种是静态的创建方式,采用define这种预编译的方式,这里可以使用一个库预编译命令:
DECLARE_TASKLET(name, func, data)    //count = 0;处于激活状态
DECLARE_TASKLET_DISABLED(name, func, data) //count = 1;处于未激活状态
第二种是动态创建方式:
static struct tasklet_struct my_tasklet;
tasklet_init(&my_tasklet, tasklet_handler, 0); //count = 0,处于激活状态。
其中tasklet_init函数也是系统函数,可以直接使用的
void tasklet_init(struct tasklet_struct *t,
		  void (*func)(unsigned long), unsigned long data)
{
	t->next = NULL;
	t->state = 0;
	atomic_set(&t->count, 0);
	t->func = func;
	t->data = data;
}









  • 3
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值