一、中断和异常

***1.1 中断和异常的概念

中断是指在CPU正常运行期间,由于内外部事件或由程序预先安排的事件引起的 CPU 暂时停止正在运行的程序,转而为该内部或外部事件或预先安排的事件服务的程序中去,服务完毕后再返回去继续运行被暂时中断的程序。Linux中通常分为同步中断(又叫硬件中断)和异步中断(又叫异常)。

  1. 同步中断是当指令执行时由 CPU 控制单元产生,之所以称为同步,是因为只有在一条指令执行完毕后 CPU 才会发出中断,而不是发生在代码指令执行期间,比如系统调用。

  2. 异步中断是指由其他硬件设备依照 CPU 时钟信号随机产生,即意味着中断能够在指令之间发生,例如键盘中断。

在intel处理器手册中,把同步和异步中断分为异常(exception)和中断(interrupt)。我们也采用这样的分类,当然有时候我们也用术语“中断信号”指这两种类型(同步及异步)。

中断是有间隔定时器和I/O设备产生的,例如,用户一次按键引起一个中断。

另一方面,异常是由程序的错误产生的,或者是由内核必须处理的异常条件产生的 。第一种情况下,内核通过发送一个每个Unix程序员都熟悉的信号来处理异常。第二种情况下,内核执行恢复异常需要的所有步骤,例如:缺页,或对内核服务的一个请求

1.2 中断信号的作用

中断信号提供了一种特殊的方式,使处理器转而去运行正常控制流之外的代码。当一个中断信号到达时,CPU必须停止它当前正在做的事情,并且切换到一个新的活动。 为了做到这一点,就要在内核态堆栈保存程序计数器的当前值,并把与中断类型相关的一个地址放入程序计数器。

中断处理与进程切换有一个明显的差异:由中断或异常处理程序执行的代码不是一个进程。更确切地说,它是一个内核控制路径,代表中断发生时正在运行的进程。

中断处理是由内核执行的最敏感的任务之一,因为它必须满足下列约束

  1. 当内核正打算去完成一些别的事情时,中断随时会到来。因此,内核和目标就是让中断尽可能快地处理完,尽其所能把更多的处理向后推迟 。因此,内核响应中断后需要进行的操作分为两部分:关键而紧急的部分,内核立即执行;其余推迟的部分内核随后执行。

  2. 因为中断随时会到来,所以内核可能正在处理其中的一个中断时,另一个中断又发生了。应该尽可能多地允许这种情况发生,因此这能维持更多的I/O设备处于忙状态。因此,中断处理程序必须编写成是相应的内核控制路径能以嵌套的方式执行。 当最后一个内核控制路径终止时,内核必须能恢复被中断进程的执行,或者,如果中断信号已导致了重新调度,内核能切换到另外的进程。

  3. 尽管内核在处理前一个中断时可以接受一个新的中断,但在内核代码中还是存在一些临界区 ,在临界区中,中断必须被禁止。必须尽可能地限制这样的临界区,因为根据以前的要求,内核,尤其是中断处理程序,应该在大部分时间内以开中断的方式运行 。(开中断就是当有中断信号产生时,单片机会进入程序中,也就是响应中断)

***1.3 中断和异常的分类:

Intel文档把中断和异常分为以下几类:

  • 中断
  • 区分中断号与中断向量
    I/O设备把中断信号发送给中断控制器(8259A)时与之相关联的是一个中断号,当中断控制器把中断信号发送给CPU时与之关联的是一个中断向量。换个角度分析就是中断号是从中断控制器层面划分,中断向量是从CPU层面划分,所以中断号与中断向量之间存在一对一映射关系。在Intel X86中最大支持256种中断,从0到255开始编号,这个8位的编号就是中断向量。其中将0到31保留用于异常处理和不可屏蔽中断。

  • 可屏蔽中断
    I/O设备发出的所有中断请求都产生可屏蔽中断。可屏蔽中断可以处于两种状态:屏蔽的或非屏蔽的;一个屏蔽的中断只要还是屏蔽的,控制单元就忽略它。

  • 非屏蔽中断 只有几个危急事件(如硬件故障)才引起非屏蔽中断。非屏蔽中断总是由CPU辨认。

  • 异常 :

    • 故障 :通常可以纠正
    • 陷阱 :在陷阱指令执行后立即报告;主要用于是为了调试程序。在这种情况下,中断信号的作用是通知调试程序一条特殊指令已被执行。
    • 中止 :发生一个严重的错误
类别 原因 异步/同步 返回行为
中断 来自I/O设备的信号 异步 总是返回到下一条指令
陷阱 有意的异常 同步 总是返回到下一条指令
故障 潜在可恢复的错误 同步 返回到当前指令
终止 不可恢复的错误 同步 不会返回

***1.4 异常中断向量表

CPU是根据中断号获取中断向量值,即对应中断服务程序的入口地址值。因此为了让CPU由中断号查找到对应的中断向量,就需要在内存中建立一张查询表,即中断向量表(在32位保护模式下该表称为中断描述符表)。80x86微机支持256个中断,对应每个中断需要安排一个中断服务程序。

  • 中断向量
    中断服务程序的入口地址。在某些计算机中,中断向量的位置存放一条跳转到中断服务程序入口地址的跳转指令。
    来存放中断向量(共256个),称这一片内存区为中断向量表,地址范围是0~3FFH
  • 中断向量地址:
    存储中断向量的存储单元地址

在AVR或ARM微处理器中,中断向量的大小也是4个字节,但其中存放的不是中断程服务程序的入口地址,而是可执行的代码。当响应中断时,硬件自动执行相应中断向量处的跳转代码,然后跳转到具体的中断服务程序的入口地址。

上面讲的是早期的80x86的中断向量表,我们讲离我们近的ARM7架构异常中断向量表,涉及篇幅较多,再另外一篇文章介绍:Linux内核ARM架构异常中断向量表

1.5 IRQ线和高级可编程中断控制器

每个能够发出中断请求的硬件设备控制器都有一条名为IRQ的输出线。所有现有的IRQ线都与一个名为可编程中断控制器的硬件电路的输入引脚相连 ,可编程中断控制器执行下列动作:
(1)监视IRQ线,检查产生的信号

