1.概述

外设产生中断后,Linux内核会执行一个具体的函数来响应中断,此函数被称为中断服务函数。中断服务函数运行在中断上下文中,若中断线程化后,则中断事件在进程上下文中处理。不同的外设中断源,其对应的中断服务函数一般也不同。因此在使用具体的外设中断之前,需要注册对应的中断服务函数,并指明软件中断号、中断标志、中断名称及传递给中断服务函数的参数等。

2.中断注册接口介绍

2.1.request_irq

使用request_irq注册一个中断服务函数,内部调用的是request_threaded_irq函数,只是将thread_fn设置为NULLirq为软件中断号,handler为中断服务函数,flags为中断标志,name中断名称,dev为传递给中断服务函数的参数,可为NULL,指明IRQF_SHARED时,必须传递此参数。返回值为0表示注册成功,非0表示注册失败。

    [include/linux/interrupt.h]
    static inline int __must_check
    request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags,
        const char *name, void *dev)
    irq:软件中断号
    handler:中断服务函数
    flags:中断标志
        #define IRQF_TRIGGER_RISING	0x00000001    // 上升沿触发
        #define IRQF_TRIGGER_FALLING 0x00000002   // 下降沿触发
        #define IRQF_TRIGGER_HIGH	0x00000004    // 高电平触发
        #define IRQF_TRIGGER_LOW	0x00000008    // 下降沿触发
        // 共享中断,多个设备共享一个中断号(中断引脚),在中断处理程序中需要查询那个外设发生了中断
        #define IRQF_SHARED		    0x00000080
        #define IRQF_PROBE_SHARED	0x00000100    // 中断处理程序允许sharing mismach发生
        #define __IRQF_TIMER		0x00000200
        // 时钟中断
        #define IRQF_TIMER		(__IRQF_TIMER | IRQF_NO_SUSPEND | IRQF_NO_THREAD)
        #define IRQF_PERCPU		    0x00000400    // 某个CPU特有的中断
        #define IRQF_NOBALANCING	0x00000800    // 禁止CPU之间的中断均衡
        #define IRQF_IRQPOLL		0x00001000    // 中断被用作轮训
        // 中断线程执行完成之前不会打开中断,直至中断线程处理完所有中断,才会重新使能中断
        #define IRQF_ONESHOT		0x00002000
        #define IRQF_NO_SUSPEND		0x00004000    // 系统suspend期间不要关闭中断
        #define IRQF_FORCE_RESUME	0x00008000    // 系统resume时必须强制使能此中断
        #define IRQF_NO_THREAD		0x00010000    // 此中断不能中断线程化
        #define IRQF_EARLY_RESUME	0x00020000    // 系统resume时提前使能此中断
    name:中断名称
    dev:传递给中断服务函数的参数,可为NULL,共享中断必须传递此参数

2.2.request_threaded_irq

使用request_threaded_irq注册一个中断服务函数和中断线程化后调用的函数。irq为软件中断号,handler为中断服务函数,thread_fn中断线程化后内核线程执行的函数,flags为中断标志,name中断名称,dev为传递给中断服务函数的参数,可为NULL,指明IRQF_SHARED时,必须传递此参数。返回值为0表示注册成功,非0表示注册失败。

    [include/linux/interrupt.h]
    int __must_check
    request_threaded_irq(unsigned int irq, irq_handler_t handler,
            irq_handler_t thread_fn,
            unsigned long flags, const char *name, void *dev)
    irq:软件中断号
    handler:中断服务函数
    thread_fn:中断线程化后调用的函数
    flags:中断标志,和request_irq的中断标志含义一样
    name:中断名称
    dev:给中断服务函数传递的参数

2.3.free_irq

request_irqrequest_threaded_irq注册的中断服务函数使用free_irq函数释放。

    [include/linux/interrupt.h]
    void free_irq(unsigned int irq, void *dev_id)
    irq:软件中断号
    dev_id:给中断服务函数传递的参数

3.源代码分析

