Linux中断子系统之中断机制

Linux内核之中断机制

一、什么是中断?

中断通常被定义为一个事件,该事件改变处理器执行的指令顺序。这样的事件与CPU芯片内外部硬件电路产生的电信号相对应。

中断通常分为同步中断和异步中断:

同步中断是当指令执行时由CPU控制单元产生的,之所以称为同步,是因为只有在一条指令终止执行后CPU才会发出中断。

异步中断是由其他硬件设备依照CPU时钟信号随机产生的。

◎ 把同步中断称为异常(exception)。

◎ 把异步中断称为中断(interrupt)。

这两类中断的共同特点是什么?如果CPU当前不处于内核态,则发起从用户态到内核态的切换。在内核中执行一个专门的例程,称为中断服务例程(interrupt service routine)。

另一方面,异常是由程序的错误产生的,或者是由内核必须处理的异常条件产生的。第一种情况下,内核通过发送一个每个Unix/Linux程序员都熟悉的信号来处理异常。第二种情况下,内核执行恢复异常需要的所有步骤,例如缺页异常等。

二、中断控制器

不同的体系结构对中断控制器有着不同的设计理念, 例如ARM公司提供了一个通用中断控制器(Generic Interrupt Controller, GIC),x86体系架构则采用了高级可编程中断控制器(Advanced Programmable Interrupt Controller, APIC)。这篇文章我们主要是基于ARM平台来介绍中断管理的实现。

