Linux内核外部中断解析(上)

嵌入式技术

1378人已加入

描述

中断分类

在中断的多种分类方法中,我们根据中断的来源来分类:

内部中断:例如软件中断指令, 溢出,除法错误等。操作系统从用户态切换到内核态。都会在一条指令执行完后才会触发,因为为同步。

外部中断:由外设提供。由于不知道何时中断会到来,因此为异步过程。

中断处理器硬件架构

外部中断的整体框架如下图:

中断

首先我们先了解几个名词:

软件中断irq:它是发生设备中断时处理器从PIC中读到的中断号码,在内核建立的中断处理框架内,会使用这个irq号来表示一个外设的中断,并调用对应的中断处理例程。

中断向量表(Vector table):除了外部中断还有异常,陷阱等异常事件,中断向量表里面的每一项都是一个中断或异常处理函数的入口地址。外部设备的中断常常对应向量表中的某一项,这是个通用的外部中断处理函数入口,因此进入通用的中断处理函数之后,系统必须要知道正在处理的中断是哪一个设备产生的,而这正是由irq决定的。中断向量表的内容由操作系统在初始化阶段来填写。对于外部中断,操作系统负责实现一个通用的外部中断处理函数,然后把这个函数的入口地址翻到中断向量表中的对应位置。

可编程中断控制器:PIC(Programmable Interrupt Controller) ,一般可通过处理器进行编程配置。

下面是从ARM开发手册当中GIC的中断控制器框架图:

中断

ARM GIC支持3种类型的中断:

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

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

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

Linux外部中断处理程序框架

外部中断处理程序架构如下:

中断

内核中断向量表初始化过程(下文在不做特别说明的状况下均以Linux5.0内核为版本进行分析)

//arch/arm64/kernel/entry.S
/**Exception vectors*/
      .pushsection ".entry.text", "ax"  
      .align  11
  ENTRY(vectors)  
      kernel_ventry  1, sync_invalid // Synchronous EL1t  
      kernel_ventry  1, irq_invalid  // IRQ EL1t  
      kernel_ventry  1, fiq_invalid  // FIQ EL1t  
      kernel_ventry  1, error_invalid // Error EL1t
      
      kernel_ventry  1, sync  // Synchronous EL1h
      kernel_ventry  1, irq  // IRQ EL1h
      kernel_ventry  1, fiq_invalid  // FIQ EL1h  
      kernel_ventry  1, error  // Error EL1h    
      
      kernel_ventry  0, sync  // Synchronous 64-bit EL0  
      kernel_ventry  0, irq  // IRQ 64-bit EL0  
      kernel_ventry  0, fiq_invalid  // FIQ 64-bit EL0  
      kernel_ventry  0, error  // Error 64-bit EL0
      
      #ifdef CONFIG_COMPAT  
      kernel_ventry  0, sync_compat, 32 // Synchronous 32-bit EL0  
      kernel_ventry  0, irq_compat, 32  // IRQ 32-bit EL0  
      kernel_ventry  0, fiq_invalid_compat, 32  // FIQ 32-bit EL0  
      kernel_ventry  0, error_compat, 32 // Error 32-bit EL0
      #else  
      kernel_ventry  0, sync_invalid, 32 // Synchronous 32-bit EL0  
      kernel_ventry  0, irq_invalid, 32 // IRQ 32-bit EL0 
      kernel_ventry  0, fiq_invalid, 32 // FIQ 32-bit EL0  
      kernel_ventry  0, error_invalid, 32  // Error 32-bit EL0
      #endifEND(vectors)

对kernel_ventry宏做解读:.align=7,说明该段代码是以2^7=128字节对齐的,这和向量表中每一个offset的大小是一致的

代码看似非常复杂,其实最终跳转到了b el()\\el()_\\label, 翻译一下,其实就是跳转到了如下这样的函数中

el1_sync_invalid
el1_irq_invalid
el1_fiq_invalid
el1_error_invalid