(2)如果一个引发信号出现在IRQ线上:

  1. 把接收到的引发信号转换成对应的向量

  2. 把这个向量存放在中断控制器的一个I/O端口,从而允许CPU通过数据总线读此向量

  3. 把引发信号发送到处理器的INTR引脚,即产生一个中断

  4. 等待,直到CPU通过把这个中断信号写进可编程中断控制器的一个I/O端口来确认它;当这种情况发生时,清INTR线。

(3)返回到第1步

IRQ线是从0开始顺序编号的,因此,第一条IRQ线通常表示成IRQ0.与IRQn并联的Intel的缺省向量是n+32.

可以有选择地禁止每条IRQ线。因此,可以对PIC编程从而禁止IRQ,也就是说,可以告诉PIC停止对给定的IRQ线发布中断,或者激活它们。禁止的中断时丢失不了的,它们一旦被激活,PIC就又把它们发送到CPU。这个特点被大多数中断处理程序使用,因为这允许中断处理程序逐次地处理同一类型的IRQ

有选择地激活/禁止IRQ线不同于可屏蔽中断的全局屏蔽/非屏蔽

高级可编程中断控制器
为了充分发挥SMP体系结构的并行性,能够把中断传递给系统中的每个CPU至关重要。基于此理由,Intel从PentiumIII开始引入了一种名为I/O高级可编程控制器(I/O Advanced Programmable InterruptController APIC)

1.5 异常

80x86发布了大约20中不同的异常,内核必须为每种异常提供一个专门的异常处理程序,对于某些异常,CPU在执行异常处理程序前会产生一个硬件出错码,并压入内核态堆栈。

每个异常都由专门的异常处理程序来处理,它们通常把一个Unix信号发送到引起异常的进程。

1.6 中断描述符表

当Intel CPU运行在32位保护模式下时,需要使用中断描述符表(Interrupt Descriptor Table,IDT)来管理中断或异常。
中断描述符(Interrupt Descriptor Table IDT)是一个系统表,它与每一个中断或异常向量相关联,每一个向量在表中有相应的中断或异常处理程序的入口地址。 内核在允许中断发生前,必须适当地初始化IDT

下面是Intel提供了三种类型的中断描述符:

  • 任务门 (task gate)
    当中断信号发生时,必须取代当前进程的那个进程的TSS选择符存放在任务门中。
  • 中断门 (interupt gate)
    包含段选择符和中断或异常处理程序的段内偏移量。当控制权转移到一个适当的段时,处理器清IF标志,从而关闭将来会发生的可屏蔽中断
  • 陷阱门 (Trap gate)
    与中断门相似,只是控制权传递到一个适当的段时处理器不修改IF标志

Linux使用与Intel稍有不同的细目分类和术语,把它们分类:

类别 原因
中断门 用户态的进程不能访问的一个Intel中断门。所有的Linux中断处理程序都通过中断门激活,并全部限制在内核态
系统门 用户态的进程可以访问的一个Intel陷阱门。通过系统门来激活三个Linux异常处理程序,它们的向量是4,5及128,因此,在用户态下,可以发布intobound及int$0x85三条汇编语言指令
系统中断门 能够被用户态进程访问的Intel中断门。与向量3相关的异常处理程序是由系统中断门激活的,因此,在用户态可以使用汇编语言指令int3
陷阱门 用户态的进程不能访问的一个Intel陷阱门。大部分Linux异常处理程序都通过陷阱门来激活。
任务门 不能被用户态进程访问的Intel任务门。Linux对“Double fault”异常的处理程序是由任务门激活的

Linux利用中断门处理中断,利用陷阱门处理异常

二、中断和异常的硬件处理

假设内核已被初始化,CPU 在保护模式下运行。

在处理下一条指令时,控制单元会检查在运行前一条指令时是否发生了一个中断或异常,如果发生,控制单元执行下列操作:

  • 确定中断或异常关联的向量 i(0~255)。
  • 读 idtr 寄存器指向的 IDT 表的第 i 项(假定包含一个中断门或陷阱门)。
  • 从 gdtr 获得 GDT 的基地址,在 GDT 中查找 IDT 表第 i 项中选择符标识的段描述符。该描述符指定中断或异常处理程序所在段的基地址。
  • 确信中断是由授权的中断发生源发出的。如果 CPL(cd 寄存器的低两位)> 段描述符(GDT 中)的描述符特权级,则产生异常,因为说明引起中断的处理程序的特权>中断处理程序的特权 。对于编程异常,还需比较 CPL 与 IDT 中的门描述符 DPL,大于则产生异常,可避免用户程序访问特殊的陷阱门或中断门。
  • 检查是否发生特权级的变化,即 CPU 不等于当前段描述符的 DPL。如果是,控制单元必须使用与新特权级相关的栈。
    – 读 tr 寄存器,访问运行进程的 TSS 段。
    – 将 TSS 中新特权级相关的栈段、栈指针装载 ss、esp 寄存器。
    – 新的栈中保存 ss、esp 以前的值。
  • 如果产生故障,用引起异常的指令的地址装载 cs 和 eip 寄存器。
  • 将 eflags、cs 及 eip 的内容保存到栈中。
  • 如果异常产生了一个硬件出错码,保存到栈中。
  • 用 IDT 表中第 i 项门描述符的段选择符和偏移量字段装载 cs 和 eip 寄存器,为中断或异常处理程序的第一条指令的逻辑地址

换句话说,处理完中断信号后,控制单元所执行的指令就是被选中处理程序的第一条指令。

总结:确定异常、中断向量;权限、特权检查;针对不同类型的异常、中断,保存不同的内容;将从异常、中断向量得到的中断或异常处理程序地址装入 cs、eip 寄存器。

中断或异常处理完后,处理程序产生 iret 指令,控制权交给被中断的进程,迫使控制单元:

用保存在栈中的值装载 cs、eip 或 eflags 寄存器。如果一个硬件码被压入栈,并在 eip 上方,执行 iret 前弹出。
检查处理程序的 CPL 是否等于 cs 中低两位,如果是,则 iret 终止;否则,转入下一步。
返回到与就特权级相关的栈,用栈中内容装载 ss 和 esp 寄存器。
检查 ds、es、fs 及 gs 段寄存器的内容,如果其中一个包含的选择符是段描述符,且其 DPL 小于 CPL,清相应的段寄存器,可禁止用户态程序(CPL=3)利用以前所用的段寄存器(DPL=0)。
总结:弹出保存在栈中的内容;根据特权级变化决定是否返回栈;清相关段寄存器,防止用户恶意访问内核空间。