由于request_irq内部调用的是request_threaded_irq,因此这里只分析request_threaded_irq函数。request_threaded_irq函数的主要工作是分配一个struct irqaction结构体并设置相关成员,设置的主要成员有中断服务函数handler,软件中断号irq、中断线程化执行的函数thread_fn及中断标志flagshandlerirqthread_fnflags根据传入的参数设置。最后将分配的struct irqaction结构体挂接到struct irq_desc结构体中的action链表下面,非共享中断action链表只有一个节点,共享中断action链表有多个节点。中断发生后,会遍历action链表,使用handlerthread_fn等处理中断。使用request_threaded_irq注册中断时需要注意一下几点:
(1)IRQ为软件(虚拟)中断号,是由硬件中断号映射而来。
(2)primary handler(中断上半部分处理函数)和threaded_fn(中断下半部分处理函数)不能同时为NULL。
(3)当primary handler为NULL且硬件中断控制器不支持硬件ONESHOT功能时,应设置IRQF_ONESHOT标志来确保不会产生中断风暴。
(4)启用了中断线程化,primary handler函数应该返回IRQ_WAKE_THREAD,以唤醒中断处理线程。

    [include/linux/interrupt.h]
    // 和具体的中断逻辑处理相关
    struct irqaction {
        irq_handler_t		handler;  // 中断服务函数,注册中断时设置
        void			*dev_id;  // 中断服务函数参数
        void __percpu		*percpu_dev_id;
        struct irqaction	*next;  // 共享中断有多个irqaction结构体,next指向下一个irqaction
        irq_handler_t		thread_fn;  // 中断线程化执行的函数
        struct task_struct	*thread;  // 指向被中断线程的task_struct
        struct irqaction	*secondary;  // 指向中断下半部分的irqaction
        unsigned int		irq;  // 软件中断号
        unsigned int		flags;  // 中断标志
        unsigned long		thread_flags;  // 线程标志
        unsigned long		thread_mask;  // 跟踪中断线程活动的位图
        const char		*name;  // 中断名称
        struct proc_dir_entry	*dir;  // 指向/proc/irq/NN/name
    } ____cacheline_internodealigned_in_smp;

    [include/linux/interrupt.h]
    request_threaded_irq
      // 检查参数,设置IRQF_SHARED标志时,必须向中断服务函数传递参数,即必须传入dev_id参数,共享中断根据
      // dev_id区分产生中断的外设
      if (((irqflags & IRQF_SHARED) && !dev_id) ||
         (!(irqflags & IRQF_SHARED) && (irqflags & IRQF_COND_SUSPEND)) ||
         ((irqflags & IRQF_NO_SUSPEND) && (irqflags & IRQF_COND_SUSPEND)))
      ->irq_to_desc  // 获取软件中断号对应的irq_desc
        ->radix_tree_lookup  // 定义CONFIG_SPARSE_IRQ,从irq_desc_tree中查找
        // 没有定义CONFIG_SPARSE_IRQ,从irq_desc数组中查找,索引为软件中断号
        return (irq < NR_IRQS) ? irq_desc + irq : NULL
      ->handler = irq_default_primary_handler  // 若没有设置中断服务函数,则设置默认的中断服务函数
      ->kzalloc  // 分配irqaction结构体内存
      // 设置irqaction结构体
      action->handler = handler;  // 设置中断处理函数
      action->thread_fn = thread_fn;  // 设置中断线程化内核线程执行的函数
      action->flags = irqflags;  // 中断标志
      action->name = devname;  // 中断名称
      action->dev_id = dev_id;  // 传递给中断服务函数的参数
      ->__setup_irq
        new->irq = irq  // 设置irqaction结构体中的软件中断号irq
        ->irq_settings_is_nested_thread  // 检查此中断是否嵌套到另一个中断线程中
            return desc->status_use_accessors & _IRQ_NESTED_THREAD
        ->new->handler = irq_nested_primary_handler  // 如嵌套,则设置新的中断处理函数
        ->irq_settings_can_thread  // 中断是否可以线程化
        ->irq_setup_forced_threading

Linux中断强制线程化是一个过渡方案,目前还有很多的驱动使用旧版本的API注册中断,这些驱动的中断处理通常采用上下半部分的方式。

          force_irqthreads  // 如果force_irqthreads定义为true,则强制将中断线程化
          new->flags |= IRQF_ONESHOT  // 强制中断服务函数线程化,其在关中断的状态下运行,因此需要设置此标志
          // 如果handler不为irq_default_primary_handler且设置了中断线程化执行的函数
          new->secondary = kzalloc()  // 则分配中断下半部分使用的irqaction结构体
          // 设置强制中断线程化的中断服务函数
          new->secondary->handler = irq_forced_secondary_handler;
          // 设置中断线程化内核线程执行的函数
          new->secondary->thread_fn = new->thread_fn;
          new->secondary->dev_id = new->dev_id;  // 设置传递给中断服务函数的参数
          new->secondary->irq = new->irq;  // 设置软件中断号
          new->secondary->name = new->name;  // 设置中断名称
          ->set_bit  // 设置强制中断线程化标志,设置到irqaction的thread_flags标志中
          new->thread_fn = new->handler
          // 设置默认的中断服务函数为irq_default_primary_handler
          new->handler = irq_default_primary_handler  
        ->setup_irq_thread  // 如果传入了thread_fn,则创建中断线程,优先级为50,调度策略为SCHED_FIFO
          if (!secondary) {  // 没有强制中断线程化
              // kthread_create创建的内核线程由内核线程kthreadd创建,返回创建线程的task_struct指针
              // 中断线程唤醒后运行的第一个函数为irq_thread
              t = kthread_create(irq_thread, new, "irq/%d-%s", irq, new->name);
          } else {  // 强制中断线程化
              t = kthread_create(irq_thread, new, "irq/%d-s-%s", irq, new->name);
              param.sched_priority -= 1;
          }
          ->sched_setscheduler_nocheck  // 设置内核线程的优先级
          new->thread = t  // 使irqaction中的thread指向创建线程的task_struct
          ->set_bit  // 设置irqaction中的标记为IRQTF_AFFINITY
        ->setup_irq_thread  // 如果需要中断下半部分,则启动处理中断下半部分的内核线程
        // 将新分配的irqaction结构体追加到irq_desc结构体的action链表的末尾,共享中断,action
        // 链表有多个节点,非共享中断,action链表只有一个节点
        *old_ptr = new
        ->irq_pm_install_action  // 电源管理相关  
        desc->irq_count = 0
        desc->irqs_unhandled = 0
        // kthread_create创建的内核线程默认不运行,需要使用wake_up_process唤醒
        ->wake_up_process(new->thread)  // 唤醒中断线程化的线程
        ->wake_up_process(new->secondary->thread)  // 唤醒中断下半部分的线程
        ->register_irq_proc  // 将中断注册到proc文件系统中

