☰
Linux硬件中断与进程调度:时钟中断和信号处理结束
2026/10/8 13:16:18 网站建设 项目流程

前言

Linux 不可能让一个进程永远占着 CPU,也不能让正在运行的进程完全感知不到外部设备。

因此,操作系统需要一种机制:

外部事件发生 ↓ CPU 暂停当前执行流 ↓ 进入内核处理 ↓ 必要时唤醒进程或切换进程

这就是中断。

I. 什么是中断

0x00 中断的基本概念

中断可以理解为:

外部事件通知 CPU,当前有更重要的事情需要处理。

比如:

  • 键盘输入;
  • 网卡收到数据;
  • 磁盘完成读写;
  • 定时器到期;
  • 设备发出硬件请求。

如果没有中断,CPU 就只能不断轮询设备:

设备完成了吗? 设备完成了吗? 设备完成了吗?

使用中断之后:

CPU 正常执行进程 ↓ 设备产生中断 ↓ CPU 响应中断 ↓ 执行中断处理代码 ↓ 返回原来的执行流

0x01 中断和异常的区别

中断:通常来自 CPU 外部的硬件或定时器 异常:通常由当前正在执行的指令引发

例如:

键盘按键 -> 外部中断 时钟到期 -> 时钟中断 除 0 -> 异常 访问非法地址 -> 异常 系统调用 -> 软件触发的内核入口

II. CPU 响应中断的过程

一个简化后的中断处理流程如下:

1. 当前进程正在运行 2. 硬件向 CPU 发送中断请求 3. CPU 保存必要的执行现场 4. CPU 根据中断号查找处理入口 5. 进入内核态 6. 执行中断处理程序 7. 判断是否需要调度 8. 恢复现场或切换到其他进程

0x00 中断号

不同的中断通常有不同的编号。

中断号 ↓ 中断描述表 ↓ 对应的处理函数

这和信号编号有点相似,但二者不是一回事:

信号:面向进程的异步通知 中断:面向 CPU 和内核的硬件事件入口

0x01 中断处理函数

中断处理函数通常运行在内核态,需要尽快完成工作。

常见工作包括:

  • 读取设备状态;
  • 从硬件寄存器取数据;
  • 清除中断状态;
  • 唤醒等待队列中的进程;
  • 设置后续软中断或工作队列。

III. 时钟中断

0x00 为什么需要时钟中断

如果没有时钟中断,操作系统很难可靠地实现:

  • 进程时间片;
  • 定时任务;
  • 睡眠唤醒;
  • 超时检测;
  • 周期性调度。

所以系统会周期性地产生时钟中断:

时钟中断 ↓ 内核获得执行机会 ↓ 更新时间信息 ↓ 检查当前进程时间片 ↓ 必要时进行调度

0x01 时间片

多个进程需要共享 CPU。

进程 A 使用一段时间 ↓ 进程 B 使用一段时间 ↓ 进程 C 使用一段时间 ↓ 回到进程 A

每个进程允许连续运行的时间,就是时间片的一种抽象。

时钟中断到来时,内核会减少当前进程的时间片计数。如果计数耗尽,就需要重新选择进程。

IV. 用 alarm 模拟时钟中断

用户程序不能直接编写真正的内核时钟中断,但是可以使用 alarm 模拟周期性事件。

0x00 alarm 的基本使用

#include<iostream>#include<signal.h>#include<unistd.h>voidhandler(intsig){std::cout<<"捕捉到信号: "<<sig<<std::endl;alarm(1);}intmain(){signal(SIGALRM,handler);alarm(1);while(true)pause();return0;}

程序流程:

alarm(1) ↓ 1 秒后产生 SIGALRM ↓ 进入 handler ↓ 再次 alarm(1) ↓ 重复执行

这和真正的内核时钟中断不是同一个层次,但可以帮助理解周期性事件驱动处理的思想。

0x01 alarm 的返回值

取消旧闹钟:

unsignedintleft=alarm(0);

返回值表示原闹钟还剩多少秒。

alarm(200);sleep(1);intn=alarm(0);std::cout<<"剩余时间: "<<n<<std::endl;

这里 alarm(0) 的作用不是等待,而是取消当前闹钟。

V. 用信号模拟简单调度器

代码中使用了一个简化的 task_struct:

classtask_struct{private:intpid;intcounter;public:task_struct(intp):pid(p),counter(5){}voiddesc(){counter--;}voidResetCounter(){counter=5;}intPid(){returnpid;}boolExpired(){returncounter<=0;}voidrun(){std::cout<<"process "<<pid<<" running"<<std::endl;}};

这里的 counter 可以理解为剩余时间片。

0x00 do_timer

voiddo_timer(intsigno){tasks[current].desc();if(tasks[current].Expired()){std::cout<<tasks[current].Pid()<<" 过期了,重新选择进行调度"<<std::endl;current=rand()%tasks.size();tasks[current].ResetCounter();}else{tasks[current].run();}alarm(1);}

执行过程:

收到 SIGALRM ↓ 当前任务 counter-- ↓ counter 是否耗尽 ↓ 没有耗尽:继续运行 耗尽:重新选择任务 ↓ 重新设置 alarm

