HaveFunWithEmbeddedSystem/Chapter8_SOC_与_Linux/8.10_中断与时钟.md

18 KiB
Raw Permalink Blame History

8.10 中断与时钟

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

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

根据是否可以屏蔽中断分为可屏蔽中断与不屏蔽中断NMI可屏蔽中断可以通过屏蔽字被屏蔽屏蔽后该中断不再得到响应而不屏蔽中断不能被屏蔽。

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

Linux 中断处理程序架构

顶半部与底半部

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

为了在中断执行时间尽可能短和中断处理需完成大量工作之间找到一个平衡点Linux 将中断处理程序分解为两个半 部顶半部top half和底半部bottom half

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

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

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

中断与文件系统

在 Linux 系统中,查看 /proc/interrupts 文件可以获得系统中断的统计信息。

# cat /proc/interrupts
           CPU0
 16:    1313862       GPC  55 Level     i.MX Timer Tick
 18:        292       GPC  26 Level     2020000.serial
 19:          0       GPC  98 Level     sai
 20:          0       GPC  50 Level     2034000.asrc
 45:          0  gpio-mxc  19 Edge      2190000.usdhc cd
152:          0  gpio-mxc  24 Edge      gpiolib
195:      25383       GPC 120 Level     20b4000.ethernet
196:          0       GPC 121 Level     20b4000.ethernet
197:          0       GPC  80 Level     20bc000.wdog
200:          0       GPC  49 Level     imx_thermal
205:          1       GPC  19 Level     rtc alarm
211:          0       GPC   2 Level     sdma
216:          3       GPC  43 Level     2184000.usb
217:        741       GPC  42 Level     2184200.usb
218:          0       GPC 118 Level     2188000.ethernet
219:          0       GPC 119 Level     2188000.ethernet
220:          0       GPC  22 Level     mmc0
221:      30726       GPC  23 Level     mmc1
223:          0       GPC   8 Level     pxp-dmaengine
224:          0       GPC  18 Level     pxp-dmaengine-std
225:          1       GPC 107 Level
226:          0       GPC  27 Level     21e8000.serial
227:          0       GPC  28 Level     21ec000.serial
228:          0       GPC  29 Level     21f0000.serial
229:          0       GPC  46 Level     dcp-vmi-irq
230:          0       GPC  47 Level     dcp-irq
232:          2       GPC   6 Level     imx-rng
IPI0:          0  CPU wakeup interrupts
IPI1:          0  Timer broadcast interrupts
IPI2:          0  Rescheduling interrupts
IPI3:          0  Function call interrupts
IPI4:          0  Single function call interrupts
IPI5:          0  CPU stop interrupts
IPI6:          0  IRQ work interrupts
IPI7:          0  completion interrupts
Err:          0

Linux 中断编程

获取中断信息:

/**
 * of_irq_get - Decode a node's IRQ and return it as a Linux irq number
 * @file drivers/of/irq.c
 * @dev: pointer to device tree node
 * @index: zero-based index of the irq
 *
 * Returns Linux irq number on success, or -EPROBE_DEFER if the irq domain
 * is not yet created.
 *
 */
int of_irq_get(struct device_node *dev, int index);

/**
 * platform_get_irq - get an IRQ for a device
 * @file drivers/base/platform.c
 * @dev: platform device
 * @num: IRQ number index
 * @brief 包含 of_irq_get()
 */
int platform_get_irq(struct platform_device *dev, unsigned int num);

注册中断处理程序:

/**
 * @file kernel/irq/manage.c
 * @param irq 要申请的硬件中断号。
 * @param handler 是向系统登记的中断处理函数,是一个回调函数,中断发生时,系统调用这个函数。为 NULL 时使用默认的处理,这个相当于中断的上半段。
 * @param thread_fn 中断发生时,如果 handler 为 NULL就直接将 thread_fn 扔到内核线程中去执行。
 * @param irqflags 是中断处理的属性
 *          IRQF_SHARED - allow sharing the irq among several devices
 *          IRQF_PROBE_SHARED - set by callers when they expect sharing mismatches to occur
 *          IRQF_TIMER - Flag to mark this interrupt as timer interrupt
 *          IRQF_PERCPU - Interrupt is per cpu
 *          IRQF_NOBALANCING - Flag to exclude this interrupt from irq balancing
 *          IRQF_IRQPOLL - Interrupt is used for polling (only the interrupt that is
 *                registered first in an shared interrupt is considered for
 *                performance reasons)
 *          IRQF_ONESHOT - Interrupt is not reenabled after the hardirq handler finished.
 *                Used by threaded interrupts which need to keep the
 *                irq line disabled until the threaded handler has been run.
 *          IRQF_NO_SUSPEND - Do not disable this IRQ during suspend.  Does not guarantee
 *                   that this interrupt will wake the system from a suspended
 *                   state.  See Documentation/power/suspend-and-interrupts.txt
 *          IRQF_FORCE_RESUME - Force enable it on resume even if IRQF_NO_SUSPEND is set
 *          IRQF_NO_THREAD - Interrupt cannot be threaded
 *          IRQF_EARLY_RESUME - Resume IRQ early during syscore instead of at device
 *                resume time.
 *          IRQF_COND_SUSPEND - If the IRQ is shared with a NO_SUSPEND user, execute this
 *                interrupt handler after suspending interrupts. For system
 *                wakeup devices users need to implement wakeup detection in
 *                their interrupt handlers.
 * @param devname An ascii name for the claiming device. 使用 cat /proc/interrupt 可以查看中断程序名字。
 * @param dev_id 在中断共享时会用到,一般设置为这个设备的设备结构体或者 NULL。
 *      注册共享中断时不能为 NULL因为卸载时需要这个做参数避免卸载其它中断服务函数。
 * @return
 *      0表示成功
 *      -INVAL表示中断号无效或处理函数指针为 NULL
 *      -EBUSY表示中断已经被占用且不能共享。
 * @brief 申请 IRQ
 */
int request_threaded_irq(unsigned int irq, irq_handler_t handler,
             irq_handler_t thread_fn, unsigned long irqflags,
             const char *devname, void *dev_id);

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);

/**
 * @param irq 申请时所用的硬件中断号。
 * @brief 这个是要卸载的中断 Action 下的哪个服务函数
 */
void free_irq(unsigned int irq, void *dev_id);
  1. 如果是采用非共享方式注册中断,则 request_irq 和 free 的最后一个参数都要为 NULL。
  2. 如果采用共享中断方式,所有使用 request_irq 注册的中断时 flags 都要加上 IRQF_SHARED 这个共享参数,表明其实共享中断。
  3. 对于共享中断,每一个申请共享的中断,申请和释放时都要给 request_irq 和 free_irq 的最后一个参数 dev 和 id_dev 传递一个指针,将来来中断的时候,将会传递这个指针到每个中断函数中,而中断函数就可以用来区分到底是不是它的中断,是则执行,不是则判断后直接退出中断处理函数即可。同时在 free_irq 时也会使用这个指针,查找这个贡献中断链表上了所有注册的 irq只有在这个指针能对的上的时候才会删除它所在的链表节点如果是最后一个节点还要释放该中断。所在在编写中断处理函数时该指针必须是唯一的通常传的这个指针是该设备结构体的地址这个每个设备不一样所以肯定是唯一的。

对于资源管理 Linux 内核提供了一组 Devm API。Devm APIs 是一组便捷易用的接口通过该接口申请的内核资源是跟设备device有关的在设备device被 detached 或者驱动driver卸载unloaded会被自动释放。针对以上中断资源管理有以下 Devm 版本的 API

