Linux进程通信---7---中断

Linux 中断是硬件与内核异步通信的核心机制,是操作系统处理硬件事件(如键盘输入、磁盘读写完成)、程序异常(如除 0、缺页)的基础。以下从基础定义、分类、核心组件、处理流程、关键机制等维度详细讲解:

Linux 中断的基础定义

中断是迫使 CPU 暂停当前执行流程,优先处理紧急事件(硬件请求 / 程序异常),处理完成后再恢复原流程的机制。

在 Linux 中,中断是内核 “感知硬件状态、处理异常” 的唯一方式 —— 硬件无法主动向用户态程序发信号,必须通过中断通知内核,再由内核转发(如转为信号给进程)。

Linux 将中断分为硬中断、软中断、异常三类,核心区别是 “触发源” 和 “同步性”:

类型触发源同步性典型场景
硬中断外部硬件(键盘、磁盘)异步(随机)键盘按下、磁盘读写完成
软中断内核主动触发异步网络数据分包、定时器到期
异常程序错误 / 特殊指令同步(必现)除 0 错误、缺页异常、系统调用

1. 硬中断(Hardware Interrupt)

  • 由外部硬件触发,需硬件支持(如中断引脚);
  • 分为 “电平触发”(硬件持续发信号,直到内核处理)和 “边沿触发”(硬件只发一次脉冲信号);
  • 受中断控制器管理(避免硬件直接轰炸 CPU)。

2. 软中断(Software Interrupt)

  • 由内核主动触发(通过指令触发,无硬件参与);
  • 是 Linux “延迟处理耗时任务” 的核心机制(见下文 “上下半机制”);
  • 典型例子:网络软中断(NET_RX_SOFTIRQ)、定时器软中断(TIMER_SOFTIRQ)。

3. 异常(Exception)

  • 由程序执行错误或特殊指令触发,与当前执行的指令同步(执行某条指令必触发);
  • 分为 “故障”(可恢复,如缺页异常)、“陷阱”(主动触发,如系统调用)、“终止”(不可恢复,如段错误)。

Linux 中断的核心组件

中断处理依赖 “硬件 + 软件” 协同,核心组件包括:

1. 硬件组件:中断控制器

负责汇总所有硬件的中断请求,统一向 CPU 发通知(避免 CPU 直接连接所有硬件)。

  • 8259A:早期单 CPU 架构的中断控制器,仅支持 8 个中断源,已被淘汰;
  • APIC(高级可编程中断控制器):多核架构下的控制器,分为:
    • Local APIC:每个 CPU 核心自带,处理本核心的中断;
    • IO APIC:处理外部硬件的中断请求,支持最多 24 个中断源,可将中断分发到任意 CPU 核心。

2. 软件组件:中断向量表(IDT)

内存中存储 “中断号→中断处理程序” 映射关系的表,是 CPU 处理中断的 “索引表”。

  • Linux 的 IDT 包含 256 个 “中断向量”(中断号 0~255);
  • 向量分配规则:
    • 0~31:保留给异常(如 0 = 除 0 错误、14 = 缺页异常);
    • 32~47:保留给硬中断(对应早期 8259A 的 16 个中断);
    • 48~255:用于软中断、系统调用(如 32 位 Linux 的系统调用是中断号 128=0x80)。

3. 软件组件:中断描述符

IDT 中的每个表项是 “中断描述符”,包含:

  • 中断处理程序的地址;
  • 权限级别(如用户态能否触发);
  • 段选择子(对应内核代码段)。

Linux 硬中断的完整处理流程

