中断与时钟

中断与定时器

所谓中断是指CPU在执行程序的过程中,出现了某些突发事件急待处理,CPU必须暂停当前程序的执行,转去处理突发事件,处理完毕后又返回原程序被中断的位置继续执行。

根据中断的来源,中断可分为内部中断和外部中断,内部中断的中断源来自CPU内部(软件中断指令、溢出、除法错误等,例如,操作系统从用户态切换到内核态需要借助CPU内部的软件中断),外部中断的中断源来自CPU外部,由外设提出请求。

根据中断是否可以屏蔽,中断可分为可屏蔽中断与不可屏蔽中断(NMI),可屏蔽中断可以通过设置中断控制器寄存器等方法被屏蔽,屏蔽后,该中断不再得到响应,而不可屏蔽中断不能被屏蔽。

根据中断入口跳转方法的不同,中断可分为向量中断和非向量中断。采用向量中断的CPU通常为不同的中断分配不同的中断号,当检测到某中断号的中断到来后,就自动跳转到与该中断号对应的地址执行。不同中断号的中断有不同的入口地址。非向量中断的多个中断共享一个入口地址,进入该入口地址后,再通过软件判断中断标志来识别具体是哪个中断。也就是说,向量中断由硬件提供服务程序入口地址,非向量中断由软件提供中断服务程序入口地址。

一个典型的非向量中断服务程序如下代码所示,它先判断中断源,然后调用不同中断源的中断服务程序。

  1. irq_handler()
  2. {
  3.     /* code */
  4.     int int_src = read_int_status();    /* 读硬件的中断相关寄存器 */
  5.     switch (int_src)    /* 判断中断源 */
  6.     {
  7.     case DEV_A:
  8.         dev_a_handler();
  9.         break;
  10.     case DEV_B:
  11.         dev_b_handler();
  12.         break;
  13.     /* code */
  14.     default:
  15.         break;
  16.     }
  17.     /* code */
  18. }

嵌入式系统以及x86 PC中大部分包含可编程中断控制器(PIC),许多MCU内部就集成了PIC。如在80386中,PIC是两片i8259A芯片的级联。通过读写PIC的寄存器,程序员可以屏蔽/使能某中断以及获得中断状态,前者一般通过中断MASK寄存器完成,后者一般通过中断PEND寄存器完成。

定时器在硬件上也依赖中断来实现,下图所示为典型得嵌入式微处理器内可编程间隔定时器(PIT)的工作原理,它接收一个时钟输入,当时钟脉冲到来时,将目前计数值增1并与预先设置的计数值(计数目标)比较,若相等,证明计数周期满,并产生定时器中断且复位目前计数值。

在ARM多核处理器里最常用的中断控制器是GIC(Generic Interrupt Controller),如下图所示,它支持3种类型的中断。

SGI(Software Generated Interrupt):软件产生的中断,可以用于多核的核间通信,一个CPU可以通过写GIC的寄存器给另外一个CPU产生中断。多核调度用的IPI_WAKEUP、IPI_TIMER、IPI_RESCHEDULE、IPI_CALL_FUNC、IPI_CALL_FUNC_SINGLE、IPI_CPU_STOP、IPI_IRQ_WORK、IPI_COMPLETION都是由SGI产生的。

PPI(Private Peripheral Interrupt):某个CPU私有外设的中断,这类外设的中断只能发给绑定的那个CPU。

SPI(Shared Peripheral Interrupt):共享外设的中断,这类外设的中断可以路由到任何一个CPU。

对于SPI类型的中断,内核可以通过如下API设定中断触发的CPU核:

  • static inline int
  • irq_set_affinity(unsigned int irq, const struct cpumask *cpumask);

在ARM Linux默认情况下,中断都是在CPU0上产生的,比如,我们可以通过如下代码把中断irq设定到CPU i 上去:

  • irq_set_affinity(irq, cpumask_of(i));

Linux中断处理程序架构

