中断与时钟
所谓中断是指 CPU 在执行程序的过程中,出现了某些突发事件时 CPU 必须暂停执行当前的程序,转去处理突发事件,处理完毕后 CPU 又返回原程序被中断的位置并继续执行。
根据中断的来源,中断可分为内部中断和外部中断,内部中断的中断源来自 CPU
内部(软件中断指令、溢出、除法错误等,例如,操作系统从用户态切换到内核态需
借助 CPU 内部的软件中断),外部中断的中断源来自 CPU 外部,由外设提出请求。根据是否可以屏蔽中断分为可屏蔽中断与不屏蔽中断(NMI) 可屏蔽中断可以通,过屏蔽字被屏蔽,屏蔽后,该中断不再得到响应,而不屏蔽中断不能被屏蔽。
根据中断入口跳转方法的不同,中断分为向量中断和非向量中断。采用向量中断
的 CPU 通常为不同的中断分配不同的中断号,当检测到某中断号的中断到来后,就
自动跳转到与该中断号对应的地址执行。不同中断号的中断有不同的入口地址。非向量中断的多个中断共享一个入口地址,进入该入口地址后再通过软件判断中断标志来识别具体是哪个中断。也就是说,向量中断由硬件提供中断服务程序入口地址,非向量中断由软件提供中断服务程序入口地址。
Linux 中断处理程序架构
设备的中断会打断内核中进程的正常调度和运行,系统对更高吞吐率的追求势必要求中断服务程序尽可能地短小精悍。但是,这个良好的愿望往往与现实并不吻合。在大多数真实的系统中,当中断到来时,要完成的工作往往并不会是短小的,它可能要进行较大量的耗时处理。为了在中断执行时间尽可能短和中断处理需完成大量工作之间找到一个平衡点,Linux 将中断处理程序分解为两个半部:顶半部(top half)和底半部(bottom half)。
顶半部完成尽可能少的比较紧急的功能,它往往只是简单地读取寄存器中的中断状态并清除中断标志后就进行“登记中断”的工作。“登记中断”意味着将底半部处理程序挂到该设备的底半部执行队列中去。这样,顶半部执行的速度就会很快,可以服务更多的中断请求。
现在,中断处理工作的重心就落在了底半部的头上,它来完成中断事件的绝大多数任务。底半部几乎做了中断处理程序所有的事情,而且可以被新的中断打断,这也是底半部和顶半部的最大不同,因为顶半部往往被设计成不可中断。底半部则相对来说并不是非常紧急的,而且相对比较耗时,不在硬件中断服务程序中执行。尽管顶半部、底半部的结合能够改善系统的响应能力,但是,僵化地认为 Linux设备驱动中的中断处理一定要分两个半部则是不对的。如果中断要处理的工作本身很少,则完全可以直接在顶半部全部完成。
在 Linux 系统中,查看/proc/interrupts 文件可以获得系统中断的统计信息
Linux 中断编程
申请和释放中断
在 Linux 设备驱动中,使用中断的设备需要申请和释放对应的中断,分别使用内核提供的 request_irq()和 free_irq()函数。
1.申请 IRQ
int request_irq(unsigned int irq,
void (*handler)(int irq, void *dev_id, struct pt_regs
*regs),
unsigned long irqflags,
const char * devname,
void *dev_id);
irq 是要申请的硬件中断号。
handler 是向系统登记的中断处理函数,是一个回调函数,中断发生时,系统调用这个函数,dev_id 参数将被传递给它。
irqflags 是中断处理的属性,若设置了 SA_INTERRUPT,则表示中断处理程序
是快速处理程序,快速处理程序被调用时屏蔽所有中断,慢速处理程序不屏蔽;若设置了 SA_SHIRQ,则表示多个设备共享中断,dev_id 在中断共享时会用到,一般
设置为这个设备的设备结构体或者 NULL。
request_irq()返回 0 表示成功,返回-INVAL 表示中断号无效或处理函数指针为NULL,返回-EBUSY 表示中断已经被占用且不能共享。与 request_irq()向对应的函数为 free_irq(),free_irq()的原型如下:
void free_irq(unsigned int irq,void *dev_id);
free_irq()中参数的定义与 request_irq()相同。使能和屏蔽中断
下列 3 个函数用于屏蔽一个中断源。
void disable_irq(int irq);
void disable_irq_nosync(int irq);
void enable_irq(int irq);
disable_irq_nosync()与 disable_irq()的区别在于前者立即返回,而后者等待目前的中断处理完成。注意,这 3 个函数作用于可编程中断控制器,因此,对系统内的所有CPU 都生效。
下列两个函数将屏蔽本 CPU 内的所有中断。
void local_irq_save(unsigned long flags);
void local_irq_disable(void);
前者会将目前的中断状态保留在 flags 中,注意 flags 被直接传递,而不是通过指针传递。后者直接禁止中断。
与上述两个禁止中断对应的恢复中断的方法如下:
void local_irq_restore(unsigned long flags);
void local_irq_enable(void);
以上各 local_开头的方法的作用范围是本 CPU 内。
Linux实现下半部的机制主要有tasklet和工作队列。
tasklet使用:
void my_tasklet_func(unsigned long); //定义一个处理函数:
DECLARE_TASKLET(my_tasklet,my_tasklet_func,data); //定义一个tasklet结构my_tasklet,与my_tasklet_func(data)函数相关联
然后,在需要调度tasklet的时候引用一个简单的API就能使系统在适当的时候进行调度运行:
tasklet_schedule(&my_tasklet);
此外,Linux还提供了另外一些其它的控制tasklet调度与运行的API:
DECLARE_TASKLET_DISABLED(name,function,data); //与DECLARE_TASKLET类似,但等待tasklet被使能
tasklet_enable(struct tasklet_struct *); //使能tasklet
tasklet_disble(struct tasklet_struct *); //禁用tasklet
tasklet_init(struct tasklet_struct *,void (*func)(unsigned long),unsigned long);
//类似DECLARE_TASKLET()
tasklet_kill(struct tasklet_struct *); // 清除指定tasklet的可调度位,即不允许调度该tasklet
工作队列:
工作队列(work queue)是Linux kernel中将工作推后执行的一种机制。这种机制和BH或Tasklets不同之处在于工作队列是把推后的工作交由一个内核线程去执行,因此工作队列的优势就在于它允许重新调度甚至睡眠。
工作队列的使用又分两种情况,一种是利用系统共享的工作队列来添加自己的工作,这种情况处理函数不能消耗太多时间,这样会影响共享队列中其他任务的处理;另外一种是创建自己的工作队列并添加工作。
工作队列的API:
API类型描述
DECLARE_WORK(name, fun)
定义和初始化work_struct结构体变量
INIT_WORK(work, fun)
初始化work_struct结构体变量
int schedule_work(struct work_struct work)
函数将当前工作添加到系统创建的工作队列中(调度工作)
workqueue_struct create_workqueue(name)
创建一个新的工作队列
create_singlethread_workqueue(name)
创建一个不与任何CPU绑定的工作线程。
也就是说workqueue_struct.flags包含WQ_UNBOUND
int queue_work(struct
workqueue_struct *wq, struct work_struct *work)
将一个工作添加到当前CPU的wq指定的工作队列中
int queue_work_on(int cpu, struct
workqueue_struct *wq, struct work_struct *work)
将一个工作添加到指定CPU的wq指定的工作队列中
void flush_workqueue(struct workqueue_struct *wq)
等待wq指向的工作队列中所有的工作节点都处理完才返回
void flush_scheduled_work(void)
等待系统工作队列中所有的工作节点都处理完才返回
void destroy_workqueue(struct workqueue_struct *wq)
销毁wq指向的工作队列
int queue_delayed_work(struct workqueue_struct *wq,
struct delayed_work *dwork, unsigned long delay)
延迟delay个时钟周期(tick)后将dwork指向的工作添加到wq指向工作队列中。t
int schedule_delayed_work(struct
delayed_work *dwork, unsigned long delay)
延迟delay个时钟周期(tick)后将dwork添
加到系统创建的工作队列中
int cancel_delayed_work(struct delayed_work *work)
取消正在等待添加到工作队列的work
工作队列的使用:
(一)利用系统共享的工作队列添加工作:
第一步:声明或编写一个工作处理函数
void my_func();
第二步:创建一个工作结构体变量,并将处理函数和参数的入口地址赋给这个工作结构体变量
DECLARE_WORK(my_work,my_func,&data); //编译时创建名为my_work的结构体变量并把函数入口地址和参数地址赋给它;
如果不想要在编译时就用DECLARE_WORK()创建并初始化工作结构体变量,也可以在程序运行时再用INIT_WORK()创建
struct work_struct my_work; //创建一个名为my_work的结构体变量,创建后才能使用INIT_WORK()
INIT_WORK(&my_work,my_func,&data); //初始化已经创建的my_work,其实就是往这个结构体变量中添加处理函数的入口地址和data的地址,通常在驱动的open函数中完成
第三步:将工作结构体变量添加入系统的共享工作队列
schedule_work(&my_work); //添加入队列的工作完成后会自动从队列中删除
或schedule_delayed_work(&my_work,tick); //延时tick个滴答后再提交工作
(二)创建自己的工作队列来添加工作
第一步:声明工作处理函数和一个指向工作队列的指针
void my_func();
struct workqueue_struct *p_queue;
第二步:创建自己的工作队列和工作结构体变量(通常在open函数中完成)
p_queue=create_workqueue(“my_queue”); //创建一个名为my_queue的工作队列并把工作队列的入口地址赋给声明的指针
struct work_struct my_work;
INIT_WORK(&my_work, my_func, &data); //创建一个工作结构体变量并初始化,和第一种情况的方法一样
第三步:将工作添加入自己创建的工作队列等待执行
queue_work(p_queue, &my_work);
//作用与schedule_work()类似,不同的是将工作添加入p_queue指针指向的工作队列而不是系统共享的工作队列
第四步:删除自己的工作队列
destroy_workqueue(p_queue); //一般是在close函数中删除
工作队列和tasklet的区别:
tasklet特性:
1.一个tasklet可在稍后被禁止或者重新启用;只有启用的次数和禁止的次数相同时,tasklet才会被执行。
2.和定时器类似,tasklet可以自己注册自己。
3.tasklet可被调度以在通常的优先级或者高优先级执行。高优先级的tasklet总会优先执行。
4.如果系统负荷不重,则tasklet会立即执行,但始终不会晚于下一个定时器滴答
5.一个tasklet可以和其它tasklet并发,但对自身来讲是严格串行处理的,也就是说,同一tasklet永远不会在多个处理器上同时运行:tasklet始终会调度自己在同一CPU上运行;
工作队列
表面来看,工作队列类似于tasklet:允许内核代码请求某个函数在将来的时间被调用。
但其实还是有很多不同:
1.tasklet在软中断上下文中运行,因此,所有的tasklet代码都是原子的。相反,工作队列函数在一个特殊的内核进程上下文中运行,因此他们有更好的灵活性尤其是,工作队列可以休眠!
2.tasklet始终运行在被初始提交的统一处理器上,但这只是工作队列的默认方式
3.内核代码可以请求工作队列函数的执行延迟给定的时间间隔
4.tasklet 执行的很快, 短时期, 并且在原子态, 而工作队列函数可能是长周期且不需要是原子的,两个机制有它适合的情形。
中断共享
多个设备共享一根硬件中断线的情况在实际的硬件系统中广泛存在,PCI 设备即是如此。Linux 2.6 支持这种中断共享。下面是中断共享的使用方法。
共享中断的多个设备在申请中断时都应该使用 SA_SHIRQ 标志,而且一个设
备以 SA_SHIRQ 申请某中断成功的前提是之前申请该中断的所有设备也都以 SA_SHIRQ 标志申请该中断。
尽管内核模块可访问的全局地址都可以作为
request_irq
(…, void *dev_id)
的最后一个参数 dev_id,
但是设备结构体指针是可传入的最佳参数。
在中断到来时,所有共享此中断的中断处理程序都会被执行,在中断处理程序顶半部中,应迅速地根据硬件寄存器中的信息比照传入的 dev_id 参数判断是否是本设备的中断,若不是,应迅速返回
内核定时器
内核定时器编程
软件意义上的定时器最终依赖硬件定时器来实现,内核在时钟中断发生后检测各定时器是否到期,到期后的定时器处理函数将作为软中断在底半部执行。实质上,时钟中断处理程序执行 update_process_timers()函数,该函数调用 run_local_timers()函数,这个函数处理 TIMER_SOFTIRQ 软中断,运行当前处理器上到期的所有定时器。
在 Linux 设备驱动编程中,可以利用 Linux 内核中提供的一组函数和数据结构来完成定时触发工作或者完成某周期性的事务。这组函数和数据结构使得驱动工程师多数情况下不用关心具体的软件定时器究竟对应着怎样的内核和硬件行为。
Linux 内核所提供的用于操作定时器的数据结构和函数如下。
timer_list
在 Linux 内核中,timer_list 结构体的一个实例对应一个定时器
struct timer_list {
struct list_head entry; //定时器列表
unsigned long expires; //定时器到期时间
void (*function)(unsigned long); //定时器处理函数
unsigned long data; //作为参数被传入定时器处理函数
struct timer_base_s *base;
};
当定时器期满后,其中第 5 行的 function()成员将被执行,而第 4 行的 data 成员则是传入其中的参数,第 3 行的 expires 则是定时器到期的时间(jiffies)
2.初始化定时器
void init_timer(struct timer_list * timer);
上述 init_timer()函数初始化 timer_list 的 entry 的 next 为 NULL,
并给 base 指针赋值。
TIMER_INITIALIZER(_function, _expires, _data)宏用于赋值定时器结构体的
function、expires、data 和 base 成员,这个宏的定义如下所示:
#define TIMER_INITIALIZER(_function, _expires, _data) {
.function = (_function),
.expires = (_expires),
.data = (_data),
.base = &__init_timer_base,
}
DEFINE_TIMER(_na me , _ functi on, _e x pires, _ da ta )宏是定义并初始化定时器成员的“快捷方式”,这个宏定义如下所示:
#define DEFINE_TIMER(_name, _function, _expires, _data)
struct timer_list _name =
TIMER_INITIALIZER(_function, _expires, _data)
此外,setup_timer()也可用于初始化定时器并赋值其成员,其源代码如下:
static inline void setup_timer(struct timer_list * timer,
void (*function)(unsigned long),
unsigned long data)
{
timer->function = function;
timer->data = data;
init_timer(timer);
}
3.增加定时器
void add_timer(struct timer_list * timer);
上述函数用于注册内核定时器,将定时器加入到内核动态定时器链表中。
4.删除定时器
int del_timer(struct timer_list * timer);
上述函数用于删除定时器。
del_timer_sync()是 del_timer()的同步版,主要在多处理器系统中使用,如果编译内核时不支持 SMP,del_timer_sync()和 del_timer()等价。
5.修改定时器的 expire
int mod_timer(struct timer_list *timer, unsigned long expires);
上述函数用于修改定时器的到期时间,在新的被传入的 expires 到来后才会执行定时器函数。
一个完整的内核定时器使用模板
1 /xxx 设备结构体/
2 struct xxx_dev
3 {
4 struct cdev cdev;
5 …
6 timer_list xxx_timer;/设备要使用的定时器/
7 };
8
9 /xxx 驱动中的某函数/
10 xxx_func1(…)
11 {
12 struct xxx_dev *dev = filp->private_data;
13 …
14 /初始化定时器/
15 init_timer(&dev->xxx_timer);
16 dev->xxx_timer.function = &xxx_do_timer;
17 dev->xxx_timer.data = (unsigned long)dev;
18
/设备结构体指针作为定时器处理函数参数/
19 dev->xxx_timer.expires = jiffies + delay;
20 /添加(注册)定时器/
21 add_timer(&dev->xxx_timer);
22 …
23 }
24
25 /xxx 驱动中的某函数/
26 xxx_func2(…)
27 {
28 …
29 /删除定时器/
30 del_timer (&dev->xxx_timer);
31 …
32 }
33
34 /定时器处理函数/
35 static void xxx_do_timer(unsigned long arg)
36 {
37 struct xxx_device *dev = (struct xxx_device *)(arg);
38 …
39 /调度定时器再执行/
40 dev->xxx_timer.expires = jiffies + delay;
41 add_timer(&dev->xxx_timer);
42 …
43 }
内核延时
inux 内核中提供了如下 3 个函数分别进行纳秒、微秒和毫秒延迟。
void ndelay(unsigned long nsecs);
void udelay(unsigned long usecs);
void mdelay(unsigned long msecs);
上述延迟的实现原理本质上是忙等待,它根据 CPU 频率进行一定次数的循环。
有时候,可以在软件中进行这样的延迟:
void delay(unsigned int time)
{
while (time–);
}
ndelay()、udelay()和 mdelay()函数的实现方式机理与此类似。
毫秒时延(以及更大的秒时延)已经比较大了,在内核中,最好不要直接使用
mdelay()函数,这将无谓地耗费 CPU 资源,对于毫秒级以上时延,内核提供了下述函数:
void msleep(unsigned int millisecs);
unsigned long msleep_interruptible(unsigned int millisecs);
void ssleep(unsigned int seconds);
上述函数将使得调用它的进程睡眠参数指定的时间,msleep()、ssleep()不能被打断,而 msleep_interruptible()则可以被打断。
内核中进行延迟的一个很直观的方法是比较当前的 jiffies 和目标 jiffies 设置为当前 jiffies 加上时间间隔的 jiffies),直到未来的 jiffies 达到目标 jiffies。
睡着延迟
睡着延迟无疑是比忙等待更好的方式,随着延迟在等待的时间到来之间进程处于睡眠状态,CPU 资源被其他进程使用。schedule_timeout()可以使当前任务睡眠指定的
jiffies 之后重新被调度执行,
msleep()和 msleep_interruptible()在本质上都是依靠包含了
schedule_timeout()
的
schedule_
timeout_uninterruptible()
和
schedule_timeout_interruptible()实现的