GIC规范支持下面中断类型:

  • 软件触发中断(Software Generated Interrupt,SGI ):通常用于多核之间通信。硬件中断号从ID0~ID15
  • 私有外设中断(Private Peripheral Interrupt,PPI ):这是每个处理核心私有的中断。硬件中断号从ID16~ID31
  • 外设中断(Shared Peripheral Interrupt,SPI):共用的外设中断。硬件中断号从ID32~IDX(IDX的意思是不同的ARM中断控制器,拥有的中断号数目不一样。

GIC中断控制器主要由两个部分组成,分别是 仲裁单元(distributor ,为每一个中断源维护一个状态机,支持inactive、pending、active和active and pending状态)和 CPI接口模块(CPU Interface)。

正是由于中断控制器的存在,使多个设备产生的中断复用处理器的一条中断线。中断控制器仲裁并按顺序转递设备发出的中断给

https://daniel-pic.oss-cn-shanghai.aliyuncs.com/interrupt.svg

中断控制器有如下主要功能:

  • 中断复用和路由「interrupt multiplexing and routing」
  • 中断优先处理「interrupt prioritizing」
  • 中断屏蔽「interrupt masking」

三、中断关键的数据结构

struct irq_desc 中断描述符

结构体struct irq_desc用于描述和管理中断,其大多数字段由中断处理核心部分所使用。如果没有选用CONFIG_SPARSE_IRQ配置选项,中断描述符和irq之间的关联映射是通过数组,根据irq能够直接索引到对应的中断描述符。否则,通过基数数「radix tree」构建两者之间的关联映射,依据irq遍历搜索基数树才能寻找到对应的中断描述符。在ARM64的内核配置中,为了能够动态分配和管理中断描述符,默认启用了CONFIG_SPARSE_IRQ选项。

struct irq_desc {
    struct irq_common_data    irq_common_data;
    struct irq_data        irq_data;           /* 中断控制器的硬件数据 */
    unsigned int __percpu    *kstat_irqs;
    irq_flow_handler_t    handle_irq;          /* 中断控制器驱动的处理函数 */
    struct irqaction    *action;               /* 设备驱动的处理函数 */
    unsigned int        status_use_accessors;  /* 中断描述符的状态 */
    unsigned int        core_internal_state__do_not_mess_with_it;
    unsigned int        depth;        /* nested irq disables */
    unsigned int        wake_depth;    /* nested wake enables */
    unsigned int        tot_count;
    unsigned int        irq_count;    /* For detecting broken IRQs */
    unsigned long       last_unhandled;    /* Aging timer for unhandled count */
    unsigned int        irqs_unhandled;
    atomic_t        threads_handled;
    int            threads_handled_last;
    raw_spinlock_t      lock;
    struct cpumask        *percpu_enabled;
    const struct cpumask    *percpu_affinity;
#ifdef CONFIG_SMP
    const struct cpumask    *affinity_hint;
    struct irq_affinity_notify *affinity_notify;
#ifdef CONFIG_GENERIC_PENDING_IRQ
    cpumask_var_t        pending_mask;
#endif
#endif
    unsigned long        threads_oneshot;
    atomic_t        threads_active;
    wait_queue_head_t       wait_for_threads;
#ifdef CONFIG_PM_SLEEP
    unsigned int        nr_actions;
    unsigned int        no_suspend_depth;
    unsigned int        cond_suspend_depth;
    unsigned int        force_resume_depth;
#endif
#ifdef CONFIG_PROC_FS
    struct proc_dir_entry    *dir;
#endif
#ifdef CONFIG_GENERIC_IRQ_DEBUGFS
    struct dentry        *debugfs_file;
    const char        *dev_name;
#endif
#ifdef CONFIG_SPARSE_IRQ
    struct rcu_head        rcu;
    struct kobject        kobj;
#endif
    struct mutex        request_mutex;
    int            parent_irq;
    struct module        *owner;
    const char        *name;
} ____cacheline_internodealigned_in_smp;

◎ action提供了一个操作链,需要在中断发生时执行。由中断通知的设备驱动程序,可以将与之相关的处理程序函数放置在此处。如果一个IRQ line允许共享,那么该链表中的成员可以是多个,否则,该链表只有一个节点。

struct irq_data 包含中断控制器的硬件数据。

struct irq_data  
{  
    unsigned int        irq;              /* 虚拟中断号 */
    unsigned long       hwirq;            /* 硬件中断号 */
    unsigned int        node;  
    unsigned int        state_use_accessors;  
    struct irq_chip     *chip;            /* 对中断控制器的操作 */
    struct irq_domain   *domain;  
    void                *handler_data;  
    void                *chip_data;  
    struct msi_desc     *msi_desc;  
#ifdef CONFIG_SMP  
   cpumask_var_t        affinity;  
#endif  
};

irq_chip: 描述中断控制器的结构体

结构体struct irq_chip用于描述和管理硬件中断芯片(控制器),称之为中断芯片描述符,其大部分字段都是由中断控制器驱动程序负责初始化的函数指针。在初始化后,内部中断处理函数"函数前面带有两个下划线__"就能直接调用它们。

struct irq_chip  
{  
    const char   *name;                   /* 中断控制器名称 */
    unsigned int    (*irq_startup)(struct irq_data *data);  
    void  (*irq_shutdown)(struct irq_data *data);  
    void  (*irq_enable)(struct irq_data *data);  
    void  (*irq_disable)(struct irq_data *data);  
    void  (*irq_ack)(struct irq_data *data);  
    void  (*irq_mask)(struct irq_data *data);  
    void  (*irq_mask_ack)(struct irq_data *data);  
    void  (*irq_unmask)(struct irq_data *data);  
    void  (*irq_eoi)(struct irq_data *data);  
int      (*irq_set_affinity)(struct irq_data *data,  
const struct cpumask *dest, bool force);  
    int      (*irq_retrigger)(struct irq_data *data);  
int   (*irq_set_type)(struct irq_data *data,  
unsigned int flow_type);  
    int   (*irq_set_wake)(struct irq_data *data, unsigned int on);  
    void  (*irq_bus_lock)(struct irq_data *data);  
    void  (*irq_bus_sync_unlock)(struct irq_data *data);  
    void  (*irq_cpu_online)(struct irq_data *data);  
    void  (*irq_cpu_offline)(struct irq_data *data);  
    void  (*irq_suspend)(struct irq_data *data);  
    void  (*irq_resume)(struct irq_data *data);  
    void  (*irq_pm_shutdown)(struct irq_data *data);  
    void  (*irq_print_chip)(struct irq_data *data, struct seq_file *p);  
    unsigned long   flags; 

    /* Currently used only by UML, mightdisappear one day.*/  
#ifdef CONFIG_IRQ_RELEASE_METHOD  
    void       (*release)(unsigned int irq, void *dev_id);  
#endif  
};

以下说明结构体的部分主要字段:

  • irq_ack:当开始处理新中断时调用的回调函数。

  • irq_mask:屏蔽该中断。

  • irq_mask_ack确认一个中断,并在接下来屏蔽该中断。

  • irq_eoi:中断结束。

  • irq_set_affinity:在多处理器系统中,可指定用哪个CPU来处理特定的IRQ。

  • set_type:设置IRQ的电流类型,比如IRQ_TYPE_EDGE_RISING和IRQ_TYPE_LEVEL_HIGH。

struct irq_domain 中断域

结构体struct irq_domain用于统一管理Linux内核采用的中断编号。在中断初始化过程中,Linux内核会从特定整数区间中选取一个整数作为一个中断源「interrupt source」的编号,并且使每个中断源都有一个独一无二且不重复的编号。如果系统仅存有一个中断控制器,那么能简单地给每个引脚对应的中断源分配一个编号。不过,如前所述,在有些情况下系统可能配置了多个中断控制器,所以中断源的编号方式将会有点复杂。因此内核需要一种机制去管理硬件中断编号「hwirq」与Linux中断编号「irq」之间的映射,以便使多个中断控制器之间不会出现重复相同的irq。

struct irq_domain {
    struct list_head link; /* 将irq_domain连接到全局链表irq_domain_list */
    const char *name;
    const struct irq_domain_ops *ops; /* irq_domain映射操作函数集 */
    void *host_data;
    unsigned int flags;
    unsigned int mapcount;   /* 已映射的hwirq数量 */

    /* Optional data */
    struct fwnode_handle *fwnode;
    enum irq_domain_bus_token bus_token;
    struct irq_domain_chip_generic *gc;
#ifdef    CONFIG_IRQ_DOMAIN_HIERARCHY
    struct irq_domain *parent;
#endif
#ifdef CONFIG_GENERIC_IRQ_DEBUGFS
    struct dentry        *debugfs_file;
#endif

    /* reverse map data. The linear map gets appended to the irq_domain */
    irq_hw_number_t hwirq_max;           /* 中断控制器的最大hwirq */
    unsigned int revmap_direct_max_irq;  /* 能为直接映射的中断控制器设置的最大hwirq */
    unsigned int revmap_size;            /* linear_revmap数组的容量 */
    struct radix_tree_root revmap_tree;  /* 通过基数树构建hwirq->irq的逆映射 */
    struct mutex revmap_tree_mutex;
    unsigned int linear_revmap[];        /* 通过数组构建hwirq->irq的逆映射 */
};

struct irq_domain_ops 是 irq_domain 映射操作函数集

irq_domain 的引入相当于一个中断控制器就是一个 irq_domain。这样一来所有的中断控制器就会出现级联的布局。利用树状的结构可以充分的利用 irq 数目,而且每一个 irq_domain 区域可以自己去管理自己 interrupt 的特性。

struct irq_domain_ops {
 int (*match)(struct irq_domain *d, struct device_node *node,
       enum irq_domain_bus_token bus_token);
 int (*select)(struct irq_domain *d, struct irq_fwspec *fwspec,
        enum irq_domain_bus_token bus_token);
 int (*map)(struct irq_domain *d, unsigned int virq, irq_hw_number_t hw);
 void (*unmap)(struct irq_domain *d, unsigned int virq);
 int (*xlate)(struct irq_domain *d, struct device_node *node,
       const u32 *intspec, unsigned int intsize,
       unsigned long *out_hwirq, unsigned int *out_type);
 ......
};
  • match:用于中断控制器设备与 irq_domain 的匹配
  • map:用于硬件中断号与 Linux 中断号的映射
  • xlate:通过 device_node,解析硬件中断号和触发方式

struct irqaction 主要是用来存设备驱动注册的中断处理函数:

struct irqaction  
{  
    irq_handler_t       handler;     /* 设备驱动里的中断处理函数 */
    unsigned long       flags;  
    void                *dev_id;     /* 设备id */
    void __percpu       *percpu_dev_id;  
    struct irqaction   *next;  
    int                 irq;         /* 中断号 */
    irq_handler_t       thread_fn;  
    struct task_struct *thread;  
    unsigned long       thread_flags;  
    unsigned long       thread_mask;  
    const char          *name;       /* 中断名称,产生中断的硬件的名字 */
    struct proc_dir_entry   *dir;  
} ____cacheline_internodealigned_in_smp;

该结构中最重要的成员是处理程序函数本身,即handler成员,这是一个函数指针,位于结构的起始处。在设备请求一个系统中断,而中断控制器通过引用发中断将该请求转发到处理器的时候,内核将调用该处理程序函数。

flag是一个标志变量,通过位图描述了IRQ的一些特性。

◎ 对共享的IRQ设置IRQF_SHARED,表示有多于一个设备使用该IRQ电路。

◎ 如果IRQ对内核熵池有贡献,将设置IRQF_SAMPLE_RANDOM。

◎ IRQF_DISABLED表示IRQ的处理程序必须在禁用中断的情况下执行。

◎ IRQF_TIMER表示时钟中断

https://daniel-pic.oss-cn-shanghai.aliyuncs.com/irq%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84.jpeg

上面的结构体 struct irq_desc 是设备驱动加载的过程中完成的,让设备树中的中断能与具体的中断描述符 irq_desc 匹配,其中 struct irqaction 保存着设备的中断处理函数。右边框内的结构体主要是在中断控制器驱动加载的过程中完成的,其中 struct irq_chip 用于对中断控制器的硬件操作,struct irq_domain 用于硬件中断号到 Linux irq 的映射。

四、硬件中断号和Linux中断号的映射

gic_init_bases()函数调用irq_domain_create_tree()创建一个irq domain并将该irq domain链接到系统irq domain链表irq_domain_list中,__irq_domain_add()是创建irq domain的核心函数。

static const struct irq_domain_ops gic_irq_domain_ops = {
    .translate = gic_irq_domain_translate,
    .alloc = gic_irq_domain_alloc,
    .free = gic_irq_domain_free,
    .select = gic_irq_domain_select,
};

/*@dist_base:Distributor的地址,
 *@rdist_regs:GIC设备中所有Redistributor的地址信息
 *@nr_redist_regions:Redistributor的个数
 *@redist_stride:Redistributor区域之间的步长,
 *@handle:是该设备节点对应的数据结构struct device_node中的fwnode,
 *         里面是处理该设备的一些回调函数
 */
static int __init gic_init_bases(void __iomem *dist_base,
                 struct redist_region *rdist_regs,
                 u32 nr_redist_regions,
                 u64 redist_stride,
                 struct fwnode_handle *handle)
{
    ......       

    /*在系统中为GIC V3注册一个irq domain的数据结构. irq_domain主要作用是映射硬件中断号*/
    gic_data.domain = irq_domain_create_tree(handle, &gic_irq_domain_ops,
                         &gic_data);
    ......
        /* 一个设备节点中可能有多个irq_domain,这些irq_domain用于不同的的,DOMAIN_BUS_WIRED
     * 表示该irq_domain用于wired IRQS
     */
        irq_domain_update_bus_token(gic_data.domain, DOMAIN_BUS_WIRED);

}

static inline struct irq_domain *irq_domain_create_tree(struct fwnode_handle*fwnode,
                     const struct irq_domain_ops *ops,
                     void *host_data)
{
    return __irq_domain_add(fwnode, 0, ~0, 0, ops, host_data);
}

/**
 * 分配并初始化一个irq_domain数据结构
 * @fwnode: 中断控制器的固件节点
 * @size: 线性映射的大小; 基树映射这个值填0,GIC V3初始化时,
 *        这个值为0,所以GIC V3中断映射方式为基树映射
 * @hwirq_max: 中断控制器支持的最大硬件中断号,GIC V3初始化时,这个值为-1
 * @direct_max:直接映射的最大值,,GIC V3初始化时,这个值为0
 * @ops:映射硬件中断号时需要调用的回调函数
 * @host_data: 中断控制器私有数据,对于GIC V3来说,这个数据就是GIC V3的全局变量gic_data
 */
struct irq_domain *__irq_domain_add(struct fwnode_handle *fwnode, int size,
                    irq_hw_number_t hwirq_max, int direct_max,
                    const struct irq_domain_ops *ops,
                    void *host_data)
{
    struct device_node *of_node = to_of_node(fwnode);
    struct irqchip_fwid *fwid;
    struct irq_domain *domain;

    static atomic_t unknown_domains;

    /*分配一个domain,(sizeof(unsigned int) * size)用于为linear_revmap成员变量分配内存空间*/
    domain = kzalloc_node(sizeof(*domain) + (sizeof(unsigned int) * size),
                  GFP_KERNEL, of_node_to_nid(of_node));
    if (!domain)
        return NULL;

    /* 对于GIC V3来说,1.设置GIC V3 irq domain对应的固件描述符fwnode,
     * 这个固件描述符用来匹配设备节点和该设备节点上的一些方法,2.根据固件描述符fwnode的名称
         * 来设置irq domain的名称
     */
    if (fwnode && is_fwnode_irqchip(fwnode)) {
        fwid = container_of(fwnode, struct irqchip_fwid, fwnode);

        switch (fwid->type) {
        case IRQCHIP_FWNODE_NAMED:
        case IRQCHIP_FWNODE_NAMED_ID:
            domain->fwnode = fwnode;
            domain->name = kstrdup(fwid->name, GFP_KERNEL);
            if (!domain->name) {
                kfree(domain);
                return NULL;
            }
            domain->flags |= IRQ_DOMAIN_NAME_ALLOCATED;
            break;
        default:
            domain->fwnode = fwnode;
            domain->name = fwid->name;
            break;
        }
#ifdef CONFIG_ACPI
    } else if (is_acpi_device_node(fwnode)) {
        struct acpi_buffer buf = {
            .length = ACPI_ALLOCATE_BUFFER,
        };
        acpi_handle handle;

        handle = acpi_device_handle(to_acpi_device_node(fwnode));
        if (acpi_get_name(handle, ACPI_FULL_PATHNAME, &buf) == AE_OK) {
            domain->name = buf.pointer;
            domain->flags |= IRQ_DOMAIN_NAME_ALLOCATED;
        }

        domain->fwnode = fwnode;
#endif
    } else if (of_node) {
        char *name;

        /*
         * DT paths contain '/', which debugfs is legitimately
         * unhappy about. Replace them with ':', which does
         * the trick and is not as offensive as '\'...
         */
        name = kasprintf(GFP_KERNEL, "%pOF", of_node);
        if (!name) {
            kfree(domain);
            return NULL;
        }

        strreplace(name, '/', ':');

        domain->name = name;
        domain->fwnode = fwnode;
        domain->flags |= IRQ_DOMAIN_NAME_ALLOCATED;
    }

    if (!domain->name) {
        if (fwnode)
            pr_err("Invalid fwnode type for irqdomain\n");
        domain->name = kasprintf(GFP_KERNEL, "unknown-%d",
                     atomic_inc_return(&unknown_domains));
        if (!domain->name) {
            kfree(domain);
            return NULL;
        }
        domain->flags |= IRQ_DOMAIN_NAME_ALLOCATED;
    }

    of_node_get(of_node);

    /* Fill structure */
    /*初始化domian*/
    INIT_RADIX_TREE(&domain->revmap_tree, GFP_KERNEL);
    mutex_init(&domain->revmap_tree_mutex);
    domain->ops = ops;
    domain->host_data = host_data;
    /*GIC V3初始化时,domain->hwirq_max = -1;*/
    domain->hwirq_max = hwirq_max;
    /*GIC V3初始化时,domain->revmap_size = 0;*/
    domain->revmap_size = size;
    /*GIC V3初始化时,domain->revmap_direct_max_irq = 0;*/
    domain->revmap_direct_max_irq = direct_max;
    irq_domain_check_hierarchy(domain);

    mutex_lock(&irq_domain_mutex);
    debugfs_add_domain_dir(domain);
    /*将该irq domain链接到系统irq domain链表irq_domain_list中*/
    list_add(&domain->link, &irq_domain_list);
    mutex_unlock(&irq_domain_mutex);

    pr_debug("Added domain %s\n", domain->name);
    return domain;
}

外设的驱动创建硬中断和虚拟中断号的映射关系

可见最终所有的domain都添加到irq_domain_list链表当中的。但是我们知道,我们调用request_irq用的是virq号,而并不是hirq,因为hirq号不是唯一的,每个domain都可能会存在一个相同的hirq,因此Linux才增加的virq编号,这个编号是独立的且唯一。现在domain已经注册好了,但是hirq和virq直接的关系还没有建立起来,因此我们还不能用virq来注册中断,如下是把hirq和virq做一个映射函数调用:

irq_create_fwspec_mapping(传入domain和参数)
    irq_find_matching_fwspec
        domain->ops->match // 查看当前domain和参数指定的domain是否匹配
    irq_domain_translate
        domain->ops->translate // 通过参数申请一个hirq号
    irq_find_mapping // 通过hirq去查询,这个hirq是否已经映射,如果已经映射,就返回virq
    irq_domain_alloc_irqs // hirq没有映射,通过该函数去申请一个virq并映射
        __irq_domain_alloc_irqs 
            irq_domain_alloc_descs // 申请一个irq_desc描述 
            irq_domain_alloc_irq_data // 为virq设置irq_data树,该virq会为它所在的domain的父domain,一直到root domain添加irq_data
            irq_domain_alloc_irqs_hierarchy // 向domain申请hirq,并和virq做映射
                domain->ops->alloc
                    irq_domain_set_hwirq_and_chip // 将hirq与virq的irq_data关联
            irq_domain_insert_irq//将virq信息插入到每一级irq_data里面(一个domain对应一个irq_data)

详细代码如下:

/*通用的IRQ说明符结构,用于传递一个特定于设备的中断描述。
 * @fwnode:    指向特定固件的描述符
 * @param_count: 设备特定参数的数量
 * @param:    特定设备的参数
 */
struct irq_fwspec {
    struct fwnode_handle *fwnode;
    int param_count;
    u32 param[IRQ_DOMAIN_IRQ_SPEC_PARAMS];
};

/**
 * 解析一个中断并将其映射到linux virq空间
 * @dev: 要映射的中断的设备的设备节点
 * @index: 要映射的中断的索引
 */
unsigned int irq_of_parse_and_map(struct device_node *dev, int index)
{
    struct of_phandle_args oirq;
    /* 这个函数通过遍历中断树来为一个节点解析一个中断,查找它所连接到的
     * 中断控制器节点,并返回可以用来检索一个Linux IRQ号的中断说明符。
     * 从DTS中解析出来的“interrupts”的值存放在oirq->ags[1]中
     */
    if (of_irq_parse_one(dev, index, &oirq))
        return 0;
      /*建立并映射该中断*/
      return irq_create_of_mapping(&oirq);
}

/*建立并映射一个中断,irq_data是从DTS中解析出来的中断属性*/
unsigned int irq_create_of_mapping(struct of_phandle_args *irq_data)
{
    struct irq_fwspec fwspec;
    /*用irq_data填充fwspec*/
    of_phandle_args_to_fwspec(irq_data->np, irq_data->args,
                  irq_data->args_count, &fwspec);
    /*正式开始建立并映射一个中断,irq_data是从DTS中解析出来的中断属性*/
    return irq_create_fwspec_mapping(&fwspec);
}

unsigned int irq_create_fwspec_mapping(struct irq_fwspec *fwspec)
{
    struct irq_domain *domain;
    struct irq_data *irq_data;
    irq_hw_number_t hwirq;
    unsigned int type = IRQ_TYPE_NONE;
    int virq;

    if (fwspec->fwnode) {
        /* 如果初始化了该设备特定的固件,首先从irq_domain_list中查找并匹配用于wired IRQS的irq_domain。
         * 对于GIC V3来说,已经在irq_domain_list注册了一个DOMAIN_BUS_WIRED的irq_domain,所以这里会匹配成功
         */
        domain = irq_find_matching_fwspec(fwspec, DOMAIN_BUS_WIRED);
        if (!domain)/*用于wired IRQS的irq_domain匹配失败,再重新匹配用于bus的irq_domain*/
            domain = irq_find_matching_fwspec(fwspec, DOMAIN_BUS_ANY);
    } else {
        /*没有指向该设备的特定固件,也就没有为该设备注册特定的irq domain,所以用系统默认的irq_default_domain*/
        domain = irq_default_domain;
    }

    if (!domain) {
        pr_warn("no irq domain found for %s !\n",
            of_node_full_name(to_of_node(fwspec->fwnode)));
        return 0;
    }
    /* 这里会调用irq_domain中的xlate方法来解码硬件中断号和linux中断类型,
     * GIC V3在初始化的时候已经设置了irq_domain中的xlate的回调函数,也就是
     * gic_irq_domain_ops中的gic_irq_domain_translate方法,解码硬件中断号
     * 和linux中断类型的过程参考gic_irq_domain_translate函数
     */
    if (irq_domain_translate(domain, fwspec, &hwirq, &type))
        return 0;

    /*
     * WARN if the irqchip returns a type with bits
     * outside the sense mask set and clear these bits.
     */
    if (WARN_ON(type & ~IRQ_TYPE_SENSE_MASK))
        type &= IRQ_TYPE_SENSE_MASK;

    /*
     * If we've already configured this interrupt,
     * don't do it again, or hell will break loose.
     */
    /*通过硬件中断号,查找对应的已经被映射的软件中断号 */
    virq = irq_find_mapping(domain, hwirq);
    if (virq) {
        /*
         * If the trigger type is not specified or matches the
         * current trigger type then we are done so return the
         * interrupt number.
         */
        if (type == IRQ_TYPE_NONE || type == irq_get_trigger_type(virq))
            return virq;

        /*
         * If the trigger type has not been set yet, then set
         * it now and return the interrupt number.
         */
        if (irq_get_trigger_type(virq) == IRQ_TYPE_NONE) {
            irq_data = irq_get_irq_data(virq);
            if (!irq_data)
                return 0;

            irqd_set_trigger_type(irq_data, type);
            return virq;
        }

        pr_warn("type mismatch, failed to map hwirq-%lu for %s!\n",
            hwirq, of_node_full_name(to_of_node(fwspec->fwnode)));
        return 0;
    }

    if (irq_domain_is_hierarchy(domain)) {
                /*GIC V3级联中断控制器的irq domain实现了alloc方法,这里直接调用其alloc方法进行硬件中断映射*/
        virq = irq_domain_alloc_irqs(domain, 1, NUMA_NO_NODE, fwspec);
        if (virq <= 0)
            return 0;
    } else {        
        virq = irq_create_mapping(domain, hwirq);
        if (!virq)
            return virq;
    }
         /*这里根据virq获取到irq_data,说明映射已经成功*/
     irq_data = irq_get_irq_data(virq);
    if (!irq_data) {
        if (irq_domain_is_hierarchy(domain))
            irq_domain_free_irqs(virq, 1);
        else
            irq_dispose_mapping(virq);
        return 0;
    }

    /* Store trigger type */
    irqd_set_trigger_type(irq_data, type);

    return virq;
}

对于级联的中断控制器,实现了自己的alloc方法, GIC V3的alloc方法为gic_irq_domain_alloc(), GIC V3通过调用irq_domain_alloc_irqs()函数来最终调用gic_irq_domain_alloc()进行中断映射,相关代码及注释如下:

static inline int irq_domain_alloc_irqs(struct irq_domain *domain,
            unsigned int nr_irqs, int node, void *arg)
{
    return __irq_domain_alloc_irqs(domain, -1, nr_irqs, node, arg, false,
                       NULL);
}
int __irq_domain_alloc_irqs(struct irq_domain *domain, int irq_base,
                unsigned int nr_irqs, int node, void *arg,
                bool realloc, const struct irq_affinity_desc *affinity)
{
    ......

    if (realloc && irq_base >= 0) {
        virq = irq_base;
    } else {
        /*从irq_base开始在中断描述符位图中查找nr_irqs个连续为0的比特位*/
        virq = irq_domain_alloc_descs(irq_base, nr_irqs, 0, node,
                          affinity);
        if (virq < 0) {
            pr_debug("cannot allocate IRQ(base %d, count %d)\n",
                 irq_base, nr_irqs);
            return virq;
        }
    }
       /*为当前domain及其从prent开始的所有domain分配一个irq_data*/
    if (irq_domain_alloc_irq_data(domain, virq, nr_irqs)) {
        pr_debug("cannot allocate memory for IRQ%d\n", virq);
        ret = -ENOMEM;
        goto out_free_desc;
    }

    mutex_lock(&irq_domain_mutex);
    /*调用domain->ops->alloc映射当前硬件中断号,对于GICV3来说,这里调用的是gic_irq_domain_alloc()函数进行映射*/
    ret = irq_domain_alloc_irqs_hierarchy(domain, virq, nr_irqs, arg);
    if (ret < 0) {
        mutex_unlock(&irq_domain_mutex);
        goto out_free_irq_data;
    }
       /*将中断加入映射表*/
    for (i = 0; i < nr_irqs; i++)
        irq_domain_insert_irq(virq + i);
    mutex_unlock(&irq_domain_mutex);

    return virq;
        ......
}

int irq_domain_alloc_irqs_hierarchy(struct irq_domain *domain,
                    unsigned int irq_base,
                    unsigned int nr_irqs, void *arg)
{
    return domain->ops->alloc(domain, irq_base, nr_irqs, arg);
}

static void irq_domain_insert_irq(int virq)
{
    struct irq_data *data;

    for (data = irq_get_irq_data(virq); data; data = data->parent_data) {
        struct irq_domain *domain = data->domain;
       /*更新当前domain被映射的次数*/
        domain->mapcount++;
        /*将硬件中断号和软件中断号的映射放入映射表中*/
        irq_domain_set_mapping(domain, data->hwirq, data);

        /* If not already assigned, give the domain the chip's name */
        if (!domain->name && data->chip)
            domain->name = data->chip->name;
    }
    /*清除virq对应的中断的IRQ_NOREQUEST标志,表示该中断已经成功映射,可以用于中断请求了*/
    irq_clear_status_flags(virq, IRQ_NOREQUEST);
}

static int gic_irq_domain_alloc(struct irq_domain *domain, unsigned int virq,
                unsigned int nr_irqs, void *arg)
{
    int i, ret;
    irq_hw_number_t hwirq;
    unsigned int type = IRQ_TYPE_NONE;
    struct irq_fwspec *fwspec = arg;
    /*根据irq_fwspec中的参数解析硬件中断号和中断类型*/
    ret = gic_irq_domain_translate(domain, fwspec, &hwirq, &type);
    if (ret)
        return ret;
    /*根据不同的中断类型安装相应的处理函数 SPI,LPI:handle_fasteoi_irq,PPI:handle_percpu_devid_irq*/
    for (i = 0; i < nr_irqs; i++) {
        ret = gic_irq_domain_map(domain, virq + i, hwirq + i);
        if (ret)
            return ret;
    }

    return 0;
}

static int gic_irq_domain_map(struct irq_domain *d, unsigned int irq,
                  irq_hw_number_t hw)
{
    struct irq_chip *chip = &gic_chip;

    if (static_branch_likely(&supports_deactivate_key))
        chip = &gic_eoimode1_chip;

    switch (__get_intid_range(hw)) {
    case PPI_RANGE:
    case EPPI_RANGE:
        /*对于PPI中断,安装handle_percpu_devid_irq函数来处理该类型的中断*/
        irq_set_percpu_devid(irq);
        irq_domain_set_info(d, irq, hw, chip, d->host_data,
                    handle_percpu_devid_irq, NULL, NULL);
        irq_set_status_flags(irq, IRQ_NOAUTOEN);
        break;

    case SPI_RANGE:
    case ESPI_RANGE:
        /*对于SPI中断,安装handle_fasteoi_irq函数来处理该类型的中断*/
        irq_domain_set_info(d, irq, hw, chip, d->host_data,
                    handle_fasteoi_irq, NULL, NULL);
        irq_set_probe(irq);
        irqd_set_single_target(irq_desc_get_irq_data(irq_to_desc(irq)));
        break;

    case LPI_RANGE:
        if (!gic_dist_supports_lpis())
            return -EPERM;
        /*对于LPI中断,安装handle_fasteoi_irq函数来处理该类型的中断*/
        irq_domain_set_info(d, irq, hw, chip, d->host_data,
                    handle_fasteoi_irq, NULL, NULL);
        break;

    default:
        return -EPERM;
    }

    return 0;
}

五、中断注册及处理

1、注册中断

所谓的中断请求其实就是启用中断号对应的中断管脚以及注册中断触发后执行的中断处理函数。一个中断管脚「interrupt line」就是一条传递中断电信号的媒介,其与中断控制器相连并且拥有一个硬件中断号「hwirq」作为标识。一旦启用的中断管脚上触发了中断,就会调用中断号对应的中断处理函数。

设备驱动中,获取到了 irq 中断号后,通常采用 request_irq /request_threaded_irq 来注册中断,其中 request_irq 用于注册普通处理的中断。request_threaded_irq 用于注册线程化处理的中断,线程化中断的主要目的把中断上下文的任务迁移到线程中,减少系统关中断的时间,增强系统的实时性

static inline int __must_check request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags, const char *name, void *dev)
{
    return request_threaded_irq(irq, handler, NULL, flags, name, dev);
}

