根据中断的来源,可分为内部中断和外部中断:内部中断来自CPU内部(软件中断指令,溢出等),外部中断来自CPU外部,有外设提出请求。
也可以分为可屏蔽中断和不可屏蔽中断:可屏蔽中断通过屏蔽字被屏蔽,屏蔽后不再得到相应。
也可以分为向量中断和非向量中断:采用向量中断的CPU通常为不同的中断分配不同的中断号,当检测到中断到来之后,就自动跳到与该中断对应的地址执行,不同的中断号有不同的地址。非向量中断的多个中断共享一个入口地址,进入该入口地址后再通过软件判断中断标志来识别具体是哪个中断。即: 向量中断由硬件提供中断服务程序的入口地址,非向量中断由软件提供中断服务的程序入口地址。
非向量中断服务程序的典型结构:
irq_handler()
{
...
int int_src = read_int_status(); //读硬件的中断相关寄存器
switch(int_src){ //判断中断源
case : DEV_A
dev_a_handler();
break;
case : DEV_A
dev_a_handler();
break;
...
default:
break;
}
...
}
linux中断处理程序架构
设备的中断会打断内核中的进程的正常调度和运行。因此要求中断服务程序尽可能短小。但是往往中断程序不会是很小的,需要大量时间来处理。
为了解决这个矛盾,linux将中断处理分解为两半部:顶半部(top half)和底半部(bottom half)。
顶半部完成紧急的功能,他往往简单的读取寄存器中的中断状态,清除中断标志位,“登记”中断。即:将底半部处理程序挂到该设备的底半部执行队列中。这样顶半部的速度会很快,可以服务更多的中断请求。 中断底半部完成中断绝大部分(几乎所有)的工作。而且中断底半部可以被新的中断打断。
中断顶半部和底半部的区别是:中断顶半部不可以被新的中断打断,而 中断底半部可以被打断,中断顶半部非常紧急,底半部则不是那么紧急,不在硬件中断服务程序中执行。
注:中断顶半部和底半部的结合改变了系统的相应能力,但是不能认为linux设备驱动的中断程序一定分为两部分,如果中断要处理的工作很少,则完全可以直接在顶半部分完成。
linux中断编程
1 申请和释放中断
在linux设备驱动中,使用中断的设备需要申请和释放对应的中断,分别使用内核提供的request_irq()和free_irq()函数
int request_irq(unsigned int irq, irq_handler_t handler,
unsigned long irqflags, const char *devname, void *dev_id);
void free_irq(unsigned int irq, void *dev_id);
2 使能和屏蔽中断
下列函数可以屏蔽一个中断源
void disable_irq(int irq );
void disable_irq_nosync(int irq );
void enable_irq(int irq );
注:如果在N号中断的顶半部调用disable_irq(N),会引起系统的死锁,因此只能调用disable_irq_nosymc(N)
3 底半部机制
linux实现底半部的机制主要有tasklet,工作队列和软中断
tasklet
tasklet使用很简单,我们只需要定义tasklet 及其处理函数并将两者关联,如:
void my_tasklet_func(unsigned long );//定义了一个处理函数
DECLARE_TASKLET(my_tasklet,my_tasklet_func,data) ;
//定义了一个tasklet结构my_tasklet,与my_tasklet_func(data)函数相关联
在需要调度tasklet的时候,引用一个tasklet_schedule()函数就能使系统在适当的时候进行调度运行。tasklet_schedule(&my_tasklet);
工作队列
工作队列的使用方法和tasklet相似,如下:
struct work_struct my_wq();//定义了一个工作队列
void my_wq_func(unsigned long );// 定义了一个处理函数
通过INIT_WORK()可以初始化这个工作队列并将工作队列与处理函数绑定:
INIT_WORK(&my_wq,(void (*)) my_func,NULL);
与tasklet_schedule() 函数对应的用于调度工作队列的函数为schedule_work()
schedule_work(&my_wq);
软中断
软中断是传统的底半部处理机制,他的执行时机通常是顶半部返回的时候,tasklet是基于软中断实现的,因此也运行与软中断的上下文。
在linux中,使用softirq 结构体表征一个软中断,使用open_softirq()函数可以注册中断对应的处理函数,而raise_softirq()函数可以触发一个软中断。
三种机制的关联和区别:
软中断和tasklet运行于软中断的上下文,仍然属于是原子上下文的一种,而工作队列则运行于进程的上下文。因此,软中断和tasklet 处理函数的时候,不能睡眠,而工作队列处理函数则可以睡眠。
硬中断,信号和软中断的区别:
硬中断是外部设备对CPU的中断,软中断是中断底半部的一种处理机制,而信号时由内核(或者其他的进程)对某个进程的中断。
内核定时器
内核定时器编程
软件意义上的定时器最终依赖硬件定时器来实现,内核在时钟中断发生后执行检测定时器是否到期,到期后的定时器处理函数将作为软中断在底半部运行。实质上,时钟中断处理程序会唤起TIMET_SOFTIRQ()软中断,运行当前处理器上到期的所有定时器。
linux内核所提供的用于操作定时器的数据结构和函数如下:
1 timer_list()
在linux中,一个timer_list实例对应一个定时器,如下:
struct timer_list{
struct list_head entry ;//定时器列表
unsigned 龙expires ; // 定时器到期时间
void (*function )(unsigned long );//定时器处理函数
unsigned long data ; //作为参数被传入定时器处理函数
struct timer_base_s *base ;
...
};
定义一个定时器:
struct timer_listmy_timer;
2 初始化定时器
void init_timer(struct timer_list *timer);
3 增加定时器
void add_timer(struct timer_list *timer);
4 删除定时器
int del_timer(struct timer_list *timer);
5 修改定时器的expire
intmod_timer(struct timer_list *timer ,unsigned long expires );
内核延迟
1 短延迟
linux内核提供了如下3个函数分别进行纳秒,微秒,毫秒级延时 (忙等待)
void ndelay(unsigned long nsecs);
void udelay(unsigned long usecs);
void mdelay(unsigned long msecs);
毫秒级的延时已经很大,在内核中,最好不要用mdelay()函数,这将是无谓的耗费CPU资源,对于毫秒级的延时,linux内核提供了如下的函数:
void msleep (unsigned long millisecs);
unsigned long msleep_interruptible (unsigned int millisecs);
void ssleep (unsigned long seconds);
2 长延时
内核中进行延时的一个很直观的方法是比较当前的jiffies 和目标jiffies ,直到未来的jiffies 达到目标jiffies 。
3 睡着延时
睡着延时是比忙等待更好的方式,睡着延时在等待的时间到来之间进程处于睡眠状态,CPU被其他进程使用。
schedule_timeout()可以使当前任务睡眠指定的jiffies之后被重新调度执行,msleep ()和 msleep_interruptible()在本质上都是依靠包含了schedule_timeout()的schedule_timeout_uninterruptible()和schedule_timeout_interruptible() 实现的。实际上,schedule_timeout() 的实现原理是向系统添加一个定时器,在定时器处理函数中唤醒参数对应的进程 。
总结:
linux的中断处理分为两个半部,顶半部处理紧急的硬件操作,底半部处理不紧急的耗时的操作,tasklet和工作队列都是调度中断底半部的良好机制,tasklet基于软中断实现。内核定时器也是依靠软中断实现。
内核的延时可以采用忙等待和睡眠等待,为了充分利用CPU资源,使系统有更好的吞吐性能,在对延迟时间的要求不是很精确的情况下,睡眠等待通常更好,而ndelay,udelay 等忙等待机制在驱动中通常是配合硬件上短时间的延时要求。