设备的中断会打断内核进程中的正常调度和运行,系统对更高吞吐率的追求势必要求中断服务程序尽量短小精悍。但是,这个良好的愿景往往与现实并不吻合。在大多数真实的系统中,当中断到来时,要完成的工作往往并不会是短小的,它可能要进行较大量的耗时处理。

下图描述了Linux内核的中断处理机制。为了在中断执行时间尽量短和中断处理需完成的工作尽量大之间找到一个平衡点,Linux将中断处理程序分解为两个半部:顶半部(Top Half)和底半部(Bottom Half)。

顶半部用于完成尽量少的比较紧急的功能,它往往只是简单地读取寄存器中的中断状态,并在清除中断标志后进行“登记中断”的工作。“登记中断”意味着将底半部处理程序挂到该设备的底半部执行队列中去。这样,顶半部执行的速度就会很快,从而可以服务更多的中断请求。

现在,中断处理工作的重心就落在了底半部的头上,需要它来完成中断事件的绝大多数任务。底半部几乎做了中断处理程序所有的工作,而且可以被新的中断打断,这也是底半部和顶半部的最大不同,因为顶半部往往被设计成不可中断。底半部相对来说并不是非常紧急的,而且相对比较耗时,不在硬件中断服务程序中执行。

尽管顶半部、底半部的结合能够完善系统的响应速度,但是,僵化地认为Linux设备驱动中的中断处理一定要分为两个半部则是不对的。如果中断要处理的工作本身很少,则完全可以直接在顶半部全部完成。

在Linux中,查看/proc/interrupts文件可以获得系统中中断的统计信息,并能统计出每一个中断号上的中断在每个CPU上发生的次数。

Linux中断编程

申请和释放中断

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

1. 申请 irq

  • static inline int
  • request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags,
  •                 const char *name, void *dev);

irq 是要申请的硬件中断号。

handler 是向系统登记的中断处理函数(顶半部),是一个回调函数,中断发生时,系统调用这个函数,dev参数将被传递给它。

flags 是中断处理的属性,可以指定中断的触发方式以及处理方式。在触发方式方面,可以是IRQF_TRIGGER_RISING等。在处理方式方面,若设置了IRQF_SHARED,则表示多个设备共享中断,dev是要传递给中断服务程序的私有数据,一般设置为这个设备的设备结构体或者NULL。

request_irq()返回0表示成功,返回-EINVAL表示中断号无效或处理函数指针为NULL,返回-EBUSY表示中断已经被占用且不能共享。

  • static inline int
  • devm_request_irq(struct device *dev, unsigned int irq, irq_handler_t handler,
  •                          unsigned long irqflags, const char *devname, void *dev_id);

此函数与request_irq()的区别是devm_开头的API申请的是内核“managed”的资源,一般不需要在出错处理和remove()接口里再显式的释放。有点类似Java的垃圾回收机制。

2. 释放 irq

与request_irq()相对应的函数为free_irq(),free_irq()的原型为:

  • extern const void *free_irq(unsigned int, void *);

free_irq()中的参数与request_irq()相同。

使能和屏蔽中断

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

  1. extern void disable_irq(unsigned int irq);
  2. extern void disable_irq_nosync(unsigned int irq);
  3. extern void enable_irq(unsigned int irq);

disable_irq_nosync()与disable_irq()的区别在于前者立即返回,而后者等待目前的中断处理完成。由于disable_irq()会等待指定的中断被处理完,因此如果在n号中断的顶半部调用disable_irq(n),会引起系统死锁,这种情况下,只能调用disable_irq_nosync(n)。

下列两个函数(或宏,具体实现依赖于CPU的体系结构)将屏蔽本CPU内的所有中断:

  1. #define local_irq_save(flags) ...
  2. #define local_irq_disable() ...

前者会将目前的中断状态保留在flags中(注意flags为unsigned long类型,被直接传递,而不是通过指针),后者直接禁止中断而不保存状态。

