中断与定时器

时间:2022-07-03 23:28:28

本章主要讲解Linux设备驱动编程中中断与定时器处理。由于中断服务程序的执行不存在于进程上下文中,所以要求中断服务程序的时间要尽量短。因此Linux在中断处理中引入了顶半部和底半部分离的机制。另外,内核对时钟的处理也采用中断方式,而内核软件定时器最终依赖于时钟中断。

中断和定时器

所谓中断就是程序执行的正常的时候,出现了突发事件,CPU停止当前的程序的执行,转去处理突发事件,处理完毕后又返回原程序被中断的位置继续执行。
中断可以被分为内部中断和外部中断,内部中断的来源来自CPU内部(软件中断指令,溢出,除法错误等,例如操作系统从用户态切换到内核态需借助CPU内部的软件中断),外部中断的中断源来自CPU外部,由外设提出请求。
根据中断入口跳转的方式不同,中断可分为向量中断和非向量中断。采用向量中断的CPU通常为不同的中断分配不同的中断号,当检测到某中断号对应的中断到来的时候,就自动跳转到该中断号对应的地址执行
非向量中断的多个中断共享一个入口地址,进入该入口地址之后,在通过软件判断中断标志来识别具体是哪个中断
一个典型的非向量中断服务程序如下所示:
先判断中断源,然后调用不同的中断源的服务程序

irq_handle()
{
...
int int_src = read_int_status(); //该硬件的中断相关寄存器
switch(int_src) {
case DEV_A:
dev_a_handler();
break;
case DEV_B:
dev_b_handler();
break;
defalut:
break;
}
...
}

Linux中断处理程序架构

我们希望是中断发生之后转而执行的中断服务程序尽量短小精悍,但是实际往往耗时较长。为了在中断服务程序执行时间短和中断服务程序实际上处理的任务量大两者之间找到平衡点,Linux将中断处理程序分为两个部分:顶半部和底半部。
顶半部:用来完成尽量少的比较紧急的功能。往往只是简单的读取寄存器的中断状态,并在清除中断标记后就进行登记中断的工作,“登记中断”意味着将底半部处理程序挂到该设备的底半部执行队列中。这样顶半部的执行速度就可以很快,从而可以服务更好的中断请求。
底半部:底半部几乎做了中断处理的绝大多数事情,而且可以被新的中断打断,这也是底半部和顶半部最大的不同,因为顶半部往往是不可中断的,底半部的耗时时间相对长,不在硬件中断服务程序中执行
如果中断服务程序耗时少,则可以不需要底半部,直接在顶半部就把中断处理完成。

Linux中断编程

在Linux的设备驱动中,使用中断的设备需要申请和释放对应的中断,并分别使用内核提供的request_irq和free_irq

申请irq

int request_irq(unsigned int irq, irq_handler_t handler, unsigned long flag, const char* name, void *dev);
irq是需要申请的硬件中断号
handler是向系统登记的中断处理函数(顶半部),是一个回调函数,中断发生时,系统调用这个函数,dev参数将被传递给它
flag是中断处理的属性,可以指定中断的触发方式以及处理方式。
dev是传递给中断服务程序的私有数据,一般是这个设备的设备结构体或者NULL。
request_irq()返回0表示成功,返回-EINVAL表示中断号无效或处理函数指针为NULL,返回-EBUSY表示中断已经被占用且不能共享
与该函数类似的还有一个devm_request_irq它申请的是内核的资源一般不需要出错处理和显示的释放

释放irq

与request_irq相对应的函数为free_irq

void free_irq(unsigned int irq, void *dev)

参数的定义与上面的一致

使能和屏蔽中断

void disable_irq(int irq);
void disable_irq_nosync(int irq);
void enable_irq(int irq);

nosync:代表是异步的,不等待中断处理完成,所以disable_irq_nosync是直接返回的,disable_irq会等待指定的中断被处理完,因此如果在n号中断的顶半部调用disable_irq(n),会引起死锁(因为中断的执行只有当中断的底半部结束才算是被处理完,顶半部就等待该处理结束,是永远等不到的)
之前有个local_irq_disable,这个是屏蔽本CPU内所有的中断。

底半部机制

Linux实现底半部的机制主要有tasklet、工作队列、软中断和线程化irq

tasklet

tasklet的执行上下文是软中断,执行时机通常是顶半部返回的时候。我们只需要定义tasklet及其处理函数,并将两者关联即可 e.g:
void my_tasklet_func(unsigned long) //定义一个处理函数
DECLARE_TASKLET(my_tasklet, my_tasklet_func, data); //函数相关联
代码DECLARE_TASKLET(my_tasklet, my_tasklet_func, data)实现了定义名称为my_tasklet的tasklet。并将其与my_tasklet_func()这个函数绑定,而传入这个函数的参数为data
在需要调度tasklet的时候引用一个tasklet_schedule()函数就能使系统在适当的时候进行调度运行

