中断和事件管理
一、中断
-
中断进入过程
- 为了提高外部事件处理的实时性,显示的处理器几乎都含有中断控制器,外设也都带有中断触发的功能。为了支持这一特性,Linux中设计了一个中断子系统来管理系统中的中断。
- 很多处理器都有中断控制器,它负责进行中断管理。下面讨论一下,当中断发生了,如何调用驱动中的中断处理函数。
- 一般在我们会事先将异常向量表写好,并标记异常向量表的起始地址,在内核启动的过程中会将异常向量表搬移到0xFFFF0000的位置,通过设置处理器的先关寄存器可以对异常向量表进行重映射当不同的中断发生后,程序会跳转到之前中断向量表的相应的处理函数的位置,去执行该处代码。
- 在中断处理函数执行完后,将进行一些恢复现场的操作。也就是要恢复到中断执行前程序的状态。
-
驱动中的中断处理
前面分析了中断的处理过程,下面来看一下如何在驱动中支持中断。
- 构造struct irpaction的结构体对象,并根据IRQ号加入到对应的链表中。我们可以调用如下的内核提供的API接口。
int request_irq(unsigned int irq,irq_handler handler,unsigned long flags,const char *name,void *dev); @irq :设备上所使用的IRQ号,这个号不是硬件手册上查到的号,而是内核中的IRQ号,这个号将会用于决定构造的struct irqaction对象被插入哪个链表,并用于初始化strut irqaction对象中的handler成员。 @handler :指向中断处理函数的指针,类型定义如下 irqreturn_t (*irq_handler_t)(int ,void*) @中断处理函数返回枚举类型的为irqreturn_t枚举值例举,如 IRQ_NONE: 不是驱动所管理设备产生的中断,用于共享中断 IRQ_HANDLED: 中断被正常处理 IRQ_WAKE_THREAD: 需要唤醒一个内核线程 @flag : 与中断相关的标志位,用于初始化struct irqaction对象中的flag成员,常用的标志如下,这些标志可以用位或的方式来设置多个。 IRQF_TRIGGER_RISING: 上升沿触发 IRQF_TRIGGER_FALLING: 下降沿触发 IRQF_TRIGGER_HIGH: 高电平触发 IRQF_TRIGGER_LOW: 低电平触发 IRQF_DISABLED: 中断函数执行期间禁止中断,将会被放弃 IRQF_SHARED: 共享中断必须设置的标志 IRQF_TIMER: 定时器专用的中断标志 @name: 该中断在/proc中的名字,用于初始阿虎struct irqaction对象中name成员。 @dev: 区别共享中断的不同设备所对应的struct irqaction对象,在struct irqaction对象从链表中移除时需要。 返回值:request_irq函数成功返回0,失败返回负值。 说明:该函数为我们构造好了一个struct irqaction对象,并加入到对应的链表后,还将对应的中断使能了。
- 注销一个中断处理函数
void free_irq(unsigned int,void *); @参数一:IRQ号 @参数二:dev_id,共享中断必须要传递一个非NULL的实参,和request_irq中的dev_id保持一致。
- 除了这些外,内核还提供了关于中断使能和禁止的函数或宏,这些函数不常用到。
local_irq_enable(): 使能本地CPU的中断 local_irq_disable(): 禁止本地CPU中断 local_irq_save(flags): 使能本地CPU中断,并将之前的中断使能状态保存在flags中。 local_irq_restore(flags): 用flags中的中断使能状态恢复中断使能标志。 void enable_irq(unsigned int irq): 使能irq指定的中断。 void disable_irq(unsigned int irq): 同步禁止irq指定的中断,即要等到irq上所有中断处理程序执行完成后才能禁止中断。很显然,在中断处理函数中不能调用。 void disable_irq_nosync(unsigned irq): 立即禁止irq指定的中断。
- 中断处理函数应该快速完成,不能消耗太长时间。因为整个中断处理的过程中,中断是禁止的,如果中断处理函数执行的时间过长,那么其他的中断将会被挂起。从而将会对其他中断的响应造成严重影响,进而影响系统性能。
- 另外我们需要记住,在中断处理函数中一定不能调用调度器,即一定不能调用可能会引起线程切换的函数。(因为一旦中断处理程序被切换,将不能再次被调度)这是内核对中断处理函数的一个严格限制。比如我们常见的copy_from_user、copy_to_user等就会引起进程切换。
二、中断下半部
- 前面我们提到了,中断处理函数应该尽快完成,否则将会影响对其他中断的及时响应,从而影响整个系统的性能。但有时候这些耗时的操作可能又避免不了。以网卡为例,当网卡在收到一个数据时会产生一个中断,在中断处理程序中需要将数据从网卡的缓存中复制出来,然后对数据包做严格的检查,检查完后再根据协议对数据包做拆包处理,最后将拆包后的数据包递交给上层。如果在这个过程中又收到新的数据,从而再次产生中断,因为上一个中断处理正在处理的过程中,所以新的中断将会被挂起,新收到的数据就得不到及时处理,因为网卡的缓冲大小有限,如果后面有更多数据包到来那么缓缓从去最终会溢出,从而产生丢包。那么发生这种情况该怎么办呢?
- Linux将中断分成了两部分,上半部和下半部(顶半部和底半部)。他们分别用以完成以下工作:
- 上半部:完成紧急但能很快完成的事情
- 下半部:完成不紧急但比较耗时操作
- 在以上面讲到的网卡的例子,中断上半部主要完成将数据从网卡的缓存复制到内存中,就是紧急但是可以快速完成的事情,需要放到上半部执行。下半部完成对包进行校验、拆解等不紧急但是比较耗时的操作。
- 另外需要注意:下半部在执行的过程中,中断被重新使能,所以如果有新的硬件中断产生,将会停止执行下半部的程序,转为执行硬件中断的上半部这样就避免了中断响应不及时的问题。
1. 软中断
下半部虽然可以推迟执行,但是我们还是希望它能尽快执行。那么什么时候可以尝试执行下半部呢?肯定是在上半部执行完成之后。也就是说中断下半部最早的执行时间是中断上啊不能不执行完成之后,但是中断还没有完全返回之前的时候。
软中断是中断下半部机制中的一种,描述软中断的结构是struct soft action,它的定义非常简单,就是内嵌了一个函数指针。内核共定义了NR_SOFTIROS(目前共十个)个,struct softirq_action对象。内核有一个整型的全局整型变量来记录是否有相应的软中断需要执行。
在软中断的执行过程中可以响应新的硬件中断,这就是说软中断适合被用来实现中断下半部机制。
虽然软中断可以被用来实现中断下半部的机制,但是软中断基本上是内核开发者预定义好的,通常用在对性能要求特别高的场合,而且需要一些内核的编程技巧,不太适合驱动开发者。
2. tasklet
前面提到,软中断通常是由内核开发者来设计的,但是内核开发者专门保留了软中断给驱动开发者,它就是TASKLET_SOFTIRQ,相应的软中断处理函数式tasklet_action。以2.6版的kernel为例,在/kernel/softirq.c/中摘录如下代码
static void tasklet_action(struct softirq_action *a)
{
struct tasklet_struct *list;
...
list = __this_cpu_read(tasklet_vec.head);
...
while(list)
{
struct tasklet_struct *t = list;
list = list->next;
...
t->func(t->data);
}
}
也就是在软中断的处理过程中,如果TASKLET_SOFTIRQ对应的比特位被设置了,则根据前面的分析,tasklet_action函数将会被调用。在代码的第5行,首先得到了一个struct tasklet_struct对象的链表,然后遍历该链表,调用其中func成员所指向的函数,并将data成员作为作为参数传递过去。data是传递给下半部函数的参数。
从而可知我们要想struct tasklet_struct结构对象,并初始化里面的成员,然后放入对应CPU的tasklet链表中,最后设置软中断号TASKLET_SOFTIRQ所对应的比特位。
/*include/linux/interrupt.h*/
struct tasklet_struct
{
struct tasklet tasklet_struct *next;
unsigned long state;
atomic_t count;
void (*func)(unsigned long);
unsigned long data;
};
对tasklet做一个总结:
- tasklet是一个特定的软中断,处于中断上下文
- tasklet_schedule函数被调用后,对应的下半部会保证被至少执行一次。
- 如果一个tasklet已经被调度,但是还没有被执行,那么新的调度将会被忽略。
3. 工作队列
前面我们讲到的中断下半部的机制,如软中断和tasklet都有一个限制,就是在中断上下文中执行不能直接或间接地调用调度器。为了解决这个问题,内核有提供了另外一种下半部机制,叫做工作队列。
工作队列:在内核启动的时候创建一个或多个(多核处理器)内核工作线程,工作线程取出工作队列中的每一个工作,然后执行,当队列中没有工作时,工作线程休眠。
工作流程:当驱动想要延迟执行某一个工作时,构造一个工作队列节点对象,然后加入到相应的工作队列,并唤醒工作线程,工作线程又取出工作队列上的节点来完成工作,所有工作玩成后又休眠。因为运行在进程上下文中,所以工作队列可以调用调度器。工作队列提供了一种延迟执行的机制,很显然这种机制也适用于终端下半部。
除了内核本身的工作队列,我们也可以使用内核的基础设施来创建自己的工作队列。下面是工作队列节点的结构类型定义:
/*include/linux/workqueue.h*/
struct work_struct{
stomic_long_t data;
struct list_head entry;
work_func_t func;
};
@data: 传递给工作的队列的参数,通常是整数,但更常用指针。
@entry: 构成工作队列的链表节点对象
@func: 工作函数,工作线程取出工作队列节点后执行,data会作为调用该函数的参数。
工作队列的总结:
- 工作队列的工作函数运行在进程上下文,可以调度调度器。
- 如果上一个工作还没有完成,有重新调度下一个工作,那么新的工作将不会被调度。
4. 延时控制
在硬件的操作中经常要用到延时,比如要保持芯片的复位时间持续多久、芯片上电时序控制等。为此内核提供了一组延时操作函数。
内核子在启动过程中会计算一个全局loops_per_jiffy的值,该变量反应了一段循环延时的代码要循环多少次才能延时一个jiffy的时间。根据jiffy这个值,就可以知道延时一微妙需要多少个循环,延时一毫秒需要多少个循环。于是内核就定义了一些靠循环来延时的宏或函数。比如:
void ndelay(unsigned long x); //纳秒级延时
udelay(n); //微秒级延时
mdelay(n); //毫秒级延时
这些延时函数都是忙等待延时,是靠白白消耗CPU的时间来获得延时的,如果没有特殊的理由(如中断上下文中获取自旋锁的情况下)不推荐使用这些函数延迟较长的时间。
比较推荐使用休眠延迟,如下:
void msleep(unsigned int msecs); //休眠不可以被信号打断,只能等到休眠时间到了才会返回
long msleep_interuptible(unsigned int msecs); //休眠可以被信号打断
void ssleep(unsigned int seconds); //休眠不可以被信号打断,只能等到休眠时间到了才会返回
5. 定时操作
有时候我们需要在设定的时间到时候自动执行一个操作,这就是定时。定时有分为单次定时和循环定时两种,所谓单次定时就是设定的时间到期之后,操作被执行了一次。而循环定时则是设定的时间到期之后操作被执行,然后再次启动定时器,下一次时间到期后操作被执行,然后再次启动定时器。如此循环反复。目前Linux中有低分辨率定时器和高分辨率定时器。
低分辨率定时器
-
也就经典定时器,其是基于一个硬件定时器,该定时器周期性的产生中断,产生中断的次数可以配置,比如我们可以设定每秒中产生多少个(内核中庸HZ来指定)中断。该定时器自开机以来产生的中断次数会被记录在jiffies全局变量中。
-
要在驱动中实现一个定时器,需要经过一下几个步骤。
- 构造一个定时器对象,调用init_timer来初始化这个对象,并对expires、function和data成员赋值。
struct timer_list{ ... struct list_head entry; unsigned long expires; struct tvec_base *base; void (*function)(unsigned long) unsigned long data; ... } @entry: 双向链表节点的对象,用于构成双向链表。 @ecpires: 定时器到期的jiffies值。 @function: 定时器到期后执行的函数 @data: 传递定时器函数的参数,通常传递一个指针
- 使用add_timer将定时器对象添加到内核的定时器链表中。
inti_timer(timer); //初始化一个定时器 void add_timer(struct timer_list *timer); //将定时器添加到内核中的定时器链表中。 int mod_timer(struct timer_list *timer,unsigned long expires); //修改定时器的expires成员,而不考虑当前定时器的状态 int del_timer(struct timer_list *timer); //从内核链表中删除该定时器,而不考虑当前定时器的状态
- 定时时间到了后,定时器函数自动被调用,如果需要周期定时,那么可以在定时函数中使用mod_timer来修饰expires。
- 在不需要定时器的时候,用del_timer来删除定时器。
内核是在定时器中断的软中断下半部来处理这些定时器的。所以定时器是在中断上下文中执行的。
高分辨率定时器
- 前面讲低分辨率从定时器是以jiffies来定时,所以定时精度受系设定影响。比如,我么设定中断200次为1秒,那么一个jiffy的时间就是5毫秒,也就是说定时器的精度就是5毫秒。
- 而对于对时间要求比较高的设备,这个精度显然是不能满足的,比如声卡。为此,内核中有开发出了义ktime_t来定义时间。类型定义如下:
/*include/linux/ktime.h*/
union ktime{
s64 tv64;
#if BITS_PER_LONG != 64 && !defined(CONFIG_KTIME_SCALAR)
struct{
# ifdef __BIG_ENDIAN
S32 sec,nsec;
# endif
}tv;
#endif
};
typedef union ktime ktime_t;
- ktime是一个共用体,可以看到它是可以精确到纳秒的(nsec)。
- 一般用ktime_set函数来初始化这个对象,常用方法如下:
ktime_t t = ktime_set(secs,nsecs);
- 分辨率定时器的结构类型定义如下。
struct hrtimer{
struct timerqueue_node node;
ktime_t _softexpires;
enum hrtimer_restart (*function)(struct hrtime *);
struct hrtimer_clock_base *base;
unsigned state;
...
};
//其中function为定时到期我们要执行的函数。