目录

1、中断简介

1.1 作用

1.2 物理实现

1.3 中断请求线IRQ

1.4 异常

2、中断处理程序

2.1 作用

2.2 上半部和下半部

2.3 中断上下文

3、中断系统

3.1 中断机制的实现

3.2 中断控制

4、下半部和软中断

4.1 下半部简介

4.2 软中断

4.3 tasklet

4.4 工作队列


1、中断简介

1.1 作用

    中断机制,是操作系统用来实现处理器和外部设备协同工作的方案,让硬件在需要的时间主动向内核发出信号。

1.2 物理实现

    中断是一种特殊的电信号,由硬件生成。中断生成和处理的主要流程:

  1. 硬件生成中断电信号,并直接送入中断控制器的输入引脚;
  2. 中断控制器接收到一个中断信号后,向处理器发送一个电信号;
  3. 处理器检测到这个电信号后,会立即中断自己当前的工作,并处理这个中断(向操作系统反映此信号的到来);
  4. 操作系统处理这个中断请求

    硬件设备在产生中断的时候,并不会考虑与处理器的时钟同步问题,也就是说,中断会随时产生,内核随时可能因为新到来的中断而被打断。

1.3 中断请求线IRQ

    中断请求线指的是中断的唯一数字标志。不同设备对应的中断不同,操作系统通过IRQ,来区分中断的来源是什么硬件设备,以提供相应的中断处理程序。

    IRQ的值是和硬件相关的。在经典的PC机上,IRQ 0是时钟中断,IRQ 1是键盘中断;而在PCI总线上的设备,中断的IRQ则是动态分配的。其他非PC的体系结构里,也有动态分配可用动态的特性。

1.4 异常

    异常也被称为同步中断,因为异常产生的时候,必须考虑和处理器时钟同步的问题。

    在处理器执行到编程失误导致的错误之类(如被0除),或者执行期间出现特殊情况(如缺页),必须靠内核来处理的时候,处理器就会产生一个异常。所以,很多处理器体系结构处理异常和处理中断的方式非常类似。

2、中断处理程序

2.1 作用

    在响应一个特定中断的时候,内核会执行一个函数:中断处理程序,或者叫中断服务例程(ISR,Interrupt Service Routine)。中断处理程序,是一个设备的驱动的一部分,一方面负责通知硬件设备中断已经被接收,另外还有其他大量和设备相关的工作。例如对网卡的中断,还需要把来自网络的数据包拷贝到内存,并处理然后再交给合适的协议栈和应用程序。

2.2 上半部和下半部

    由于中断随时可能发生,所以必须保证中断处理程序可以快速执行;但是中断处理程序可能又会处理大量的任务,两者之间存在矛盾,所以一般会把中断处理的过程分成两部分:上半部和下半部。

    上半部也叫硬中断,是通常意义上的中断处理程序,用来接收中断,和简单的、有时限的处理工作,例如对中断接收后进行应答或者复位硬件等需要在所有中断被禁止的情况下完成的工作。而其他的允许稍后完成的工作,则会推迟到下半部在合适的时间完成。Linux有多种机制来实现下半部,其中一种就是软中断。

    对于网卡的中断处理,上半部会执行 通知硬件、拷贝网络数据报到内存并继续读取新数据包,这些重要、紧急且与硬件相关的工作,因为网卡接收的网络数据包的缓存大小通常是固定的、有限的,一旦被延迟可能造成缓存溢出。而数据包的处理等操作,则由下半部来完成。

2.3 中断上下文

    中断处理程序运行在中断上下文中。中断上下文也被称为原子上下文,因为该上下文中执行的代码是不可阻塞的。

    中断上下文不可以睡眠,所以中断上下文中的代码中,不允许使用睡眠函数。

    中断上下文的执行,是打断了其他代码,甚至是其他中断线上的中断处理程序的执行,所以是有严格的时间限制的,所以中断上下文中的代码必须简洁迅速。尽量把工作分离出来放到下半部执行,因为下半部可以在更合适的实现来执行。

    在Linux 2.6版本中,内核为每个中断处理程序分部增加了一个中断栈,大小为1页(32位系统是4KB,64位系统是8KB)。

3、中断系统