tasklet_schedule(&my_tasklet)

使用tasklet做为底半部处理中断的设备驱动程序模板如下所示:

//定义tasklet和底半部函数并将它们关联
void xxx_do_tasklet(unsigned long);
DECLARE_TASKLET(xxx_tasklet, xxx_do_tasklet, 0);

//中断处理底半部
void xxx_do_tasklet(unsigned long)
{
...
}

//中断处理顶半部
irqreturn_t xxx_interrupt(int irq, void *dev_id)
{
...
tasklet_schedule(&xxx_tasklet);
...
}

//设备驱动加载模块
int __init xxx__init(void)
{
...
//申请中断
result = request_irq(xxx_irq, xxx_interrupt, 0, "xxx", NULL);
...
return IRQ_HANDLED;
}

//设备驱动模块卸载函数
void __exit xxx_exit(void)\
{
...
//释放中断
free_irq(xxx_irq, xxx_interrupt);
}

从上述程序中可以看出:在模块加载函数中申请中断,并在模块卸载函数中释放中断,xxx_irq中断号的中断处理函数是xxx_interrupt,在该函数里面调用tasklet_schedule(&xxx_tasklet),该函数会在适当的时间调度xxx_tasklet的关联函数xxx_do_tasklet进行处理。

工作队列

工作队列的使用方法与tasklet相似,但是工作队列的执行上下文是内核线程,因此可以调度和睡眠。下面的代码用于定义一个工作队列和一个底半部执行函数。
struct work_struct my_wq; //定义一个工作队列
void my_wq_func(struct work_sturct *work) //定义一个处理函数
通过INIT_WORK可以初始化这个工作队列并将工作队列和处理函数绑定:
INIT_WORK(&my_wq, my_wq_func);
与tasklet_schedule对应的调度工作队列执行的函数schedule_work(),如:
schedule_work(&my_wq);

软中断

软中断是一种传统的底半部处理机制,它的执行时机通常是顶半部返回的时候,tasklet是基于软中断实现的,因此也运行于软中断上下文。
在Linux内核中使用softirq_action结构体表征一个软中断,这个结构体包含软中断处理函数指针和传递给该函数的参数。使用open_softing函数可以注册软中断对应的处理函数,而raise_softing函数可以触发一个软中断。
软中断和tasklet运行于软中断上下文,仍然属于原子上下文的一种,而工作队列则运行于进程上下文。因此,在软中断和tasklet处理函数中不允许睡眠,而在工作队列处理函数中允许睡眠。
local_bh_disable和local_bh_enable是内核中用于禁止和使能软中断以及tasklet底半部机制的函数
一般来说,驱动的编写者不会也不宜直接使用softirq

硬中断、软中断、信号

硬中断是外部设备对CPU的中断,软中断是中断底半部的一种处理机制,信号则是由内核或者其他进程对某个进程的中断。在涉及系统调用的场合,人们也常说通过软中断陷入内核,此时软中断的概念是指由软件指令引发的中断。和上一个软中断是不同的概念

threaded_irq(线程中断)

在内核中,除了可以通过request_irq和devm_request_irq申请中断以外,还可以用request_threaded_irq和devm_request_threaded_irq申请。
这两个函数相比而言,多了一个参数thread_fn。用这两个API申请中断的时候,内核会为相应的中断号分配一个内核线程。这个线程只对应这个中断号

中断共享

多个设备共享一根硬件中断线的情况在实际的硬盘系统中广泛的存在,Linux支持这种中断共享
1. 共享中断的多个设备在申请中断的时候,都应该使用IRQF_SHARED标志,而且一个设备以IRQF_SHARED标志申请某中断成功的 前提是该中断未被申请或者虽然被申请了,但是申请该中断的所有设备也都以IRQF_SHARED标志申请该中断
2. 尽管内核模块可访问的全局地址都可以作为request_irq中的void* dev_id这个参数,但是设备结构体指针显示是可传入的最佳参数
3. 在中断到来时,会遍历执行该中断所有的中断处理程序,知道某一个函数返回IRQ_HANDLED。而在中断处理程序的顶半部,应根据硬件寄存器中的信息比照传入dev_id参数迅速的判断是否为本设备的中断、若不是,则迅速返回IRQ_NONE
中断共享的代码模板:
//中断处理顶半部
irqreturn_t xxx_interrupt(int irq, void *dev_id)
{

int status = read_int_status(); //获取中断源
if (! is_myint(dev_id, status)) //判断是否为本设备的中断
return IRQ_NONE; //不是本设备中断,立即返回
//是本设备中断,进行处理

return IRQ_HANDLED; //返回IRQ_HANDLED表明中断已经被处理
}