下面分析一下free_irq的执行过程,主要功能是释放掉request_threaded_irq注册的资源。

    [include/linux/interrupt.h]
    free_irq
      ->irq_to_desc  // 根据软件中断号获取中断描述符
      desc->affinity_notify = NULL  // 如果定义CONFIG_SMP,则将中断对CPU的亲和力设置为NULL
      ->__free_irq
        ->irq_to_desc  // 根据软件中断号获取中断描述符
        ->in_interrupt  // 检测是否在中断上下文中free IRQ
        ->raw_spin_lock_irqsave  // 获取自旋锁并禁止中断
        // 遍历action链表,查找要释放的action
        action_ptr = &desc->action;
        for (;;) {
            action = *action_ptr;
            if (action->dev_id == dev_id)  // action中的dev_id和传入的dev_id相等,则跳出循环
                break;
            action_ptr = &action->next;
        }
        // 从链表中移除此action
        *action_ptr = action->next
        ->irq_pm_remove_action  // 电源管理相关操作
        // 如果action链表为空,说明此中断不需要处理,需要关闭中断
        if (!desc->action) {  // action链表为空
            irq_shutdown(desc);  // 关闭此中断
            irq_release_resources(desc);  // 释放中断资源
        }
        ->raw_spin_unlock_irqrestore  // 释放自旋锁
        ->unregister_handler_proc  // 注销proc文件系统中的中断信息
        ->synchronize_irq  // 同步中断,等待其他CPU处理完要free的中断任务
          ->irq_to_desc  // 获取中断描述符
          ->__synchronize_hardirq
            do {
                unsigned long flags;
                while (irqd_irq_inprogress(&desc->irq_data))  // 获取中断处理状态
                    // arm32 cpu_relax执行一条内存屏障指令,arm64执行一条yield指令和内存屏障指令
                    // yield指令可以降低CPU功耗
                    cpu_relax();
                // 上述获取中断处理状态缺乏严格的内存屏障,可能存在误读的情况,因此这里再次读取
                raw_spin_lock_irqsave(&desc->lock, flags);  // 加锁
                inprogress = irqd_irq_inprogress(&desc->irq_data);  // 读取
                raw_spin_unlock_irqrestore(&desc->lock, flags);  // 解锁
            } while (inprogress);  // 循环读取,直到其他CPU处理完中断
          ->wait_event  // 等待中断处理线程处理完中断
        ->kthread_stop  // 停止中断处理线程
        ->kthread_stop  // 停止中断下半部分处理线程
        ->kfree  // 释放中断下半部分secondary的内存
      ->kfree  // 释放action对应的内存

4.zynq7k串口0的中断注册

zynq7k串口0的中断注册在cdns_uart_startup函数中完成,irq为已经映射好的软件中断号, cdns_uart_isr 为注册的中断处理函数,flags为0,中断类型和中断触发方式已在设备树中指定,即串口0的中断类型为SPI中断,中断触发方式为高电平,中断名称为"xuartps"port为给中断处理函数传递的参数。中断处理函数cdns_uart_startup在Linux内核高层中断处理中分析。

    [drivers/tty/serial/xilinx_uartps.c]
    #define CDNS_UART_NAME		"xuartps"  // 中断名称
    cdns_uart_startup
      ->request_irq(port->irq, cdns_uart_isr, 0, CDNS_UART_NAME, (void *)port)  // 注册中断

参考资料

  1. Linux kernel V4.6版本源码
  2. 《奔跑吧 Linux内核:基于Linux 4.x内核源代码问题分析》
  3. 《Zynq-7000 SoC Technical Reference Manual》
  4. 《ARM® Generic Interrupt Controller Architecture version 2.0》
Logo

瓜分20万奖金 获得内推名额 丰厚实物奖励 易参与易上手

更多推荐