3.1 中断机制的实现

  1. 设备产生中断,通过总线将电信号发送给中断控制器;
  2. 如果中断线是激活的(中断线允许被屏蔽),中断控制器将中断信号发送给处理器。在大多数体系结构里,这一步是通过电信号给处理器的特定管脚发送一个信号;
  3. 处理器如果没有禁止这个中断,则立即停止正在进行的事情,关闭中断系统,并跳转到内存中预定义的位置,执行那里的代码。这个预定义的位置,是内核设置的,中断程序的入口。对于每条中断线,内核都会跳转到一个唯一的位置,内核以此获取中断的IRQ号;
  4. 内核首先在栈中保存中断的IRQ号,并保存寄存器的值,这些值是被中断任务的;
  5. 内核调用do_IRQ()方法响应中断:
    1. 对接收的中断进行应答,并禁止这条线的中断传递;
    2. do_IRQ()检查这条中断线是否有一个有效的处理程序,而且这条程序已经启动,但是当前没有执行;
    3. 如果符合条件,do_IRQ()调用handle_IRQ_event()运行这条中断线的中断处理程序;
    4. 如果不符合条件,或者handle_IRQ_event()执行完成后,do_IRQ()会调用ret_from_intr()方法,结束中断过程,返回中断前执行的用户地址空间。

3.2 中断控制

    Linux内核提供了一套用来操作中断的系统调用,可以用来屏蔽处理器的中断,或者屏蔽掉一部分、甚至某一条中断线的中断。这些可以在<asm/system.h>和<asm/irq.h>中找到。

    Linux之所以要实现中断控制系统,主要是因为处理器需要同步。通过禁止中断,可以防止某个或者某些中断处理程序不会抢占当前内核执行的代码;此外,禁止中断还可以禁止内核抢占。但是禁止中断和禁止内核抢占,并不能防止其他处理器并发访问。想要防止其他处理器的并发访问,需要Linux的锁保护机制来实现。

1、禁止和激活全部中断

    linux内核提供了接口,用于禁止或激活全部中断线的中断请求:

  1. local_irq_disable():禁止全部中断线的中断请求;
  2. local_irq_enable():激活全部中断线的中断请求。

    这两个接口会有一些问题:它们会无条件的禁止和激活所有的中断线。如果有部分中断线本来就是被禁止的,local_irq_enable()也会将它们恢复。在很多场景激活所有的中断线可能会带来一些问题。

2、禁止中断和恢复

  由于禁止和激活全部中断会带来一些问题,Linux内核提供了禁止中断并恢复禁止前状态的调用:

  1. unsigned long flags:值传递的一些参数
  2. local_irq_save(flags):保存本地中断的当前状态,然后禁止这些中断线的中断请求;
  3. local_irq_restore(flags):恢复本地中断控制状态到保存的状态。

3、禁止指定中断线的中断

    一些情况下,操作系统只需要禁止某条中断线的中断,即屏蔽这条中断线。例如在执行某条中断线的中断处理程序时,一般会屏蔽这条中断线。

    Linux提供了相应的系统调用:

  1. disable_irq(unsigned long irq):在当前中断线的中断处理程序执行完成后,屏蔽这条中断线;
  2. disable_irq_nosync(unsigned long irq):立即屏蔽这条中断线;
  3. enable_irq(unsigned long irq):激活某条中断线;
  4. synchronize_irq(unsigned long irq):等待某条中断线的中断处理程序退出,如果该处理程序正在执行,则处理程序退出后方法才会返回。

4、下半部和软中断

    根据上面的内容可知:

  • 中断处理过程的上半部就是硬中断,主要负责处理中断过程中和硬件相关的、对时间敏感的操作;
  • 下半部主要用来处理一些比较耗时的操作,linux有很多中实现方式,包括tasklet、软中断、工作队列。即软中断是下半部的一种实现方式。

4.1 下半部简介

1、为什么要使用下半部

  1. 如果所有任务都放到上半部执行,会导致处理器较长时间不能处理一部分甚至全部中断:当一个中断正在处理的时候,这个中断线在所有处理器上都会被屏蔽;甚至如果一个处理程序是IRQF_DISABLED类型的,它在执行的过程中会屏蔽所有中断
  2. 中断处理器不在进程上下文中执行,所以它们不能被阻塞,这在很多场景会有限制;
  3. 中断处理程序是异步执行的。

    所以应该尽量缩短中断处理程序的执行时间。这样,我们会将和硬件不相干的、对时间不太敏感的操作,尽量放到下半部执行