//设备驱动模块加载函数
int xxx_init(void)
{

//申请共享中断
result = request_irq(sh_irq, xxx_interrupt, IRQF_SHARED, “xxx”, xxx_dev);
….
}

//设备驱动卸载模块
void xxx_exit(void)
{

//释放中断
free_irq(xxx_irq, xxx_interrupt);

}

内核定时器

内核定时器编程

软件意义上的定时器最终依赖硬件定时器来实现,内核在时钟中断发生后检测各定时器是否到期,到期后的定时器处理函数将作为软中断在底半部执行。实质上,时钟中断处理程序会换起TIMER_SOFTIRQ软中断,运行当前处理器上到期的所有定时器。
在Linux设备驱动编程中,可以利用Linux内核中提供的一组函数和数据结构来完成定时触发工作或者完成某种周期性的事务。这组函数和数据结构使得驱动程序师在多数情况下不用关心具体的软件定时器究竟对应着怎样的内核和硬件行为。
1.time_list
在Linux内核中,timer_list结构体的一个实例对应一个定时器,e.g:

struct timer_list {
struct list_head entry;
unsigned long expire;// 定时器到期的时间
struct tvec_base *base;

void (*function) (unsigned long); //定时器期满了之后,该function将被执行
unsigned long data; //data成员是传入其中的参数

int slack;
#ifdef CONFIG_TIMER_STATS
int start_pid;
void *start_site;
char start_comm[16];
#endif
#ifdef CONFIG_LOCKDEP
struct lockdep_map lockdep_map;
#endif
};

2.初始化定时器
init_timer是一个宏,它的原型等价于:
void init_timer(struct timer_list *timer);
上述的init_timer函数初始化timer_list的entry的next为NULL,并给base指针赋初值。
TIMER_INITIALIZER(_function, _expires, _data)宏用于赋值定时器结构体的function,expire,data,base成员,等价于:

#define TIMER_INITIALIZER(_function, _expires, _data) {   \
.entry = {.prev = TIMER_ENTRY_STATIC}, \
.function = {_function}, \
.expire = {_expire}, \
.data = {_data}, \
.base = &boot_tvec_bases, \
}

DEFINE_TIMER(_name, _function, _expire, _data)宏是定义并初始化定时器成员的“快捷方式”,这个宏定义为:

#define DEFINE_TIMER(_name, _function, _expire, _data) \
struct timer_list _name = \
TIMER_INITIALIZER(_function, _expires, _data)

另外setup_timer()也可用于初始化定时器并赋值其成员,其源代码:

#define __setup_timer(_timer, _fn, _data, _flags)   \
do { \
__init_timer((_timer), (_flags)); \
(_timer)->function = (_fn); \
(_timer)->data = (_data);
}while(0)

3.增加定时器
。。。。

内核中延迟的工作delayed_work

利用工作队列和定时器完成了delayed_work,结构体如下:

struct delayed_work {
struct work_struct work;
strcut timer_list timer;

//target work_queue
struct workqueue_struct *wq;
int cpu;
};

通过如下函数调度一个delayed_work在指定的延时后执行
int schedule_delayed_work(struct delayed_work *work, unsigned long delay);
当指定的delay到来时,delayed_work结构体中的work成员work_func_t类型成员func()会被执行。work_func_t类型定义为:

typedef void(*work_func_t) (struct work_struct *work);

delay参数的单位是jiffies(自系统启动以来产生的节拍总数,每次时钟中断处理程序都会增加该值),因此常见的用法是:

schedule_delayed_work(&work, msecs_to_jiffies(poll_interval));
msecs_to_jiffies:将毫秒转化为jiffies

如果要周期性的执行任务,通常会在delayed_work的工作函数中再次调用schedule_delayed_work(),周而复始。
使用如下的函数来取消delayed_work:
int cancel_delayed_work(struct delayed_work *work);
int cancel_delayed_work_sync(struct delayed_work *work);

内核延时

内核延时主要分为短延迟、长延迟、睡着延迟。
短延迟:纳秒、微秒和毫秒延迟
。。。

总结

Linux的中断处理分为两个半部,顶半部处理紧急的硬件操作,底半部处理不紧急的耗时操作。
Tasklet和工作队列都是调度中断底半部的良好机制,tasklet和内核定时器都是基于软中断实现。

内核中的延时可以采用忙等待或睡眠等待,为了充分利用CPU资源,使系统有更好的吞吐性能,在对延迟时间的要求不是很精确的情况下,睡眠等待通常是值得推荐的。