一、中断
Linux内核支持众多处理器架构,它对各种架构都做了相当的适配,Linux内核中的中断管理可以分为以下四层。
- 硬件层,比如CPU和中断控制器的连接
- 处理器架构管理层,比如CPU中断异常处理
- 中断控制器管理层,比如IRQ中断异常的处理
- Linux内核通用中断处理器层,比如中断注册和中断处理
硬件中断号和Linux中断号的映射
1.1 软中断和tasklet
中断管理中有一条很重要的原则:硬件中断处理程序应该执行得越快越好,这是因为硬件中断处理程序会打断其他重要代码的执行,为了避免打断时间过长,硬件中断处理应该尽快执行完毕。话有一个原因是中断处理程序会在关中断的情况下执行,所谓关中断指的是关闭本地CPU的所有中断响应,这也要求中断最好尽快执行完毕。
为了应对中断执行时间应该尽可能短的要求,Linux中断管理引入了一个很重要的设计理念——上下半部机制。在Linux内核中,上下半部机制是一种用于处理中断和延迟任务的机制,它将中断处理分为两个部分:上半部(也称为中断服务例程,ISR,或者硬中断)和下半部(也称为延迟任务处理,或者称为软中断)。这种机制的设计目的是为了提高系统的效率和响应速度。
上半部是中断处理的第一部分,它在硬件中断发生时被调用。上半部的执行具有较高的优先级,它会在中断发生时立即打断当前的进程执行,转而执行中断服务例程。上半部的执行时间通常很短,因为它只完成一些紧急且必要的操作,比如读取硬件寄存器的状态、关闭中断源等。上半部的执行环境是内核空间,它不能睡眠,也不能使用可能引起睡眠的函数。
下半部是中断处理的第二部分,它在上半部执行完毕后被调度执行。与上半部不同,下半部的执行优先级较低,它可以执行一些相对不那么紧急但可能需要更多时间来完成的任务,比如数据处理、内存分配等。下半部的执行环境也是内核空间,但它可以睡眠,可以使用可能引起睡眠的函数。
1.2 软中断
软中断是Linux内核很早引入的机制,最早可以追到Linux2.3开发期间。软中断是预留给系统中对时间要求最严格和最重要的下半部使用的,而且目前驱动中只有块设备和网络子系统使用了软中断。系统静态定义了若干软中断类型,并且Linux内核开发者不希望用户再扩充新的软中断类型,如有需要,建议使用tasklet
机制。
其中索引号越小,软中断的优先级越高,从而在一轮软中断处理中得到优先执行。
- HI_SOFTIRQ,优先级为0,是优先级最高的软中断类型。
- TIMER_SOFTIRQ,优先级为1,是用于定时器的软中断。
- NET_TX_SOFTIRQ,优先级为2,是用于发送网络数据包的软中断。
- NETRX_SOFTIRQ,优先级为3,是用于接收网络数据包的软中断。
- BLOCKSOFTIRQ和BLOCKIOPOLL_SOFTIRQ,优先级分别是4和5,是用于块设备的软中断。
- TASKLET_SOFTIRQ,优先级为6,是专门为tasklet机制准备的软中断。
- SCHED_SOFTIRQ,优先级为7,用于进程调度以及负载均衡。
- HRTIMER_SOFTIRQ,优先级为8,是一种高精度定时器。
- RCU_SOFTIRQ,优先级为9,是专门为RCU服务的软中断
1.3 tasklet
tasklet
是利用软中断实现的一种下半部机制,本质是软中断的变种,运行在软中断的上下文中,其数据结构如下:
[include/linux/interrupt.h]
struct tasklet_struct
{
struct tasklet_struct *next;
unsigned long state;
atomic_t count;
void (*func)(unsigned long);
unsigned long data;
};
其中next指向下一个tasklet节点;state用于表示tasklet是正在运行还是准备中;count用于规定tasklet是否处于激活状态,如果不是0则表示不允许执行;func用于指向tasklet的处理程序,而data就是传递给tasklet的参数。
每个CPU维护两个tasklet链表,一个是普通优先级的task_vec
,另外一个是高优先级的tasklet_hi_vec
。他们都是Pre-CPU变量。如果要在驱动中使用tasklet机制,则可以使用DECLARE_TASKLET
静态声明或者tasklet_init()
动态初始化一个tasklet,调度则使用tasklet_schedule
函数
1.4 工作队列机制
工作队列机制是除了软中断和tasklet以外最常用的下半部机制之一。工作队列的基本原理是把work(需要推迟执行的函数)交由一个内核线程执行,并且总是在进程上下文中执行。工作队列的优点是利用进程上下文来执行中断下半部操作,因此工作队列允许重新调度和睡眠,是异步执行的进程上下文,另外还能解决因软中断和tasklet执行时间过长而导致系统实时性下降等问题。当驱动程序或内核子系统在进程上下文中有异步执行的工作任务时,可以使用工作项来描述工作任务。把工作项添加到一个队列中,然后由一个内核线程执行工作任务的回调函数。
当驱动程序或者内核子系统在进程上下文中有异步执行的工作任务的时候,可以使用工作项来描述工作任务,然后把这个工作项添加到一个工作队列中,然后使用一个内核线程执行工作任务的回调函数,这个内核线程被称为worker。
工作队列和tasklet的区别是,tasklet主要运行在中断的上下文中,属于中断处理的下半部,因此也是不能睡眠不能阻塞的,而工作队列则运行在进程上下文中,属于内核线程,它可以睡眠也可以被阻塞。这个根本区别也带来了一些其他特性上的不同,比如说tasklet适合快速响应、不能被阻塞的场景,而工作队列适合执行所需时间较长并且可能被阻塞的任务
1.5 中断中的同步和异步
在中断处理中进行同步操作,核心原则是不能引发睡眠或阻塞(中断上下文无法调度),因此对同步机制的选择有严格限制。简单来说:不能使用mutex,谨慎使用spinlock(需配合中断屏蔽),还可根据场景选择中断屏蔽、seqlock等更轻量的机制。
1.5.1 为什么中断中不能用mutex?
mutex(互斥锁)的核心特性是“获取失败时会睡眠”,这与中断上下文的运行规则完全冲突:
- 中断上下文属于内核紧急处理路径,不允许调度或睡眠(没有进程描述符,无法被挂起和唤醒)。
- 若在中断中使用mutex,当锁被占用时,mutex会尝试将当前执行流(中断处理程序)挂起,这会直接导致系统崩溃(无法处理睡眠请求)。
因此,mutex绝对不能用于中断上下文的同步,它仅适用于进程上下文(如用户态线程、内核线程)。
1.5.2 spinlock在中断中可以用,但有严格前提
spinlock(自旋锁)的特性是“获取失败时会自旋等待(忙等)”,不会睡眠,理论上符合中断上下文的要求。但直接使用可能导致死锁,必须满足以下条件:
关键前提:禁止本地中断
当进程上下文持有spinlock时,若此时发生中断,且中断处理程序也尝试获取同一个spinlock,会触发死锁:
- 进程被中断打断,无法释放spinlock;
- 中断处理程序自旋等待锁释放,而持有锁的进程被中断阻塞,永远无法执行——形成死锁。
解决方法:在中断中获取spinlock前,必须先禁止本地CPU的中断,确保持有锁期间不会被同CPU的中断打断。具体操作如下:
场景 | 操作步骤 | 对应内核API(以Linux为例) |
---|---|---|
中断处理程序用spinlock | 1. 禁止本地中断; 2. 获取spinlock; 3. 执行临界区; 4. 释放spinlock; 5. 恢复中断。 | spin_lock_irqsave(lock, flags) spin_unlock_irqrestore(lock, flags) |
spin_lock_irqsave
:原子执行“禁止中断”和“获取锁”,并保存中断状态到flags
;spin_unlock_irqrestore
:释放锁后,通过flags
恢复中断状态(确保不影响其他中断设置)。
1.5.3 其他适用于中断的同步机制
除了spinlock,以下机制更适合中断场景,需根据同步需求选择:
机制 | 适用场景 | 核心原理 | 优缺点 |
---|---|---|---|
中断屏蔽(local_irq_disable) | 仅需同步同CPU的中断与进程上下文,且临界区极短。 | 直接禁止当前CPU的所有中断,确保临界区不被中断打断。 | 优点:最简单,无锁开销; 缺点:过度屏蔽中断会降低系统响应性,仅适用于极短操作。 |
seqlock(序列锁) | 读多写少场景,中断中读取共享数据,进程上下文写入。 | 写者更新数据前递增序列值,写完后再递增;读者读取前后检查序列值,若一致则数据有效。 | 优点:读者(中断中)无需加锁,效率高; 缺点:写者需保证操作原子性,不适合复杂写操作。 |
RCU(Read-Copy-Update) | 读端在中断中,写端在进程上下文,且可接受短暂的数据不一致(最终一致)。 | 读端无锁访问;写端复制数据修改后替换指针,等待旧数据不再被引用后释放。 | 优点:读端零开销; 缺点:写端复杂度高,延迟释放旧数据可能占用内存。 |
1.5.4 中断同步的核心原则
- 禁止睡眠:任何可能导致睡眠的机制(如mutex、信号量)都绝对禁用。
- 操作轻量化:临界区必须极短,避免自旋或中断屏蔽时间过长影响系统响应。
- 避免死锁:使用spinlock时必须配合中断屏蔽(如
spin_lock_irqsave
),防止中断与进程上下文争夺锁。
- 若需在中断与进程间同步简单数据,优先用spinlock + 中断屏蔽(最通用);
- 读多写少场景用seqlock(效率更高);
- 极短临界区且仅同CPU同步,用中断屏蔽(最简单)。
这些机制的核心是在“不睡眠”的约束下,通过硬件级原子操作或执行流控制,确保共享资源的访问安全性。
在中断处理中,原子操作是安全且高效的同步选择,因为它们不会引发睡眠、阻塞或上下文切换,完全符合中断上下文“快速执行、不能被阻塞”的核心要求。适合中断场景的原子操作主要分为整数原子操作和位原子操作,两者均通过硬件指令实现,确保操作的不可分割性。
一、为什么中断中适合用原子操作?
原子操作的核心特性是**“不可中断性”**:整个操作在硬件层面一次性完成,不会被任何中断或调度打断。这使其完美适配中断上下文的约束:
- 无需等待(如自旋锁的“忙等”),执行时间固定且极短(通常是单条CPU指令);
- 不依赖进程上下文(如mutex的睡眠/唤醒机制),可直接在中断处理程序中使用;
- 适合保护简单共享数据(如计数器、标志位),避免复杂锁机制的开销。
二、中断中常用的原子操作类型及场景
中断中可安全使用的原子操作主要分为两类,具体功能和适用场景如下:
- 整数原子操作(针对
atomic_t
类型)
atomic_t
是内核提供的专门用于原子操作的整数类型(通常为32位),通过一系列函数实现原子的加减、赋值、比较等操作,适用于计数器、引用计数等场景(如统计中断发生次数、资源使用计数)。
操作类型 | 函数示例(以Linux内核为例) | 功能描述 | 中断场景应用举例 |
---|---|---|---|
初始化 | atomic_t v = ATOMIC_INIT(0); | 初始化原子变量为0 | 初始化中断触发次数计数器 |
加法 | atomic_add(1, &v); | 原子地将v加1 | 每次中断触发时,计数器+1 |
减法 | atomic_sub(2, &v); | 原子地将v减2 | 中断中释放资源,引用计数-2 |
自增/自减 | atomic_inc(&v); / atomic_dec(&v); | 原子地将v加1/减1 | 递增/递减共享缓冲区的元素数量 |
比较并设置 | atomic_cmpxchg(&v, old, new); | 若v等于old,则设为new,返回原始值 | 中断中安全更新状态标志(如“已处理”标记) |
读取 | atomic_read(&v); | 原子地读取v的值 | 中断处理中获取当前计数器值 |
特点:操作对象是整数,适合数值类共享数据的同步,所有操作均为原子性,不会被中断或其他执行流打断。
. 位原子操作(针对内存中的位)
位原子操作用于对内存中的单个位进行原子操作,适用于标志位、状态位的同步(如标记“设备是否就绪”“是否需要后续处理”)。这类操作直接作用于普通内存地址,无需特殊类型。
操作类型 | 函数示例(以Linux内核为例) | 功能描述 | 中断场景应用举例 |
---|---|---|---|
置位(1) | set_bit(n, &addr); | 原子地将addr的第n位设为1 | 中断中标记“数据已接收”(置位标志) |
清位(0) | clear_bit(n, &addr); | 原子地将addr的第n位设为0 | 中断处理完成后,清除“数据待处理”标志 |
位翻转 | change_bit(n, &addr); | 原子地翻转addr的第n位(0变1,1变0) | 切换设备的中断使能状态 |
测试并置位 | test_and_set_bit(n, &addr); | 原子地测试第n位,若为0则设为1,返回原始值 | 中断中抢占资源(如缓冲区),避免重复处理 |
测试并清位 | test_and_clear_bit(n, &addr); | 原子地测试第n位,若为1则设为0,返回原始值 | 中断处理前检查并清除“中断请求”标志 |
特点:针对单个位操作,开销比整数原子操作更小,适合用标志位进行简单同步的场景(如中断与进程上下文之间的状态通知)。
-
适用范围有限:
原子操作仅适用于简单数据(整数、位) 的同步,无法保护复杂数据结构(如链表、队列)。若需操作复杂结构,需结合自旋锁(配合中断屏蔽)使用。 -
避免过度使用:
虽然原子操作高效,但频繁的原子操作可能导致CPU缓存震荡(多CPU竞争同一原子变量时),影响性能。中断中的原子操作应尽可能精简。 -
类型匹配:
整数原子操作必须使用专用的atomic_t
类型(而非普通int
),否则可能因编译器优化或对齐问题破坏原子性。 -
跨CPU安全:
现代CPU的原子操作指令(如x86的LOCK
前缀)天然支持多CPU环境,因此中断中使用的原子操作在多处理器系统中也是安全的,无需额外处理跨CPU同步。
中断中可安全使用的原子操作主要是整数原子操作(atomic_t
系列)和位原子操作(set_bit
等),它们通过硬件级指令保证操作的不可分割性,满足中断上下文“不睡眠、快速执行”的要求。
其核心优势在于:无需复杂的锁机制,仅通过简单指令即可保护共享数据,特别适合计数器、标志位等简单场景。但对于复杂数据结构的同步,仍需结合自旋锁(并禁用本地中断)实现。选择时需根据共享数据的类型和操作复杂度,在“效率”和“安全性”之间找到平衡。