2、什么任务会放到下半部

    Linux操作系统目前没有办法强制要求哪些任务放到下半部,目前完全取决于开发者自己去判断。但是有一些规则提供借鉴:

  1. 如果一个任务,对时间非常敏感,则应该放到上半部;
  2. 如果一个任务,和硬件相关,则应该放到上半部;
  3. 如果一个任务,要求不被其他任务打断,尤其是不被同类型的中断打断,则应该放到上半部;
  4. 其他的任务都应该放到下半部。

3、下半部的实现方式

    对于现在的Linux 2.6来说,主要有三种方式实现下半部:

  1. 软中断:有些地方会混淆软中断和下半部的概念,实际上软中断是下半部的实现方式之一。
  2. tasklet:tasklet实际上是基于软中断实现的,是在性能和易用性之间寻求平衡的产物。对于大部分软中断,使用tasklet即可。
  3. 工作队列:和上面两种不同,工作队列可以把任务退后,交给内核线程去执行。工作队列是工作在进程上下文中的,用于进程的各种特性,比如睡眠等。

4.2 软中断

    软中断是编译期间动态分配的,不像tasklet一样可以动态的注册和销毁。软中断最多只有32个软中断,大部分下半部都是通过tasklet来实现的,目前只用了9个左右。

    软中断运行在软中断上下文中,软中断的执行会抢占进程上下文。软中断执行过程中,不允许重新调度和睡眠。

1、资源抢占

  1. 软中断不会被另一个软中断打断,只有硬件的中断处理程序才能打断软中断;
  2. 另一个软中断可以在其他处理器上执行;
  3. 同一种软中断,也可以在另一个处理器上执行

2、什么时候应该使用软中断实现下半部

    软中断应该保留给系统中对时间要求最严格的下半部使用。目前只有两个子系统直接使用软中断:网络和SCSI。此外,内核定时器和tasklet也是基于软中断实现的。由于同一个软中断可以在不同处理器上同时执行,所以软中断要求对共享资源的处理要非常严格,锁保护的要求很高。

3、软中断的触发和执行

    一个注册的软中断被标记后才会被执行,这被称为软中断的触发。通常中断处理程序会在返回前标记相应的软中断,使其稍后会被执行。

    待处理的软中断,被检查和执行的时机包括:

  1. 从一个硬件中断代码处返回;
  2. 在ksoftirqd内核线程中;
  3. 在显式检查和执行待处理的软中断的代码中,如网络子系统中。

4.3 tasklet

    tasklet是基于软中断实现的一种下半部机制,所以也是运行在软中断上下文的。

    相比于直接使用软中断,tasklet的接口更简单,锁保护的要求也更低。tasklet可以静态定义,也可以动态初始化。

    tasklet由两类软中断代表:HI_SOFTIRQ和TASKLET_SOFTIRQ。这两者唯一的区别在于,HI_SOFTIRQ类型的软中断先于TASKLET_SOFTIRQ类型的软中断执行。

1、资源抢占

  1. tasklet不会被其他tasklet打断,只有硬件的中断处理程序才能打断tasklet;
  2. 如果一个tasklet正在一个处理器上执行,则这个tasklet不允许在本处理器和其他处理器上再次触发;
  3. 不同的tasklet可以同时在不同的处理器上执行。

4.4 工作队列

    工作队列可以把任务推后,交由一个内核线程来完成。工作队列的任务是运行在进程上下文中的,这一点和软中断、tasklet不同,工作队列可以使用进程的特性,最重要的是可以重新调度和睡眠。

    一般来说,如果任务需要睡眠或者重新调度,就需要使用工作队列;但是如果不需要,一般使用tasklet来实现。使用工作队列的场景一般包括:需要获取大量内存的场景、需要获取信号量或者锁的场景、需要阻塞式的执行IO的场景等。

    理论上可以用创建内核线程的方式来代替工作队列,但是由于随便创建内核线程会带来其他问题,所以实际上并不建议直接创建内核线程,而应使用工作队列的形式。

Logo

更多推荐