以 “键盘按下” 为例,硬中断的处理流程是:

  1. 硬件触发中断:键盘完成输入,向 “IO APIC” 发送中断请求,并带上自己的中断号(如键盘对应中断号 1);
  2. 中断控制器转发:IO APIC 将中断请求分发到某 CPU 核心的 Local APIC;
  3. CPU 响应中断
    • CPU 暂停当前执行的指令,关中断(避免新中断打断当前处理);
    • 保护现场:将当前程序的寄存器(PC、SP、通用寄存器等)保存到内核栈(相当于 “存档”);
  4. 查找中断处理程序:CPU 根据中断号,从 IDT 中找到对应的中断描述符,跳转到对应的中断服务程序(ISR);
  5. 执行上半部分(Top Half)
    • 执行 ISR(中断服务程序),完成 “最快必须处理的工作”(如读取键盘硬件的输入数据到内存);
    • 标记 “下半部分任务待处理”;
  6. 触发下半部分(Bottom Half):ISR 执行完后,触发下半部分任务(处理耗时逻辑);
  7. 恢复现场:CPU 从内核栈中恢复之前保存的寄存器数据;
  8. 开中断 + 继续执行:CPU 重新打开中断,继续执行之前暂停的程序。

Linux 中断的 “上下半机制”(核心优化)

硬中断处理时,CPU 会关中断(避免嵌套导致混乱)—— 若中断处理耗时太长,会导致其他硬件的中断被 “饿死”。

因此 Linux 将中断处理拆分为上半部分(Top Half)+ 下半部分(Bottom Half),平衡 “响应速度” 和 “处理耗时”:

1. 上半部分(Top Half):中断服务程序(ISR)

  • request_irq()注册,与中断号绑定;
  • 执行时机:关中断状态下,必须快速完成(通常几微秒);
  • 核心工作:读取硬件数据、标记硬件状态、触发下半部分任务;
  • 示例:键盘中断的上半部分→读取键盘的输入字节到内核缓冲区。

2. 下半部分(Bottom Half):延迟处理耗时任务

执行时机:开中断状态下(不阻塞其他中断),可处理耗时逻辑;Linux 提供 3 种下半部分实现,适用场景不同:

实现方式核心特点适用场景
软中断静态定义(内核初始化时注册)、可并行执行高频、性能敏感的任务(如网络数据分包)
Tasklet基于软中断实现、同一 Tasklet 不能并行执行驱动中处理中断的延迟任务(如设备状态同步)
工作队列运行在进程上下文(可睡眠)处理超耗时任务(如磁盘数据写入)
(1)软中断
  • 定义方式:通过enum softirq_action注册,Linux 默认定义了 10 种软中断(如NET_RX_SOFTIRQTIMER_SOFTIRQ);
  • 触发方式:在 ISR 中调用raise_softirq(软中断号)
  • 执行时机:CPU 从 “中断上下文” 返回 “进程上下文” 前,统一执行待处理的软中断。
(2)Tasklet
  • 本质是 “动态软中断”,避免软中断的 “静态注册” 限制;
  • 定义方式:通过DECLARE_TASKLET()声明,绑定处理函数;
  • 触发方式:在 ISR 中调用tasklet_schedule(&tasklet)
  • 特点:同一 Tasklet 不会在多个 CPU 上并行执行(避免竞争)。
(3)工作队列
  • 唯一运行在进程上下文的下半部分机制;
  • 核心优势:可调用 “可能睡眠的函数”(如kmalloc(GFP_KERNEL)schedule());
  • 定义方式:通过DECLARE_WORK()声明工作项,绑定处理函数;
  • 触发方式:在 ISR 中调用schedule_work(&work)

Linux 中断的管理接口(驱动开发常用)

内核提供了一组函数,用于注册 / 管理中断处理程序:

1. 注册中断:request_irq()

#include <linux/interrupt.h>
irqreturn_t (*handler)(int irq, void *dev_id);

int request_irq(
    unsigned int irq,        // 中断号
    irqreturn_t (*handler)(int, void*),  // 中断服务程序(上半部分)
    unsigned long flags,     // 中断标志(如IRQF_SHARED=共享中断)
    const char *name,        // 中断名称(用于/proc/interrupts查看)
    void *dev_id             // 设备私有数据(共享中断时用于区分设备)
);
  • 返回值:0 = 成功,负数 = 失败;
  • 标志参数示例:
    • IRQF_SHARED:允许多个设备共享同一个中断号;
    • IRQF_TRIGGER_RISING:边沿触发(上升沿);
    • IRQF_TRIGGER_HIGH:电平触发(高电平)。