el1_sync:
...
el1_irq:
   kernel_entry 1             
   enable_da_f
   #ifdef CONFIG_TRACE_IRQFLAGS
   bl  trace_hardirqs_off
   #endif
   irq_handler
   #ifdef CONFIG_PREEMPT
   ldr   x24, [tsk, #TSK_TI_PREEMPT]  // get preempt count             
   cbnz  x24, 1f  // preempt count != 0             
   bl   el1_preempt              
   .macro  irq_handler              
   ldr_l  x1, handle_arch_irq              
   mov  x0, sp              
   irq_stack_entry              
   blr  x1              
   irq_stack_exit
 ...     
el1_fiq:
...
el1_error:
...
el0_sync:
...
el0_irq:
...
el0_fiq:
...
el0_error:
...

ARM64中,当外部中断发生时, 会调用el1_irq, 在上述20行中,会call handle_arch_irq. 此为一个函数指针, 在GIC中断控制器在做初始化时设置的. 而中断来临时,handle_domain_irq会被call 到.

//kernel/irq/handle.c
int __init set_handle_irq(void(*handle_irq)(stuct pt_regs*)){
    ....
    handle_arch_irq = handle_irq;
    ....
}
//drivers/irqchip/irq-gic.c
static void _exception_irq_entry gic_handle_irq(struct pt_regs* regs){
  .....
  //调用对应的通用中断处理函数.
  handle_domain_irq(gic- >domain,irqnr,regs);
  .....
}
//drivers/irqchip/irq-gic.c
static int __init __gic_init_bases(struct gic_chip_data *gic,int irq_start, struct fwnode_handle *handle){
   ....
   set_handle_irq(gic_handle_irq);//中断向量指向的irq, 汇编语言会调用到此函数
   ....
}
//drivers/irqchip/irq-gic.c
static int gic_init_bases(struct gic_chip_data *gic, int irq_start,struct fwnode_handle *handle){
   ....
   //分配irq_desc, 里面会call gic_irq_domain_map函数, 设置一吃handle的电信号处理函数
   irq_base = irq_alloc_descs(irq_start, 16, gic_irqs,numa_node_id());
   ....
}
//driver/irqchip/irq-gic.c
static int gic_irq_domain_map(struct irq_domain *d, unsigned int irq,irq_hw_number_t hw){
   struct gic_chip_data *gic = d- >host_data;       
   //根据中断号的不同,处理不同的handle, driver 的实现依赖于硬件的GIC 硬件spec, 参考规范      
   if (hw < 32) {               
         irq_set_percpu_devid(irq);  
         //设置真正的PIC的一级handle,处理PIC的电信号             
         irq_domain_set_info(d, irq, hw, &gic- >chip, d- >host_data,handle_percpu_devid_irq, NULL, NULL); 
         irq_set_status_flags(irq, IRQ_NOAUTOEN);        
    } else {
         //设置真正的PIC的一级handle,处理PIC的电信号 
         irq_domain_set_info(d, irq, hw, &gic- >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)));       
     }
     return 0;
}
//kernel/irq/chip.c
//irq > 32的电信号处理函数, 下面几个是函数的层级,一步一步call到最终bsp注册处理的函数当中.
void handle_fasteio_iq(struct irq_desc *desc){
    ...   
    handle_irq_event(desc);     
    ...
}
//kernel/irq/handle.c
irqreturn_t handle_irq_event(struct irq_desc *desc)){    
    ...    
    handle_irq_event_percpu(desc);    
    ...
}
//kernel/irq/handle.c
irqreturn_t handle_irq_event_percpu(struct irq_desc *desc){    
    ...    
    retval = __handle_irq_event_percpu(desc, &flags);    
    add_interrupt_randomness(desc- >irq_data.irq, flags);    
    ...
}
//kernel/irq/handle.c
//电信号上半部最终处理的函数,这里面会去call bsp 工程师注册的上半部的函数
irqreturn_t __handle_irq_event_percpu(struct irq_desc *desc, unsigned int *flags){    
     ...
     for_each_action_of_desc(desc, action) {      
         ...
         res = action- >handler(irq, action- >dev_id);//设备driver的上半部callback函数
         ....
         switch (res) { 
             //上半部的返回值决定了下半部的触发方式,下节从BSP角度去看会用到.  
             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);//唤醒线程化传输的下半部thread_fn                      
                /* Fall through to add to randomness */              
              case IRQ_HANDLED:  
                  *flags |= action- >flags;   
                   break;                     
                   ....
           }               
        }     
    ...
}
//kernel/irq/handle.c
//唤醒线程化传输的下半部thread_fn                      
void __irq_wake_thread(struct irq_desc *desc, struct irqaction *action){     
    ...     
    wake_up_process(action- >thread);
 }