三、中断和异常处理程序的嵌套执行

每个中断或异常都会引起一个内核控制路径(内核态代码),或者说代表当前进程在内核态执行单独的指令序列。 例如:当I/O设备发出一个中断时,相应的内核控制路径的第一部分指令就是那些把寄存器的内容保存在内核堆栈的指令,而最后一部分指令就是恢复寄存器内容并让CPU返回到用户态的那些指令

内核控制路径可以任意嵌套 :一个中断处理程序可以被另一个中断处理程序“中断”,因此引起内核控制路径的嵌套执行,如图4-3所示。其结果是,对中断进行处理的内核控制路径,其最后一部分指令并不总能使当前进程返回到用户态:如果嵌套深度大于1,这些指令将执行上次被打断的内核控制路径,此时的CPU依然运行在内核态

假设内核没有bug,那么大部分异常就只在CPU处理内核态时发生,事实上,异常要么是有编程错误引起,要不是由调试程序触发。然后,“Page Fault(缺页)”异常发生在内核态。

一个中断处理程序即可以抢占其他的中断处理程序,也可以抢占异常处理程序。相反,异常处理程序从不抢占中断处理程序。 在内核态能触发的唯一异常就是刚刚描述的缺页异常。但是,中断处理程序从不执行可以导致缺页得操作。

四、异常处理

CPU产生的大部分异常都由Linux解释为出错条件。当其中一个异常发生时,内核就向引起异常的进程发送一个信号向它通知一个反常条件。

例如,如果进程执行了一个被0除的操作,CPU就产生一个“Divide error”异常,并由响应的异常处理程序向当前进程发送一个SIGFPE信号,这个进程将采取若干必要的步骤来(从出错中)恢复或者终止运行(如果没有为这个信号设置处理程序的话)。

我们在jave层经常会用try catch来捕获异常,然后对异常进行处理。

Unix(包括Linux)下的C语言编程中是不会使用try catch的,的确C也没有这个语法。然而当运行时的错误异常被抛出时,系统会产生信号发送给进程,如果进程没有做信号响应函数的话,就会被中断运行并且产生core文件,通过core文件可以查看程序的崩溃原因、当时的调用堆栈、当时的变量值等等信息,当然这是另外一个话题。

因此在Unix下,与try catch起到相似作用的东西就是信号相应函数

信号放在后面的文章介绍

五、***中断处理机制


Linux中断机制由三部分组成:

  • 中断子系统初始化:内核自身初始化过程中对中断处理机制初始化,例如中断的数据结构以及中断请求等。
  • 中断或异常处理:中断整体处理过程。
  • 中断API:为设备驱动提供API,例如注册,释放和激活等。

5.1中断子系统初始化

中断描述符表初始化需要经过两个过程:

  • 第一个过程在内核引导过程。由两个步骤组成,首先给分配IDT分配2KB空间(256中断向量,每个向量由8bit组成)并初始化;然后把IDT起始地址存储到IDTR寄存器中。
  • 第二个过程内核在初始化自身的start_kernal函数中使用trap_init初始化系统保留中断向量,使用init_IRQ完成其余中断向量初始化。

中断请求队列初始化
init_IRQ调用pre_intr_init_hook,进而最终调用init_ISA_irqs初始化中断控制器以及每个IRQ线的中断请求队列。

5.2中断或异常处理

    中断处理过程:设备产生中断,并通过中断线将中断信号送往中断控制器,如果中断没有被屏蔽则会到达CPU的INTR引脚,CPU立即停止当前工作,根据获得中断向量号从IDT中找出门描述符,并执行相关中断程序。

    异常处理过程:异常是由CPU内部发生所以不会通过中断控制器,CPU直接根据中断向量号从IDT中找出门描述符,并执行相关中断程序。

中断控制器处理主要有5个步骤:1.中断请求 2.中断响应 3.优先级比较 4.提交中断向量 5.中断结束。这里不再赘述5个步骤的具体流程。

CPU处理流程主要有6个步骤:1.确定中断或异常的中断向量 2.通过IDTR寄存器找到IDT 3.特权检查 4.特权级发生变化,进行堆栈切换 5.如果是异常将异常代码压入堆栈,如果是中断则关闭可屏蔽中断 6.进入中断或异常服务程序执行。这里不再赘述6个步骤的具体流程。

正如前面解释的那样,内核只要给引起异常的进程发送一个Unix信号就能处理大多数异常。因此,要采取的行动被延迟,直到进程接受到这个信号,所以,内核就能很快地处理异常。

但是这种给当前进程发送一个Unix信号的方法并不适合中断,
中断处理依赖于中断类型:

  • I/O中断: 某些I/O设备需要关注;相应的中断处理程序必须查询设备以确定适当的操作过程

  • 时钟中断: 某种时钟产生一个中断;这种中断告诉内核一个固定的时间间隔已经过去。这些中断大部分是作为I/O中断来处理的

  • 处理器间中断 :多处理器系统中一个CPU对另一个CPU发出一个中断

5.3 中断API

内核提供的API主要用于驱动的开发。

  • 注册IRQ:
int request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags, const char *name, void *dev);
  • 释放IRQ:
void free_irq(unsigned int, void *);

注:IRQ线资源非常宝贵,我们在使用时必须先注册,不使用时必须释放IRQ资源。

激活当前CPU中断:
local_irq_enable();禁止当前CPU中断:
local_irq_disable();激活指定中断线:
void enable_irq(unsigned int irq);禁止指定中断线:
void disable_irq(unsigned int irq);禁止指定中断线:
void disable_irq_nosync(unsigned int irq);

5.4 编写一个I/O中断

单独放在linux内核学习10.2:编写一个I/O中断介绍

5.5 中断向量

物理IRQ可以分配给32-238范围内的任何向量。不过,Linux使用向量128实现系统调用。

五、***中断上半部和底半部

玩过 MCU 的人都知道,中断服务程序的设计最好是快速完成任务并退出,因为此刻系统处于被中断中。但是在 ISR 中又有一些必须完成的事情,比如:清中断标志,读/写数据,寄存器操作等。