request_irq函数的调用关系如下:

https://daniel-pic.oss-cn-shanghai.aliyuncs.com/irq%E6%B5%81%E7%A8%8B%E5%9B%BE.png

以下为request_threaded_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)
{
    struct irqaction *action;
    struct irq_desc *desc;
    int retval;

    if (irq == IRQ_NOTCONNECTED)
        return -ENOTCONN;

    /*
     * Sanity-check: shared interrupts must pass in a real dev-ID,
     * otherwise we'll have trouble later trying to figure out
     * which interrupt is which (messes up the interrupt freeing
     * logic etc).
     *
     * Also IRQF_COND_SUSPEND only makes sense for shared interrupts and
     * it cannot be set along with IRQF_NO_SUSPEND.
     */
    if (((irqflags & IRQF_SHARED) && !dev_id) ||
        (!(irqflags & IRQF_SHARED) && (irqflags & IRQF_COND_SUSPEND)) ||
        ((irqflags & IRQF_NO_SUSPEND) && (irqflags & IRQF_COND_SUSPEND)))
        return -EINVAL;

    desc = irq_to_desc(irq);
    if (!desc)
        return -EINVAL;

    if (!irq_settings_can_request(desc) ||
        WARN_ON(irq_settings_is_per_cpu_devid(desc)))
        return -EINVAL;

    if (!handler) {
        if (!thread_fn)
            return -EINVAL;
        handler = irq_default_primary_handler;
    }

    action = kzalloc(sizeof(struct irqaction), GFP_KERNEL);
    if (!action)
        return -ENOMEM;

    action->handler = handler;
    action->thread_fn = thread_fn;
    action->flags = irqflags;
    action->name = devname;
    action->dev_id = dev_id;

    retval = irq_chip_pm_get(&desc->irq_data); /* Enable power for an IRQ chip */
    if (retval < 0) {
        kfree(action);
        return retval;
    }

    retval = __setup_irq(irq, desc, action);  
    if (retval) {
        irq_chip_pm_put(&desc->irq_data);
        kfree(action->secondary);
        kfree(action);
    }

#ifdef CONFIG_DEBUG_SHIRQ_FIXME
    if (!retval && (irqflags & IRQF_SHARED)) {
        /*
         * It's a shared IRQ -- the driver ought to be prepared for it
         * to happen immediately, so let's make sure....
         * We disable the irq to make sure that a 'real' IRQ doesn't
         * run in parallel with our fake.
         */
        unsigned long flags;

        disable_irq(irq);
        local_irq_save(flags);

        handler(irq, dev_id);

        local_irq_restore(flags);
        enable_irq(irq);
    }
#endif
    return retval;
}
2 、中断流处理函数的调用流程

