当前位置: 首页 > news >正文

中断与信号

硬件层面的中断

1. 外设:准备就绪

当按下键盘、网卡收到数据包、磁盘读写完成时,外设硬件会产生一个电信号,通过专用线路发给中断控制器

  • 每个设备都有自己的 “专属线路”,不会和别的设备信号混在一起。
  • 这个信号就是硬件中断请求,意思是:“我这边有数据 / 事件了,CPU 快来处理我!”

2. 中断控制器:“我来当中间调度员”

中断控制器收到一堆设备的中断请求后,会做两件关键的硬件事:

  1. 优先级仲裁:如果多个设备同时发请求,它会按预设优先级(比如时钟中断 > 网卡 > 键盘)选出 “最急的那个”。
  2. 通知 CPU + 传编号
  • 向 CPU 的INTR 引脚发一个电信号,告诉 CPU “有设备找你”。
  • 同时把这个设备对应的 ** 中断号(IRQ 编号)** 放到数据总线上,让 CPU 知道是 “哪个设备” 在喊它。

3. CPU:“先暂停手头工作,存好现场”

CPU 在执行完当前指令后,检测到 INTR 引脚的信号,就会立刻暂停正在跑的程序(不管是用户进程还是内核代码),然后硬件自动完成 “保护现场”

  • 把当前的程序计数器(PC)标志寄存器(EFLAGS)通用寄存器这些关键状态,压入内核栈里存起来。
  • 这一步是纯硬件行为,不需要软件插手,目的是:等会儿处理完中断,能精确回到刚才被打断的地方,继续执行,就像没被打断过一样。
  • 同时 CPU 会暂时关闭可屏蔽中断,防止新的中断干扰这次处理。

4. CPU:“查中断向量表,找到处理函数入口”

CPU 拿到中断号后,会用这个编号当索引,去查内存里的中断向量表(IDT)

  • IDT 是内核启动时就预先建好的硬件数据结构,每个中断号都对应一个表项,里面存着:
    • 这个中断对应的 ** 中断服务程序(ISR)** 的内存地址
    • 权限、段信息等硬件执行需要的参数
  • 比如:中断号 1 → 键盘中断 → 指向 “处理键盘输入” 的 ISR 地址;中断号 14 → 硬盘中断 → 指向 “处理磁盘读写完成” 的 ISR 地址。
  • 这一步是硬件寻址逻辑,CPU 直接跳转到对应的 ISR 代码开始执行。

5. 执行中断服务程序(ISR):“快速搞定硬件操作”

ISR 是内核里写给对应设备的硬件处理代码,在中断上下文里执行,有严格限制:

  • 必须非常快,不能睡眠、不能调用阻塞函数(比如不能等待磁盘 IO),因为 CPU 还 “占着” 中断,别的设备没法发请求。
  • 典型操作:
    1. 读外设寄存器:从设备 I/O 端口或内存映射地址(MMIO)读数据(比如键盘扫描码、网卡数据包头)。
    2. 清中断标志:向设备寄存器写一个值,告诉设备 “我已经收到你的请求了,别再发信号了”。
    3. 延后耗时操作:把复杂处理(比如解析完整网络包、保存文件)交给软中断 / 工作队列,让内核稍后在进程上下文里慢慢处理。

6. 恢复现场:“回到原来的工作流”

ISR 执行完后,CPU 硬件会自动从内核栈里恢复之前保存的寄存器、PC、标志位,然后重新打开可屏蔽中断:

  • 整个状态和中断发生前一模一样,CPU 继续执行刚才被打断的程序。
  • 外设和中断控制器也会重置中断请求状态,等待下一次事件触发。

一句话总结:

外设喊一声 → 控制器排好队 → CPU 存好现场 → 查表找处理函数 → 快速处理硬件 → 恢复原状继续干活

闹钟中断

电脑主板上存在一颗独立的硬件实时时钟与可编程定时器,它依靠主板电池独立供电,即便电脑断网、拔掉电源数周后重新开机,系统仍能显示准确时间,这正是该硬件持续工作的直接证明。

闹钟中断 = 由硬件定时器周期性、自动触发的同步中断。

内核启动时配置好触发周期后,硬件会以固定时间间隔自动向 CPU 发送中断信号,强制打断当前运行的进程,使操作系统或其他进程重新获取 CPU 资源,避免单一进程长期独占 CPU 导致系统卡死或失效,让操作系统始终保有对 CPU 的控制权