与上述两个禁止中断对应的恢复中断的函数(或宏)是:

  1. #define local_irq_restore(flags) ...
  2. #define local_irq_enable() ...

以上各以local_开头的方法的作用范围是本CPU内。

底半部机制

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

1. tasklet

tasklet的使用较简单,它的执行上下文是软中断,执行时机通常是顶半部返回的时候。我们只需要定义tasklet及其处理函数,并将两者关联即可,例如:

  1. void my_tasklet_func(unsigned long);    /* 定义一个处理函数 */
  2. DECLARE_TASKLET(my_tasklet, my_tasklet_func, data);
  3. /* 定义一个tasklet结构体my_tasklet,与 my_tasklet_func(data) 函数相关联 */

代码DECLARE_TASKLET实现了定义名称为my_tasklet的tasklet,并将其与my_tasklet_func()这个函数绑定,而传入这个函数的参数为data。

在需要调度tasklet的时候引用一个tasklet_schedule()函数就能使系统在适当的时候进行调度运行:

  • static inline void tasklet_schedule(struct tasklet_struct *t);

使用tasklet作为底半部处理中断的设备驱动程序模板如下代码所示(仅包含与中断相关的部分)。

  1. /* 定义 tasklet 和底半部函数并将它们关联*/
  2. void xxx_do_tasklet(unsigned long);
  3. DECLARE_TASKLET(xxx_tasklet, xxx_do_tasklet, 0);
  4.  
  5. /* 中断处理底半部 */
  6. void xxx_do_tasklet(unsigned long)
  7. {
  8.     /* ... code ... */
  9. }
  10.  
  11. /* 中断处理顶半部 */
  12. irqreturn_t xxx_interrupt(int irq, void* dev_id)
  13. {
  14.     /* ... code ... */
  15.     tasklet_schedule(&xxx_tasklet);
  16.     /* ... code ... */
  17. }
  18.  
  19. /* 设备驱动模块加载函数 */
  20. int __init xxx_init(void)
  21. {
  22.     /* ... code ... */
  23.     /* 申请中断 */
  24.     result = request_irq(xxx_irq, xxx_interrupt, 0, "xxx", NULL);
  25.     /* ... code ... */
  26.     return IRQ_HANDLED;
  27. }
  28.  
  29. /* 设备驱动模板卸载函数 */
  30. void __exit xxx_exit(void)
  31. {
  32.     /* ... code ... */
  33.     /* 释放中断 */
  34.     free_irq(xxx_irq, xxx_interrupt);
  35.     /* ... code ... */
  36. }

上述程序在模块加载函数中申请中断(第24行),并在模块卸载函数中释放它(第34行)。对应于xxx_irq的中断处理程序被设置为xxx_interrupt()函数,在这个函数中,第15行执行的tasklet_schedule()调度被定义的tasklet函数xxx_do_tasklet()在适当的时候执行。

2. 工作队列

工作队列的使用方法和tasklet非常相似,但是工作队列的执行上下文是内核线程,因此可以调度和睡眠。下面的代码用于定义一个工作队列和一个底半部执行函数:

  1. struct work_struct my_wq;  /* 定义一个工作队列 */
  2. void my_wq_func(struct work_struct* work);  /* 定义一个处理函数 */

通过INIT_WORK()可以初始化这个工作队列并将工作队列与处理函数绑定。

  • INIT_WORK(&my_wq, my_wq_func);
  • /* 初始化工作队列并将其与处理函数绑定 */

与tasklet_schedule()对应的用于调度工作队列执行的函数为schedule_work(),如:

  • schedule_work(&my_wq); /* 调度工作队列执行 */