尽管中断控制器千差万别,但采用的中断流处理函数(比如handle_level_irq()、handle_percpu_irq()、handle_fasteoi_irq()、handle_edge_irq()等等)却大同小异。基于ARMv8架构实现的AArch64核心在捕获到中断控制器触发的中断时会生成一个异步中断异常,该异常会使CPU暂停当前执行来响应处理中断。在中断异常发生后,CPU会执行函数指针handle_arch_irq所指向的一个回调函数,其是在中断控制器驱动初始化时调用set_handle_irq()设定的。接着调用执行函数指针desc->handle_irq所指向的一个回调函数,而且desc->handle_irq指向的回调函数也是在由中断控制器驱动初始化时根据中断类型负责设定的。

daniel

看一下handle_fasteoi_irq()函数,其处理常见设备触发的中断。

void handle_fasteoi_irq(struct irq_desc *desc)
{
    struct irq_chip *chip = desc->irq_data.chip;

    raw_spin_lock(&desc->lock);

    if (!irq_may_run(desc))
        goto out;

    desc->istate &= ~(IRQS_REPLAY | IRQS_WAITING);

    /*
     * If its disabled or no action available
     * then mask it and get out of here:
     */
    if (unlikely(!desc->action || irqd_irq_disabled(&desc->irq_data))) {
        desc->istate |= IRQS_PENDING;
        mask_irq(desc);
        goto out;
    }

    kstat_incr_irqs_this_cpu(desc);
    if (desc->istate & IRQS_ONESHOT)
        mask_irq(desc);

    handle_irq_event(desc);

    cond_unmask_eoi_irq(desc, chip);

    raw_spin_unlock(&desc->lock);
    return;
out:
    if (!(chip->flags & IRQCHIP_EOI_IF_HANDLED))
        chip->irq_eoi(&desc->irq_data);
    raw_spin_unlock(&desc->lock);
}