两次闹钟中断触发的间隔时间段内,CPU 完全交付给某一进程执行,操作系统内核处于 “休眠” 状态,不主动干预进程的指令流。

所以CPU 的频率是性能的关键体现。频率越高,单位时间内时钟周期就越多,能执行的指令越多、进程切换与任务调度的轮转次数也越频繁,整体处理能力就越强,性能也就越好。

图中的 2.6GHz 是 CPU 常规状态下的运行频率,而操作系统真正的调度周期,是由内核在这个硬件频率的基础上,通过时钟中断重新分配、管理和控制的

闹钟中断也属于硬件中断。

软件中断

除了上面的硬件,中断也能由软件(程序)触发。软件中断不是硬件发的,而是软件(程序)自己主动执行指令,主动触发的中断。

硬件中断是外部事件打断CPU,而软件中断则是进程主动进入CPU请求资源

在Linux系统中,所有的系统调用函数都会被保存在一张数组中:

假设我们执行下面的代码:

ssize_t n = read(fd, buf, count);

首先我们要明白,read函数最终是需要调用Linux系统的sys_read系统函数的。在这之前,C语言标准库会做出调用前的准备

mov ebx, fd ; 放第1个参数 mov ecx, buf ; 放第2个参数 mov edx, count ; 放第3个参数 mov eax, 3 ; 放read的系统调用号(告诉内核要调用sys_read)

当执行到下面的汇编代码时,软件中断会立刻产生,CPU会从用户态转到内核态,并保护现场,跳转到内核的中断处理函数:

int 0x80 ; 老式x86 ; 或 syscall ; 现代x86_64

之后内核从eax里拿到3,去sys_call_table[3]找到sys_read,执行内核逻辑,完成读文件操作。最后内核把结果放到eax,恢复现场,回到用户态,read函数把eax的值返回。

调用系统函数本质是选取对应的数组下标。

从中断来解释操作系统

从上面的结论可以看出,OS是跑在中断上面的OS本质上就是一个死循环如果OS需要每时每刻去询问外设或进程其的轮询状况,那么OS的效率会变得极低

但是,在中断的帮助下,OS可以一直处于工作状态,当其接受到中断信号后,OS会暂停并保存当前的工作进度转而去处理中断带来的工作,当插入的任务完成后,OS会恢复之前的进度,继续执行未完成的工作

总的来说,中断信号是喊OS干活的闹钟

用户态和内核态

操作系统运行时,除了管理用户应用程序,自身也需要运行内核代码、维护内核数据结构。如果允许用户态程序直接访问内核资源、使用内核级权限,系统极易被异常操作破坏甚至崩溃。同时,每个进程拥有独立的地址空间和私有数据,必须严格隔离、互不干扰;与各进程都拥有自己的用户页表相比,内核页表是全局唯一、所有进程共享的核心组件,因此需要通过内核页表用户页表分离,实现权限隔离、地址隔离和系统稳定运行。

所以操作系统需独立运行内核代码与管理内核数据,必须实现用户态与内核态分离

32位下,Linux的虚拟地址空间为4G,从低到高,其中0~3G属于用户态,3~4G属于内核态。用户态和内核态各自拥有一份页表结构:用户页表和内核页表。页表中专门有区分用户态和内核态的变量DPL,如果DPL为00(0),那么就是内核态;如果为11(3),则为用户态。

用户态只管 “算逻辑”,内核态只管 “碰硬件、管资源”,两者靠系统调用切换,一起把程序跑完。

例如:

假如程序要执行scanf函数:

scanf("%d", &a);

1. 一开始:全程 用户态

  • 程序的代码、变量、栈、堆,都在0~3G 用户空间
  • 用的是用户页表
  • 只能碰自己的数据,不能碰键盘、硬盘、网卡

调用scanf,它是用户态库函数,它自己拿不到键盘输入。


2. 必须求助内核:进入 内核态

用户态做不了硬件操作,于是:

  • 库函数准备好系统调用号
  • 执行int 0x80/syscall指令
  • CPU 从用户态 → 内核态
  • 权限从 Ring3 → Ring0
  • 同时可以用:用户页表 + 内核页表

3. 内核态真正干活

内核做三件事:

  1. 用内核页表访问键盘驱动、读取按键数据
  2. 用用户页表把数据写到你程序的变量&a
  3. 做完清理现场

这里是关键:内核必须同时用两张表,才能完成一次读写!

  • 读硬件 → 内核页表
  • 写回用户变量 → 用户页表

4. 回到用户态继续跑