2. 释放中断:free_irq()

void free_irq(unsigned int irq, void *dev_id);
  • 必须与request_irq()irqdev_id对应;
  • 若中断是共享的,仅当最后一个设备释放时,才会真正卸载中断处理程序。

3. 临时禁用 / 启用中断

  • 禁用当前 CPU 的所有硬中断:local_irq_disable()
  • 启用当前 CPU 的所有硬中断:local_irq_enable()
  • 禁用指定中断号:disable_irq(unsigned int irq)(会等待当前 ISR 执行完成)。

中断上下文 vs 进程上下文

中断处理程序运行在中断上下文中,与 “进程上下文” 有本质区别:

维度中断上下文进程上下文
所属实体无进程(绑定 CPU 核心)某个用户态 / 内核态进程
调度性不可调度(不能睡眠)可被调度(能睡眠)
可用函数仅 “异步安全函数”(如memcpy可调用任意函数(包括睡眠函数)
内存分配仅用GFP_ATOMIC(不睡眠)可用GFP_KERNEL(可睡眠)

关键注意点:中断上下文不能调用schedule()kmalloc(GFP_KERNEL)等可能睡眠的函数,否则会导致系统死锁。

Linux 异常的处理(以缺页异常为例)

异常是 “同步中断”,以最常见的 “缺页异常(中断号 14)” 为例,处理流程是:

  1. 进程访问某虚拟地址,但该地址未映射到物理内存(或内存已被换出到磁盘);
  2. CPU 触发 “缺页异常”,跳转到 IDT 中对应的异常处理程序do_page_fault()
  3. 内核检查虚拟地址的合法性:
    • 若地址非法:向进程发送SIGSEGV信号(段错误),进程崩溃;
    • 若地址合法:
      • 分配物理页框;
      • 若内存已被换出,从磁盘中读入数据到物理页;
      • 更新页表,将虚拟地址映射到物理页;
  4. 恢复进程上下文,进程继续执行(感知不到异常)。

这张图展示的是计算机硬件 + 操作系统的 “中断处理完整流程”,核心是 “外部设备完成工作后,如何通知 CPU 并让系统处理”,下面按步骤拆解:

1. 涉及的核心组件

  • 外部设备:键盘、磁盘、网卡等硬件,完成任务后会主动 “报信”;
  • 中断控制器:汇总所有外部设备的 “报信请求”(因为设备太多,CPU 不能直接接所有设备),统一向 CPU 发通知;
  • CPU:负责暂停当前工作、处理中断、再恢复原工作;
  • 中断向量表(IDT):内存中存 “中断号→对应处理程序” 的映射表(比如中断号 n 对应 “处理键盘输入” 的程序);
  • 操作系统中断服务:内存中存的具体处理逻辑(比如 “读取键盘输入”“处理磁盘数据”)。

2. 完整流程(对应图中步骤)

  1. 外设就绪:外部设备(比如键盘、磁盘)完成了自己的工作(比如你按下键盘输入完成、磁盘读写完成);
  2. 发起中断:设备向 “中断控制器” 发送 “我完成了” 的请求,并带上自己的 “中断号”(每个设备对应唯一中断号,比如键盘是中断号 1);
  3. 通知 CPU:中断控制器把 “有设备请求处理” 的信号发给 CPU;
  4. CPU 获取中断号:CPU 收到通知后,从中断控制器那里拿到具体的 “中断号 n”;
  5. CPU 保护现场:CPU 暂停当前正在执行的程序,把当前程序的寄存器数据(相当于 “工作进度”)保存下来(方便之后恢复);
  6. 执行中断处理:CPU 根据 “中断号 n”,去 “中断向量表(IDT)” 里找到对应的 “中断处理程序地址”,然后执行操作系统里对应的 “中断服务”(比如中断号对应 “处理键盘”,就执行 “读取键盘输入” 的逻辑);
  7. 恢复现场,继续工作:中断处理完成后,CPU 把之前保存的 “工作进度” 恢复回来,继续执行原来暂停的程序。

核心作用

中断是硬件和系统 “异步协作” 的核心:设备不用一直等 CPU 询问 “你完成没”,而是主动 “喊 CPU 处理”,既提升了设备效率,也让 CPU 能同时处理多个任务。

CPU 的 “现场保护”

现场保护的本质是:给 CPU 的执行进度 “存档”,保证中断 / 异常处理不会打断原程序的执行逻辑 —— 没有现场保护,程序遇到任何中断都会 “失忆”,根本无法正常运行。

1. 先明确:什么是 “CPU 现场”?

“现场” 指的是CPU 当前执行程序的 “上下文状态”—— 也就是 CPU 内部寄存器里的所有数据,这些数据是程序执行的 “进度记录”:

  • 程序计数器(PC):记录 “下一条要执行的指令地址”(相当于你写作业时 “记住当前写到哪一行”);
  • 栈指针(SP):记录当前程序栈的位置(相当于你记着 “笔放在哪一页的哪一行”);
  • 通用寄存器(如 eax、ebx):存程序的临时变量、计算中间结果(相当于你草稿纸上的临时计算数据);
  • 状态寄存器:记录 CPU 的运行状态(如是否进位、是否允许中断)。

2. 为什么需要 “现场保护”?

CPU 是 “单任务执行” 的(同一时间只能跑一个程序的指令),当遇到中断 / 异常(比如外设请求、程序出错)时,必须:

  1. 暂停当前正在执行的程序;
  2. 去处理中断 / 异常;
  3. 处理完后,回到暂停点继续执行原程序。

如果不保护现场,处理完中断回来,CPU 会 “忘记” 之前的进度(比如不知道下一条指令在哪、临时变量是什么),原程序就会执行混乱、甚至崩溃。

3. 现场保护的时机 & 过程

时机:CPU 收到中断 / 异常信号,决定暂停当前程序的瞬间(对应之前中断流程里的 “步骤 5”)。

过程:

  • CPU 把自己内部所有关键寄存器的数据,保存到内存中(通常是当前进程的栈 / 内核栈)
  • 保存的内容是 “完整的上下文状态”(PC、SP、通用寄存器、状态寄存器等),相当于给当前程序的执行进度 “打了个存档”。

4. 现场保护的后续:现场恢复

当中断 / 异常处理完成后,CPU 会做 “现场恢复”:把之前保存到内存里的寄存器数据,重新写回 CPU 的寄存器中

此时 CPU 的状态和暂停前完全一致,就能 “无缝衔接” 地回到之前暂停的指令,继续执行原程序。

通俗例子

你正在写作业(CPU 执行程序):

  • 写到第 10 行,草稿纸上记着临时计算结果(这是 “CPU 现场”);
  • 突然有人敲门(中断),你先记住 “写到第 10 行、草稿纸的计算数据”(保护现场)
  • 去开门(处理中断);
  • 回来后,回到第 10 行、拿起草稿纸的计算数据(恢复现场),继续写作业。

时钟中断

时钟中断是由硬件定时器周期性触发的硬中断, 是系统的 “节奏器”,它以固定时间间隔(比如默认 10ms)自动触发,让内核能完成多任务调度、计时等核心工作, 使得整个系统拥有 “时间感知”。

1. 核心本质

  • 触发源:CPU 内部的硬件定时器(如 x86 的 APIC 定时器、早期的 PIT 芯片),按预设的 “节拍间隔”(比如 10ms)向 CPU 发送中断请求;
  • 属于硬中断:是异步、周期性的硬件触发中断。

2. 关键作用(系统离不开它)

时钟中断是 Linux 实现 “多任务、计时、超时” 的基础:

  • 进程调度:内核靠时钟中断判断 “进程时间片是否用完”,到时间就切换进程(实现多任务并发);
  • 系统计时:系统时间(当前时间戳)、进程的运行时长,都是通过时钟中断的 “节拍数累加” 计算的;
  • 超时处理sleep()alarm()等定时器,以及网络请求的超时检测,都靠时钟中断判断 “时间是否到了”;
  • 触发软中断:定期触发定时器软中断,处理延迟任务(如下半部分逻辑)。

3. 周期(节拍率 HZ)

Linux 用 “HZ” 表示时钟中断的 “每秒触发次数”:

  • 比如 HZ=100 → 每 10ms 触发一次;
  • 常见取值:100、250(4ms)、1000(1ms)—— 节拍越密,时间精度越高,但内核开销也越大。

简化流程

硬件定时器到时间 → 发中断请求 → CPU 响应并执行时钟中断处理程序 → 内核更新时间、检查进程调度、处理超时 → 恢复现场继续执行原程序。

不可重入函数(Non-Reentrant)可重入函数(Reentrant)

用最通俗的生活例子理解:

  • 可重入函数:像自动售货机 —— 你投币买水到一半,有人打断你去买零食,回来你继续投币,售货机仍能正确给你水(逻辑独立、不依赖 “半完成” 的状态);
  • 不可重入函数:像手工记账本 —— 你记到一半(写了 “收入 100” 但没写 “元”),有人打断你去记另一笔账,回来你忘了之前写到哪,账本就乱了(依赖全局状态、操作不原子)。

核心定义

函数类型核心定义通俗理解
可重入函数函数执行过程中被异步打断(如信号、中断、多线程调度),再次调用(重入)后,原流程和新流程都能正确执行,结果不受影响多个人同时用、中途被打断再用,都不会乱,逻辑完全 “自给自足”
不可重入函数函数执行过程中被异步打断后重入,会导致数据错乱、逻辑异常、结果错误只能 “一次性干完”,中途被打断就乱套,依赖全局状态 / 共享资源

关键前提:“重入” 的核心是异步打断 + 再次调用—— 比如信号处理函数打断主程序的malloc,又在信号处理函数里调用malloc,就是典型的 “重入不可重入函数”,必然出问题。

本质区别:为什么可重入函数 “不乱”?

可重入与不可重入的核心差异,在于是否依赖 “非私有资源” 和 “非原子操作”,用表格清晰对比:

对比维度可重入函数不可重入函数
依赖全局 / 静态变量❌ 完全不用(仅用参数 / 局部变量)✅ 依赖(比如strtok用静态变量存分割位置)
操作共享资源(文件 / 内存)❌ 仅操作函数内私有资源✅ 操作全局共享资源(比如printf用全局输出缓冲区)
调用其他不可重入函数❌ 只调用可重入函数✅ 调用malloc/printf等不可重入函数
非原子操作❌ 仅用原子操作(一步完成)✅ 有分步操作(比如 “读全局变量→修改→写回”)
内存分配 / 释放❌ 不调用malloc/free(改内存池)✅ 调用malloc/free

不可重入函数的 “坑”:具体怎么乱的?

举个经典例子 ——strtok(字符串分割函数,不可重入):

// 不可重入的根源:strtok用静态变量保存“上次分割的位置”
char *strtok(char *str, const char *delim);

// 主程序执行:
char str[] = "a,b,c,d";
strtok(str, ","); // 第一次调用,静态变量存“b,c,d”的起始地址
// 此时信号触发,信号处理函数里也调用strtok:
strtok("x,y,z", ","); // 静态变量被覆盖为“y,z”的起始地址
// 信号处理完,主程序继续调用strtok:
strtok(NULL, ","); // 本应取“b”,但静态变量被改,实际取到“y”,结果完全错了

而可重入版本strtok_r(r=reentrant)就解决了这个问题 —— 把 “分割位置” 从静态变量改成参数传入(私有资源),就算重入也不会乱:

// 可重入版本:用saveptr(局部变量)保存分割位置,不依赖全局
char *strtok_r(char *str, const char *delim, char **saveptr);

常见的可重入 / 不可重入函数举例

1. 可重入函数(放心用,尤其是信号处理 / 多线程)

这些函数仅依赖参数和局部变量,无全局状态,操作原子:

  • 内存操作:memcpymemsetstrcpystrcmp(仅操作传入的参数);
  • 系统调用:writeread_exitclose(内核级原子操作,不依赖用户态全局资源);
  • 基础运算:abssqrt(仅处理参数,无副作用)。

2. 不可重入函数(绝对不能在信号处理函数里用!)

这些函数依赖全局 / 静态资源,或有分步操作:

  • 标准 IO:printffprintfputs(用全局输出缓冲区,分步写入);
  • 内存管理:mallocfreecalloc(操作全局内存池,分步修改链表);
  • 定时器:sleepalarm(修改全局定时器状态);
  • 字符串处理:strtok(静态变量)、asctime(静态缓冲区);
  • 其他:rand(静态随机数种子)、getenv(全局环境变量表)。

关键应用场景:为什么你必须关心?

这部分和“信号捕获” 强相关 ——信号处理函数必须用可重入函数,否则必出问题!

场景 1:信号处理打断不可重入函数

主程序正在执行malloc(修改全局内存池链表):

主程序:malloc → 拆链表节点(只拆了一半)
↓ 信号触发(比如SIGINT)
信号处理函数:又调用malloc → 继续改同一个内存池链表
↓ 信号处理完,主程序继续
主程序:malloc的链表已经乱了 → 内存泄漏/程序崩溃

场景 2:多线程调用不可重入函数

多线程同时调用printf

线程1:printf("hello") → 写了“he”到全局缓冲区,被调度走
线程2:printf("world") → 覆盖缓冲区为“world”,输出
线程1:继续执行 → 缓冲区剩下的“llo”被输出
最终结果:worldllo(完全错乱)

核心原因:信号处理是 “异步打断”,多线程是 “并发执行”,二者都会触发函数的 “重入”—— 不可重入函数扛不住这种场景,可重入函数则完全没问题。

如何编写可重入函数?(实战规则)

只要遵守以下规则,就能写出安全的可重入函数:

  1. 绝不使用全局 / 静态变量:所有数据都通过参数传入(传值,而非传指针共享),或用函数内局部变量(栈上分配,每个调用独立);
  2. 绝不调用不可重入函数:比如信号处理函数里,不能用printf/malloc/sleep,改用write(可重入)输出、_exit退出;
  3. 绝不操作共享资源:不写全局文件、不修改全局配置,仅操作函数内创建的私有资源;
  4. 只用原子操作:避免 “读 - 改 - 写” 分步操作(比如count++是三步:读 count→+1→写回,非原子),改用原子指令(如__sync_fetch_and_add);
  5. 不依赖函数执行顺序:函数执行结果仅由输入参数决定,不受 “是否被打断” 影响(纯函数思想)。

正面例子(可重入函数):

// 计算两数之和,仅用参数和局部变量,无全局依赖
int add(int a, int b) {
    int temp = a + b; // 局部变量,栈上分配,每个调用独立
    return temp;
}

// 信号处理函数里的可重入输出(用write替代printf)
void sig_handler(int sig) {
    char msg[] = "signal caught\n";
    write(1, msg, sizeof(msg)-1); // write是可重入的系统调用
    _exit(0); // _exit是可重入的退出函数(exit不可重入)
}

反面例子(不可重入函数):

int count = 0; // 全局变量
// 不可重入:依赖全局变量,count++是非原子操作
int increment() {
    count++; // 读count→+1→写回,中途被打断会乱
    return count;
}

核心总结

  1. 核心判断:可重入函数 =“自给自足”(仅用参数 / 局部变量),不可重入函数 =“依赖外部状态”(全局 / 静态 / 共享资源);
  2. 关键风险:不可重入函数被异步打断(信号)/ 并发调用(多线程)会导致数据错乱,可重入函数则安全;
  3. 实战要求:信号处理函数、多线程核心逻辑必须用可重入函数,禁用printf/malloc等不可重入函数;
  4. 编写规则:不碰全局、不调不可重入函数、只用原子操作、结果仅由参数决定。

简单说:可重入函数是 “不怕打断的函数”,不可重入函数是 “一打断就乱的函数”—— 在异步 / 并发场景下,选可重入函数是唯一安全的选择。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值