在 Linux 中,同样也是这个要求,希望尽快的完成 ISR。但事与愿违,有些 ISR 中任务繁重,会消耗很多时间,导致响应速度变差。Linux 中针对这种情况,将中断分为了两部分:

  1. 上半部(top half):收到一个中断,立即执行,有严格的时间限制,只做一些必要的工作,比如:应答,复位等。此时为开中断(运行被其他中断打断)

  2. 底半部(bottom half):能够被推迟到后面完成的任务会在底半部进行。在适合的时机,下半部会被开中断执行。而下半部则相对来说并不是非常紧急的,通常还是比较耗时的,因此由系统自行安排运行时机,不在中断服务上下文中执行

这里有一些经验可供借鉴:

  1. 如果一个任务对时间十分敏感,将其放在上半部。
  2. 如果一个任务和硬件有关,将其放在上半部。
  3. 如果一个任务要保证不被其他中断打断,将其放在上半部。
  4. 其他所有任务,考虑放在下半部。

#六、 底半部实现机制

软中断:

Tasklet

工作队列

6.1 软中断

参考:https://www.cnblogs.com/LoyenWang/p/13124803.html

概念:
(1) 硬中断
由与系统相连的外设(比如网卡、硬盘)自动产生的。主要是用来通知操作系统系统外设状态的变化。比如当网卡收到数据包的时候,就会发出一个中断。我们通常所说的中断指的是硬中断(hardirq)。

硬中断的机制和驱动编写在上面已经介绍

(2) 软中断
为了满足实时系统的要求,中断处理应该是越快越好。linux为了实现这个特点,当中断发生的时候,硬中断处理那些短时间就可以完成的工作,而将那些处理事件比较长的工作,放到中断之后来完成,也就是软中断(softirq)来完成。产生软中断的进程一定是当前正在运行的进程,因此它们不会中断CPU。

(3) 中断嵌套
Linux下硬中断是可以嵌套的,但是没有优先级的概念,也就是说任何一个新的中断都可以打断正在执行的中断,但同种中断除外。软中断不能嵌套,但相同类型的软中断可以在不同CPU上并行执行。

(4) 硬中断和软中断的区别

  • 软中断是执行中断指令产生的,而硬中断是由外设引发的。

  • 硬中断的中断号是由中断控制器提供的,软中断的中断号由指令直接指出,无需使用中断控制器。

  • 硬中断处理程序要确保它能快速地完成任务,这样程序执行时才不会等待较长时间,称为上半部。

  • 软中断处理硬中断未完成的工作,是一种推后执行的机制,属于下半部。

  • 硬中断是可屏蔽的(NMI硬中断不可屏蔽);

编写两个中断服务函数的区别
1.软中断发生的时间是由程序控制的,而硬中断发生的时间是随机的
2.软中断是由程序调用发生的,而硬中断是由外设引发的
3.硬件中断处理程序要确保它能快速地完成它的任务,这样程序执行时才不会等侍较长时间
编写这两类的中断处理程序我感觉区别不太大

软中断特点:

  1. 静态定义的接口,有32个(与tasklet不同)
  2. 软中断不能被自己打断(即单个cpu上软中断不能嵌套执行),只能被硬件中断打断(上半部)。
  3. 其他软中断(甚至是相同类型的软中断)可以在其他处理器同时执行由于这点,所以对临界区需要加锁保护。(与tasklet不同)
  4. 不能休眠
  5. 可以并发运行在多个CPU上(即使同一类型的也可以),这意味着任何共享数据都严格的锁保护。(与tasklet不同)

软中断的数据结构
内核用softirq_action结构管理软中断的注册和激活等操作,它的定义如下:

struct softirq_action
{void    (*action)(struct softirq_action *);
};

softirq不支持动态分配,Linux kernel提供了静态分配,关键的结构体描述如下,可以类比硬件中断来理解:
内核目前实现了11种类型的软中断,它们是:

/* 支持的软中断类型,可以认为是软中断号, 其中从上到下优先级递减 */
enum
{HI_SOFTIRQ=0,       /* 最高优先级软中断 */TIMER_SOFTIRQ,      /* Timer定时器软中断 */NET_TX_SOFTIRQ,     /* 发送网络数据包软中断 */NET_RX_SOFTIRQ,     /* 接收网络数据包软中断 */BLOCK_SOFTIRQ,      /* 块设备软中断 */IRQ_POLL_SOFTIRQ,   /* 块设备软中断 */TASKLET_SOFTIRQ,    /* tasklet软中断 */SCHED_SOFTIRQ,      /* 进程调度及负载均衡的软中断 */HRTIMER_SOFTIRQ, /* Unused, but kept as tools rely on thenumbering. Sigh! */RCU_SOFTIRQ,    /* Preferable RCU should always be the last softirq, RCU相关的软中断 */NR_SOFTIRQS
};* 软件中断描述符,只包含一个handler函数指针 */
struct softirq_action {void (*action)(struct softirq_action *);
};
/* 软中断描述符表,实际上就是一个全局的数组 */
static struct softirq_action softirq_vec[NR_SOFTIRQS] __cacheline_aligned_in_smp;/* CPU软中断状态描述,当某个软中断触发时,__softirq_pending会置位对应的bit */
typedef struct {unsigned int __softirq_pending;unsigned int ipi_irqs[NR_IPI];
} ____cacheline_aligned irq_cpustat_t;
/* 每个CPU都会维护一个状态信息结构 */
irq_cpustat_t irq_stat[NR_CPUS] ____cacheline_aligned;/* 内核为每个CPU都创建了一个软中断处理内核线程 */
DEFINE_PER_CPU(struct task_struct *, ksoftirqd);

软中断相关API:

  • 注册软中断处理函数
void open_softirq(int nr, void (*action)(struct softirq_action *))
{softirq_vec[nr].action = action;
}
其中nr 是软中断类型,void (*action)(struct softirq_action *)是软中断函数指针例如:
网络收发的软中断
open_softirq(NET_TX_SOFTIRQ, net_tx_action);
open_softirq(NET_RX_SOFTIRQ, net_rx_action);
  • 触发软中断
调用raise_softirq()来触发软中断。
  • local_bh_disable()和local_bh_enable()是内核用于禁止和使能软中断和tasklet底半部机制的函数。

而不管是用什么方法唤起,软中断都要在do_softirq()中执行。

软中断一般在执行完中断上半部后,延后执行