内核把结果给程序:

  • 切回用户态
  • 只能用用户页表
  • 继续执行后面的代码

sigaction函数

#include <signal.h> int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
参数作用
signum要处理的信号(比如SIGINT(2)、SIGTERM(15)、SIGSEGV(11))
act新的信号处理规则(告诉内核 “怎么处理这个信号”)
oldact保存原来的处理规则(可选,传 NULL 表示不保存)

成功就返回0,失败返回-1并设error。

struct sigaction结构体

struct sigaction { void (*sa_handler)(int); // 简单信号处理函数 void (*sa_sigaction)(int, siginfo_t *, void *); // 带详细信息的处理函数 sigset_t sa_mask; // 额外要屏蔽的信号集合 int sa_flags; // 控制行为的标志位 void (*sa_restorer)(void); // 已废弃,无需关注 };

sa_masksigaction函数第二个参数指向的struct sigaction结构体核心字段,核心解决信号处理函数执行时被其他信号打断,导致递归调用、逻辑混乱甚至程序崩溃的问题。

sa_mask专门解决“一个信号的处理函数正在执行时,被另一个信号强行打断,导致逻辑乱掉甚至程序崩溃”的问题:

比如给SIGINT(按 Ctrl+C 触发)注册处理函数时,把SIGQUIT(按 Ctrl+\ 触发)加到sa_mask里;当程序收到SIGINT并开始执行它的处理函数(比如要跑 5 秒的业务逻辑),内核会自动把SIGINT(当前正在处理的信号)和SIGQUITsa_mask里加的信号)都放进进程的信号屏蔽字中。这时候就算一直按 Ctrl+\ 发SIGQUIT,这个信号也会被内核拦住 —— 不会出现 “SIGINT处理到一半,突然暂停去执行SIGQUIT处理函数” 的打断问题。

SIGINT处理函数跑完,内核会自动去掉这两个临时屏蔽的信号,恢复原本的规则,之前被拦住的SIGQUIT这时候才会生效处理,这样就能保证SIGINT的处理逻辑从头到尾完整执行,不会被打断。

#include <stdio.h> #include <signal.h> #include <unistd.h> // SIGINT处理函数 void sigint_handler(int signum) { printf("\n=== 开始处理SIGINT(Ctrl+C),接下来5秒屏蔽SIGQUIT(Ctrl+\\)===\n"); sleep(5); // 核心业务逻辑:5秒内若按Ctrl+\,SIGQUIT会被阻塞 printf("=== SIGINT处理完成,解除SIGQUIT屏蔽,此时按过的Ctrl+\\会立即生效===\n"); } int main() { struct sigaction act; // 1. 配置信号处理规则 act.sa_handler = sigint_handler; // 指定SIGINT的处理函数 sigemptyset(&act.sa_mask); // 清空屏蔽集合 sigaddset(&act.sa_mask, SIGQUIT); // 关键:在sa_mask中添加SIGQUIT act.sa_flags = 0; // 默认行为 // 2. 注册规则:第二个参数act传入包含sa_mask的配置 if (sigaction(SIGINT, &act, NULL) == -1) { perror("sigaction注册失败"); return 1; } printf("程序运行中:\n1. 先按Ctrl+C触发SIGINT\n2. 触发后5秒内按Ctrl+\\测试屏蔽效果\n"); while (1) sleep(1); // 保持程序运行 return 0; }

可重入函数

1. p->next = head; // 步骤1:新节点指向原头节点 4. head = p; // 步骤4:头指针更新为新节点

在插入函数中,步骤1、4的全部完成才能视为成功插入!

  • 主函数(main):插入node1

    • 执行步骤 1(node1->next = head):node1指向了原来的头节点。
    • 此时!CPU 发生时钟中断,切换到内核态
  • 内核态处理中断

    • 内核发现有信号(比如SIGINT),准备切回用户态执行信号处理函数
  • 信号处理函数(sighandler):插入node2

    • 执行步骤 1(node2->next = head):node2->next指向了当前的head(也就是node1)。
    • 执行步骤 4(head = node2):head指针指向node2
    • 此时!信号处理函数执行完毕,返回
  • 回到主函数(main)

    • 继续执行步骤 4(head = node1):Bug 发生!head被重新赋值回node1,导致刚刚插入的node2彻底丢失,只剩下node1指向原来链表,内存泄漏;如果node2还没来得及赋值head就被中断,可能会导致链表成环(死循环)。

