做人呢,最紧要就系开心啦

linux中断处理总结

1,634次阅读
一条评论

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

0. 为什么有中断?

中断,本质上是外设发生了事变,需要异步的通知(经由中断控制器,路由给)CPU;

这个过程涉及三部分硬件:外设 -> 中断控制器 ->CPU

linux 中断处理总结

1 中断处理过程:

外设事变发生后,发送中断信号给中断控制器,中断控制器经过仲裁,路由给 CPU;

linux 中断处理总结

上图是中断控制器的状态机转换图,一个完整的中断发生过程如下(假设现在有一个触摸屏的外设,配置为高电平触发):

  • 中断控制器默认是 inactive 状态;

  • 外设有事件发生(比如按下触摸屏),触摸屏会产生一个中断信号,送给中断控制器, 如上图中的 A1, 中断控制器变为 pending 状态,此时外设送给中断控制器的信号线保持高电平;

  • 中断控制器经过仲裁,选择一个最高优先级中断信号,路由给 CPU,CPU 没有 ACK 之前,一直维持信号线的电平状态;

  • ARM 收到中断信号,开始响应时,硬件自动屏蔽 CPU 中断(清除 daif 位),软件从中断控制器读出具体是哪个中断产生,然后对中断控制器进行 ACK;若边沿触发中断控制器可能清掉 pending(电平触发,不会清),此时中断控制器处于 active and pending(D 路径)或 active(C 路径);

  • CPU 执行中断处理程序,在顶半部,disbable 中断源,中断服务程序退出时,软件恢复 CPSR,并对中断控制器 EOI;

  • 中断控制器,收到 EOI,变为 inactive 状态,再选择下一个 pending 的中断信号,发给 CPU;

  • 中断处理的底半部,处理掉外设中断事件源 (比如读取触摸屏的坐标,访问相应寄存器) 之后,外设的 pending 信号被清除掉,enable 中断源(操作中断控制器);

linux 中断处理总结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 中断处理总结

硬件中断号由硬件电路决定,通常对应配置在设备树里;软件中断号由 linux 决定,一一对应;

void irq_domain_insert_irq(int virq) 
{
    ...
    /// 设置硬件中断号与软件中断号对应关系
    irq_domain_set_mapping(domain, data->hwirq, data);
    ...

}

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 上;

linux 中断处理总结

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

linux 中断处理总结

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);

}

linux 中断处理总结

wakeup_softirqd也是执行软中断的一个路径,当软中断过多时,放到 [ksoftirqd/n] 线程;

软中断的执行点

  • 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 类似:
旧内核每个 CPU 一个 workqueue 线程,串行完成所有 workqueue; 新内核实现一个内核线程池, 动态创建线程并行完成 workqueue;

  // 申请 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;}

第一次回调参数:handler,中断顶半部,运行在中断上下文;
第二个回调参数: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:

ret = request_irq(irq, ve_spc_irq_handler, 
                    IRQF_TRIGGER_HIGH| IRQF_ONESHOT, "vexpress-spc", info);

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);
static int irq_setup_forced_threading(struct irqaction *new)
{if (!force_irqthreads())
        return 0;
    ...

    new->flags |= IRQF_ONESHOT;  /// 强制 IRQF_ONESHOT

    ...
    /* Deal with the primary handler */
    set_bit(IRQTF_FORCED_THREAD, &new->thread_flags);
    new->thread_fn = new->handler;
    new->handler = irq_default_primary_handler;
    return 0;
}

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();

正文完
 
admin
版权声明:本站原创文章,由 admin 2022-05-18发表,共计6175字。
转载说明:除特殊说明外本站文章皆由CC-4.0协议发布,转载请注明出处。
评论(一条评论)
雅克 博主
2022-06-20 17:23:29 回复

softirq,驱动一般都不用

 Windows  Chrome