通用中断框架:

//kernel/irq/irqdesc.h
static inline int handle_domain_irq(struct irq_domain *domain,unsigned int hwirq, struct pt_regs *regs){  
      return __handle_domain_irq(domain, hwirq, true, regs);
}
//kernel/irq/irqdesc.c
int __handle_domain_irq(struct irq_domain *domain, unsigned int hwirq,bool lookup, struct pt_regs *regs){  
  struct pt_regs *old_regs = set_irq_regs(regs);  
  unsigned int irq = hwirq;  
  int ret = 0;  
  irq_enter();
  #ifdef CONFIG_IRQ_DOMAIN  
  if (lookup)        
    irq = irq_find_mapping(domain, hwirq);
  #endif  
  /** Some hardware gives randomly wrong interrupts.  Rather   * than crashing, do something sensible.   */  
  if (unlikely(!irq || irq >= nr_irqs)) {
    ack_bad_irq(irq);           
    ret = -EINVAL;
   } else {
     generic_handle_irq(irq);  
    }
    irq_exit();  
    set_irq_regs(old_regs);  
    return ret;
}
//kernel/irq/irqdesc.c
int generic_handle_irq(unsigned int irq){  
  struct irq_desc *desc = irq_to_desc(irq);  
  if (!desc)    
      return -EINVAL;  
   generic_handle_irq_desc(desc);  
   return 0;
}
//kernel/irq/irqdesc.h
static inline void generic_handle_irq_desc(struct irq_desc *desc){
   desc- >handle_irq(desc);//处理电信号类型相关函数.
}

通用中断会处理下面几件事情: 1. 上述第7行: 将一个pt_regs的指针变量保存

  1. 第10行更新系统一些统计量,开始进入上半部HARDIRO
  2. 第20行核心处理函数,会调用注册的handle
  3. 第22行 进入下半部执行.
  4. 第23行,恢复pt_regs

内核当中关于外部中断的关键数据结构以及关系:

中断

中断

以上handle_irq 与action之间的层次关系得出以下结论:

  1. 一个PIC设备可以挂载多个handle_irq,也就是在/proc/interrupts 下面看到的具有相同的PIC设备,反映到数据结构里面就是irq_chip 指向相同对象。一个IRQ_line(handle_irq) 可以挂载多个设备,这些设备共享irq号,以irq_action 里的dev_id来区分不同的具体设备,具体设备有具体的action。
  2. 对于一个PIC设备,其中断处理分为两级,第一级调用是irq_desc[irq].handle_irq, 第二级是特定的中断处理例程ISR,在handle_irq的内部通过irq_desc[irq].action->handler调用。第一级函数在平台初始化时被安装到irq_desc数组中,第二级函数的注册发生在设备驱动程序调用request_irq安装对应设备的中断处理例程时。
  3. PIC由内核/SOC工程师完成,ISR由BSP工程师完成。

总结

中断控制器一般在soc当中(例如文中的GICv2)或者单独的一颗chip(例如早期x86的8259A),上文分析的为站到内核工程师的角度去分析PIC部分, BSP工程师通常下driver时用的API能工作起来依靠上述架构, 下节我们继续分析站到BSP工程师角度从API方面看如何添加irq_action来让driver达到我们工作的预期.

打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分