可重入函数是指:函数在执行过程中(被中断),允许另一个线程 / 信号处理函数调用它,且执行完毕后,原函数继续执行依然能得到正确结果,不会造成数据错乱。上面的insert函数就不是可重入函数。

可重入函数的标准:

不使用全局 / 静态变量:只使用栈上变量或参数。

不调用不可重入函数:比如 malloc、printf(标准库可能有全局锁)、strtok(内部有静态缓冲区)。

原子操作:指令序列不被中断(即执行过程不能被切分)。

对于是否可重入,我们要将其看成函数的一个特性,而不是一个BUG

volatile变量类型修饰符

#include <stdio.h> #include <signal.h> // 未加volatile:编译器将flag缓存到寄存器,不读内存 int flag = 0; // 信号处理函数:修改内存中的flag为1 void handler(int sig) { flag = 1; // 仅修改内存值,寄存器中的值仍为0 } int main() { signal(SIGINT, handler); // 注册Ctrl+C信号 // 编译器优化:循环内只读寄存器的旧值0,永远不退出 while (flag == 0); printf("循环退出\n"); return 0; }

在上面代码中,如果编译器的优化程度很高,就会导致flag变量被存入寄存器的隐形问题:这时虽然能提高读取效率,但就会导致程序一致读取的是寄存器中的值,导致内存中的变量改变无效,造成死循环。

而加volatile后,编译器禁止寄存器缓存,while每次读取内存中flag的真实值,信号修改后循环立即退出。

#include <stdio.h> #include <signal.h> // 加volatile:强制每次读内存,而非寄存器 volatile int flag = 0; void handler(int sig) { flag = 1; } int main() { signal(SIGINT, handler); while (flag == 0); // 每次读内存中flag的真实值 printf("循环退出\n"); return 0; }
http://www.cnnetsun.cn/news/1364566.html

相关文章:

  • 计算机毕业设计之springboot学生会事务管理平台的设计与实现
  • Web3未落地,Web4已破局:AI+区块链重构互联网下一代图景
  • afc登录后,右上角站内消息图标如何去掉?
  • YOLOv8与伏羲模型联动:基于视频流的实时恶劣天气检测与预报系统
  • AnythingtoRealCharacters2511部署教程:NVIDIA Jetson Orin Nano边缘端轻量部署方案
  • LightOnOCR-2-1B应用案例:多语言文档批量处理,解放双手
  • StructBERT语义匹配系统效果对比:专业术语与日常用语匹配精度
  • Mirage Flow在数学工具开发中的应用:MathType插件
  • 神经符号AI:让机器人“想”得更清楚,“做”得更精准
  • ClearerVoice-Studio语音处理全流程保姆级教学:5分钟搞定降噪分离
  • 智能组合实体员中的树形结构管理与遍历算法
  • 春秋云镜-多CVE实战:从Wuzhicms到SEMCMS的SQL注入漏洞复现与手法解析
  • 考研复试离散数学核心考点与实战解析
  • 职场PUA最隐蔽的6句“专业话术”,听起来很对,实则在摧毁你【职场反PUA30天 Day2】
  • AI头像生成器在计算机视觉中的实际应用
  • Python自动化:3分钟搞定微信收藏链接批量导出到TXT(附完整代码)
  • PyTorch实战:CUDA_VISIBLE_DEVICES环境变量的高效配置与多GPU管理
  • Redis Manager:一站式Redis集群管理平台从部署到运维实践指南
  • OpenStack Train版三节点部署实战:从CentOS 7.6配置到Dashboard访问
  • 避坑指南:ZCU111开发板VADJ_FMC电压修改后重启失效的解决方案
  • H3C R4900 G3 服务器RAID配置与BIOS固件升级实战指南
  • ESXI 7.0保姆级教程:如何正确挂载外接机械硬盘(含常见错误排查)
  • 快速优化IDEA插件下载体验:国内节点加速与hosts配置实战
  • Huber损失函数实战:如何在PyTorch中实现异常值鲁棒的回归模型
  • 视觉问答新挑战:OK-VQA数据集深度解析与常见问题避坑指南
  • 造相-Z-Image惊艳案例:超写实静物摄影风格(金属反光/玻璃通透感/布料褶皱)
  • 如何从初级程序员成长为高级工程师?
  • 通义千问2.5-7B-Instruct问题解决:部署常见错误及解决方法汇总
  • 计算机论文写作避坑指南:从选题到投稿的5个关键步骤
  • 第2节 从零开始:Coze工作流与剪映小助手的草稿创建实战