linux中断处理总结

时间:2023-02-24 14:00:00

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执行中断处理程序,中断服务程序退出时,软件恢复CPSR,并对中断控制器EOI;
  • 中断控制器,收到EOI,变为inactive状态,再选择下一个pending的中断信号,发给CPU;
  • 中断处理的底半部,处理掉外设中断事件源(比如读取触摸屏的坐标,访问相应寄存器)之后,外设的pending信号被清除掉;

linux中断处理总结linux相对应的API:

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

补充: ## 2.disable_irq(n)可能并未生效?

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


若期间有中断产生呢? **enable_irq(n)使能n号中断后,n号中断服务程序立即执行;**这样,就算disable_irq(n)期间有n号中断产生,也不会漏掉,只是延后执行; ```cpp
//__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决定,一一对应; ## 5.中断分类

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

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


![](https://www.daodaodao123.com/wp-content/uploads/2022/05/irq4.png)参考一个gic的内部电路图: ![](https://www.daodaodao123.com/wp-content/uploads/2022/05/irq5.png)## 6.为什么一定要有底半步?

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

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

注:n号中断上下文不能调disable_irq(n),进程上下文可以调用disable_irq(n); ```cpp void disable_irq(unsigned int irq) { if (!__disable_irq_nosync(irq)) ///等待正在执行的ISR结束,在n号中断ISR中调用disable_irq(n)会导致死锁 synchronize_irq(irq); }


退出顶半部后,在底半步处理完外设事件,再使能对应中断号 ```cpp
enable_irq(num);

而在进程上下文中,可以直接调用disable_irq(n)/enable_irq(n); ```cpp 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一种)代替; ```cpp
tasklet_init(&tasklet,  xxx_func_tasklet,xxx_data)
///中断上下文:
xx_isr()
{
    ...
    tasklet_hi_schedule(&tasklet);//优先级高于tasklet_schedule
    tasklet_schedule(&tasklet);

}

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

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

bh_enable相关函数会调用到这里: ```cpp 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类似: ```cpp
//申请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顺序执行; 新内核用线程池实现,动态创建撤销线程; ```cpp 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。 ```cpp
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补丁

强制中断线程化,将低半部内容放到线程执行; ```cpp request_irq(n, xxx_isr, 0); 转化为 request_threaded_irq(n,irq_default_primary_handler,xxx_isr,IRQF_ONESHOT);


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

**屏蔽本地中断:**```cpp
local_irq_disable()
local_irq_enable()

在驱动中使用local_irq_disable通常是个bug; 禁止底半部``` local_bh_disable() local_bh_enable()


**屏蔽中断源:**```cpp
disable_irq(n)
enable_irq(n)

当中断和进程竟态;```cpp //进程上下文 spin_lock_irqsave(); spin_unlock_restore();

//中断上下文 spin_lock(); spin_unlock();


irq:解决本核的抢占问题; spin_lock:解决多核间的抢占; **软中断和进程竟态:**软中断的抢占由中断引起,中断退出时,调用软中断; ```cpp
//进程上下文
spin_lock_bh();
spin_unlock_bh();///软中断调用点bh_enable()

//软中断上下文
spin_lock();
spin_unlock();