zoukankan      html  css  js  c++  java
  • 《中断学习(一) —— Linux中断流程以及处理机制》

    1.触发中断后的基本流程

      发生中断后,CPU跳转到异常向量表去执行相对应的指令。

      执行 ldr pc, _irq这条指令。(以上都是由硬件直接触发,irq是我们写好的代码)

      ldr pc, _irq这条指令主要流程分为:保护现场,判断中断源,调用中断处理函数,恢复现场。

    异常向量表:

    _start: b reset
    ldr pc, _undefined_instruction
    ldr pc, _software_interrupt
    ldr pc, _prefetch_abort
    ldr pc, _data_abort
    ldr pc, _not_used
    ldr pc, _irq //发生中断时, CPU 跳到这个地址执行该指令 **假设地址为 0x18**
    ldr pc, _fiq
    

      异常向量表,每一条指令对应一种异常。
      发生复位时, CPU 就去 执行第 1 条指令: b reset。
      发生中断时, CPU 就去执行“ldr pc, _irq”这条指令。
      这些指令存放的位置是固定的。比如对于 ARM9 芯片中断向量的地址是 0x18。 (这里指的是偏移位置) 
      当然,向量表的位置并不总是从 0 地址开始,很多芯片可以设置某个 vector base 寄存器,指定向量表在其他位置,比如设置 vector base 为 0x80000000,指定为 DDR 的某个地址。但是表中的各个异常向量的偏移地址,是固定的:复位向量偏移地址是 0,中断是 0x18。 

    2.Linux系统对中断的处理
    2.1 ARM程序运行过程

      首先需要明白一个概念:ARM芯片属于RISC。
      RISC的特点是:对内存只有读、写指令。对于数据的运算是在cpu内部实现。

    举例:a = a + b;这条算法,在ARM中分四步实现:

    1. 从内存a中读出a的值
    2. 从内存b中读出b的值
    3. 在cpu中计算a+b
    4. 将a+b的值写入到内存a中

      在ARM中有不同的工作模式,不同的工作模式对应着不同寄存器组。因此从内存中读出的数据就是存放在寄存器中。(ARM寄存器及功能介绍

      因此实际的ARM中的操作是:

    1. 把内存a的值读取CPU寄存器R0
    2. 把内存b的值读入CPU寄存器R1
    3. 把R0、R1相加,存入R0
    4. 把R0的值写入内存a

     2.2 进程、线程、中断的切换

       进程、线程、中断的核心是栈。栈 (stack) 是一种串列形式的数据结构。这种数据结构的特点是后入先出

     那么程序被中断,怎么保存现场?

    1.   程序A正常运行中,突然中断触发。程序A被打断,这个时候会暂停程序A运行,将程序A中所有的寄存器保存下来。这也就保存现场。
    2.   这些寄存器会被保存在内存中,而这块内存就被称为栈。
    3.   然后ARM去执行中断程序。
    4.   等中断程序执行完了,从栈中恢复刚被保存的寄存器值。

      进程和线程的切换也是大致如此。

    3.Linux中断

      Linux中有硬件中断和软件中断。但是对于硬件中断的处理有两个原则:不能嵌套,越快越好。(早起Linux版本是支持中断嵌套)

      当ARM处理器收到中断的时候,它进入中断模式,同时ARM处理器的CPSR寄存器的IRQ位会被硬件设置为屏蔽IRQ。

    Linux为什么不支持中断嵌套?

      中断嵌套,可能导致栈溢出。因为要一直保存现场。

    对于比较耗时的中断怎么办?

    1.将中断分为上、下两个部分。上半部分简单的接收数据(中断触发会被屏蔽,硬件自动屏蔽),将耗时的处理放在下半部分(可以被其他硬件中断打断)。《tasklet机制和workqueue机制》

    2.用内核线程用来处理线程。

    3.1 硬件中断

      对于按键中断等硬件产生的中断,称之为“硬件中断” (hard irq)。每个硬件中断都有对应的处理函数,比如按键中断、网卡中断的处理函数肯定不一样。 
      为方便理解,你可以先认为对硬件中断的处理是用数组来实现的,数组里存放的是函数指针:
      

      当硬件触发中断后,会自动跳转到异常向量表的irq中,然后判断中断源,再跳转到对应的中断服务函数中。以上都是硬件自己做的。

      实际的中断处理流程比这个复杂多了。以上只是简化了解。

    3.2 软件中断:

      比如发送异步通知机制,就是一种软件中断。

      

       对于X号中断,只需要把它的flag设置为1就表示发生了该中断。

    3.3 软件中断什么时候去执行?

      Linux系统中,各种硬件中断发生的很频繁,至少定时器中断每10ms发生一次(心跳)。所以在处理完硬件中断后,再去处理软件中断

       

    硬件中断A处理过程中,没有其他中断发生:

    1. 中断A发生,count++等于1。
    2. 硬件屏蔽中断,中断A上半部运行处理。count--等于0。
    3. count++等于1,硬件开启接收硬件中断,执行软件中断也就是所谓的中断下半部。
    4. count--等于0。然后退出。

    硬件中断A处理过程中,又再次发生了中断A:(以下两种情况的不同之处在于最后红字部分)

      1.第一次中断A发生,执行到第⑥时,一开中断后,中断 A 又再次使得 CPU 跳到中断向量表。 这时 count 等于 1,并且中断A下半部的代码并未执行。
      2.CPU 又从①开始再次执行中断 A 的上半部代码,在① 中count++等于2,到③后count--等于1。
      3.在第④步发现 count 等于 1,所以直接结束当前第 2 次中断的处理。
      4.重点来了,第 2 次中断发生后,打断了第一次中断的第⑦步处理。当第 2 次中断处理完毕, CPU会继续去执行第⑦步 。
      可以看到,发生 2 次硬件中断 A 时,它的上半部代码执行了 2 次,但是下半部代码只执行了一次。

    硬件中断 A 处理过程中,又再次发生了中断 B:
      1.第一次中断A发生,执行到第⑥时,一开中断后,中断 B 又再次使得 CPU 跳到中断向量表。 这时 count 等于 1,并且中断下半部的代码并未执行。  
      2.CPU 又从①开始执行中断 B 的上半部代码,在① 中count++等于2,到③后count--等于1。
      3.在第④步发现 count 等于 1,所以直接结束当前第 2 次中断的处理。
      4.重点来了,第 2 次中断发生后,打断了第一次中断的第⑦步处理。当第 2 次中断处理完毕, CPU会继续去执行第⑦步 。
      最后:在第⑦步里,它会去执行中断 A 的下半部,也会去执行中断 B 的下半部。 所以,多个中断的下半部,是汇集在一起处理的。
    注意:
      中断上半部、下半部的执行过程中,不能休眠。
    3.4 为什么中断上下文不能休眠?(参考《为什么Linux不能在中断中睡眠》
    如果在中断上半部睡眠,会发生?
      在中断上半部执行的时候,硬件是屏蔽了中断接收的。所以操作系统接收不到任何中断(包括时钟中断),系统就会瘫痪。(调度器依赖的时钟节拍也会没有)
    中断下半部(软中断)不能睡眠:
      不能睡眠的关键是因为上下文。
      操作系统以进程调度为单位,进程的运行在进程的上下文中,以进程描述符作为管理的数据结构。进程可以睡眠的原因是操作系统可以切换不同进程的上下文,进行调度操作,这些操作都以进程描述符为支持。 中断运行在中断上下文,没有一个所谓的中断描述符来描述它,它不是操作系统调度的单位。一旦在中断上下文中睡眠,首先无法切换上下文(因为没有中断描述符,当前上下文的状态得不到保存),其次,没有人来唤醒它,因为它不是操作系统的调度单位。 此外,中断的发生是非常非常频繁的,在一个中断睡眠期间,其它中断发生并睡眠了,那很容易就造成中断栈溢出导致系统崩溃。

    上下文错乱:

      睡眠函数会调用schedule(),切换进程时,保存当前的进程上下文(CPU寄存器的值、进程的状态以及堆栈中的内容),以便以后恢复此进程运行。中断发生后,内核会先保存当前被中断的进程上下文(在调用中断处理程序后恢复);但在中断处理程序里,CPU寄存器的值肯定已经变化了吧(最重要的程序计数器PC、堆栈SP等),如果此时因为睡眠或阻塞操作调用了schedule(),则保存的进程上下文就不是当前的进程context了,所以不可以在中断处理程序中调用schedule()。

      Linux是以进程为调度单位的,调度器只看到进程内核栈,而看不到中断栈。在独立中断栈的模式下,如果linux内核在中断路径内发生了调度(从技术上讲,睡眠和调度是一个意思),那么linux将无法找到“回家的路”,未执行完的中断处理代码将再也无法获得执行机会,因为没有人能够唤醒它。

    还有一个其他人的总结:

      Linux 设计中,中断处理时不能睡眠,这个内核中有很多保护措施,一旦检测到内核会异常。

      当 一个进程A因为中断被打断时,中断处理程序会使用 A 的内核栈来保存上下文,因为是“抢”的 A 的CPU,而且用了 A 的内核栈,因此中断应该尽可能快的结束。如果 do_IRQ 时又被时钟中断打断,则继续在 A 的内核栈上保存中断上下文,如果发生调度,则 schedule 进 switch_to,又会在 A 的 task_struct->thread_struct 里保存此时时种中断的上下文。

      假如其是在睡眠时被时钟中断打断,并 schedule 的话,假如选中了进程 A,并 switch_to 过去,时钟中断返回后则又是位于原中断睡眠时的状态,抛开其扰乱了与其无关的进程A的运行不说,这里的问题就是:该如何唤醒之呢??

      另外,和该中断共享中断号的中断也会受到影响。

    如果条件满足了(即:有中断描述符,并成为调度器的调度单位,栈也不溢出了,理论上是可以做到中断睡眠的),中断是可以睡眠的,但会引起很多问题:

      例如,你在时钟中断中睡眠了,那操作系统的时钟就乱了,调度器也了失去依据;例如,你在一个IPI(处理器间中断)中,其它CPU都在死循环等你答复,你却睡眠了,那其它处理器也不工作了;

      例如,你在一个DMA中断中睡眠了,上面的进程还在同步的等待I/O的完成,性能就大大降低了……还可以举出很多例子。

      所以,中断是一种紧急事务,需要操作系统立即处理,不是不能做到睡眠,是它没有理由睡眠。

     4.执行中断下半部的三种机制(具体可以查看《中断下半部处理tasklet机制、workqueue机制和threaded irq》)

    4.1 tasklet机制

      tasklet机制主要用在下半部要做的事耗时不是太长。

      注意:使用tasklet机制的时候,不能使用睡眠或者会产生调度的函数。3.3的流程就是tasklet机制。

    4.2 workqueue工作队列

      workqueue主要用在事情很多,并且复杂。因此执行时间比较长。

      tasklet机制是使用软件中断去实现的。但是如果下半部的运行时间很长,那么就会导致APP卡死。因为不能进行上下文切换。

      所以可以用内核线程去实现。在中断上半部唤醒内核线程。内核线程和 APP 都一样竞争执行, APP 有机会执行,系统不会卡顿。 
      这个内核线程是系统帮我们创建的,一般是 kworker 线程,内核中有很多这样的线程:
      

       kworker 线程要去“工作队列” (work queue)上取出一个一个“工作” (work),来执行它里面的函数。
    4.3 threaded irq
      work 来线程化地处理中断,一个 worker 线程只能由一个 CPU 执行,多个中断的 work 都由同一个 worker 线程来处理,在单 CPU 系统中也只能忍着了。但是在 SMP 系统中,明明有那么多 CPU 空着,你偏偏让多个中断挤在这个 CPU 上? 
      新技术 threaded irq,为每一个中断都创建一个内核线程;多个中断的内核线程可以分配到多个 CPU
     


      

     




      

      

  • 相关阅读:
    CNN5 调用 C实现pool2d im2col col2im
    CUDA学习3 Max pooling (python c++ cuda)
    CUDA学习2 基础知识和Julia示例
    CUDA学习1 在Visual Studio和CodeBlocks上配置
    线性搜索
    CNN4 参数优化
    CNN3 im2col
    CNN2 多层卷积
    爬虫:Scrapy8
    爬虫:Scrapy7
  • 原文地址:https://www.cnblogs.com/zhuangquan/p/13865867.html
Copyright © 2011-2022 走看看