使用工作队列中断底半部的设备驱动程序模板如下代码所示(仅包含与中断相关的部分)。

  1. /* 定义工作队列和关联函数 */
  2. struct work_struct xxx_wq;
  3. void xxx_do_work(struct work_struct* work);
  4.  
  5. /* 中断处理底半部 */
  6. void xxx_do_work(struct work_struct* work)
  7. {
  8.     /* ... code ... */
  9. }
  10.  
  11. /* 中断处理顶半部 */
  12. irqreturn_t xxx_interrupt(int irq, void* dev_id)
  13. {
  14.     /* ... code ... */
  15.     schedule_work(&xxx_wq);
  16.     /* ... code ... */
  17.     return IRQ_HANDLED;
  18. }
  19.  
  20. /* 设备驱动模块加载函数 */
  21. int __init xxx_init(void)
  22. {
  23.     /* ... code ... */
  24.     /* 申请中断 */
  25.     result = request_irq(xxx_irq, xxx_interrupt, 0, "xxx", NULL);
  26.     /* ... code ... */
  27.     /* 初始化工作队列 */
  28.     INIT_WORK(&xxx_wq, xxx_do_work);
  29.     /* ... code ... */
  30.     return IRQ_HANDLED;
  31. }
  32.  
  33. /* 设备驱动模板卸载函数 */
  34. void __exit xxx_exit(void)
  35. {
  36.     /* ... code ... */
  37.     /* 释放中断 */
  38.     free_irq(xxx_irq, xxx_interrupt);
  39.     /* ... code ... */
  40. }

与tasklet不同的是,上述程序在设计驱动模块加载函数中增加了初始化工作队列的代码(第28行)。

工作队列早期的实现是在每个CPU核上创建一个worker内核线程,所有在这个核上调度的工作都在该worker线程中执行,其并发性显然差强人意。在Linux2.6.36以后,转而实现了“Concurrency-managed workqueues”,简称cmwq,cmwq会自动维护工作队列的线程池以提高并发性,同时保持了API的向后兼容。

3. 软中断

软中断(Softirq)也是一种传统的底半部处理机制,它的执行时机通常是顶半部返回的时候,tasklet是基于软中断实现的,因此也运行于软中断上下文。

在Linux内核中,用softirq_action结构体表征一个软中断,这个结构体包含软中断处理函数和传递给该函数的参数。使用open_softirq()函数可以注册软中断对应的处理函数,而raise_softirq()函数可以触发一个软中断。

软中断和tasklet运行于软中断上下文,仍然属于原子上下文的一种,而工作队列则运行于进程上下文。因此,在软中断和tasklet处理函数中不允许睡眠,而在工作队列处理函数中允许睡眠。

local_bh_disable()和local_bh_enable()是内核中用于禁止和使能软中断以及tasklet底半部机制的函数。

内核中采用softirq的地方包括HI_SOFTIRQ等,一般来说,驱动的编写者不会也不宜直接使用softirq。

之前介绍的异步通知所基于的信号也类似于中断,现在,总结一下硬中断、软中断和信号的区别:硬中断是外部设备对CPU的中断,软中断是中断底半部的一种处理机制,而信号则是由内核(或其他进程)对某个进程的中断。在涉及系统调用的场合,人们也常说用过软中断(例如ARM为swi)陷入内核,此时软中断的概念是指由软件指令引发的中断,和我们这个地方说的softirq是两个完全不同的概念,一个是software,一个是soft。

需要特别说明的是,软中断以及基于软中断的tasklet如果在某段时间内大量出现的话,内核会把后续软中断放入ksoftirqd内核线程中执行。总的来说,中断优先级高于软中断,软中断又高于任何一个线程。软中断适度线程化,可以缓解高负载情况下系统的响应。

4. threaded_irq

在内核中,除了可以通过request_irq()、devm_request_irq()申请中断以外,还可以通过request_threaded_irq()和devm_request_threaded_irq()申请。这两个函数的原型为:

  • extern int
  • request_threaded_irq(unsigned int irq, irq_handler_t handler,
  • irq_handler_t thread_fn,
  • unsigned long flags, const char *name, void *dev);
  •  
  • extern int
  • devm_request_threaded_irq(struct device *dev, unsigned int irq,
  • irq_handler_t handler, irq_handler_t thread_fn,
  • unsigned long irqflags, const char *devname,
  • void *dev_id);