/**
 * @file kernel/irq/devres.c
 * @param dev 设备结构体。
 * @param irq 要申请的硬件中断号。
 * @param handler 是向系统登记的中断处理函数,是一个回调函数,中断发生时,系统调用这个函数(运行于中断上下文),
        函数需要判断是否为自身中断,若是则关闭自身中断并返回 IRQ_WAKE_THREAD。
        为 NULL 时使用默认的处理。
 * @param thread_fn 中断发生时,如果 handler 为 NULLthread_fn 将在默认 handler 执行完毕后以内核线程形式运行。
 *      否则待 handler 返回 IRQ_WAKE_THREAD 后以内核线程形式执行。
 * @param irqflags 是中断处理的属性
 *          IRQF_SHARED - allow sharing the irq among several devices
 *          IRQF_PROBE_SHARED - set by callers when they expect sharing mismatches to occur
 *          IRQF_TIMER - Flag to mark this interrupt as timer interrupt
 *          IRQF_PERCPU - Interrupt is per cpu
 *          IRQF_NOBALANCING - Flag to exclude this interrupt from irq balancing
 *          IRQF_IRQPOLL - Interrupt is used for polling (only the interrupt that is
 *                registered first in an shared interrupt is considered for
 *                performance reasons)
 *          IRQF_ONESHOT - Interrupt is not reenabled after the hardirq handler finished.
 *                Used by threaded interrupts which need to keep the
 *                irq line disabled until the threaded handler has been run.
 *          IRQF_NO_SUSPEND - Do not disable this IRQ during suspend.  Does not guarantee
 *                   that this interrupt will wake the system from a suspended
 *                   state.  See Documentation/power/suspend-and-interrupts.txt
 *          IRQF_FORCE_RESUME - Force enable it on resume even if IRQF_NO_SUSPEND is set
 *          IRQF_NO_THREAD - Interrupt cannot be threaded
 *          IRQF_EARLY_RESUME - Resume IRQ early during syscore instead of at device
 *                resume time.
 *          IRQF_COND_SUSPEND - If the IRQ is shared with a NO_SUSPEND user, execute this
 *                interrupt handler after suspending interrupts. For system
 *                wakeup devices users need to implement wakeup detection in
 *                their interrupt handlers.
 * @param devname An ascii name for the claiming device. 使用 cat /proc/interrupt 可以查看中断程序名字。
 * @param dev_id 在中断共享时会用到,一般设置为这个设备的设备结构体或者 NULL。
 *      注册共享中断时不能为 NULL因为卸载时需要这个做参数避免卸载其它中断服务函数。
 * @return
 *      0表示成功
 *      -INVAL表示中断号无效或处理函数指针为 NULL
 *      -EBUSY表示中断已经被占用且不能共享。
 * @brief 申请 IRQ
 */
extern int __must_check
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);

static inline int __must_check
devm_request_irq(struct device *dev, unsigned int irq, irq_handler_t handler,
         unsigned long irqflags, const char *devname, void *dev_id)
{
    return devm_request_threaded_irq(dev, irq, handler, NULL, irqflags,
                         devname, dev_id);
}

extern void devm_free_irq(struct device *dev, unsigned int irq, void *dev_id);

禁能 / 使能中断源:

// 禁能中断源
void disable_irq(int irq);
void disable_irq_nosync(int irq);
// 使能中断源
void enable_irq(int irq);

disable_irq_nosync() 与 disable_irq() 的区别在于前者立即返回,而后者等待目前的中断处理完成。注意,这 3 个函数作用于可编程中断控制器,因此,对系统内的所有 CPU 都生效。

而 local_irq_save() 和 local_irq_disable() 系列函数只屏蔽本 CPU 内的所有中断。

底半部机制

TODO: threaded_irq

中断底半部的机制主要有softirq、tasklet 和 work queue。

tasklet 基于 softirq 实现所以两者很相近。work queue 与它们完全不同,它靠内核线程实现。

softirq