/** Exit an interrupt context. Process softirqs if needed and possible:*/
void irq_exit(void)  //退出硬件中断
{#ifndef __ARCH_IRQ_EXIT_IRQS_DISABLEDlocal_irq_disable(); //关闭所有禁止当前CPU中断
#elseWARN_ON_ONCE(!irqs_disabled());
#endifaccount_irq_exit_time(current);preempt_count_sub(HARDIRQ_OFFSET);/*判断当前是否有硬件中断嵌套,并且是否有软中断在pending状态,注意:这里只有两个条件同事满足时,才可以调用do_softirq()进入软中断。也就是说确认当前所有硬件中断处理完成,且有硬件中断安装了软中断时才会进入。*/if (!in_interrupt() && local_softirq_pending()) invoke_softirq(); //这里就是调用do_softirq()执行tick_irq_exit();rcu_irq_exit();trace_hardirq_exit(); /* must be last! */
}
/*
invoke_softirq函数中,根据中断处理是否线程化进行分类处理,
如果中断已经进行了强制线程化处理(中断强制线程化,
需要在启动的时候传入参数threadirqs),
那么直接通过wakeup_softirqd唤醒内核线程来执行,
否则的话则调用__do_softirq函数来处理
*/
static inline void invoke_softirq(void)
{if (!force_irqthreads) {#ifdef CONFIG_HAVE_IRQ_EXIT_ON_IRQ_STACK/** We can safely execute softirq on the current stack if* it is the irq stack, because it should be near empty* at this stage.*/__do_softirq();
#else/** Otherwise, irq_exit() is called on the task stack that can* be potentially deep already. So call softirq in its own stack* to prevent from any overrun.*/do_softirq_own_stack();
#endif} else {wakeup_softirqd();}
}

Linux内核会为每个CPU都创建一个内核线程ksoftirqd,通过smpboot_register_percpu_thread函数来完成,其中当内核线程运行时,在满足条件的情况下会执行run_ksoftirqd函数,如果此时有软中断处理请求,调用__do_softirq来进行处理


上图中的逻辑可以看出,最终的核心处理都放置在__do_softirq函数中完成:

  • local_softirq_pending函数用于读取__softirq_pending字段,可以类比于设备驱动中的状态寄存器,用于判断是否有软中断处理请求;
  • 软中断处理时会关闭Bottom-half,处理完后再打开;
  • 软中断处理时,会打开本地中断,处理完后关闭本地中断,这个地方对应到上文中提到的Top-half和Bottom-half机制,在Bottom-half处理的时候,是会将中断打开的,因此也就能继续响应其他中断,这个也就意味着其他中断也能来打断当前的Bottom-half处理;
  • while(softirq_bit = ffs(pending)),循环读取状态位,直到处理完每一个软中断请求;
  • 跳出while循环之后,再一次判断是否又有新的软中断请求到来(由于它可能被中断打断,也就意味着可能有新的请求到来),有新的请求到来,则有三个条件判断,满足的话跳转到restart处执行,否则调用wakeup_sotfirqd来唤醒内核线程来处理

__do_softirq既然可以在中断处理过程中调用,也可以在ksoftirqd中调用,那么softirq的执行可能有两种context,插张图吧:

让我们来思考最后一个问题:硬件中断触发的时候是通过硬件设备的电信号,那么软中断的触发是通过什么呢?答案是通过raise_softirq接口:

  • 可以在中断处理过程中调用raise_softirq来进行软中断处理请求,处理的实际也就是上文中提到过的irq_exit退出硬件中断上下文之后再处理;
  • raise_softirq_irqoff函数中,最终会调用到or_softirq_pending,该函数会去读取本地CPU的irq_stat中__softirq_pending字段,然后将对应的软中断号给置位,表明有该软中断的处理请求;
  • raise_softirq_irqoff函数中,会判断当前的请求的上下文环境,如果不在中断上下文中,就可以通过唤醒内核线程来处理,如果在中断上下文中处理,那就不执行;
  • 多说一句,在软中断整个处理流程中,会经常看到in_interrupt()的条件判断,这个可以确保软中断在CPU上的串行执行,避免嵌套;

6.2 tasklet

因为内核已经定义好了10种软中断类型,并且不建议我们自行添加额外的软中断,所以对软中断的实现方式,我们主要是做一个简单的了解,对于驱动程序的开 发者来说,无需实现自己的软中断。但是,对于某些情况下,我们不希望一些操作直接在中断的handler中执行,但是又希望在稍后的时间里得到快速地处 理,这就需要使用tasklet机制。 tasklet是建立在软中断上的一种延迟执行机制,它的实现基于TASKLET_SOFTIRQ和HI_SOFTIRQ这两个软中断类型。

从上文中分析可以看出,tasklet是软中断的一种类型,那么两者有啥区别呢?先说结论吧:

  • 软中断类型内核中都是静态分配,不支持动态分配,而tasklet支持动态和静态分配,也就是驱动程序中能比较方便的进行扩展;
  • 软中断可以在多个CPU上并行运行,因此需要考虑可重入问题,而tasklet会绑定在某个CPU上运行,运行完后再解绑,不要求重入问题,当然它的性能也就会下降一些;

**tasklet的特点**

  • 接口简单
  • 一个tasklet不会抢占另一个tasklet
  • tasklet可以动态创建和静态创建
  • 因为是靠软中断实现,所有不能睡眠
  • 两个相同类型的tasklet不可以在多个处理器是同时运行,但是两个不同类型的tasklet就可以
  • 作为一种优化措施,一个tasklet总在调度它的处理器上执行。

tasklet相关API:

/* 静态分配tasklet */
DECLARE_TASKLET(name, func, data)/* 动态分配tasklet */
void tasklet_init(struct tasklet_struct *t, void (*func)(unsigned long), unsigned long data);/* 禁止tasklet被执行,本质上是增加tasklet_struct->count值,以便在调度时不满足执行条件 */
void tasklet_disable(struct tasklet_struct *t);/* 使能tasklet,与tasklet_diable对应 */
void tasklet_enable(struct tasklet_struct *t);/* 调度tasklet,通常在设备驱动的中断函数里调用 */
void tasklet_schedule(struct tasklet_struct *t);/* 杀死tasklet,确保不被调度和执行, 主要是设置state状态位 */
void tasklet_kill(struct tasklet_struct *t);

数据结构

  • DEFINE_PER_CPU(struct tasklet_head, tasklet_vec)为每个CPU都分配了tasklet_head结构,该结构用来维护struct tasklet_struct链表,需要放到该CPU上运行的tasklet将会添加到该结构的链表中,内核中为每个CPU维护了两个链表tasklet_vec和tasklet_vec_hi,对应两个不同的优先级,本文以tasklet_vec为例;
  • struct tasklet_struct为tasklet的抽象,几个关键字段如图所示,通过next来链接成链表,通过state字段来标识不同的状态以确保能在CPU上串行执行,func函数指针在调用task_init()接口时进行初始化,并在最终触发软中断时执行;

流程分析

  • tasklet本质上是一种软中断,所以它的调用流程与上文中讨论的软中断流程是一致的;
  • 调度tasklet运行的接口是tasklet_schedule,如果tasklet没有被调度则进行调度处理,将该tasklet添加到CPU对应的链表中,然后调用raise_softirq_irqoff来触发软中断执行;
  • 软中断执行的处理函数是tasklet_action,这个在softirq_init函数中通过open_softirq函数进行注册的;
  • tasklet_action函数,首先将该CPU上tasklet_vec中的链表挪到临时链表list中,然后再对这个list进行遍历处理,如果满足执行条件则调用t->func()执行,并continue跳转遍历下一个节点。如果不满足执行条件,则继续将该tasklet添加回原来的tasklet_vec中,并再次触发软中断;

软中断和tasklet的区别和相同
区别:

  • 使用软中断的人屈指可数,大部分使用tasklet

  • tasklet比软中断接口更简单,锁保护要求低

  • tasklet更容易使用,使用范围更广

  • tasklet同一个处理程序的多个实例不能在多个处理器上运行,但是软中断可以

  • tasklet可以动态创建和静态创建、软中断只可以静态创建

相同:
do_softirq()会尽可能早地在下一个合适的时机执行。由于大部分tasklet和软中断都是在中断处理程序中被设置为待处理状态,所有最近一个中断返回时看起来就是执行do_softirq()的最佳时刻

tasklet使用示例

#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/interrupt.h>
#include <linux/irq.h>
#include <linux/delay.h>
#include <linux/time.h>
#include <linux/input.h>
#include <linux/init.h>
#include <linux/gpio.h>#include <asm/io.h>
#include <asm/irq.h>#define IMX_GPIO_NR(bank, nr)               (((bank) - 1) * 32 + (nr)) //平台相关
#define CYNO_GPIO_BEEP_NUM                  IMX_GPIO_NR(6,10) //本程序使用的gpio口//定义gpio引脚的结构体
static struct pin_desc{int irq;unsigned char *name;unsigned int pin;
};//实例化一个具体的引脚
static struct pin_desc beep_desc = {0,"beep_num",CYNO_GPIO_BEEP_NUM
};//生命tasklet触发函数,也就是中断下半部函数
void beep_tasklet_func(unsigned long data);//生命一个tasklet,名字为beep_tasklet,并且关联触发函数
DECLARE_TASKLET(beep_tasklet, beep_tasklet_func, 0);int flag = 0;//中断下半部函数实现
void beep_tasklet_func(unsigned long data){flag++;printk(KERN_INFO "-------\n");if(flag >= 60){printk(KERN_INFO "%s : flag = %d\n", __func__, flag);flag = 0;}
}/*
无论什么时候调用tasklet_schedule
一定是上半部代码执行结束,再执行下半部代码
*/
static irqreturn_t beep_interrupt_handler(int irq, void *dev_id)
{printk(KERN_INFO "%s : top tasklet func\n", __func__);tasklet_schedule(&beep_tasklet);    //触发下半部代码printk(KERN_INFO "%s : bottom tasklet func\n", __func__);return IRQ_HANDLED;
}//模块加载执行
static int interrupt_tasklet_init(void)
{int ret;printk(KERN_INFO "%s\n", __func__);//申请gpioif(gpio_request(beep_desc.pin ,beep_desc.name)){printk(KERN_ERR "%s : request gpio %d error\n", __func__, beep_desc.pin);goto err_gpio_request;}//设置gpio方向为输入gpio_direction_input(beep_desc.pin);//动态获取irq端口号beep_desc.irq = gpio_to_irq(beep_desc.pin);printk(KERN_INFO "%s : the irq num is %d\n", __func__, beep_desc.irq);//申请中断,并设置触发方式为下降沿,设置中断处理函数(上半部)    ret = request_irq(beep_desc.irq, beep_interrupt_handler , IRQF_TRIGGER_FALLING , beep_desc.name , &beep_desc);if(ret){printk(KERN_ERR "%s : request_irq is error\n", __func__);goto err_request_irq;}printk("%s : init end\n", __func__);return 0;//处理错误
err_request_irq:free_irq(beep_desc.irq, &beep_desc);err_gpio_request:gpio_free(beep_desc.pin);return -1;
}//驱动卸载执行
static void interrupt_tasklet_exit(void)
{printk("%s\n", __func__);free_irq(beep_desc.irq, &beep_desc);gpio_free(beep_desc.pin);
}module_init(interrupt_tasklet_init);
module_exit(interrupt_tasklet_exit);MODULE_AUTHOR("xiaolei");
MODULE_DESCRIPTION("interrupt tasklet use");
MODULE_LICENSE("GPL");

6.3 工作队列

参考:https://www.cnblogs.com/LoyenWang/p/13185451.html
http://kernel.meizu.com/linux-workqueue.html
1. 什么是workqueue
     Linux中的Workqueue机制就是为了简化内核线程的创建。通过调用workqueue的接口就能创建内核线程。并且可以根据当前系统CPU的个数创建线程的数量,使得线程处理的事务能够并行化。workqueue是内核中实现简单而有效的机制,他显然简化了内核daemon的创建,方便了用户的编程.

  • Workqueue工作队列是利用内核线程来异步执行工作任务的通用机制;
  • Workqueue工作队列可以用作中断处理的Bottom-half机制,利用进程上下文来执行中断处理中耗时的任务,因此它允许睡眠,而Softirq和Tasklet在处理任务时不能睡眠;

为什么使用workqueue
中断的 bottom half机制 比如 tasklet 都是在 中断上下文(softirq)中执行,而在 中断上下文 中通常是不能执行 休眠 操作中,假如有某些特殊的 bottom half 需要休眠,此时则不能使用 task。

workqueue 是运行在进程上下文中的,因而可以执行休眠操作,这是和其他 bottom half机制 有本质的区别,大大方便了在驱动中处理中断。

通常,在工作队列和软中断/tasklet中作出选择非常容易。可使用以下规则:

  • 如果推后执行的任务需要睡眠,那么只能选择工作队列。
  • 如果推后执行的任务需要延时指定的时间再触发,那么使用工作队列,因为其可以利用timer延时(内核定时器实现)。
  • 如果推后执行的任务需要在一个tick之内处理,则使用软中断或tasklet,因为其可以抢占普通进程和内核线程,同时不可睡眠。
  • 如果推后执行的任务对延迟的时间没有任何要求,则使用工作队列,此时通常为无关紧要的任务。
    实际上,工作队列的本质就是将工作交给内核线程处理,因此其可以用内核线程替换。但是内核线程的创建和销毁对编程者的要求较高,而工作队列实现了内核线程的封装,不易出错,所以我们也推荐使用工作队列。

2. 数据结构
关于 workqueue 中几个概念都是 work 相关的数据结构非常容易混淆,大概可以这样来理解:

  • work :工作。
  • workqueue :工作的集合。workqueue 和 work 是一对多的关系。
  • worker :工人。在代码中 worker 对应一个 work_thread() 内核线程。
  • worker_pool:工人的集合。worker_pool 和 worker 是一对多的关系。
  • pwq(pool_workqueue):中间人 / 中介,负责建立起 workqueue 和 worker_pool 之间的关系。workqueue 和 pwq 是一对多的关系,pwq 和 worker_pool 是一对一的关系。

    最终的目的还是把 work( 工作 ) 传递给 worker( 工人 ) 去执行,中间的数据结构和各种关系目的是把这件事组织的更加清晰高效。

2.1 work
struct work_struct用来描述work,初始化一个work并添加到工作队列后,将会将其传递到合适的内核线程来进行处理,它是用于调度的最小单位。

2.2 workqueue
内核中工作队列分为两种:

  • bound:绑定处理器的工作队列,每个worker创建的内核线程绑定到特定的CPU上运行;
  • unbound:不绑定处理器的工作队列,创建的时候需要指定WQ_UNBOUND标志,内核线程可以在处理器间迁移;

2.3 worker

  • 每个worker对应一个内核线程,用于对work item的处理;
  • worker根据工作状态,可以添加到worker_pool的空闲链表或忙碌列表中;
  • worker处于空闲状态时并接收到工作处理请求,将唤醒内核线程来处理;
  • 内核线程是在每个worker_pool中由一个初始的空闲工作线程创建的,并根据需要动态创建和销毁;

2.4 worker_pool

  • worker_pool是一个资源池,管理多个worker,也就是管理多个内核线程;
  • 针对绑定类型的工作队列,worker_pool是Per-CPU创建,每个CPU都有两个worker_pool,对应不同的优先级,nice值分别为0和-20;
  • 针对非绑定类型的工作队列,worker_pool创建后会添加到unbound_pool_hash哈希表中;
  • worker_pool管理一个空闲链表和一个忙碌列表,其中忙碌列表由哈希管理;

2.5 pool_workqueue

  • pool_workqueue充当纽带的作用,用于将workqueue和worker_pool关联起来;

再来张图,首尾呼应一下:

3. 流程分析
3.1 workqueue子系统初始化
3.2 work调度
3.3 worker动态管理
参考:https://www.cnblogs.com/LoyenWang/p/13185451.html

4 .API

  • 创建工作队列
    驱动程序可以创建并使用它们自己的工作队列,或者使用内核的一个工作队列。
//创建工作队列
struct workqueue_struct *create_workqueue(const char *name);
  • 创建工作队列的任务
    工作队列任务可以在编译时或者运行时创建
//编译是创建
DECLARE_WORK(name, void (*function)(void *), void *data);
//运行时创建
INIT_WORK(struct work_struct *work, void (*function)(void *), void *data);
  • 将任务添加到工作队列中
//添加到指定工作队列
int queue_work(struct workqueue_struct *queue, struct work_struct *work);<br>
int queue_delayed_work(struct workqueue_struct *queue, struct work_struct<br>
*work, unsigned long delay);//添加到内核默认工作队列
int schedule_work(struct work_struct *work);
int schedule_delayed_work(struct work_struct *work, unsigned long delay);说明:
delay: 保证至少在经过一段给定的最小延迟时间以后,工作队列中的任务才可以真正执行。
  • 队列和任务的清除操作
//取消任务
int cancel_delayed_work(struct work_struct *work);//清空队列中的所有任务
void flush_workqueue(struct workqueue_struct *queue);//销毁工作队列
void destroy_workqueue(struct workqueue_struct *queue);

5 .demo:
https://www.cnblogs.com/coversky/p/15256587.html
https://blog.csdn.net/Chasing_Chasing/article/details/89644598

调度的两种方式:

  • 1: 单独调度 work_struct
       API:  schedule_work()

  • 2: 调度执行一个workqueue_struct 里面的某个任务。
      API:   queue_work()

demo 如下: schedule_work

#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/slab.h>
#include <linux/kobject.h>
#include <linux/list.h>
#include <linux/kthread.h>
#include <asm/ptrace.h>
#include <linux/sched.h>
#include <linux/delay.h>
#include <linux/interrupt.h>
int data  = 10;static struct workqueue_struct *workqueue;
static struct work_struct       work1;
static struct work_struct       work2;static void do_work1(struct work_struct *arg)
{printk(KERN_INFO "do_work1 .....\r\n");
}static void do_work2(struct work_struct *arg)
{printk(KERN_INFO "do_work2 .....\r\n");
}int threadfn(void *data)
{static int count =  0 ;int args = *(int *)data;printk(KERN_INFO "enter thead_fn");while(1){msleep(2*1000);printk(KERN_INFO "threadfn data: %d, count: %d\r\n",args , ++count);schedule_work(&work1);schedule_work(&work2);}
}static int __init test_kobj_init(void)
{workqueue = create_workqueue("wanghb_queue");INIT_WORK(&work1,do_work1);INIT_WORK(&work2,do_work2);struct task_struct *  thread =  kthread_create( threadfn,(void * )&data,"mythread");if(thread != NULL){printk(KERN_INFO "thread create success\r\n");wake_up_process(thread);}else{printk(KERN_ERR "thread create err\r\n");}return 0;
}static void __exit test_kobj_exit(void)
{printk(KERN_INFO "test_kobj_exit \r\n");return;
}module_init(test_kobj_init);
module_exit(test_kobj_exit);MODULE_AUTHOR("LoyenWang");
MODULE_LICENSE("GPL");

log 如下:

demo: queue_work

#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/slab.h>
#include <linux/kobject.h>
#include <linux/list.h>
#include <linux/kthread.h>
#include <asm/ptrace.h>
#include <linux/sched.h>
#include <linux/delay.h>
#include <linux/interrupt.h>
int data  = 10;static struct workqueue_struct *workqueue;
static struct work_struct       work1;
static struct work_struct       work2;static void do_work1(struct work_struct *arg)
{printk(KERN_INFO "do_work1 .....\r\n");
}static void do_work2(struct work_struct *arg)
{printk(KERN_INFO "do_work2 .....\r\n");
}int threadfn(void *data)
{static int count =  0 ;int args = *(int *)data;printk(KERN_INFO "enter thead_fn");while(1){msleep(2*1000);printk(KERN_INFO "threadfn data: %d, count: %d\r\n",args , ++count);queue_work(workqueue,&work1);queue_work(workqueue,&work2);}
} static int __init test_kobj_init(void)
{workqueue = create_workqueue("wanghb_queue");INIT_WORK(&work1,do_work1);INIT_WORK(&work2,do_work2);struct task_struct *  thread =  kthread_create( threadfn,(void * )&data,"mythread");if(thread != NULL){printk(KERN_INFO "thread create success\r\n");wake_up_process(thread);}else{printk(KERN_ERR "thread create err\r\n");}return 0;
}static void __exit test_kobj_exit(void)
{printk(KERN_INFO "test_kobj_exit \r\n");destroy_workqueue(workqueue);return;
}module_init(test_kobj_init);
module_exit(test_kobj_exit);MODULE_AUTHOR("LoyenWang");
MODULE_LICENSE("GPL");

linux内核学习10:中断和异常相关推荐

  1. Linux内核深入理解中断和异常(1)

    Linux内核深入理解中断和异常(1) rtoax 2021年3月 1. 中断介绍 内核中第一个子系统是中断(interrupts). 1.1. 什么是中断? 我们已经在这本书的很多地方听到过 中断( ...

  2. Linux内核深入理解中断和异常(8):串口驱动程序

    Linux内核深入理解中断和异常(8):串口驱动程序 rtoax 2021年3月 /*** start_kernel()->setup_arch()->idt_setup_early_tr ...

  3. Linux内核深入理解中断和异常(7):中断下半部:Softirq, Tasklets and Workqueues

    Linux内核深入理解中断和异常(7):中断下半部:Softirq, Tasklets and Workqueues rtoax 2021年3月 0x00-0x1f architecture-defi ...

  4. Linux内核深入理解中断和异常(6):IRQs的非早期初始化

    Linux内核深入理解中断和异常(6):IRQs的非早期初始化 rtoax 2021年3月 0x00-0x1f architecture-defined exceptions and interrup ...

  5. Linux内核深入理解中断和异常(3):异常处理的实现(X86_TRAP_xx)

    Linux内核深入理解中断和异常(3):异常处理的实现(X86_TRAP_xx) rtoax 2021年3月 /*** start_kernel()->setup_arch()->idt_ ...

  6. Linux内核深入理解中断和异常(2):初步中断处理-中断加载

    Linux内核深入理解中断和异常(2):初步中断处理-中断加载 rtoax 2021年3月 1. 总体概览 关于idt_table结构的填充,在5.10.13中流程为: idt_setup_early ...

  7. Linux内核深入理解中断和异常(4):不可屏蔽中断NMI、浮点异常和SIMD

    Linux内核深入理解中断和异常(4):不可屏蔽中断NMI.浮点异常和SIMD rtoax 2021年3月 本文介绍一下几种trap: //* External hardware asserts (外 ...

  8. Linux内核深入理解中断和异常(5):外部中断

    Linux内核深入理解中断和异常(5):外部中断 rtoax 2021年3月 1. 外部中断简介 外部中断包括:键盘,鼠标,打印机等. 外部中断包括: I/O interrupts; IO中断 Tim ...

  9. linux内核学习10.1:Linux内核ARM7架构异常中断向量表

    参考:https://www.cnblogs.com/douzi2/p/5112743.html 当异常中断发生时,系统执行完当前指令后,将跳转到相应的异常中断处理程序处执行.在异常中断处理程序执行完 ...

  10. Linux内核学习--内存管理模块

    Linux内核学习--内存管理模块 首先,Linux内核主要由五个部分组成,他们分别是:进程调度模块.内存管理模块.文件系统模块.进程间通信模块和网络接口模块. 本部分所讲的内存是内存管理模块,其主要 ...

最新文章

  1. 链接全局变量再说BSS段的清理
  2. Mckinsey insights 2
  3. 中文课程!台大李宏毅机器学习公开课2019版上线
  4. cufon,在网页上画出特殊字体
  5. 光电整纬机狭缝检测工作原理
  6. pytorch---之pin_memory
  7. docker搭建swoole简易聊天室
  8. 女孩必读:打死不能嫁的36种男人
  9. FIR滤波器——Matlab实现
  10. lex 词法分析 linux,Lex词法分析器
  11. 对tensor不同的索引方法和索引越界问题解决思路
  12. Restful API详解
  13. Windows Internet Information Services(IIS) 与 inetpub 文件夹
  14. 玉米生吃好还是熟吃好 各种情况分析
  15. RH2288v3常用的知识
  16. 【经典C程序】判断闰年
  17. centos7配置端口转发
  18. 又火了一个,看小说也能学 JavaScript?
  19. Marvolo Gaunt's Ring 【CodeForces 855B】
  20. 测试用例设计方法有哪些?举例说明

热门文章

  1. 【智能制造】36页精彩PPT:探讨智能制造的三驾马车
  2. 985硕士在2021秋招没有offer是一种什么体验?
  3. 交换机Trunk详解
  4. 博主力推!!NRF52832 BLE 抓包sniffer来了!附带安装使用说明
  5. 网络安全·网络入侵检测系统
  6. 彻底删除禁止conime.exe启动运行方法
  7. asp html css样式,aspupload
  8. wps是用python语言开发的吗_wps是用什么语言开发的
  9. Lower power design UPF 学习
  10. 17.CRT的绿色版安装和使用。