中断
中断
任何操作系统内核的核心任务,都包含了对连接到计算机上的硬件设备进行有效管理,如硬盘、鼠标、键盘等。而想管理这设备,首先要能和它们互相通信才行。众所周知,处理器的速度跟外围硬件设备的速度往往不是一个数量级的,因此如果让处理器在发出一个请求后,专门等待响应,显然是不合理的。为了协同和这些外围设备的工作而又不降低机器的整体性能,使用了中断机制:让硬件在需要的时候再向内核发送信号。
异常
异常是一种与中断类似的机制(在操作系统中,讨论中断不得不及到它)。它在产生时必须考虑与处理器时钟同步(异常常被称为同步中断)。在处理器执行到某些特殊场景时(例如,除0错误、缺页等),必须靠内核来处理时,就会产生一个异常。因为许多处理器体系结构处理异常与处理中断的方式类似,因此,内核对他们的处理也是类似的。
异常是由处理器引起的,而中断是硬件产生的。
中断处理程序
在响应一个特定中断时,内核会执行一个名叫中断处理程序(ISR)的函数。产生中断的每个设备都有其相应的中断处理程序,一个设备的中断处理程序是它设备驱动程序的一部分。设备驱动程序是用于对设备进行管理的内核代码(一般由设备厂家提供实现)。
在linux中,中断处理程序就是一个普通的C程序,以特定的方式声明,被内核调用来响应中断的,它们运行于我们称之为中断上下文的特殊上下文中。因为中断随时可能发生,而中断程序也需要随时执行,因此对于内核的其他部分来说,中断处理程序需要在尽可能短的时间内完成作业。
注册中断处理程序
驱动程序可以通过request_irq()函数注册一个中断处理程序,并且激活给定的中断线:
//注册中断处理程序
int request_irq(unsigned int irq /*中断号或中断线号*/,
irqreturn_t (*handler)(int, void *, struct pt_regs *),
unsigned long flags,
const char *dev_name,
void *dev);
//释放中断处理程序
void free_irq(unsigned int irq, void *dev);
如果指定的中断线不是共享的,则free_irq删除处理程序的同时禁用这条中断线。如果中断线是共享的,则仅删除dev所对应的处理程序,中断线只有当最后一个处理程序被删除时才会被禁用。
中断线是指以共享的形式将多个处理程序注册到同一个中断线,并通过dev来做区分,它处理设备中断的流程如下:
重入和中断处理程序
linux中中断处理程序是无须重入的。当一个给定的中断处理程序正在执行时,相应的中断线在所有处理器上都会被屏蔽掉
,已防止同一中断线上接收到另一个新的中断。
上半部和下半部
中断上下文
当执行一个中断处理程序时,内核处于中断上下文
中。与进程上下文不同,进程上下文可以通过current宏关联当前进程,并且进程是以进程上下文的方式链接到内核中的,因此,进程上下文可以睡眠,也可以调用调度程序。而中断上下文和进程没有任何瓜葛,因此它是无法睡眠和从新调度的。
中断上下文有较为严格的时间限制,因为他打断了其他代码的执行。所以一定要牢记:所有中断处理程序必须尽可能地迅速、简洁。
中断处理机制的实现
设备产生中断 ->
通过总线吧电信号发给中断控制器 ->
如果中断已激活,中断控制器会把中断发往处理器(通过电信号给处理器特定管脚发送一个信号) ->
处理器立即停止它正在做的事,关闭中断系统
,然后跳到内存中预定的位置
开始执行代码(中断处理程序的入口点,每条中断线都有唯一的位置)。
注:这里可以看出软中断和硬中断的区别:软中断是软件引起陷入内核
,再有内核调用对应的处理函数,具有一定的同步性
;而硬中断是硬件通知到处理器,由处理器
中断当前进程的运行,并通过内核去调用对应的处理函数,这也是为什么中断上下文中无法睡眠或调度的原因。
中断控制
linux内核提供了一组接口用于操作机器上的中断状态。这些接口为我们提供了能够禁止当前处理器的中断系统,或者是屏蔽掉整个机器的一条中断线的能力。
一般来说,控制中断系统的目的主要是提供同步。通过禁止中断,可以确保某个中断处理程序不会抢占当前的代码。
在多处理器系统中,内核代码一般通过获取锁来防止数据被并发访问,获取锁的同时也需要禁止本地中断。
中断控制的函数(禁止\激活)
禁止或激活当前处理器的本地中断。
local_irq_disable();
local_irq_enable();
保存中断状态以及恢复中断状态(以下函数是宏定义,必须在同一个函数中调用)。
unsigned long flags;
local_irq_save(flags);
local_irq_restore(flags);
禁止或激活某一条中断线(所有处理器)。
disable_irq(irq);//必须等待中断线上所有处理程序完成后,才返回
disable_irq_nosync(irq);//不需等待
enable_irq(irq);
synchronize_irq(irq);//等待特定中断处理器程序完成
下半部和推后执行工作
下半部的任务就是执行与中断处理密切相关但中断处理程序本身不执行的工作。在理想的情况下,我们希望中断处理程序将所有的工作都交给下半部去执行,因为我们希望中断处理程序中完成的工作越少越好(尽快返回)。
虽然上下两部分划分工作并没有明确的规则,但是还是有一些可借鉴之处:
(1)如果任务对时间非常敏感,将其放在中断处理程序中执行
(2)如果任务和硬件相关,将其放在中断处理程序中执行
(3)如果任务需要保证不被其他中断打断,~
(4)其他任务,尽量放在下半部执行
不同的下半部方法
实现中断处理程序的方法只有一种,而实现一个下半部会有许多不同的方法。
1. BH
提供一个静态创建、由32个bottom halves(下半部函数)组成的链表,上半部分由一个32位整数中的一位来标识那个BH可以使用。每个BH都在全局范围内使用,在不同的处理器,两个BH也不可以同时执行。
2. 任务队列
内核定义了一组队列,每种类型的队列都包含一个由等待调用的函数组成的链表。
3. 软中断(目前正在使用)
软中断时一组静态定义的下半部接口,有32个(目前只用到了不到10个),它们可以在所有处理器上同时执行(即使是同类型的也可以)。
一个软中断不会抢占另一个软中断(能抢占软中断的只有中断处理程序
)。不过,其他软中断可以在其他处理器上同时运行,甚至是相同类型的软中断。
软中断的执行
一个注册的软中断必须在被标记后才会执行。这被称为触发软中断。通常,中断处理程序在返回时回标记它的软中断,使其在稍后执行。在下列地方,待处理的软中断会被执行:
(1)从一个硬件中断代码返回时
(2)在ksoftirqd(软中断线程)内核线程中
(3)在一些显示检查和执行待处理的软中断代码中。
不管是用什么方式唤起的,软中断都会在函数do_softirq()中执行。该函数会循环遍历软中断数组,依次调用待处理的软中断程序。
使用软中断
软中断是保留给系统对时间要求最严格以及最重要的下半部使用(例如,网络和SCSI)。此外,内核定时器和tasklet都是建立在软中断上的。
-
分配索引,在编译期间,通过在<linux/interrupt.h>中定义的一个枚举类型来静态声明软中断(即在软中断数据组中添加一个新的项)。建立一个新的软中断必须在此枚举类型中加入新的项,并且项的位置决定了它的优先级(执行顺序)。
软中断处理程序在执行的时候允许响应中断
,但它自己不能休眠
。在一个处理程序运行的时候,当前处理器的软中断被禁止
,但其他处理器仍可以执行别的软中断(即使是同一个软中断被再次触发)。这意味着共享数据需要严格的锁保护。
引入软中断的主要原因是其可扩展性。如果不需要扩展到多个处理器,使用tasklet更适合(本质也是软中断),只不过不能同时在多个处理器上运行同一个类型的tasklet。 -
函数
open_softirq(irq,action)
注册中断处理程序,函数raise_softirq(irq)
可以将一个软中断设置为挂起状态(触发软中断前会先禁止中断,触发后再恢复中断)。
注意:这里的软中断(下半部程序)和用户空间产生的软中断(软件中断)不是同一个概念。
4. tasklet(目前正在使用)
tasklet是一种基于软中断实现的动态创建
的下半部实现机制。两种不同类型的tasklet可以在不同的处理器上同时执行,但是同类型的不可以。
tasklet的使用
(1)通常应该选择tasklet,软中断只在那些执行频率很高和连续性要求很高的情况下才需要使用。
(2)tasklet本身也是软中断(因此tasklet的处理程序也不能睡眠),由两个软中断类型(中断号)表示:HI_SOFTIRQ和TASKLET_SOFTIRQ。两者的唯一区别是前者先于后者执行
(3)tasklet由tasklet_struct结构表示,每个结构体单独代表一个tasklet。
(4)tasklet调度过程
ksoftirqd
每个处理器都有一组辅助软中断的内核线程,根据处理器编号它们被命名为ksoftirqd0、ksoftirqd1、ksoftirqdn。当内核出现大量的软中断的时候,这些内核线程就会协助处理它们。
5. 工作队列(目前正在使用)
工作队列可以把工作推后,交由一个内核线程去执行(进程上下文),因此工作队列是允许重新调度甚至是睡眠的
(和tasklet最大的区别)。 工作队列类似于生产者-消费者模式。
工作队列结构
工作队列通过workqueue_struct定义。一旦定义了工作队列,会为每个处理器关联一个内核线程。通过内核提供的api可以将work_struct类型的工作任务添加到工作列表中去。当处理器上的工作线程被唤醒时,就会执行工作列表上的所有工作任务。
内核为我们提供了一个event类型的工作队列,并提供了一套专用的api来操作这个工作队列。
三种下半部接口的比较
下半部 | 上下文 | 顺序执行保障(数据安全) | 效率 |
---|---|---|---|
软中断 | 中断 | 没有 | 最高 |
tasklet | 中断 | 同类型不能同时执行 | 较高 |
工作队列 | 进程 | 没有(和进程上下文一样被调度) | 高 |
下半部和数据同步
软中断和tasklet与中断类似,几乎可以在任何时刻异步发生,也就是可能随时打断当前正在执行的代码。为了保证共享数据的安全,常见的做法是先得到一个锁,然后再禁止下半部的处理。
禁止下半部
禁止下半部和激活下半部的函数(当前处理器),如下:
void local_bh_disable();
void local_bh_enable();
注:最重要的是要明白,和中断一样,下半部也是可以被禁止的。
下半部和锁
在与下半部配合使用时,必须小心的使用锁机制。
- 如果进程上下文和下半部共享数据,则加锁的同时需要
禁止下半部的执行
。 - 如果下半部和中断处理程序共享数据,则加锁的同时需要
禁止中断
。 - 下半部和下半部之间共享数据,无需禁止下半部执行(同一个处理器上,不会有tasklet或者软中断之间
相互抢占
的情况)。