0x01 main

intmain(){alarm(1);signal(SIGALRM,do_timer);srand(time(nullptr));tasks.emplace_back(1);tasks.emplace_back(2);tasks.emplace_back(3);tasks.emplace_back(4);tasks.emplace_back(5);for(;;)pause();}

这个程序的关键点是:

main 不主动轮询 main 使用 pause() 等待事件 SIGALRM 到来后执行 do_timer do_timer 决定是否切换任务

VI. 调度发生在什么时候

0x00 进程主动让出 CPU

进程可能因为以下原因主动离开运行状态:

  • 调用 sleep;
  • 调用 pause;
  • 读取管道时没有数据;
  • 等待锁;
  • 等待子进程;
  • 等待网络数据。

这些操作通常会让当前进程进入等待状态。

0x01 时间片耗尽

即使进程不主动等待,时钟中断也会让内核定期检查它的运行时间。当时间片用完之后,内核可以选择其他可运行进程。

0x02 更高优先级任务唤醒

如果某个等待中的进程被设备或信号唤醒,内核可能需要重新比较可运行任务的优先级。

调度会综合考虑:

运行状态 优先级 时间片 等待原因 是否被唤醒

VII. 信号处理和调度的关系

0x00 信号处理不是独立线程

信号 handler 通常运行在接收信号的进程上下文中,不是操作系统额外创建的一个普通线程。

进程 A 正在运行 main ↓ 信号到来 ↓ 内核保存 main 的现场 ↓ 同一个进程转去执行 handler ↓ handler 返回 ↓ 恢复 main

0x01 handler 执行期间的信号

handler 执行期间,当前信号通常会被暂时屏蔽,另外还可以通过 sigaction 的 sa_mask 指定额外屏蔽的信号。

新的信号产生 ↓ 进入 pending ↓ handler 返回后再处理

VIII. 从中断到信号

硬件事件和进程信号之间可以通过内核连接起来:

硬件设备产生中断 ↓ CPU 进入内核态 ↓ 中断处理函数读取设备状态 ↓ 内核更新某个进程状态 ↓ 唤醒进程或产生信号 ↓ 进程返回用户态时检查信号 ↓ 执行 handler

例如终端输入 Ctrl + C 时,可以抽象为:

键盘产生硬件事件 ↓ 终端驱动处理输入 ↓ 终端识别控制字符 ↓ 向前台进程发送 SIGINT ↓ SIGINT 进入目标进程 pending ↓ 目标进程执行默认动作或 handler

IX. 信号处理的结束阶段

0x00 handler 返回

handler 执行完之后,内核需要恢复原来的执行现场:

handler 执行完成 ↓ 恢复寄存器 恢复程序计数器 恢复栈指针 恢复原来的信号屏蔽字 ↓ 回到 main 原来的位置

0x01 还有 pending 信号怎么办

如果返回用户态之前,pending 中还存在可递达信号,内核可能继续处理这些信号。

处理一个信号 ↓ 重新检查 pending ↓ 还有可处理信号? ├── 是:继续处理 └── 否:恢复用户态执行

0x02 默认动作和进程终止

信号到来 ↓ 加入 pending ↓ 内核检查到信号可递达 ↓ 查到默认动作是 Terminate ↓ 释放进程资源 ↓ 通知父进程 ↓ 父进程收到 SIGCHLD

如果信号导致异常终止,父进程还可以通过 waitpid 得到退出信号编号。

X. 退出信息和 SIGCHLD

intstatus=0;pid_t ret=waitpid(child,&status,0);if(ret>0){std::cout<<"exit signal: "<<(status&0x7F)<<std::endl;}

子进程执行除 0 的链路:

子进程执行异常指令 ↓ 产生 SIGFPE ↓ 默认动作终止子进程 ↓ 父进程收到 SIGCHLD ↓ 父进程 waitpid 回收

XI. 用户态、内核态、中断和信号的完整链路

用户程序运行在用户态 ↓ 系统调用、异常或硬件中断发生 ↓ CPU 进入内核态 ↓ 内核处理中断或异常 ↓ 更新进程状态、pending 或等待队列 ↓ 必要时触发调度 ↓ 选择当前应该运行的进程 ↓ 返回用户态 ↓ 检查目标进程的信号 ↓ 执行 handler 或默认动作 ↓ 恢复现场继续运行,或者结束进程

这条链路就是“信号结束”的完整含义:

信号不是停在 pending 表里 而是经过内核判断后完成递达 最后回到用户态或终止进程

XII. 总结

中断

硬件或定时器通知 CPU

异常

当前指令执行时出现特殊情况

时钟中断

周期性进入内核 更新时间 检查时间片 触发调度

信号递达

事件产生 ↓ 进入内核 ↓ 保存到 pending ↓ 检查 block ↓ 执行 handler 或默认动作 ↓ 恢复现场

最终可以这样理解:

用户态负责运行普通程序,内核态负责管理系统资源,中断负责把外部事件带进内核,信号负责把事件结果通知给进程,调度器负责决定接下来谁运行。

至此,消息队列、信号量、信号、用户态/内核态、中断和调度就串成了一条完整的 Linux 学习路线。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询