驱动篇:中断与时钟(一)
发布日期:2021-06-29 11:34:46 浏览次数:2 分类:技术文章

本文共 5637 字,大约阅读时间需要 18 分钟。

驱动篇:中断与时钟(一)

由于中断服务程序的执行并不存在于进程上下文(当一个进程在执行时,CPU的所有寄存器中的值、进程的状态以及堆栈中的内容被称为该进程的上下文。当内核需要切换到另一个进程时,它需要保存当前进程的所有状态,即保存当前进程的上下文,以便在再次执行该进程时,能够得到切换时的状态执行下去,在Linux内核中,中断处理程序运行期间是不能发生进程切换的,因此,也就不能够使用睡眠。因为中断的内核控制路径在恢复时需要的所有数据都存放在被中断进程的内核栈中,如果发生了进程切换,那么在恢复时就找不到之前的那个进程,因为也就不能够获得那个进程的内核栈中的数据,使得中断能够正确的退出。),因此,要求中断服务程序的时间尽可能地短。因此,Linux在中断处理中引入了顶半部和底半部分离的机制。另外,内核中对时钟的处理也采用中断方式,而内核软件定时器最终依赖于时钟中断。

1.中断与定时器
所谓中断是指CPU 在执行程序的过程中,出现了某些突发事件时CPU 必须暂停执行当前的程序,转去处理突发事件,处理完毕后CPU 又返回原程序被中断的位置并继续执行。根据中断的来源,中断可分为内部中断和外部中断,内部中断的中断源来自CPU内部(软件中断指令、溢出、除法错误等,例如,操作系统从用户态切换到内核态需借助CPU内部的软件中断),外部中断的中断源来自CPU外部,由外设提出请求。根据是否可以屏蔽中断分为可屏蔽中断与不屏蔽中断(NMI),可屏蔽中断可以通过屏蔽字被屏蔽,屏蔽后,该中断不再得到响应,而不屏蔽中断不能被屏蔽。
根据中断入口跳转方法的不同,中断分为向量中断和非向量中断。采用向量中断的CPU 通常为不同的中断分配不同的中断号,当检测到某中断号的中断到来后,就自动跳转到与该中断号对应的地址执行。不同中断号的中断有不同的入口地址。非向量中断的多个中断共享一个入口地址,进入该入口地址后再通过软件判断中断标志来识别具体是哪个中断。也就是说,向量中断由硬件提供中断服务程序入口地址,非向量中断由软件提供中断服务程序入口地址

非向量中断服务程序的典型结构

irq_handler() {
... int int_src = read_int_status(); /*读硬件的中断相关寄存器*/ switch (int_src) /*判断中断源*/ {
case DEV_A: dev_a_handler(); break; case DEV_B: dev_b_handler(); break; ...default:break; } ... }

嵌入式系统以及X86 PC 中大多包含可编程中断控制器(PIC),许多MCU 内部就集成了PIC。如在80386 中,PIC是两片i8259A芯片的级联。通过读写PIC的寄存器,程序员可以屏蔽/使能某中断及获得中断状态,前者一般通过中断MASK 寄存器完成,后者一般通过中断PEND 寄存器完成。定时器在硬件上也依赖中断来实现,图10.1 给出了典型的嵌入式微处理内可编程间隔定时器(PIT)的工作原理,它接收一个时钟输入,当时钟脉冲到来时,将目前计数值增1并与预先设置的计数值(计数目标)比较,若相等,证明计数周期满,产生定时器中断并复位目前计数值。

在这里插入图片描述2.Linux中断处理程序架构
设备的中断会打断内核中进程的正常调度和运行,为了在中断执行时间尽可能短和中断处理需完成大量工作之间找到一个平衡点,Linux将中断处理程序分解为两个半部:顶半部(top half)和底半部(bottomhalf)。
顶半部完成尽可能少的比较紧急的功能,它往往只是简单地读取寄存器中的中断状态并清除中断标志后就进行“登记中断”的工作。“登记中断”意味着将底半部处理程序挂到该设备的底半部执行队列中去。这样,顶半部执行的速度就会很快,可以服务更多的中断请求。

底半部几乎做了中断处理程序所有的事情,而且可以被新的中断打断,这也是底半部和顶半部的最大不同,因为顶半部往往被设计成不可中断。底半部则相对来说并不是非常紧急的,而且相对比较耗时,不在硬件中断服务程序中执行。尽管顶半部、底半部的结合能够改善系统的响应能力,但是,僵化地认为Linux设备驱动中的中断处理一定要分两个半部则是不对的。如果中断要处理的工作本身很少,则完全可以直接在顶半部全部完成。

在 Linux 系统中,查看/proc/interrupts文件可以获得系统中断的统计信息,如下所示。在单处理器的系统中,第一列是中断号,第二列是向CPU0 产生该中断的次数,之后的是对于中断的描述。

在这里插入图片描述3.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 表示中断已经被占用且不能共享。

2.释放IRQ

