ARM64中断处理过程: https://www.daodaodao123.com/?p=146 上文总结了ARM64裸机中断处理的详细过程,这里主要总结下linux中断处理相关内容;

0.为什么有中断?

中断,本质上是外设发生了事变,需要异步的通知(经由中断控制器,路由给)CPU; 这个过程涉及三部分硬件:外设->中断控制器->CPU

1中断处理过程:

外设事变发生后,发送中断信号给中断控制器,中断控制器经过仲裁,路由给CPU; 上图是中断控制器的状态机转换图,一个完整的中断发生过程如下(假设现在有一个触摸屏的外设,配置为高电平触发):

  • 中断控制器默认是inactive状态;
  • 外设有事件发生(比如按下触摸屏),触摸屏会产生一个中断信号,送给中断控制器, 如上图中的A1, 中断控制器变为pending状态,此时外设送给中断控制器的信号线保持高电平;
  • 中断控制器经过仲裁,选择一个最高优先级中断信号,路由给CPU,CPU没有ACK之前,一直维持信号线的电平状态;
  • ARM收到中断信号,开始响应时,硬件自动屏蔽CPU中断(清除daif位),软件从中断控制器读出具体是哪个中断产生,然后对中断控制器进行ACK;若边沿触发中断控制器可能清掉pending(电平触发,不会清),此时中断控制器处于active and pending(D路径)或active(C路径);
  • CPU执行中断处理程序,中断服务程序退出时,软件恢复CPSR,并对中断控制器EOI;
  • 中断控制器,收到EOI,变为inactive状态,再选择下一个pending的中断信号,发给CPU;
  • 中断处理的底半部,处理掉外设中断事件源(比如读取触摸屏的坐标,访问相应寄存器)之后,外设的pending信号被清除掉;

linux相对应的API:

disable_irq(n);         //操作中断控制器,屏蔽n号中断;
local_irq_disable();   //关闭CPU中断

2.disable_irq(n)可能并未生效?

linux里的设计,很多地方都是惰性原则,禁止n号中断,可能并未真正执行,假如逻辑执行期间,n中断并未产生,不会有任何问题,还提高性能;

disable_irq(n);
...              //n号中断发生, 延后执行 
enable_irq(n)

若期间有中断产生呢? **enable_irq(n)使能n号中断后,n号中断服务程序立即执行;**这样,就算disable_irq(n)期间有n号中断产生,也不会漏掉,只是延后执行;

//__enable_irq-->irq_startup-->check_irq_resend->irq_sw_resend
/* Tasklet to handle resend: */
static DECLARE_TASKLET(resend_tasklet, resend_irqs);

static int irq_sw_resend(struct irq_desc *desc)
{
   unsigned int irq = irq_desc_get_irq(desc);

   /*
    * Validate whether this interrupt can be safely injected from
    * non interrupt context
    */
   if (handle_enforce_irqctx(&desc->irq_data))
       return -EINVAL;

   /*
    * If the interrupt is running in the thread context of the parent
    * irq we need to be careful, because we cannot trigger it
    * directly.
    */
   if (irq_settings_is_nested_thread(desc)) {
       /*
        * If the parent_irq is valid, we retrigger the parent,
        * otherwise we do nothing.
        */
       if (!desc->parent_irq)
           return -EINVAL;
       irq = desc->parent_irq;
   }

   /* Set it pending and activate the softirq: */
   set_bit(irq, irqs_resend);
   tasklet_schedule(&resend_tasklet);
   return 0;
}

3.向量中断和非向量中断

向量中断:不同的中断跳转到不同地址,比如x86; 非向量中断,跳转到一个入口地址,通过寄存器来判断具体是哪个中断; linux的实现,最终不同中断信号跳转到不同的irq_desc[]分支;

4.什么是中断号?

linux中断号是个纯软件概念,其与中断控制器的硬件中断号,非线性一 一对应; 实际硬件电路中,中断控制器可能是多级级联的; 硬件中断号由硬件电路决定,通常对应配置在设备树里;软件中断号由linux决定,一一对应;

5.中断分类

在一个多核系统中,中断分为三类; PPI:只能本核响应,比如TWD; IPI: 用于多核间的通信,比如smp调度; SPI: 共享外围设备中断,可以路由给任何一个核; 对于SPI类型的中断,内核可以通过API设定中断触发的CPU核,默认都是在CPU0上产生的;

extern  int irq_set_affinity(unsigned int irq, const struct cpumask *cpumask)

irq_set_affinity(n,cpumask_of(i));//把n中断设定到CPU_i上;

参考一个gic的内部电路图:

6.为什么一定要有底半步?

案例:

CPU外接I2C触摸屏 当触摸事件发生时,产生中断信号,中断控制器将中断信号路由给CPU; CPU执行中断服务程序,必须处理外部事件,清理触摸屏的中断pending信号; 而读I2C设备可以睡眠,在中断ISR读取I2C设备,可能引起死锁; 不读的话,中断退出,触摸屏中断信号未清除,又会触发中断;

解决方案:

必须在进程上下文,读I2C设备清除外设中断电平信号; 在中断服务程序顶半部,屏蔽中断控制器对应中断

disable_irq_nosync(num);  

注:n号中断上下文不能调disable_irq(n),进程上下文可以调用disable_irq(n);

void disable_irq(unsigned int irq)
{
    if (!__disable_irq_nosync(irq))   ///等待正在执行的ISR结束,在n号中断ISR中调用disable_irq(n)会导致死锁
        synchronize_irq(irq); 
}