软中断支持 SMP同一个 softirq 可以在不同的 CPU 上同时运行softirq 必须是可重入的。软中断是在编译期间静态分配的,它不像 tasklet 那样能被动态的注册或去除。kernel/softirq.c 中定义了一个包含 32 个 softirq_action 结构体的数组。每个被注册的软中断都占据该数组的一项。因此最多可能有 32 个软中断。2.6 版本的内核中定义了六个软中断HI_SOFTIRQ、TIMER_SOFTIRQ、NET_TX_SOFTIRQ、NET_RX_SOFTIRQ、SCSI_SOFTIRQ、TASKLET_SOFTIRQ。

软中断的特性:

  1. 一个软中断不会抢占另外一个软中断。
  2. 唯一可以抢占软中断的是中断处理程序。
  3. 其他软中断 (包括相同类型的) 可以在其他的处理其上同时执行。
  4. 一个注册的软中断必须在被标记后才能执行。
  5. 软中断不可以自己休眠 (即调用可阻塞的函数或 sleep 等)。
  6. 索引号小的软中断在索引号大的软中断之前执行。

tasklet

引入 tasklet最主要的是考虑支持 SMP提高 SMP 多个 cpu 的利用率;两个相同的 tasklet 决不会同时执行。tasklet 可以理解为 softirq 的派生,所以它的调度时机和软中断一样。对于内核中需要延迟执行的多数任务都可以用 tasklet 来完成,由于同类 tasklet 本身已经进行了同步保护,所以使用 tasklet 比软中断要简单的多而且效率也不错。tasklet 把任务延迟到安全时间执行的一种方式在中断期间运行即使被调度多次tasklet 也只运行一次,不过 tasklet 可以在 SMP 系统上和其他不同的 tasklet 并行运行。在 SMP 系统上tasklet 还被确保在第一个调度它的 CPU 上运行,因为这样可以提供更好的高速缓存行为,从而提高性能。

tasklet 的特性:.不允许两个两个相同类型的 tasklet 同时执行,即使在不同的处理器上。

work queue

如果推后执行的任务需要睡眠,那么就选择工作队列。另外,如果需要用一个可以重新调度的实体来执行你的下半部处理,也应该使用工作队列。它是唯一能在进程上下文运行的下半部实现的机制,也只有它才可以睡眠。这意味着在需要获得大量的内存时、在需要获取信号量时,在需要执行阻塞式的 I/O 操作时它都会非常有用。work queue 造成的开销最大,因为它要涉及到内核线程甚至是上下文切换。这并不是说 work queue 的低效,但每秒钟有数千次中断,就像网络子系统时常经历的那样,那么采用其他的机制可能更合适一些。 尽管如此,针对大部分情况工作队列都能提供足够的支持。

工作队列特性:

  1. 工作队列会在进程上下文中执行!
  2. 可以阻塞。(前两种机制是不可以阻塞的)
  3. 可以被重新调度。(前两种只可以被中断处理程序打断)
  4. 使用工作队列的两种形式:
    1. 缺省工作者线程 (works threads)
    2. 自建的工作者线程
  5. 在工作队列和内核其他部分之间使用锁机制就像在其他的进程上下文一样。
  6. 默认允许响应中断。
  7. 默认不持有任何锁。

4、softirq 和 tasklet 共同点

软中断和 tasklet 都是运行在中断上下文中,它们与任一进程无关,没有支持的进程完成重新调度。所以软中断和 tasklet 不能睡眠、不能阻塞,它们的代码中不能含有导致睡眠的动作,如减少信号量、从用户空间拷贝数据或手工分配内存等。也正是由于它们运行在中断上下文中,所以它们在同一个 CPU 上的执行是串行的,这样就不利于实时多媒体任务的优先处理。

总结

下半部 上下文 顺序执行保障
软中断 中断 没有
Tasklet 中断 同类型不能同时执行
工作队列 进程 没有(和进程上下文一样被调度)

简单地说,一般的驱动程序的编写者需要做两个选择。 首先,你是不是需要一个可调度的实体来执行需要推后完成的工作――从根本上来说,有休眠的需要吗?要是有,工作队列就是你的惟一选择。 否则最好用 tasklet。要是必须专注于性能的提高那么就考虑 softirq。