由此可见,它们比request_irq()、devm_request_irq()多了一个参数thread_fn。用这两个API申请中断的时候,内核会为相应的中断号分配一个对应的内核线程。注意这个线程只针对这个中断号,如果其他中断也通过request_threaded_irq()申请,自然会得到新的内核线程。

参数handler对应的函数执行于中断上下文,thread_fn参数对应的函数则执行于内核线程。如果handler结束的时候,返回值是IRQ_WAKE_THREAD,内核会调度对应线程执行thread_fn对应的函数。

request_threaded_irq()和devm_request_threaded_irq()支持在irqflags中设置IRQF_ONESHOT标记,这样内核会自动帮助我们在中断上下文中屏蔽对应的中断号,而在内核调度thread_fn执行后,重新使能该中断号。对于我们无法在上半部清除中断的情况,IRQF_ONESHOT特别有用,避免了中断服务程序一退出,中断就泛洪的情况。

handler参数可以设置为NULL,这种情况下,内核会用默认的irq_default_primary_handler()代替handler,并会使用IRQF_ONESHOT标记。

中断共享

多个设备共享一根硬件中断线的情况在实际的硬件系统中广泛存在,Linux支持这种中断共享。下面是中断共享的使用方法。

1)共享中断的多个设备在申请中断时,都应该使用IRQF_SHARED标志,而且一个设备以IRQF_SHARED申请某中断成功的前提是该中断未被申请,或该中断虽然被申请了,但是之前申请该中断的所有设备也都是以IRQF_SHARED标志申请该中断。

2)尽管内核模块可访问的全局地址可以作为reques_irq()的最后一个参数dev_id,但是设备结构体指针显然是可传入的最佳参数。

3)在中断到来时,会遍历执行共享此中断的所有中断处理程序,直到某一个函数返回IRQ_HANDLED。在中断处理程序顶半部中,应根据硬件寄存器的信息比照传入的dev_id参数迅速地判断是否为本设备的中断,若不是,应迅速返回IRQ_NONE。

以下代码给出了使用共享中断的设备驱动程序的模板(仅包含与共享中断机制相关的部分)。

  1. /* 中断处理顶半部 */
  2. irqreturn_t xxx_interrupt(int irq, void* dev_id)
  3. {
  4.     /* ... code ... */
  5.     int status = read_int_status();    /* 获知中断源 */
  6.     if (!is_myint(dev_id, status)) /* 判断是否为本设备中断 */
  7.          return IRQ_NONE;  /* 不是本设备中断,立即返回 */
  8.  
  9.     /* 是本设备中断,进行处理 */
  10.     /* ... code ... */
  11.     return IRQ_HANDLED;   /* 返回 IRQ_HANDLED 表明中断已被处理 */
  12. }
  13.  
  14. /* 设备驱动模块加载函数 */
  15. int __init xxx_init(void)
  16. {
  17.     /* ... code ... */
  18.     /* 申请共享中断 */
  19.     result = request_irq(xxx_irq, xxx_interrupt, IRQF_SHARED, "xxx", xxx_dev);
  20.     /* ... code ... */
  21. }
  22.  
  23. /* 设备驱动模板卸载函数 */
  24. void __exit xxx_exit(void)
  25. {
  26.     /* ... code ... */
  27.     /* 释放中断 */
  28.     free_irq(xxx_irq, xxx_interrupt);
  29.     /* ... code ... */
  30. }

思考和总结

这一部分本来还有一个GPIO按键中断的实例,但是看不懂就没有添加进去。这篇文章介绍了很多Linux下和中断相关的操作函数,但感觉中断这部分和硬件以及体系结构关联还是很大的,希望后续能买开发板进行实践一番,感觉还要学习ARM裸板的相关知识。还从这篇文章中了解到了一个比较重要的概念:顶半部和底半部。