退出顶半部后,在底半步处理完外设事件,再使能对应中断号

enable_irq(num);

而在进程上下文中,可以直接调用disable_irq(n)/enable_irq(n);

disable_irq(n);
...              //n号中断发生,不会死锁 
enable_irq(n)

7.底半部:

顶半部不能太久,堵住了后面的进程; 顶半部屏蔽了中断,堵住了后面的中断; 顶半部不能睡眠,但是i2c,spi外设访问可能睡眠; 底半部有软中断工作队列线程化irq

中断类型 上下文 能否抢占
顶半部 中断上下文 不可以
softirq(tasklet) 软中断上下文 不可以
workqueue 进程上下文 可以
threaded_irq 进程上下文 可以

顶半部退出,立马执行软中断,软中断可以被硬件中断打断; 工作队列和线程化IRQ,跟普通线程一样接受调度

7.1 软中断,tasklet

内核中采用softirq的地方包括HI_SOFTIRQ、TIMER_SOFTIRQ、NET_TX_SOFTIRQ、NET_RX_SOFTIRQ、TASKLET_SOFTIRQ等,实际驱动编程一般不直接使用软中断(内核固定了用途),用tasklet(softirq一种)代替;

tasklet_init(&tasklet,  xxx_func_tasklet,xxx_data)
///中断上下文:
xx_isr()
{
    ...
    tasklet_hi_schedule(&tasklet);//优先级高于tasklet_schedule
    tasklet_schedule(&tasklet);
}

wakeup_softirqd也是执行软中断的一个路径,当软中断过多时,放到[ksoftirqd/n]线程; 软中断的执行点: 62. IRQ上半部返回时,先执行软中断,再执行其他线程;

  • BH_ENABLE相关函数,比如spin_unlock_bh();
  • kthread_irqd线程与普通线程一样被调度;

bh_enable相关函数会调用到这里:

  void __local_bh_enable_ip(unsigned long ip, unsigned int cnt)
  {
      WARN_ON_ONCE(in_irq());
      lockdep_assert_irqs_enabled();
  #ifdef CONFIG_TRACE_IRQFLAGS
      local_irq_disable();
  #endif
      /*
       * Are softirqs going to be turned on now:
       */
      if (softirq_count() == SOFTIRQ_DISABLE_OFFSET)
          lockdep_softirqs_on(ip);
      /*
       * Keep preemption disabled until we are done with
       * softirq processing:
       */
      __preempt_count_sub(cnt - 1);
  
      if (unlikely(!in_interrupt() && local_softirq_pending())) {
          /*
           * Run softirq if any pending. And do it in its own stack
           * as we may be calling this deep in a task call stack already.
           */
          do_softirq();  ///执行软中断
      }
  
      preempt_count_dec();
  #ifdef CONFIG_TRACE_IRQFLAGS
      local_irq_enable();
  #endif
      preempt_check_resched();
  }
  

7.2 workqueue

传统的workqueue用法语tasklet类似:

  //申请workqueue
  struct work_struct my_wq;
  void my_wq_func(struct work_struct *work);
  
  INIT_WORK(&my_wq, my_wq_func);
  
  ///中断上下文:
  xx_isr()
  {
      ...
      schedule_work(&my_wq);  //调度工作队列
      ...
      return IRQ_HANDLED;
  }

7.3 中断线程化

Linux实时补丁: 清掉软中断上下文,所有软中断放在softirqd线程执行; 线程化执行,可以睡眠; 老版本实现,每个核一个线程,该核所有softirq顺序执行; 新内核用线程池实现,动态创建撤销线程;

  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)
  xxx_isr()
  {
  return IRQ_WAKE__THREAD;
  }
  

第一次回调参数:irq_handler_t handler,中断顶半部,运行在中断上下文; 第二个回调参数:irq_handler_t thread_fn,运行在进程上下文; handler可以为NULL,这时内核默认用irq_default_primary_handler()代替handler,并会使用IRQF_ONESHOT。

  static irqreturn_t irq_default_primary_handler(int irq, void *dev_id)
  {
      return IRQ_WAKE_THREAD;
  }
  

7.4 IRQF_ONESHOT:

1.linux内核自动disable_irq(n); 2.执行xxx_isr,唤醒内核线程irq/n; 3.内核线程irq/n执行xxx_thread_fn; 4.内核自动enable_irq(n); 设置IRQF_ONESHOT, 可以把顶半部置为空,内核用默认函数irq_default_primary_handler()替换顶半部;

8. preempt_rt补丁

强制中断线程化,将低半部内容放到线程执行;

  request_irq(n, xxx_isr, 0);
  转化为
  request_threaded_irq(n,irq_default_primary_handler,xxx_isr,IRQF_ONESHOT);

9.Linux常用到的中断,锁相关API:

屏蔽本地中断:

  local_irq_disable()
  local_irq_enable()

在驱动中使用local_irq_disable通常是个bug; 禁止底半部

  local_bh_disable()
  local_bh_enable()

屏蔽中断源:

  disable_irq(n)
  enable_irq(n)

当中断和进程竟态;

  //进程上下文
  spin_lock_irqsave();
  spin_unlock_restore();
  
  //中断上下文
  spin_lock();
  spin_unlock();

irq:解决本核的抢占问题; spin_lock:解决多核间的抢占;

软中断和进程竟态: 软中断的抢占由中断引起,中断退出时,调用软中断;

  //进程上下文
  spin_lock_bh();
  spin_unlock_bh();///软中断调用点bh_enable()
  
  //软中断上下文
  spin_lock();
  spin_unlock();