handle_irq_event函数如下:

irqreturn_t handle_irq_event(struct irq_desc *desc)
{
    irqreturn_t ret;

    desc->istate &= ~IRQS_PENDING;
    irqd_set(&desc->irq_data, IRQD_IRQ_INPROGRESS);
    raw_spin_unlock(&desc->lock);

    ret = handle_irq_event_percpu(desc);

    raw_spin_lock(&desc->lock);
    irqd_clear(&desc->irq_data, IRQD_IRQ_INPROGRESS);
    return ret;
}

下面代码展示了__handle_irq_event_percpu()函数如何调用执行传递给request_irq()的中断处理函数。

irqreturn_t __handle_irq_event_percpu(struct irq_desc *desc, unsigned int *flags)
{
    irqreturn_t retval = IRQ_NONE;
    unsigned int irq = desc->irq_data.irq;
    struct irqaction *action;

    record_irq_time(desc);
    /* 遍历中断描述符「irq_desc」的中断动作「irqaction」链表 */
    for_each_action_of_desc(desc, action) {
        irqreturn_t res;

        /*
         * If this IRQ would be threaded under force_irqthreads, mark it so.
         */
        if (irq_settings_can_thread(desc) &&
            !(action->flags & (IRQF_NO_THREAD | IRQF_PERCPU | IRQF_ONESHOT)))
            lockdep_hardirq_threaded();

        trace_irq_handler_entry(irq, action);
        res = action->handler(irq, action->dev_id);
        trace_irq_handler_exit(irq, action, res);

        if (WARN_ONCE(!irqs_disabled(),"irq %u handler %pS enabled interrupts\n",
                  irq, action->handler))
            local_irq_disable();

        switch (res) {
        case IRQ_WAKE_THREAD:
            /*
             * Catch drivers which return WAKE_THREAD but
             * did not set up a thread function
             */
            if (unlikely(!action->thread_fn)) {
                warn_no_thread(irq, action);
                break;
            }

            __irq_wake_thread(desc, action);

            fallthrough;    /* to add to randomness */
        case IRQ_HANDLED:
            *flags |= action->flags;
            break;

        default:
            break;
        }

        retval |= res;
    }

    return retval;
}
3、释放IRQ

释放中断的方案,与前述过程刚好相反。首先,通过硬件相关的函数chip->shutdown通知中断控制器该IRQ已经删除,接下来将相关数据项从内核的一般数据结构中删除。

void free_irq(unsigned int, void *);

在IRQ处理程序需要删除一个共享的中断时,IRQ编号本身不足以标识该IRQ。在这种情况下,为提供唯一标识,还必须使用前面讲述的dev_id。内核扫描所有注册的处理程序的链表,直至找到一个匹配的处理程序。这时才能移除该项。

六、总结

中断这块的内容大概说完了,不过这里也只是从硬件得到响应后的硬件中断,因为在开始也说过,由于CPU的资源宝贵,而在中断处理期间又不能有任何的抢占操作,所以在硬件中断的过程中,各个中断处理例程只是把一些必须的操作在这里面执行完,然后就把控制权交回,真正做后续处理的是后面将介绍的软中断。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值