void free_irq(unsigned int irq,void *dev_id);

3.使能和屏蔽中断

下列函数用于屏蔽一个中断源

void disable_irq(int irq);void disable_irq_nosync(int irq);disable_irq_nosync()与disable_irq()的区别在于前者立即返回,而后者等待目前的中断处理完成。注意,这两个函数作用于可编程中断控制器,因此,对系统内的所有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内。

4.底半部机制

Linux 系统实现底半部的机制主要有tasklet、工作队列和软中断。
1.tasklet
tasklet的使用较简单,我们只需要定义tasklet及其处理函数并将两者关联,例如:

void my_tasklet_func(unsigned long); /*定义一个处理函数*/DECLARE_TASKLET(my_tasklet, my_tasklet_func, data);/*定义一个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, struct pt_regs*regs) {
...tasklet_schedule(&xxx_tasklet);... } /*设备驱动模块加载函数*/ int __init xxx_init(void) {
... /*申请中断*/ result = request_irq(xxx_irq, xxx_interrupt, SA_INTERRUPT, "xxx", NULL); ... } /*设备驱动模块卸载函数*/ void __exit xxx_exit(void) {
... /*释放中断*/ free_irq(xxx_irq, xxx_interrupt); ... }上述程序在模块加载函数中申请中断(第24~25 行),并在模块卸载函数中释放它(第34 行)。对应于xxx_irq 的中断处理程序被设置为xxx_interrupt()函数,在这个函数中,第15 行的tasklet_schedule(&xxx_tasklet)调度的tasklet 函数xxx_do_tasklet()在适当的时候得到执行。中断处理程序顶半部的返回类型为irqreturn_t,它定义为int,中断处理程序顶半部一般返回IRQ_HANDLED

2.工作队列

工作队列的使用方法和tasklet非常相似,下面的代码用于定义一个工作队列和一个底半部执行函数。

struct work_struct my_wq; /*定义一个工作队列*/void my_wq_func(unsigned long); /*定义一个处理函数*/通过INIT_WORK()可以初始化这个工作队列并将工作队列与处理函数绑定,如下所示:INIT_WORK(&my_wq, (void (*)(void *)) my_wq_func, NULL);/*初始化工作队列并将其与处理函数绑定*/与tasklet_schedule()对应的用于调度工作队列执行的函数为schedule_work(),如:schedule_work(&my_wq);/*调度工作队列执行*/

工作队列使用模板

/*定义工作队列和关联函数*/struct work_struct xxx_wq; void xxx_do_work(unsigned long); /*中断处理底半部*/ void xxx_do_work(unsigned long) {
... } /*中断处理顶半部*/ irqreturn_t xxx_interrupt(int irq, void *dev_id, struct pt_regs*regs) {
... schedule_work(&xxx_wq); ... }/*设备驱动模块加载函数*/ int xxx_init(void) {
... /*申请中断*/ result = request_irq(xxx_irq, xxx_interrupt, SA_INTERRUPT, "xxx", NULL);... /*初始化工作队列*/INIT_WORK(&xxx_wq, (void (*)(void *)) xxx_do_work, NULL); ... } /*设备驱动模块卸载函数*/ void xxx_exit(void) {
... /*释放中断*/ free_irq(xxx_irq, xxx_interrupt); ... }

尽管Linux 专家们多建议在设备第一次打开时才申请设备的中断并在最后一次关闭时释放中断以尽量减少中断被这个设备占用的时间,但是,大多数情况下,为求省事,大多数驱动工程师还是将中断申请和释放的工作放在了设备驱动的模块加载和卸载函数中。

3.软中断

软中断是用软件方式模拟硬件中断的概念,实现宏观上的异步执行效果,tasklet也是基于软中断实现的。

异步通知所基于的信号也类似于中断,现在,总结一下硬中断、软中断和信号的区别:硬中断是外部设备对CPU 的中断,软中断通常是硬中断服务程序对内核的中断,而信号则是由内核(或其他进程)对某个进程的中断。在 Linux内核中,用softirq_action结构体表征一个软中断,这个结构体中包含软中断处理函数指针和传递给该函数的参数。使用open_softirq()函数可以注册软中断对应的处理函数,而raise_softirq()函数可以触发一个软中断。

软中断和 tasklet仍然运行于中断上下文,而工作队列则运行于进程上下文。因此,软中断和tasklet处理函数中不能睡眠,而工作队列处理函数中允许睡眠。local_bh_disable()和local_bh_enable()是内核中用于禁止和使能软中断和tasklet底半部机制的函数。

转载地址:https://blog.csdn.net/zytgg123456/article/details/109934784 如侵犯您的版权,请留言回复原文章的地址,我们会给您删除此文章,给您带来不便请您谅解!

上一篇:驱动篇:内存与 I/O 访问(一)(摘录)
下一篇:驱动篇:异步通知与异步 I/O(一)

发表评论

最新留言

路过,博主的博客真漂亮。。
[***.116.15.85]2024年04月09日 22时12分34秒