精华内容
下载资源
问答
  • 内核IRQ中断向量
    千次阅读
    2019-08-01 19:45:11

    首先看一下vector_irq的定义,此每处理器数组变量,保存每个处理器上中断向量所对应的中断号,其以中断向量值为索引。系统中定义了256个中断向量。相关代码如下:

    typedef int vector_irq_t[NR_VECTORS];
    
    DEFINE_PER_CPU(vector_irq_t, vector_irq) = {
        [0 ... NR_VECTORS - 1] = VECTOR_UNDEFINED,
    };
    
    
    #define NR_VECTORS  256
    

    另一个是used_vector的定义,其为一个位图变量,用来标记系统中已经使用的中断向量。

    DECLARE_BITMAP(used_vectors, NR_VECTORS);
    

    中断向量初始化

    中断初始化入口函数init_IRQ。为支持传统的PIC类型的终端控制器,如8259,内核预留了IRQ0_VECTOR 到 IRQ15_VECTOR 共16个中断向量,分别对应IRQ号0到15。在初始化时,也不是全部预留这16个中断向量,而是根据检测到的传统PIC中断控制器所支持中断数量在预留,有可能小于16个。x86_init.irqs.intr_init()指针对应的为native_init_IRQ函数,接下来介绍。

    void __init init_IRQ(void)
    {
    
        for (i = 0; i < nr_legacy_irqs(); i++)
            per_cpu(vector_irq, 0)[IRQ0_VECTOR + i] = i;
    
        x86_init.irqs.intr_init();
    }
    

    函数apic_intr_init负责分配系统自身所需的中断向量。x86架构的IDT中断描述表可用的外部中断向量由0x20(FIRST_EXTERNAL_VECTOR)开始,其中0x80(IA32_SYSCALL_VECTOR)为系统调用所用的中断向量;0x30-0x3f共16个中断向量保留给ISA使用。interrupt函数参见文件arch/x86/kernel/entry_64.S中的定义,表示中断处理程序的入口地址,for_each_clear_bit_from循环将所有未使用的中断向量的处理程序入口设定为interrupt的相应偏移。最后对于传统的中断控制器,irq2用作两片控制器的级联。

    void __init native_init_IRQ(void)
    {
        apic_intr_init();
    
        i = FIRST_EXTERNAL_VECTOR;
        for_each_clear_bit_from(i, used_vectors, NR_VECTORS) {
            set_intr_gate(i, interrupt[i - FIRST_EXTERNAL_VECTOR]);
        }
    
        if (!acpi_ioapic && !of_ioapic)
            setup_irq(2, &irq2);
    }
    

    系统中断向量分配

    函数apic_intr_init。首先分配SMP相关的中断向量,参见函数smp_intr_init,随后是分配其他的向量。这些中断向量值由大到小分配,首先是:SPURIOUS_APIC_VECTOR(0xff),目前最小的是:LOCAL_TIMER_VECTOR(0xef)。内核中定义了变量first_system_vector表示当前系统中断向量的最小值。

    static void __init apic_intr_init(void)
    {
        smp_intr_init();
    
    #ifdef CONFIG_X86_THERMAL_VECTOR
        alloc_intr_gate(THERMAL_APIC_VECTOR, thermal_interrupt);
    }
    
    static void __init smp_intr_init(void)
    {
    #ifdef CONFIG_SMP
    #if defined(CONFIG_X86_64) || defined(CONFIG_X86_LOCAL_APIC)
        alloc_intr_gate(RESCHEDULE_VECTOR, reschedule_interrupt);
    }
    

    alloc_intr_gate宏由alloc_system_vector和set_intr_gate两个函数组成。前者为注册的系统中断向量设置used_vectors中的使用标志,并且更新系统最小向量值:first_system_vector。后者set_intr_gate函数设置处理器的IDT表。

    static inline void alloc_system_vector(int vector)
    {
        if (!test_bit(vector, used_vectors)) {
            set_bit(vector, used_vectors);
            if (first_system_vector > vector)
                first_system_vector = vector;
        } else {
            BUG();
        }
    }
    
    #define alloc_intr_gate(n, addr)                \
        do {                            \
            alloc_system_vector(n);             \
            set_intr_gate(n, addr);             \
        } while (0)
    

    外部中断向量分配

    由如下函数irq_alloc_hwirqs可见,首先调用函数__irq_alloc_descs分配中断号,内核中已经使用的中断号标记在全局位图变量allocated_irqs中,函数的第一个参数表示期望的中断号,-1表示由系统来选择中断号。系统将在allocated_irqs位图中选择一个不为零的位返回,作为新的中断号。

    函数arch_setup_hwirq负责为中断号查找合适的中断向量。注意在执行过程中出现错误的话,函数将执行回退处理,包括由函数irq_free_descs复位之前分配中断号时设置的allocated_irqs中的位。arch_teardown_hwirq函数是对应arch_setup_hwirq函数的清理函数。

    unsigned int irq_alloc_hwirqs(int cnt, int node)
    {
        int i, irq = __irq_alloc_descs(-1, 0, cnt, node, NULL);
    
        if (irq < 0)
            return 0;
    
        for (i = irq; cnt > 0; i++, cnt--) {
            if (arch_setup_hwirq(i, node))
                goto err;
    
            irq_clear_status_flags(i, _IRQ_NOREQUEST);
        }
        return irq;
    
    err:
        for (i--; i >= irq; i--) {
            irq_set_status_flags(i, _IRQ_NOREQUEST | _IRQ_NOPROBE);
            arch_teardown_hwirq(i);
        }
        irq_free_descs(irq, cnt);
        return 0;
    }
    

    arch_setup_hwirq函数的主体是由__assign_irq_vector函数完成,其定义如下。由于终端级别是由中断向量后4位决定的,而本地的APIC不能够很好的处理多个同级别的中断,所以内核以2的4次方,即16为区间,进行中断向量的分配。

    使用静态变量current_vector和current_offset分别记录当前的区间起始值和区间内偏移。第一次分配中断向量时,current_vector为33,偏移值为1,所以第一个可供分配的中断向量为34。

    static int __assign_irq_vector(int irq, struct irq_cfg *cfg, const struct cpumask *mask)
    {
        static int current_vector = FIRST_EXTERNAL_VECTOR + VECTOR_OFFSET_START;
        static int current_offset = VECTOR_OFFSET_START % 16;
    

    以下的中断向量查找由两个循环组成。第一个循环遍历中断可用的处理器列表,由APIC控制器的可用处理器列表和在线处理器列表中第一个同时置位的处理器开始,第二个循环(内部循环)遍历所有可用的中断向量vector,如果没有合适的选择,即vector的值又回到了最初的current_vector,进行第二次循环,遍历第二个可用的处理器。

    获取第二个可用处理器的算法为:1)当前分配的处理器域domain(处理器列表),与老的域值old_domain进行或操作;2)将上一步的结果和APIC处理器列表进行与操作;3)在第二步所得结果和在线处理器列表中获取第一个同时置位的处理器。此即下一个循环要遍历的处理器。

    1287     cpumask_clear(cfg->old_domain);
    1288     cpu = cpumask_first_and(mask, cpu_online_mask);
    1289     while (cpu < nr_cpu_ids) {
    1290         int new_cpu, vector, offset;
    1291
    1294         apic->vector_allocation_domain(cpu, tmp_mask, mask);
    
    1335         if (unlikely(current_vector == vector)) {
    1340             cpumask_or(cfg->old_domain, cfg->old_domain, tmp_mask);
    1345             cpumask_andnot(tmp_mask, mask, cfg->old_domain);
    1348             cpu = cpumask_first_and(tmp_mask, cpu_online_mask);
    1350             continue;
    1351         }
    

    内部的循环由标签和goto语句组成。每次遍历16长度的区间中的第一个偏移向量值,一直遍历完所有的区间。之后,当vector向量值大于最大可用向量值first_system_vector时,重新由第一个区间的第二个偏移值开始遍历。

    如果遍历的向量值没有被使用,即used_vectors位图没有置位。并且,tmp_mask中的所有有效处理器上vector_irq映射还没有被占用,表明此中断向量可用。

    1324         vector = current_vector;
    1325         offset = current_offset;
    1326 next:
    1329         vector += 16;
    1330         if (vector >= first_system_vector) {
    1331             offset = (offset + 1) % 16;
    1332             vector = FIRST_EXTERNAL_VECTOR + offset;
    1333         }
    1334
    1335         if (unlikely(current_vector == vector)) {
    1340             cpumask_or(cfg->old_domain, cfg->old_domain, tmp_mask);
    1345             cpumask_andnot(tmp_mask, mask, cfg->old_domain);
    1348             cpu = cpumask_first_and(tmp_mask, cpu_online_mask);
    1350             continue;
    1351         }
    1352
    1353         if (test_bit(vector, used_vectors))
    1355             goto next;
    1357
    1358         for_each_cpu_and(new_cpu, tmp_mask, cpu_online_mask) {
    1359             if (per_cpu(vector_irq, new_cpu)[vector] > VECTOR_UNDEFINED)
    1360                 goto next;
    1361         }
    

    成功找到可用的中断向量之后,更新静态的current_vector和current_offset的值,并更新tmp_mask中所有处理器的vector_irq中断向量和中断号映射值,写入分配的irq中断号,表明此中断向量已被占用。使用cfg中的domain成员记录有效的处理器列表。

    除去第一次申请中断向量外,系统也可能动态的调整中断向量,及为其服务的处理器列表,old_domain用来保存调整前的处理器列表,move_in_progress表示是否正处于调整期间,调整结束之后old_domain为空,move_in_progress也值为0。

    1362         /* Found one! */
    1363         current_vector = vector;
    1364         current_offset = offset;
    1365         if (cfg->vector) {
    1371             cpumask_copy(cfg->old_domain, cfg->domain);
    1374             cfg->move_in_progress = cpumask_intersects(cfg->old_domain, cpu_online_mask);
    1377         }
    
    1380         for_each_cpu_and(new_cpu, tmp_mask, cpu_online_mask)
    1381             per_cpu(vector_irq, new_cpu)[vector] = irq;
    1382         cfg->vector = vector;
    1383         cpumask_copy(cfg->domain, tmp_mask);
    1387
    1388         err = 0;
    1389         break;
    

    另外,在中断调整时,如果服务的处理器列表调整前后一致,直接跳出循环(cpumask_equal(tmp_mask, cfg->domain))。如果新的处理器列表是调整前处理器列表的子集,移除多余的处理器,并更新domain为调整之后的处理器列表。

    1294         apic->vector_allocation_domain(cpu, tmp_mask, mask);
    1295
    1296         if (cpumask_subset(tmp_mask, cfg->domain)) {
    1297             err = 0;
    
    1303             if (cpumask_equal(tmp_mask, cfg->domain))
    1304                 break;
    1305             /*
    1306              * New cpumask using the vector is a proper subset of the current in use mask. So cleanup the vector
    1308              * allocation for the members that are not used anymore.
    1309              */
    1310             cpumask_andnot(cfg->old_domain, cfg->domain, tmp_mask);
    1313
    1314             cfg->move_in_progress = cpumask_intersects(cfg->old_domain, cpu_online_mask);
    1317             cpumask_and(cfg->domain, cfg->domain, tmp_mask);
    1320
    1321             break;
    1322         }
    

    以上的介绍打乱了代码的顺序,以行号为准。

    内核版本:3.10.0

    更多相关内容
  • IRQ外部中断
  • 本文主要对ARM中断模式(IRQ)和快速中断模式(FIQ)区别进行了说明,希望对你的学习有所帮助。
  • 针对ARM7微控制器的IRQ中断向量地址动态分配在中断向量寄存器中的特点,提出将中断向量固化到ROM中的思路,构建IRQ中断防御体系,从而增强了系统的可靠性和安全性。
  • IRQ外部中断改进
  • IRQ中断详解

    2013-11-26 10:30:24
    详细介绍了IRQ中断的问题,介绍了IRQ的初始化,编写等。
  • NRF24L01 使用IRQ处理收发中断,六发一收 NRF24L01 使用IRQ处理收发中断,六发一收
  • 中断访问pc机中断IRQ0

    2013-03-18 14:30:38
    基于TC2.0在内核模式下访问IRQ0调试成功。
  • IRQ中断 起始代码

    2013-12-02 09:30:50
    介绍了IRQ中断的起始代码的编写 一些注意的事项 以及中断中每句话的含义
  • STM32的USART_Irq串口中断方式(直接操作寄存器)。内有详细的注释和文档。很好的学习资料。
  • 3. irqbalance处理流程 3.1 build_object_tree 3.2 clear_work_stats 3.3 parse_proc_interrupts 3.4 parse_proc_stats 3.5 update_migration_status 3.6 calculate_placement 3.7 activate_mapping 4. ...

    目录

    1. object tree

    2. 数据结构

    2.2. topo_obj

    3. irqbalance处理流程

     3.1 build_object_tree

    3.2 clear_work_stats

    3.3 parse_proc_interrupts

    3.4 parse_proc_stats

    3.5 update_migration_status

    3.6 calculate_placement

    3.7 activate_mapping

    4. powersave mode

    5. 补充


    本文档基于irqbalance-1.5.0

    源码链接:https://launchpad.net/ubuntu/+source/irqbalance/

    1. object tree

           Irqbalance是用户空间用于优化中断的一个工具,通过周期性的(默认10s)统计各个cpu上的中断情况,重新对中断进行再分配,实现各个cpu上中断负载相对均衡。中断均衡是建立再“object tree”的基础之上的,object   tree则是通过系统的拓扑结构建立的分层结构。根据系统结构属性NUMA node/packet/cache affinity可以将系统划分为自上而下的四层:node->package->cache->cpu。

           以16核双路服务器为例,系统有两个numa node,每个节点包含两个cluster,每个cluster包含4个cores,共享l2 cache。其结构简图如下:

                                                 

       对应的object tree拓扑结构如图:

     

    其中:

    (1)每个节点为一个object,通过struct topo_obj描述。

    (2)上下层之间的节点通过parent/child指针管理

    (3)每一层都有一个全局链表头指针,用于组织管理处于同一层的所有节点。

     

    2. 数据结构

    2.1 Irq_info

           在树形拓扑结构建立之后,就需要统计各个节点上中断负载的信息,以便为重新分配中断提供依据。对于各个中断信息通过struct irq_info来描述。下面结合各个字段介绍下irqlabalance中几个比较关键的概念。

    struct irq_info {
         int irq;       //中断号
         int class;
         int type;
         int level;
         int flags;
         struct topo_obj *numa_node;    //中断当前所在node节点对应的object
         cpumask_t cpumask;
         uint64_t irq_count;
         uint64_t last_irq_count;
         uint64_t load;
         int moved;
         struct topo_obj *assigned_obj; //中断被分配到节点对应的object
         char *name;
    };

    (1) 中断类型(irq_info.class)

    Irqbalance根据中断所属device的pci配置空间class code把中断分成了以下8种类型字。

    #define IRQ_OTHER       0
    #define IRQ_LEGACY      1
    #define IRQ_SCSI        2
    #define IRQ_TIMER       3
    #define IRQ_ETH         4
    #define IRQ_GBETH       5
    #define IRQ_10GBETH     6
    #define IRQ_VIRT_EVENT  7

    其中:irq_info.class 与 pci配置空间 class code的映射关系如下:

        

     

     

    (2) 中断层级(irq_info.level)

    每种中断类型根据静态映射分别对应一种分配方式,分配方式一共有4种。

    #define BALANCE_NONE           0  //表示中断不能进行迁移
    #define BALANCE_PACKAGE        1  //表示中断只能在package层进行均衡
    #define BALANCE_CACHE          2  //表示中断只能在cache层进行均衡
    #define BALANCE_CORE           3  //表示中断只能在core层进行均衡

    中断类型与其映射关系如表:

        

     

    (3) 中断计数

           irq_info.irq_count 表示本次统计irq中断在各个cpu上产生的中断次数之

           irq_info.last_irq_count表示上次统计irq中断在各个cpu上的产生的中断次数之

        

     

    (4) 中断的负载

           表示两次统计这段时间内中断增加所带来的负担。在irqbalance中把中断消耗的时间来衡量cpu的负担。

    计算方法详见3.4(4).

    2.2. topo_obj

    “对象树“中的每个节点均为一个对象,通过struct topo_obj进行描述。

    struct topo_obj {
         uint64_t load;
         uint64_t last_load;
         uint64_t irq_count;
         enum obj_type_e obj_type;  //区别该节点位于哪一层。
         int number;                // object对应的索引
         int powersave_mode;
         cpumask_t mask;           //该节点下包含哪几个cpu
         GList *interrupts;           //组织被分配到该obj的中断
         struct topo_obj *parent;     //指向其上一层父对象的obj
         GList *children;            //组织管理该obj包含的下层obj
         GList *numa_nodes;      //指向最顶层所在node节点的obj
         GList **obj_type_list;
    };

    (1) object节点的负载(topo_obj.load)

    • cpu节点的load计算方法:

        在/proc/stat获取每个cpu的信息如下:

          

        其中第6/7项,分别代表自系统启动以来硬/软中断累计时间(单位是jiffies)。

        cpu负载:单个周期(10s)内,cpu处理软、硬中断的时间之和。

    • cpu拓扑层以上层各个节点的负载:

        父节点负载等于各孩子节点负载的总和 。

    (2) 节点中断管理指针(topo_obj.interrupts)

           所有被分配到该节点上的中断都挂在该指针上。

    (3) 节点中断计数(topo_obj.irq_count)

    分配到该节点上的所有中断(即interrupts指针管理的链表上的中断)在单位周期(10s)内增加的次数之和。

    (4) 节点的object类型(topo_obj.obj_type)

           object的类型,用来表明该对象处在对象树的哪一层次。Irqbalance 定义了如下4种类型。

         OBJ_TYPE_CPU,
         OBJ_TYPE_CACHE,
         OBJ_TYPE_PACKAGE,
         OBJ_TYPE_NODE
    

    (5) powersave_mode

           用来表示该object是否处在省电模式。具体介绍详见4。

           Irqbalance默认是关闭powersave_mode的,但是用户可以通过irqbalance -p <n>来设置power_thresh。当系统内object的负载较小时,会自动切换到省电模式。

    3. irqbalance处理流程

    Irqbalance会周期性的(10s)统计系统中断的情况,主要的处理流程图如下:

        

     

     

    下面针对各个部分作具体介绍:

     3.1 build_object_tree

    (1)作用:

    主要实现建立拓扑结构各个节点以及中断数据结构,并初始化。

    (2)实现:

           通过遍历/sys/devices/system/node/node[*],决定有多少OBJ_TYPE_NODE的对象。

      通过遍历/sys/devices/system/cpu/cpu[*],以及是否online,决定有多少OBJ_TYPE_CPU的对象

      通过遍历/sys/devices/system/cpu/cpu[*]/cache/index[MAX]/shared_cpu_map决定有多少OBJ_TYPE_CACHE对象。

      通过遍历/topology/physical_package_id决定有多少OBJ_TYPE_PACKAGE的对象。

      通过遍历/sys/bus/pci/devices/0000:00:[**].[*]/下irq以及msi,建立各个irq的数据,并结合proc/irq/下文件初始化中断数据结构。这样irqbalance就知道该irq属于哪个node以及smp_affinity.

    3.2 clear_work_stats

    清除各个对象节点上分配中的负载值,以便重新赋值。

    3.3 parse_proc_interrupts

           通过分析/proc/interrupts文件,更新各个irq在所有cpu上中断次数的累加和。

    3.4 parse_proc_stats

    (1) 计算各个对象节点的load,并更新到topo_obj.load。

    cpu节点的负载: 通过分析/proc/stats统计硬/软中断的累计时间。

    其余节点的负载:父节点负载=各孩子节点负载的总和

    (2) 计算各个对象节点上所有中断在单位周期(10s) 内新增加的次数,并更新到load_slice.irq_count。

    (3) 计算各个对象节点上单位周期内平均中断数local_count。

           由于分配到上层节点的中断最终要“均分”给下层,所以obj平均中断数loacl_count=obj.irq_cpunt +parent_obj.irq_count/(parent_obj的子节点数)。如图所示:

         

     

     (4) 计算对象上各个中断负载 irq_info.load

           单次中断对obj节点的负载贡献值(load_slice),即节点负载除以平均中断数:

               load_slice = topo_obj.load/local_count.

           那么该对象节点上各个中断负载就等于单次中断负载贡献值与中断次数的乘积:

          irq_info.load= load_slice*(irq_info.irq_count- irq_info.last_irq_count)

    3.5 update_migration_status

    通过平衡算法找出需要重新分配的中断。自下而上遍历各个拓扑层,针对每一层:

    (1)遍历该层各个对象节点,计算该层的平均负载avg_load、标准方差std_deviation以及节点中负载最小值min_load.

    (2)再次遍历该层各个拓扑节点,找到大于min­_load的节点,然后把该节点中的中断按照中断的irq_info.class由大到小并负载情况由大到小进行排序,然后依次从该节点移除,放到表头为rebalance_irq_list的链表中。

    注意:中断从节点迁移后会更新该节点的负载以及min_load,当两者最接近时停止迁移中断。

        也就是说该步骤过后,需要迁移的中断都被放在了表头为rebalance_irq_list的链表中,后续会将这些中断重新分配。

    3.6 calculate_placement

    (1) 将存在rebalance_irq_list链表中的中断重新排序:优先按照中断的irq_info.class由大到小排列,如果class相同则按照load由大到小排列。

    (2) 首先根据中断所在的numa nodeid将中断分配到不同的node 节点上。从这里可以看出中断是不会跨numa 节点迁移的,只能在同一numa node内部进行优化。

    (3) 自上而下遍历各个个拓扑层,对于每一拓扑层:

          遍历该层节点上的中断,对每一个中断:

            遍历该节点所有孩子节点,将其迁移到负载最小的孩子节点上。

    3.7 activate_mapping

           通过修改/proc/irq/[*]/smp_affinity,使处理生效

    4. powersave mode

      Irqbalance支持powersave mode,默认是关闭的,但是用户可以通过irqbalance -p <n>来设置阈值power_thresh。开启该mode,Irqbalance会根据系统内object的负载情况,会自动将某个cpu object切换省电模式/正常模式。

      前面3.5小节(1)中,当遍历cpu层各个对象节点,根据load值计算出cpu平均负载avg_load、标准方差std_deviation后。

           (1) 如果load + std_deviation <= avg_load时,表明该cpu的负载较低,则会记录该cpu object,同时记录低负载的cpu个数

           (2)如果load - std_deviation >= avg_load时,表明该cpu 的负载比较高,也会记录高负载的cpu个数。

     

      在遍历完整个cpu层的所有节点后,如果不存在高负载cpu,同时低负载的cpu个数大于设定阈值,则会将最后记录的cpu对象的powersaved_mode字段置1. 后续在从rebalance_irq_list的链表重新分配中断时,就不会在分配到该cpu上。

      一旦系统中存在高负载cpu,irqlabalance就会清除所有cpu 对象的powersaved_mode,恢复正常模式。

    5. 补充

    1. 中断再分配时,需要先自上而下遍历各层各节点,将符合条件的节点上的中断先迁移到表头为rebalance_irq_list的链表中,然后再将链表中的中断按node->package->cache->cpu逐层分配到各个节点中(分配时会根据irq_info.level决定最终分配到哪一级节点上)。那么刚建立号中断数据库时,各个节点上还没有中断那么是如何操作的呢?

           刚建的中断数据库由指真interrupts_db来管理,初次建立好后会将所有的中断迁移到rebalance_irq_list中。然后将rebalance_irq_list链表中中断按node->package->cache->cpu逐层分配到各个节点中。

    2. 如果一个package中有两个node时,irqbalance的拓扑层是如何划分的?

          

      在这种情况下,cache domian的父节点是package,但是numa节点的子节点是cache domians,而不再是packages,同时package的子节点任然是是cache domians。如图所示:

     

     

    展开全文
  • Linux中断 - IRQ Domain介绍 一、概述 在linux kernel中,我们使用下面两个ID来标识一个来自外设的中断: 1、IRQ number。CPU需要为每一个外设中断编号,我们称之IRQ Number。这个IRQ number是一个虚拟的...

    Linux中断 - IRQ Domain介绍

    一、概述

    在linux kernel中,我们使用下面两个ID来标识一个来自外设的中断:

    1、IRQ number。CPU需要为每一个外设中断编号,我们称之IRQ Number。这个IRQ number是一个虚拟的interrupt ID,和硬件无关,仅仅是被CPU用来标识一个外设中断。

    2、HW interrupt ID。对于interrupt controller而言,它收集了多个外设的interrupt request line并向上传递,因此,interrupt controller需要对外设中断进行编码。Interrupt controller用HW interrupt ID来标识外设的中断。在interrupt controller级联的情况下,仅仅用HW interrupt ID已经不能唯一标识一个外设中断,还需要知道该HW interrupt ID所属的interrupt controller(HW interrupt ID在不同的Interrupt controller上是会重复编码的)。

    这样,CPU和interrupt controller在标识中断上就有了一些不同的概念,但是,对于驱动工程师而言,我们和CPU视角是一样的,我们只希望得到一个IRQ number,而不关系具体是那个interrupt controller上的那个HW interrupt ID。这样一个好处是在中断相关的硬件发生变化的时候,驱动软件不需要修改。因此,linux kernel中的中断子系统需要提供一个将HW interrupt ID映射到IRQ number上来的机制,这就是本文主要的内容。

     

    二、历史

    关于HW interrupt ID映射到IRQ number上 这事,在过去系统只有一个interrupt controller的时候还是很简单的,中断控制器上实际的HW interrupt line的编号可以直接变成IRQ number。例如我们大家都熟悉的SOC内嵌的interrupt controller,这种controller多半有中断状态寄存器,这个寄存器可能有64个bit(也可能更多),每个bit就是一个IRQ number,可以直接进行映射。这时候,GPIO的中断在中断控制器的状态寄存器中只有一个bit,因此所有的GPIO中断只有一个IRQ number,在该通用GPIO中断的irq handler中进行deduplex,将各个具体的GPIO中断映射到其相应的IRQ number上。如果你是一个足够老的工程师,应该是经历过这个阶段的。

    随着linux kernel的发展,将interrupt controller抽象成irqchip这个概念越来越流行,甚至GPIO controller也可以被看出一个interrupt controller chip,这样,系统中至少有两个中断控制器了,一个传统意义的中断控制器,一个是GPIO controller type的中断控制器。随着系统复杂度加大,外设中断数据增加,实际上系统可以需要多个中断控制器进行级联,面对这样的趋势,linux kernel工程师如何应对?答案就是irq domain这个概念。

    我们听说过很多的domain,power domain,clock domain等等,所谓domain,就是领域,范围的意思,也就是说,任何的定义出了这个范围就没有意义了。系统中所有的interrupt controller会形成树状结构,对于每个interrupt controller都可以连接若干个外设的中断请求(我们称之interrupt source),interrupt controller会对连接其上的interrupt source(根据其在Interrupt controller中物理特性)进行编号(也就是HW interrupt ID了)。但这个编号仅仅限制在本interrupt controller范围内。

     

    三、接口

    1、向系统注册irq domain

    具体如何进行映射是interrupt controller自己的事情,不过,有软件架构思想的工程师更愿意对形形色色的interrupt controller进行抽象,对如何进行HW interrupt ID到IRQ number映射关系上进行进一步的抽象。因此,通用中断处理模块中有一个irq domain的子模块,该模块将这种映射关系分成了三类:

    (1)线性映射。其实就是一个lookup table,HW interrupt ID作为index,通过查表可以获取对应的IRQ number。对于Linear map而言,interrupt controller对其HW interrupt ID进行编码的时候要满足一定的条件:hw ID不能过大,而且ID排列最好是紧密的。对于线性映射,其接口API如下:

    static inline struct irq_domain *irq_domain_add_linear(struct device_node *of_node,
                         unsigned int size,---------该interrupt domain支持多少IRQ
                         const struct irq_domain_ops *ops,---callback函数
                         void *host_data)-----driver私有数据
    {
        return __irq_domain_add(of_node, size, size, 0, ops, host_data);
    }

    (2)Radix Tree map。建立一个Radix Tree来维护HW interrupt ID到IRQ number映射关系。HW interrupt ID作为lookup key,在Radix Tree检索到IRQ number。如果的确不能满足线性映射的条件,可以考虑Radix Tree map。实际上,内核中使用Radix Tree map的只有powerPC和MIPS的硬件平台。对于Radix Tree map,其接口API如下:

    static inline struct irq_domain *irq_domain_add_tree(struct device_node *of_node,
                         const struct irq_domain_ops *ops,
                         void *host_data)
    {
        return __irq_domain_add(of_node, 0, ~0, 0, ops, host_data);
    }

    (3)no map。有些中断控制器很强,可以通过寄存器配置HW interrupt ID而不是由物理连接决定的。例如PowerPC 系统使用的MPIC (Multi-Processor Interrupt Controller)。在这种情况下,不需要进行映射,我们直接把IRQ number写入HW interrupt ID配置寄存器就OK了,这时候,生成的HW interrupt ID就是IRQ number,也就不需要进行mapping了。对于这种类型的映射,其接口API如下:

    static inline struct irq_domain *irq_domain_add_nomap(struct device_node *of_node,
                         unsigned int max_irq,
                         const struct irq_domain_ops *ops,
                         void *host_data)
    {
        return __irq_domain_add(of_node, 0, max_irq, max_irq, ops, host_data);
    }

    这类接口的逻辑很简单,根据自己的映射类型,初始化struct irq_domain中的各个成员,调用__irq_domain_add将该irq domain挂入irq_domain_list的全局列表。

    2、为irq domain创建映射

    上节的内容主要是向系统注册一个irq domain,具体HW interrupt ID和IRQ number的映射关系都是空的,因此,具体各个irq domain如何管理映射所需要的database还是需要建立的。例如:对于线性映射的irq domain,我们需要建立线性映射的lookup table,对于Radix Tree map,我们要把那个反应IRQ number和HW interrupt ID的Radix tree建立起来。创建映射有四个接口函数:

    (1)调用irq_create_mapping函数建立HW interrupt ID和IRQ number的映射关系。该接口函数以irq domain和HW interrupt ID为参数,返回IRQ number(这个IRQ number是动态分配的)。该函数的原型定义如下:

    extern unsigned int irq_create_mapping(struct irq_domain *host,
                           irq_hw_number_t hwirq);

    驱动调用该函数的时候必须提供HW interrupt ID,也就是意味着driver知道自己使用的HW interrupt ID,而一般情况下,HW interrupt ID其实对具体的driver应该是不可见的,不过有些场景比较特殊,例如GPIO类型的中断,它的HW interrupt ID和GPIO有着特定的关系,driver知道自己使用那个GPIO,也就是知道使用哪一个HW interrupt ID了。

    (2)irq_create_strict_mappings。这个接口函数用来为一组HW interrupt ID建立映射。具体函数的原型定义如下:

    extern int irq_create_strict_mappings(struct irq_domain *domain,
                          unsigned int irq_base,
                          irq_hw_number_t hwirq_base, int count);

    (3)irq_create_of_mapping。看到函数名字中的of(open firmware),我想你也可以猜到了几分,这个接口当然是利用device tree进行映射关系的建立。具体函数的原型定义如下:

    extern unsigned int irq_create_of_mapping(struct of_phandle_args *irq_data);

    通常,一个普通设备的device tree node已经描述了足够的中断信息,在这种情况下,该设备的驱动在初始化的时候可以调用irq_of_parse_and_map这个接口函数进行该device node中和中断相关的内容(interrupts和interrupt-parent属性)进行分析,并建立映射关系,具体代码如下:

    unsigned int irq_of_parse_and_map(struct device_node *dev, int index)
    {
        struct of_phandle_args oirq;

        if (of_irq_parse_one(dev, index, &oirq))----分析device node中的interrupt相关属性
            return 0;

        return irq_create_of_mapping(&oirq);-----创建映射,并返回对应的IRQ number
    }

    对于一个使用Device tree的普通驱动程序(我们推荐这样做),基本上初始化需要调用irq_of_parse_and_map获取IRQ number,然后调用request_threaded_irq申请中断handler。

    (4)irq_create_direct_mapping。这是给no map那种类型的interrupt controller使用的,这里不再赘述。

     

    四、数据结构描述

    1、irq domain的callback接口

    struct irq_domain_ops抽象了一个irq domain的callback函数,定义如下:

    struct irq_domain_ops {
        int (*match)(struct irq_domain *d, struct device_node *node);
        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);
    };

    我们先看xlate函数,语义是翻译(translate)的意思,那么到底翻译什么呢?在DTS文件中,各个使用中断的device node会通过一些属性(例如interrupts和interrupt-parent属性)来提供中断信息给kernel以便kernel可以正确的进行driver的初始化动作。这里,interrupts属性所表示的interrupt specifier只能由具体的interrupt controller(也就是irq domain)来解析。而xlate函数就是将指定的设备(node参数)上若干个(intsize参数)中断属性(intspec参数)翻译成HW interrupt ID(out_hwirq参数)和trigger类型(out_type)。

    match是判断一个指定的interrupt controller(node参数)是否和一个irq domain匹配(d参数),如果匹配的话,返回1。实际上,内核中很少定义这个callback函数,实际上struct irq_domain中有一个of_node指向了对应的interrupt controller的device node,因此,如果不提供该函数,那么default的匹配函数其实就是判断irq domain的of_node成员是否等于传入的node参数。

    map和unmap是操作相反的函数,我们描述其中之一就OK了。调用map函数的时机是在创建(或者更新)HW interrupt ID(hw参数)和IRQ number(virq参数)关系的时候。其实,从发生一个中断到调用该中断的handler仅仅调用一个request_threaded_irq是不够的,还需要针对该irq number设定:

    (1)设定该IRQ number对应的中断描述符(struct irq_desc)的irq chip

    (2)设定该IRQ number对应的中断描述符的highlevel irq-events handler

    (3)设定该IRQ number对应的中断描述符的 irq chip data

    这些设定不适合由具体的硬件驱动来设定,因此在Interrupt controller,也就是irq domain的callback函数中设定。

    2、irq domain

    在内核中,irq domain的概念由struct irq_domain表示:

    struct irq_domain {
        struct list_head link;
        const char *name;
        const struct irq_domain_ops *ops; ----callback函数
        void *host_data;

        /* Optional data */
        struct device_node *of_node; ----该interrupt domain对应的interrupt controller的device node
        struct irq_domain_chip_generic *gc; ---generic irq chip的概念,本文暂不描述

        /* reverse map data. The linear map gets appended to the irq_domain */
        irq_hw_number_t hwirq_max; ----该domain中最大的那个HW interrupt ID
        unsigned int revmap_direct_max_irq; ----
        unsigned int revmap_size; ---线性映射的size,for Radix Tree map和no map,该值等于0
        struct radix_tree_root revmap_tree; ----Radix Tree map使用到的radix tree root node
        unsigned int linear_revmap[]; -----线性映射使用的lookup table
    };

    linux内核中,所有的irq domain被挂入一个全局链表,链表头定义如下:

    static LIST_HEAD(irq_domain_list);

    struct irq_domain中的link成员就是挂入这个队列的节点。通过irq_domain_list这个指针,可以获取整个系统中HW interrupt ID和IRQ number的mapping DB。host_data定义了底层interrupt controller使用的私有数据,和具体的interrupt controller相关(对于GIC,该指针指向一个struct gic_chip_data数据结构)。

    对于线性映射:

    (1)linear_revmap保存了一个线性的lookup table,index是HW interrupt ID,table中保存了IRQ number值

    (2)revmap_size等于线性的lookup table的size。

    (3)hwirq_max保存了最大的HW interrupt ID

    (4)revmap_direct_max_irq没有用,设定为0。revmap_tree没有用。

    对于Radix Tree map:

    (1)linear_revmap没有用,revmap_size等于0。

    (2)hwirq_max没有用,设定为一个最大值。

    (3)revmap_direct_max_irq没有用,设定为0。

    (4)revmap_tree指向Radix tree的root node。

     

    五、中断相关的Device Tree知识回顾

    想要进行映射,首先要了解interrupt controller的拓扑结构。系统中的interrupt controller的拓扑结构以及其interrupt request line的分配情况(分配给哪一个具体的外设)都在Device Tree Source文件中通过下面的属性给出了描述。这些内容在Device Tree的三份文档中给出了一些描述,这里简单总结一下:

    对于那些产生中断的外设,我们需要定义interrupt-parent和interrupts属性:

    (1)interrupt-parent。表明该外设的interrupt request line物理的连接到了哪一个中断控制器上

    (2)interrupts。这个属性描述了具体该外设产生的interrupt的细节信息(也就是传说中的interrupt specifier)。例如:HW interrupt ID(由该外设的device node中的interrupt-parent指向的interrupt controller解析)、interrupt触发类型等。

    对于Interrupt controller,我们需要定义interrupt-controller和#interrupt-cells的属性:

    (1)interrupt-controller。表明该device node就是一个中断控制器

    (2)#interrupt-cells。该中断控制器用多少个cell(一个cell就是一个32-bit的单元)描述一个外设的interrupt request line。?具体每个cell表示什么样的含义由interrupt controller自己定义。

    (3)interrupts和interrupt-parent。对于那些不是root 的interrupt controller,其本身也是作为一个产生中断的外设连接到其他的interrupt controller上,因此也需要定义interrupts和interrupt-parent的属性。

     

    六、Mapping DB的建立

    1、概述

    系统中HW interrupt ID和IRQ number的mapping DB是在整个系统初始化的过程中建立起来的,过程如下:

    (1)DTS文件描述了系统中的interrupt controller以及外设IRQ的拓扑结构,在linux kernel启动的时候,由bootloader传递给kernel(实际传递的是DTB)。

    (2)在Device Tree初始化的时候,形成了系统内所有的device node的树状结构,当然其中包括所有和中断拓扑相关的数据结构(所有的interrupt controller的node和使用中断的外设node)

    (3)在machine driver初始化的时候会调用of_irq_init函数,在该函数中会扫描所有interrupt controller的节点,并调用适合的interrupt controller driver进行初始化。毫无疑问,初始化需要注意顺序,首先初始化root,然后first level,second level,最好是leaf node。在初始化的过程中,一般会调用上节中的接口函数向系统增加irq domain。有些interrupt controller会在其driver初始化的过程中创建映射

    (4)在各个driver初始化的过程中,创建映射

     

    2、 interrupt controller初始化的过程中,注册irq domain

    我们以GIC的代码为例。具体代码在gic_of_init->gic_init_bases中,如下:

    void __init gic_init_bases(unsigned int gic_nr, int irq_start,
                   void __iomem *dist_base, void __iomem *cpu_base,
                   u32 percpu_offset, struct device_node *node)
    {
        irq_hw_number_t hwirq_base;
        struct gic_chip_data *gic;
        int gic_irqs, irq_base, i;

    ……
    对于root GIC
            hwirq_base = 16;
            gic_irqs = 系统支持的所有的中断数目-16。之所以减去16主要是因为root GIC的0~15号HW interrupt 是for IPI的,因此要去掉。也正因为如此hwirq_base从16开始


        irq_base = irq_alloc_descs(irq_start, 16, gic_irqs, numa_node_id());申请gic_irqs个IRQ资源,从16号开始搜索IRQ number。由于是root GIC,申请的IRQ基本上会从16号开始


        gic->domain = irq_domain_add_legacy(node, gic_irqs, irq_base,
                        hwirq_base, &gic_irq_domain_ops, gic);---向系统注册irq domain并创建映射

    ……
    }

    很遗憾,在GIC的代码中没有调用标准的注册irq domain的接口函数。要了解其背后的原因,我们需要回到过去。在旧的linux kernel中,ARM体系结构的代码不甚理想。在arch/arm目录充斥了很多board specific的代码,其中定义了很多具体设备相关的静态表格,这些表格规定了各个device使用的资源,当然,其中包括IRQ资源。在这种情况下,各个外设的IRQ是固定的(如果作为驱动程序员的你足够老的话,应该记得很长篇幅的针对IRQ number的宏定义),也就是说,HW interrupt ID和IRQ number的关系是固定的。一旦关系固定,我们就可以在interupt controller的代码中创建这些映射关系。具体代码如下:

    struct irq_domain *irq_domain_add_legacy(struct device_node *of_node,
                         unsigned int size,
                         unsigned int first_irq,
                         irq_hw_number_t first_hwirq,
                         const struct irq_domain_ops *ops,
                         void *host_data)
    {
        struct irq_domain *domain;

        domain = __irq_domain_add(of_node, first_hwirq + size,----注册irq domain
                      first_hwirq + size, 0, ops, host_data);
        if (!domain)
            return NULL;

        irq_domain_associate_many(domain, first_irq, first_hwirq, size); ---创建映射

        return domain;
    }

    这时候,对于这个版本的GIC driver而言,初始化之后,HW interrupt ID和IRQ number的映射关系已经建立,保存在线性lookup table中,size等于GIC支持的中断数目,具体如下:

    index 0~15对应的IRQ无效

    16号IRQ  <------------------>16号HW interrupt ID

    17号IRQ  <------------------>17号HW interrupt ID

    ……

    如果想充分发挥Device Tree的威力,3.14版本中的GIC 代码需要修改。

     

    3、在各个硬件外设的驱动初始化过程中,创建HW interrupt ID和IRQ number的映射关系

    我们上面的描述过程中,已经提及:设备的驱动在初始化的时候可以调用irq_of_parse_and_map这个接口函数进行该device node中和中断相关的内容(interrupts和interrupt-parent属性)进行分析,并建立映射关系,具体代码如下:

    unsigned int irq_of_parse_and_map(struct device_node *dev, int index)
    {
        struct of_phandle_args oirq;

        if (of_irq_parse_one(dev, index, &oirq))----分析device node中的interrupt相关属性
            return 0;

        return irq_create_of_mapping(&oirq);-----创建映射
    }

    我们再来看看irq_create_of_mapping函数如何创建映射:

    unsigned int irq_create_of_mapping(struct of_phandle_args *irq_data)
    {
        struct irq_domain *domain;
        irq_hw_number_t hwirq;
        unsigned int type = IRQ_TYPE_NONE;
        unsigned int virq;

        domain = irq_data->np ? irq_find_host(irq_data->np) : irq_default_domain;--A
        if (!domain) {
            return 0;
        }


        if (domain->ops->xlate == NULL)--------------B
            hwirq = irq_data->args[0];
        else {
            if (domain->ops->xlate(domain, irq_data->np, irq_data->args,----C
                        irq_data->args_count, &hwirq, &type))
                return 0;
        }

        /* Create mapping */
        virq = irq_create_mapping(domain, hwirq);--------D
        if (!virq)
            return virq;

        /* Set type if specified and different than the current one */
        if (type != IRQ_TYPE_NONE &&
            type != irq_get_trigger_type(virq))
            irq_set_irq_type(virq, type);---------E
        return virq;
    }

    A:这里的代码主要是找到irq domain。这是根据传递进来的参数irq_data的np成员来寻找的,具体定义如下:

    struct of_phandle_args {
        struct device_node *np;---指向了外设对应的interrupt controller的device node
        int args_count;-------该外设定义的interrupt相关属性的个数
        uint32_t args[MAX_PHANDLE_ARGS];----具体的interrupt相当属性的定义
    };

    B:如果没有定义xlate函数,那么取interrupts属性的第一个cell作为HW interrupt ID。

    C:解铃还需系铃人,interrupts属性最好由interrupt controller(也就是irq domain)解释。如果xlate函数能够完成属性解析,那么将输出参数hwirq和type,分别表示HW interrupt ID和interupt type(触发方式等)。

    D:解析完了,最终还是要调用irq_create_mapping函数来创建HW interrupt ID和IRQ number的映射关系。

    E:如果有需要,调用irq_set_irq_type函数设定trigger type

    irq_create_mapping函数建立HW interrupt ID和IRQ number的映射关系。该接口函数以irq domain和HW interrupt ID为参数,返回IRQ number。具体的代码如下:

    unsigned int irq_create_mapping(struct irq_domain *domain,
                    irq_hw_number_t hwirq)
    {
        unsigned int hint;
        int virq;

    如果映射已经存在,那么不需要映射,直接返回
        virq = irq_find_mapping(domain, hwirq);
        if (virq) {
            return virq;
        }


        hint = hwirq % nr_irqs;-------分配一个IRQ 描述符以及对应的irq number
        if (hint == 0)
            hint++;
        virq = irq_alloc_desc_from(hint, of_node_to_nid(domain->of_node));
        if (virq <= 0)
            virq = irq_alloc_desc_from(1, of_node_to_nid(domain->of_node));
        if (virq <= 0) {
            pr_debug("-> virq allocation failed\n");
            return 0;
        }

        if (irq_domain_associate(domain, virq, hwirq)) {---建立mapping
            irq_free_desc(virq);
            return 0;
        }

        return virq;
    }

    对于分配中断描述符这段代码,后续的文章会详细描述。这里简单略过,反正,指向完这段代码,我们就可以或者一个IRQ number以及其对应的中断描述符了。程序注释中没有使用IRQ number而是使用了virtual interrupt number这个术语。virtual interrupt number还是重点理解“virtual”这个词,所谓virtual,其实就是说和具体的硬件连接没有关系了,仅仅是一个number而已。具体建立映射的函数是irq_domain_associate函数,代码如下:

    int irq_domain_associate(struct irq_domain *domain, unsigned int virq,
                 irq_hw_number_t hwirq)
    {
        struct irq_data *irq_data = irq_get_irq_data(virq);
        int ret;

        mutex_lock(&irq_domain_mutex);
        irq_data->hwirq = hwirq;
        irq_data->domain = domain;
        if (domain->ops->map) {
            ret = domain->ops->map(domain, virq, hwirq);---调用irq domain的map callback函数
        }

        if (hwirq < domain->revmap_size) {
            domain->linear_revmap[hwirq] = virq;----填写线性映射lookup table的数据
        } else {
            mutex_lock(&revmap_trees_mutex);
            radix_tree_insert(&domain->revmap_tree, hwirq, irq_data);--向radix tree插入一个node
            mutex_unlock(&revmap_trees_mutex);
        }
        mutex_unlock(&irq_domain_mutex);

        irq_clear_status_flags(virq, IRQ_NOREQUEST); ---该IRQ已经可以申请了,因此clear相关flag

        return 0;
    }

     

    七、将HW interrupt ID转成IRQ number

    创建了庞大的HW interrupt ID到IRQ number的mapping DB,最终还是要使用。具体的使用场景是在CPU相关的处理函数中,程序会读取硬件interrupt ID,并转成IRQ number,调用对应的irq event handler。在本章中,我们以一个级联的GIC系统为例,描述转换过程

    1、GIC driver初始化

    上面已经描述了root GIC的的初始化,我们再来看看second GIC的初始化。具体代码在gic_of_init->gic_init_bases中,如下:

    void __init gic_init_bases(unsigned int gic_nr, int irq_start,
                   void __iomem *dist_base, void __iomem *cpu_base,
                   u32 percpu_offset, struct device_node *node)
    {
        irq_hw_number_t hwirq_base;
        struct gic_chip_data *gic;
        int gic_irqs, irq_base, i;

    ……
    对于second GIC
            hwirq_base = 32; 
            gic_irqs = 系统支持的所有的中断数目-32。之所以减去32主要是因为对于second GIC,其0~15号HW interrupt 是for IPI的,因此要去掉。而16~31号HW interrupt 是for PPI的,也要去掉。也正因为如此hwirq_base从32开始


        irq_base = irq_alloc_descs(irq_start, 16, gic_irqs, numa_node_id());申请gic_irqs个IRQ资源,从16号开始搜索IRQ number。由于是second GIC,申请的IRQ基本上会从root GIC申请的最后一个IRQ号+1开始


        gic->domain = irq_domain_add_legacy(node, gic_irqs, irq_base,
                        hwirq_base, &gic_irq_domain_ops, gic);---向系统注册irq domain并创建映射

    ……
    }

    second GIC初始化之后,该irq domain的HW interrupt ID和IRQ number的映射关系已经建立,保存在线性lookup table中,size等于GIC支持的中断数目,具体如下:

    index 0~32对应的IRQ无效

    root GIC申请的最后一个IRQ号+1  <------------------>32号HW interrupt ID

    root GIC申请的最后一个IRQ号+2  <------------------>33号HW interrupt ID

    ……

    OK,我们回到gic的初始化函数,对于second GIC,还有其他部分的初始化内容:

    int __init gic_of_init(struct device_node *node, struct device_node *parent)
    {

    ……

        if (parent) {
            irq = irq_of_parse_and_map(node, 0);--解析second GIC的interrupts属性,并进行mapping,返回IRQ number
            gic_cascade_irq(gic_cnt, irq);---设置handler
        }
    ……
    }

    上面的初始化函数去掉和级联无关的代码。对于root GIC,其传入的parent是NULL,因此不会执行级联部分的代码。对于second GIC,它是作为其parent(root GIC)的一个普通的irq source,因此,也需要注册该IRQ的handler。由此可见,非root的GIC的初始化分成了两个部分:一部分是作为一个interrupt controller,执行和root GIC一样的初始化代码。另外一方面,GIC又作为一个普通的interrupt generating device,需要象一个普通的设备驱动一样,注册其中断handler。

    irq_of_parse_and_map函数相信大家已经熟悉了,这里不再描述。gic_cascade_irq函数如下:

    void __init gic_cascade_irq(unsigned int gic_nr, unsigned int irq)
    {
        if (irq_set_handler_data(irq, &gic_data[gic_nr]) != 0)---设置handler data
            BUG();
        irq_set_chained_handler(irq, gic_handle_cascade_irq);---设置handler
    }

    2、具体如何在中断处理过程中,将HW interrupt ID转成IRQ number

    在系统的启动过程中,经过了各个interrupt controller以及各个外设驱动的努力,整个interrupt系统的database(将HW interrupt ID转成IRQ number的数据库,这里的数据库不是指SQL lite或者oracle这样通用数据库软件)已经建立。一旦发生硬件中断,经过CPU architecture相关的中断代码之后,会调用irq handler,该函数的一般过程如下:

    (1)首先找到root interrupt controller对应的irq domain。

    (2)根据HW 寄存器信息和irq domain信息获取HW interrupt ID

    (3)调用irq_find_mapping找到HW interrupt ID对应的irq number

    (4)调用handle_IRQ(对于ARM平台)来处理该irq number

    对于级联的情况,过程类似上面的描述,但是需要注意的是在步骤4中不是直接调用该IRQ的hander来处理该irq number因为,这个irq需要各个interrupt controller level上的解析。举一个简单的二阶级联情况:假设系统中有两个interrupt controller,A和B,A是root interrupt controller,B连接到A的13号HW interrupt ID上。在B interrupt controller初始化的时候,除了初始化它作为interrupt controller的那部分内容,还有初始化它作为root interrupt controller A上的一个普通外设这部分的内容。最重要的是调用irq_set_chained_handler设定handler。这样,在上面的步骤4的时候,就会调用13号HW interrupt ID对应的handler(也就是B的handler),在该handler中,会重复上面的(1)~(4)。

    展开全文
  • IRQ外部中断改进1

    2019-04-17 15:31:47
    IRQ外部中断改进1
  • FIQ与IRQ中断

    千次阅读 2021-02-02 10:50:58
    快速中断请求(Fast Interrupt Request,FIQ)与外部中断请求IRQ FIQ和IRQ是两种不同类型的中断,ARM为了支持这两种不同的中断,提供了对应的叫做FIQ和IRQ处理器模式(ARM有7种处理模式)。 一般的中断控制器里我们...

    快速中断请求(Fast Interrupt Request,FIQ)外部中断请求IRQ
    FIQ和IRQ是两种不同类型的中断,ARM为了支持这两种不同的中断,提供了对应的叫做FIQ和IRQ处理器模式(ARM有7种处理模式)。
    一般的中断控制器里我们可以配置与控制器相连的某个中断输入是FIQ还是IRQ,所以一个中断是可以指定为FIQ或者IRQ的,为了合理,要求系统更快响应,自身处理所耗时间也很短的中断设置为FIQ,否则就设置了IRQ。
    如果该中断设置为了IRQ,那么当该中断产生的时候,中断处理器通过IRQ请求线告诉ARM,ARM就知道有个IRQ中断来了,然后ARM切换到IRQ模式运行。类似的如果该中断设置为FIQ,那么当该中断产生的时候,中断处理器通过FIQ请求线告诉ARM,ARM就知道有个FIQ中断来了,然后切换到FIQ模式运行。
    FIQ比IRQ有更高优先级,如果FIQ和IRQ同时产生,那么FIQ先处理。
    FIQ的中断向量地址在0x0000001C,而IRQ的在0x00000018。(也有的在FFFF001C以及FFFF0018),写过完整汇编系统的都比较明白这点的差别,18只能放一条指令,为了不与1C处的FIQ冲突,这个地方只能跳转,而FIQ不一样,1C以后没有任何中断向量表了,这样可以直接在1C处放FIQ的中断处理程序,由于跳转的范围限制,至少少了一条跳转指令。
    IRQ的响应并不及时,从Verilog仿真来看,IRQ会延迟几个指令周期才跳转到中断向量处,看起来像是在等预取的指令执行完。FIQ的响应不清楚,也许比IRQ快。
    中断延迟:从外部中断请求信号发出到执行对应的中断服务程序ISR的第一条指令所需要的时间。通过软件程序设计来缩短中断延迟的方法有:中断优先级和中断嵌套。
    FIQ和IRQ(外部中断模式)之间有很大的区别。FIQ模式必须尽快处理,处理结束后离开这个模式;IRQ模式可以被FIQ模式中断,但IRQ不能中断FIQ模式;为使FIQ模式响应更快,FIQ模式具有更多的影子(Shadow)寄存器。FIQ模式必须禁用中断;如果一个中断例程必须重新启用中断,应使用IRQ模式而不是FIQ模式。
    ARM体系中异常中断种类:
    复位(Reset):当处理器的复位引脚有效时,系统产生复位异常中断,程序跳转到复位异常中断处理程序处执行。复位异常中断通常用在下面两种情况下:系统加电时,系统复位时,跳转到复位中断向量处执行,称为软复位。
    未定义指令(Undefined):当ARM处理器或者是系统中的协处理器认为当前指令未定义时,产生未定义的指令异常中断。可以通过该异常中断机制仿真浮点向量运算。
    软中断(software interrupt):这是一个由用户定义的中断指令。可用于用户模式下的程序调用特权操作指令。在实时操作系统(RTOS)中可以通过该机制实现系统功能调用。
    指令预取中止:如果处理器预取的指令的地址不存在,或者该地址不允许当前指令访问,当该被预取的指令执行时,处理器产生指令预取中止异常中断。
    数据访问中止:如果数据访问指令的目标地址不存在,或者该地址不允许当前指令访问,处理器产生数据访问中止异常中断。
    外部中断请求(IRQ):当处理器的外部中断请求引脚有效,而且CPSR寄存器中的I控制位被清除时,处理器产生外部中断请求异常中断。系统中各个外设通常通过该异常中断请求处理器服务。
    快速中断请求(FIQ):当处理器的外部快速中断请求引脚有效,而且CPSR寄存器中的F控制位被清除时,处理器产生外部中断请求(FIQ)异常中断。

    异常中断向量表及异常中断优先级:
    中断异常向量表中指定了个异常中断及其处理程序的对应关系。它通常存放在存储地址的低端。在ARM体系中,异常中断向量表的大小为32个字节。其中,每个异常中断占据4个字节大小,保留4个字节空间。
    每个异常中断对应的中断向量表中的4个字节的空间中存放一个跳转指令或者一个向PC寄存器中赋值的数据访问指令。通常这两种指令,程序将跳转到相应的异常中断处理程序处执行。
    当几个异常中断同时发生时,就必须按照一定的次序来处理这些异常中断。在ARM中通过给异常中断赋予一定的优先级来实现这种处理次序。当然有异常中断是不可能同时发生的,如指令预取中止异常和软中断(SWI)异常中断是由同一条指令的执行触发的,它们是不可能同时发生的。处理器执行某个特定的异常中断的过程中,称为处理器处于特定的中断模式。各异常中断的中断向量地址以及中断的处理优先级如下表所示。
    在这里插入图片描述
    异常中断使用的寄存器:
    各异常中断对应着一定的处理器模式。应用程序通常运行在用户模式下。ARM中的处理器模式如下表所示。
    在这里插入图片描述
    进入和退出异常中断的过程:
    下面介绍处理器对于各种异常中断的响应过程以及从异常中断处理程序中返回的方法。对于不同的异常中断处理程序,返回地址以及使用的指令是不同的。
    ARM处理器对异常中断的响应过程如下:
    (1).保存处理器当前状态、中断屏蔽位以及各条件标志位。这是通过将当前程序状态寄存器CPSR的内容保存到将要执行的异常中断对应的SPSR寄存器中实现的。各异常中断有自己的物理SPSR寄存器。
    (2).设置当前程序状态寄存器CPSR中相应的位。包括设置CPSR中的位,使处理器进入相应的执行模式;设置CPSR中的位,禁止IRQ中断,当进入FIQ模式时,禁止IRQ中断。
    (3).将寄存器lr_mode设置成返回地址。
    (4).将程序计数器(PC)值,设置成该异常中断的中断向量地址,从而跳转到相应的异常中断处理程序处执行。
    ARM中断处理流程
    一、Linux中断的简易模型:
    当一个IRQ产生后,CPU会首先自动跳转到IRQ的中断向量(这个中断向量是可设置的),在这个IRQ中断向量里又是一个跳转指令,CPU再次跳转,跳转后的代码主要完成的工作是保存一些寄存器,然后读取中断寄存器经过计算(这个计算并不是单纯的跟INTPND对应)得到中断号,然后跳转到一个中断处理的通用函数,并把中断号传过去(汇编向C传参数)。
    在这个通用处理函数里,根据中断号找到我们自己设定的中断处理函数,然后执行。
    我们注意到,最后一步linux会去找我们自己设定的中断处理函数,这个函数是我们事先通过注册来挂接在一个结构数组(这个数组里的元素个数即为中断号的个数)上,linux会去遍历这个数组(如果使用到了共享中断,即一个中断号上注册多个中断服务程序,则每个中断服务程序都会被执行到)。
    可以进一步归纳为,linux的中断机制可以分为两部分:
    1、定义的中断处理函数是如何注册到linux系统的;
    2、当中断发生时,linux如何自动跳转并找出中断号,然后根据中断号来找到注册在该中断号上的中断处理函数并执行;
    接下来将从两个方面来详细说明这个机制,
    第一个方面是,自己定义的中断处理函数是如何注册到系统中断去的(必须要进行注册,linux才能找到并执行);
    另一个方面是,中断发生时,linux具体会做些什么进而找到我们注册的函数并执行。
    二、Linux中断注册过程:
    设备驱动的中断注册一般在probe函数中实现,以TP为例:
    在这里插入图片描述
    TP注册成功后,通过串口可以知道得到的中断号为 irq=298
    在Linux内核中用于申请中断的函数是request_irq(),函数原型在Kernel/irq/manage.c中定义,其源码如下:
    在这里插入图片描述
    irq:要申请的硬件中断号;
    handler:向系统注册的中断处理函数,是一个回调函数,中断发生时,系统调用这个函数,dev参数将被传递给它;
    flags:中断处理的属性,若设置了IRQF_DISABLED ,则表示中断处理程序是快速处理程序,快速处理程序被调用时屏蔽所有中断,慢速处理程序不屏蔽;若设置了IRQF_SHARED,则表示多个设备共享中断,若设置了IRQF_SAMPLE_RANDOM,表示对系统熵有贡献,对系统获取随机数有好处。(这几个flag是可以通过或的方式同时使用的)
    name:设置中断名称,通常是设备驱动程序的名称 在cat /proc/interrupts中可以看到此名称。
    在这里插入图片描述
    dev:在中断共享时会用到,一般设置为这个设备的设备结构体或者NULL。

    另一种更容易理解的说法:
    irq:就是中断号,什么意思,就是说你要放在irq_desc[]这个数组的哪里,换句话就是申请哪个中断号,
    irq_handler_t handler:这个就是中断处理函数句柄,flags中断标志位是用来决定中断类型的,比如是快速中断或者共享中断等属性,
    name:就是设备驱动的名称
    dev:就是设备id,主要用在共享中断里面用来制定中断服务需要参考的数据地址,简单点就是在调用中断服务函数的时候会当作参数床给中断服务函数。
    过程是怎样的呢?当用户调用这个函数的时候,内核会创建一个irqaction结构体,然后把它加入到irq_desc[]数组
    中就可以了。当CPU收到中断请求后就通过中断好找到中断例程描述符再找到中断函数执行即可。
    request_irq()返回0表示成功,返回-INVAL表示中断号无效或处理函数指针为NULL,返回-EBUSY表示中断已经被占用且不能共享。
    request_irq()函数做了封装,该函数调用了request_threaded_irq()如下:
    在这里插入图片描述
    而其实最终将自己定义的中断服务函数加入到Linux中还是__setup_irq()这个函数,
    在这里插入图片描述
    从上面的函数可以看到,通过这个while循环,将自己定义的中断服务函数被加入到这个中断号的中断函数链表末尾,在__setup_irq这个函数里,完成一个irqaction的加入,从而我们中断产生时,linux能顺利找到我们自己定义的中断处理函数。
    三、底层中断处理分析
    Linux里有中断号的概念,在中断发生时,linux会执行所有注册在该中断号上的所有函数。
    那么linux里的中断向量是怎么设定的呢?中断号是怎么算处理的呢?为什么所有注册在中断号上的函数会在中断产生时被执行呢?这里我们将对这些问题仔细分析。
    在中断发生时,处理器的第一个动作是自动(硬件自己完成)跳转到该中断的中断向量,
    中断向量表的起始地址可以有两个位置:arm的异常和复位向量表有两种选择,一种是低端向量,向量地址位于0x00000000,另一种是高端向量,向量地址位于0xffff0000,
    Linux选择使用高端向量模式,也就是说,当异常发生时,CPU会把PC指针自动跳转到始于0xffff0000开始的某一个地址上。可以通过CP15协处理器c1寄存器中V位(bit[13])控制。V和中断向量表的对应关系如下:
    V=0—0x00000000~0x0000001C
    V=1—0xffff0000~0xffff001C
    当配置好了中断向量起始地址后,只要有中断发生,CPU就会自动跳到这中断向量处,不管这个地址处挂的是SDRAM或ROM。
    当设置好了中断向量起始地址后,我们就需要在这些地址上放上我们的中断向量,由于linux启动时,内核代码会有一个从ROM中搬到RAM中的过程,中断向量也需要般过来,实际上在linux中,向量表建立的函数为
    在这里插入图片描述
    中断的分发:
    中断是处理器核异常的一种,所以处理器设计中,外设中断引起处理器异常,处理器跳转到异常向量表的相应异常入口取指执行。
    我们主要来看产生中断后,处理器跳转到中断异常入口后的执行流程。
    对于arm处理器,执行流程如下:
    vector_irq —>irq_handler —> arch_irq_handler_default —> asm_do_IRQ —>handle_IRQ
    在arch_irq_handler_default中调用get_irqnr_and_base获取中断号,传给asm_do_IRQ作为参数。
    get_irqnr_and_base由板级支持包实现。
    到这里顺便提醒下,中断注意事项:
    1.首次申请中断时,默认情况中断是打开时能的。
    2.中断上半部要使用disable_irq_nosync()而不是disable_irq(),否则会导致死锁。
    3disable_irq()/enable_irq()可以多次嵌套调用,但是一定要在数量上配对。
    4.执行中断上半部时当前中断线是被屏蔽的,包括其他cpu,当前其他irq number不会被屏蔽。
    5.一个软中断不会抢占另一个软中断,唯一可抢占它的只有中断处理程序,因为在处理软中断处理程序的时候,中断是开启的,但它和中断处理程序一样不能休眠。本地软中断被禁止,但是其他处理器上可执行软中断,甚至是同类的中断,那么处理函数就会被执行两次,数据就会遭到破坏了。因此如果没必要,我们就用tasklet, 它的同一个处理函数不会在两个处理器上同时运行,这样也就避免了加锁的麻烦。
    中断向量表在arch/arm/kernel/entry_armv.S文件的结尾部分,如下:
    在这里插入图片描述
    不过要注意位于__vectors_start和__vectors_end之间的是真正的向量跳转表,位于__stubs_start和__stubs_end之间的是处理跳转的部分。
    例如:
    vector_stub irq,IRQ_MODE, 4
    以上这一句把宏展开后实际上就是定义了vector_irq,根据进入中断前的cpu模式,分别跳转到__irq_usr或__irq_svc。
    系统启动阶段,位于arch/arm/kernel/traps.c中的early_trap_init()被调用:
    在这里插入图片描述
    以上两个memcpy会把__vectors_start开始的代码拷贝到0xffff0000处,把__stubs_start开始的代码拷贝到0xFFFF0000+0x1000处,这样,异常中断到来时,CPU就可以正确地跳转到相应中断向量入口并执行他们。
    对于系统的外部设备来说,通常都是使用IRQ中断,所以我们只关注__irq_usr和__irq_svc,两个函数最终都会进入irq_handler这个宏:
    在这里插入图片描述
    接下来这个宏在entry-macro-multi.S,到这里后,就跳转到C调用了,asm_do_IRQ。
    在这里插入图片描述
    此函数在irq.c函数中实现,
    在这里插入图片描述
    在该文件下查看调用关系之后,最后来到关机函数:
    在这里插入图片描述
    接下来就简要的介绍一下三个结构体中重要变量的具体含义:
    struct irq_desc {
    structirq_data irq_data; //保存中断请求irq和chip相关的数据
    irq_flow_handler_t handle_irq; //指向一个跟当前设备中断触发电信号类型相关的函数,比方说如果是边沿触发,那么就指向一个边沿触发类型的函数.在handle_irq指向的函数内部会调用
    //设备特定的中断服务例程.特定平台linux系统在初始化阶段会提供handle_irq的具体实现

    structirqaction       *action;        /* IRQ action list */ irqaction链表,是对某一具体设备的中断处理的抽象,设备驱动程序会通过
                       //request_irq来向这个链表中注册自己的中断处理函数,这个action结构体中有个next成员变量,会把
                       //注册的ISR串连起来,当然如果此irq line上只有一个设备,那么这个action就对应这个设备的中断处理程序
    
        unsignedint           status_use_accessors;//处理中断时的irq状态都保存在这个成员变量中
        constchar             *name;        //会显示在/proc/interrupts中
    


    };

    struct irq_data {
    unsigned int irq; //中断请求号
    structirq_chip *chip; //当前中断来自的PIC,chip是对PIC的一个抽象,屏蔽硬件平台上PIC的差异,软件提供统一的对PIC操作的接口.这些函数接口主要用来屏蔽或启用当前
    //中断,设定外部设备中断触发电信号的类型,向发出中断请求的设备发送中断响应信号,平台初始化函数负责实现该平台对应的PIC函数,并安装对irq_desc数组中

    };

    struct irqaction {
    irq_handler_t handler; //中断服务例程,即我们熟知的ISR,当我们调用request_irq时会把我们实现的ISR注册到这里
    void *dev_id; //调用handler时传递的参数,在多个设备共享一个irq号时特别重要,设备驱动程序就是通过此成员变量来标识自己,在free_irq时用到
    structirqaction *next; //串连下一个action

    };
    通过以上的介绍,我们可以知道,一个中断对应两个层次,一个是handle_irq,一个是action.前者对应irqline上的处理动作,后者对应设备相关的中断处理,也就是说,一条irq line对应一个handle_irq,而这条irq line上可以挂载多个设备,即多个设备都可以通过同一个irqline产生中断,action成员变量用来串连挂载在此irq line上的各个设备驱动的ISR。如果irq line上只有一个设备驱动注册,那么这个action成员变量即是此设备驱动的ISR。
    接着desc->handle_irq(irq, desc)这个调用走,handle_irq是在__irq_set_handler_locked()函数中赋值,得到处理句柄。
    在这里插入图片描述
    到处中断处理的大致流程走完。

    ARM对异常(中断)的处理过程:
    1、初始化:
    1)设置中断源,使其可以产生中断
    2)设置中断控制器(使能屏蔽、优先级)
    3)设置CPU的中断总开关(使能中断)
    2、执行程序,CPU每执行一条指令都会检查是否有异常(中断)产生--------------------硬件实现
    3、中断产生-------->CPU检查到有异常(中断)产生,开始处理:
    1)CPU针对不同的异常,会跳到不同的异常向量地址执行------------------------------硬件实现
    异常向量:是一个跳转指令,再跳去执行某个函数
    2)该函数A处理内容:
    a)保存现场
    b)处理异常(中断)---------------------->调用不同的函数B-------------软件实现
    c)恢复现场
    3)函数B实现功能:
    a)判断中断源-------------------------------------软件实现
    b)调用执行相应的中断服务函数
    中断源:产生中断信号-------------->中断控制器---------------->CPU(CPU每执行一条指令都会去检查是否有异常产生-------硬件实现)----------------->检查到中断------->CPU跳到相应的中断向量地址执行(硬件实现)(存放跳转指令)----------->再跳到某个函数A(保存现场;调用函数B;恢复现场)------------->再跳到某个函数B(判断中断源;执行相应 的中断服务函数)

    展开全文
  • [RK3568 Android11] 教程之硬件中断(IRQ)

    千次阅读 2021-12-11 12:25:31
    一、什么是中断 所谓中断,是指CPU在执行程序的过程中,出现了某突发事件需立即处理;CPU必须暂停当前的程序,转去处理突发事件,待处理完毕后CPU又返回原程序被中断的位置继续执行。 中断系统是一个处理器重要的...
  • Linux中断——request_irq

    千次阅读 2021-04-06 11:17:30
    linux 中断irq 前言 中断注册与释放 irq相关 查看/proc/interrupts 查看/proc/stat IRQ Number问题 Handler相关 flag相关 linux 中断irq 前言 前面一篇文章已经对中断做了一些简单介绍《关于Linux...
  • set_irq_affinity

    2020-06-02 11:34:55
    可以进行中断绑定指定的cpu,提高网卡收包效率 把下面“eth1” 修改成对应的网卡名称 irq=$(cat /proc/interrupts | grep eth1 | cut -d':' -f 1); echo $irq for i in $irq ; do sudo cat /proc/irq/$i/smp_...
  • 如何解决电脑中断请求IRQ冲突汇编.pdf
  • 什么是中断IRQ和I-O端口地址什么是中断IRQ和I-O端口地址
  • IRQ与FIQ是ARM处理器的两种不同类型的中断模式: IRQ(Interrupt Request):指中断模式。 FIQ(Fast Interrupt Request):指快速中断模式。 两种中断模式的区别: 1、在速度方面:FIQ大于IRQ(FIQ所使用的寄存器...
  • 描述了linux中断的上半部即中断的注册、中断处理。中断的下半部机制,包括软中断、tasklet和工作队列
  • IRQ,FIQ定义: 这就是个普通中断,当我们程序定义了该中断,并且在程序运行的时候产生了IRQ中断,则此时的芯片是这样运行的------中断处理器吧利用IRQ请求线来高速ARM,ARM就知道有个IRQ中断来了,然后ARM切换到IRQ...
  • XS128 IRQ中断模块

    2013-03-26 09:41:24
    飞思卡尔XS128 IRQ中断模块,文件里面直接是工程模块,可直接编译下载,同时附带说明文档txt
  • IRQ外部中断模块

    2013-09-04 10:20:07
    IRQ外部中断模块
  • 在上一篇文章中我们分析了中断的三个结构体irq_desc、irq_chip、irqaction,并且简单分析了中断发生的流程,实际上上述的操作我们是不需要自己实现的,内核已经实现了,我们需要做的是如何去注册一个中断,编写相应...
  • 非向量IRQ中断

    2013-04-20 14:02:26
    ARM非向量IRQ中断技术,C编写,编译器KEIL3,仿真器proteus7
  • 产生中断的每个设备都有一个相应的中断处理程序(本质上中断处理程序通常不是和特定设备关联,而是和特定中断关联,也就是说,如果一个设备可以产生多种不同的中断,那么该设备就可以对应多个中断处理...
  • linux下查看中断请求记录。通过cpu的中断请求的响应,可以看出cpu都在为哪些设备干活,干的活有多少量等信息。[~]$ cat /proc/interruptsCPU0 CPU1 CPU2 CPU30: 1383283707 0 0 0 IO-APIC-edge timer1: ...
  • 标签:如何解决irq冲突it如何解决IRQ冲突轻松分配板卡资源中断是计算机处理特殊问题的一个过程,当计算机执行程序的过程中出现某个特殊情况时,会暂时中止现行程序,转去执行这一事件的程序,处理完毕之后再回到原来...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 77,846
精华内容 31,138
关键字:

中断irq8