Re:Linux系统篇(四十六)信号篇·四:一文串联操作系统底层:时钟中断、内核态切换、系统调用与 Linux 信号处理全解析


◆ 博主名称: 小此方-CSDN博客 大家好,欢迎来到小此方的博客。
⭐️Linux系列个人专栏: 【主题曲】Linux
⭐️此方的GitHub: github_此方
⭐️ Re系列专栏:我们思考 (Rethink) · 我们重建 (Rebuild) · 我们记录 (Record)


文章目录


概要&序論

  Hello,大家好我是此方 ,前面我们讲了信号是概念,信号的产生手段和信号的处理。今天我们来讲信号的捕捉和硬件中断时钟中断。内容部分涉及硬件。本文可以说是信号篇中最核心的内容。我们开始吧。

当前阶段:

一、信号捕捉流程图

1.1什么是信号捕捉

  信号处理有三种情况,依据hander表的三种情况。

  • 忽略:进程直接把信号丢弃,当成什么也没发生。
  • 默认:执行操作系统内核为该信号设定的默认核心动作(通常是终止进程、暂停进程或产生 Core Dump 核心转储)。
  • 捕捉:调用用户自定义的信号处理函数。

1.2 信号捕捉的全流程

  如果信号的处理动作是用户自定义函数 ,在信号递达时就调用这个函数,这称为捕捉信号。

  由于信号处理函数的代码是在用户空间的代码段,处理过程比较复杂,举例如下:

  • 用户程序注册了 SIGQUIT 信号的处理函数 sighandler
  • 当前正在执行 main 函数,这时发生中断或异常切换到内核态。
  • 在中断处理完毕后要返回用户态的 main 函数之前检查到有信号 SIGQUIT 递达。
  • 内核决定返回用户态后不是恢复 main 函数的上下文继续执行,而是执行 sighandler 函数,

sighandlermain 函数使用不同的堆栈空间,它们之间不存在调用和被调用的关系,是两个独立的控制流程。

  • sighandler 函数返回后自动执行特殊的系统调用 sigreturn 再次进入内核态。
  • 如果没有新的信号要递达,这次再返回用户态就是恢复 main 函数的上下文继续执行了。

  这个流程图在面试的时候经常考到,如果能够给面试官讲清楚,非常的加分。 简化成形象图就是一个∞:

  细节:如果你去给面试官画这张图,信号检测的这个点一定在这条直线的下面!

  开个小玩笑,它长得像什么?

1.3详细介绍细节

1.3.1为什么信号捕捉函数在执行完毕后要"再次陷入内核然后回到main执行流"而不"直接回到main执行流"?

  你的自定义信号处理函数 sighandler() 执行完了。问题来了:执行完后,程序怎么知道接下来该去哪?它没有 main() 剩下的地址信息。所以必须调用一个特殊的系统调用:sigreturn 。程序再次 进入内核态,去找内核接头。

  内核收到 sys_sigreturn() 的请求,它在内核里把之前保存的 main() 函数的现场(各种寄存器、上下文、打断时的下一条指令地址 )给恢复出来。

  最终,内核把控制权安全交回给 main() 。你的主程序从之前第 1 步被打断的地方,继续往下执行。

1.3.2如何演示默认处理和忽略处理

  如果信号的处理动作是默认或者忽略 , 那么比自定义捕捉要简单得多。

  • 如果是忽略 :直接将对应的信号位由 1 置为 0 ,然后直接返回用户态的 main 主控制流程继续向下执行。
  • 如果是默认:大多数默认动作是直接杀死进程。内核会直接终止进程(或转储并杀死、暂停进程等),甚至不再需要返回用户态。

1.3.3在自定义捕捉函数中触发内核陷入会发生什么

  肯定不会直接简单地"跳回"第二步 ,而是会重新触发一次完整的"用户态 → \rightarrow → 内核态"的切换流程。

1.3.4hander函数如何自动执行sigreturn系统调用

  我们都知道,main函数调用func函数。会为func函数开辟栈帧,同时在栈顶压入返回地址。func函数栈帧开辟,是从高地址向低地址移动固定长度的指针位置func函数调用完毕,指针又往回移动固定长度。此时刚好指向我们在main函数栈顶压入的那个地址,实现成功返回。

  那么可不可以修改这个返回值的地址呢?完全可以。就在sighander函数退出内核态、返回用户态之前的这最后几微秒里,内核动用了它的特权:

内核直接把手伸进了你的用户栈 里,强行在栈顶写入了 sigreturn 的地址。

  然后汇编pop这个返回地址到CPU的pc指针,CPU拿到的就是这个sigreturn系统调用的地址。于是触发系统调用再次陷入内核。

1.3.5死循环进程陷入内核

1.4内核态与用户态

1.4.1内核态与用户态的感性理解

  内核态与用户态的内容非常重要,我想随着学习的深入分两次讲解,这是第一次。

  先说得通俗一点:蓝色形态(用户态),和红色形态(内核态)操作系统正是通过这种形态(模式)的切换,既保护了底层的安全,又给了上层软件运行的自由。

  • 由操作系统执行,执行操作系统代码,访问操作系统数据的时候,CPU被切换为内核态。
  • 执行用户自己写的代码。访问用户进程数据的时候,CPU被切换为用户态。
  • 内核态的权限级别更高,权限级别这块儿暂时讲不了。

  "陷入"或者"坠入"内核,只是一个为了方便程序员理解而发明的拟人化比喻。听起来好像程序掉进了一个深渊或者换了一个地方,但物理现实根本不是那么回事。

1.4.2为什么内核跳转到用户自定义函数必须切换用户态

  既然内核态的权限级别比用户态高,那么在内核态下,CPU一样可以执行用户态的代码 !那么为什么内核跳转到用户自定义函数必须切换用户态?

  因为用户写的方法中如果有非法操作比如删除配置文件 。这些操作在权限更强的内核态下可以被执行,但是用户态不可执行,如果你不切换形态就去执行用户态的代码,别有用心的用户会对你的系统造成严重破坏

1.4.3关于死循环进程

  这段内容和本文内容相关性不大。只是提到了,补充一下,有人可能会问:在代码里写一个死循环while(true){},这个进程还能进入内核吗?

  完全可以,它也是一个可以被调度的进程,当时间片一过,就会被CP强制保护现场,切换下去,此时就深入内核了。进程进入内核是一个非常简单并且高频的事情。

  那么为什么唯独这个进程跑起来你的操作系统明显感觉比较卡?

  • 别的普通进程 : 操作系统虽然给了它们相同的时间片,但它们大多只运行很短一段时间,就因为等待磁盘、网络、用户输入等 I/O 事件,主动放弃剩余时间片,进入睡眠状态
  • 死循环 while(true){} 进程 : 它几乎不会发生 I/O,也不会主动阻塞,因此每次都会把分配给自己的整个时间片全部跑满,直到时钟中断到来,被内核强制剥夺 CPU。
  • 导致的根本原因 :因此,其他进程释放出来的大量 CPU 时间最终都会被这个死循环进程消耗掉,使某个 CPU 核长期接近 100% 占用,系统响应速度明显下降,看起来就会很卡。

二、操作系统是怎么转起来的

  操作系统管理硬件,它是怎么知道硬件上有数据的?于是就有猜测:直接让操作系统轮询扫描所有的硬件文件对应的缓冲区,有数据了,就拷贝到用户层缓冲区。但是这样对操作系统来说太忙了!操作系统还要做很多别的事情。

于是我们引入新的概念------硬件中断。

2.1CPU和硬件是连接在一起的

  回到进程篇的第一篇,当初讲冯诺依曼体系的时候,我刻意回避了这个问题:我们的外部设备要把数据加载到内存中,这样CPU才会读取这些数据。但是不代表外部设备不和CPU关联

  在电路板上,外部设备和CPU连在一起 ,硬件就可以间接的通过CPU的针脚来进行信号沟通(依靠高低电平表示01

2.2 CPU通过中断控制器收发控制信号

2.2.1中断控制器是CPU获取硬件控制信息和下发指令的关键

  准确的说,CPU下发硬件指令是给控制器下发的。

  有一种板子叫做8259,(8259已经过时了,第二代APIC 替代了 8259,并把 8259 的功能全部继承并大幅扩展。

  我们的主板上面有一个或者多个8259的集联,在焊接电路的时候,就将对应的中断信息入口连接设备 。一旦设备就绪,设备就会给中断控制器发一个高电频 ,那么这个中断控制器就会给CPU的特定针脚发动我们对应的控制信息。CPU知道了有设备已经就绪了。于是CPU去访问中断控制器,获取中断控制器中的中断号码(从寄存器中获取)知道哪个设备准备好了。

2.2.2什么是CPU针脚

  针脚长什么样子?

  针脚就是芯片上的引脚。比如CPU:

复制代码
+---------------+                     
|      CPU      |
+---------------+
|||||||||||||||||  引脚(Pin)

  每个Pin都有用途。例如:VCC,GND,RESET,CLK,IRQ,NMI 。其中IRQ就是中断引脚。

  例如以前的8259A自己也有很多Pin。

  中断控制器最后必须通知CPU。CPU只需要知道:"有人找我。"至于是谁找它:8259A告诉CPU。

复制代码
磁盘------>IRQ1------>8259A------>INT------>CPU IRQ Pin

2.2.3简单了解------什么是主板

  主板长什么样?

  主板就是一块电路板。它上面焊着:CPU(插槽),内存,南桥/北桥(老架构),PCH(现代Intel),中断控制器(现代很多已经集成),USB控制器,SATA控制器,网卡,PCIe设备......

  扯远了,我们回来,给出结论:依靠这一套机制,哪一个外部设备好了,CPU就知道。OS再也不关注,外部设备是否准备好,而是它准备好,会叫我。

2.3硬件中断的处理流程

2.3.1设备控制器与设备控制器上的寄存器

  几乎每个硬件设备都有自己的设备控制器,因为每种设备都需要专门的控制逻辑;而这些设备控制器通常不会直接连接CPU,而是把中断请求统一发送给系统中的中断控制器,再由中断控制器通知CPU。

  所以,"控制器"这个词在计算机体系结构里至少有两层含义:

  1. 设备控制器:控制具体设备(键盘控制器、磁盘控制器、USB控制器等)。
  2. 中断控制器:统一管理来自各设备控制器的中断请求并通知CPU。

  寄存器不止是CPU有,设备的控制器上面也有 。CPU将进来的请求(虚拟地址 )转换为物理地址 。发送命令+物理地址+数据 给硬件。硬件控制器比如磁盘控制器

  • 控制寄存器:存命令。
  • 地址寄存器:存地址。
  • 数据寄存器:存数据。

2.3.2从硬件到CPU

  磁盘就绪------>磁盘控制器向中断控制器发送高电平------>中断控制器接收高电平------>中断控制器内部根据针脚,生成并保存中断号n------>中断控制器同时CPU针脚------>CPU获取中断号。

2.3.3中断向量表

  中断向量表是一个函数指针数组,其中被操作系统注册了大量的中断处理函数。你可以简单理解,中断向量表的下标就是中断号。

2.3.4从CPU到操作系统

  CPU使用获取来的中断号去中断向量表中索引,得到中断处理方法并调用。 在执行中断处理方法之前CPU会进行现场保护,把当前执行流寄存器的数据保存起来,处理完中断之后再去读取被保护数据。

  你觉得以上这个过程熟悉吗?

  • 发中断 --- 发信号?
  • 保存中断号 --- 记录信号?
  • 中断号 --- 信号编号?
  • 处理中断 --- 处理信号?自定义捕捉?

  我们前三篇学的信号,本质是用软件来模拟硬件中断的。 两者在思想原理上很像。但是实现上完全不同。

2.4操作系统是被动执行任务的

2.4.1操作系统在中断未到达前在干嘛

  我们用初代Linux的代码看一看。

  操作系统在中断未到达前处于持续的暂停中什么都没做!

2.4.2时钟中断

  时钟中断的部分内容我放在软件条件 那里了【2.2闹钟产生信号Re:Linux系统篇(四十四)信号篇·二:Linux 信号的产生原理:硬件异常、软件条件与内核处理机制

  早期,科学家沿用的硬件中断的整套逻辑,把内核架构中的时钟源和硬件控制器放在了一起,在中断向量表中注册了"进程切换"函数。

  时钟源以固定的频率向CPU发送信号,CPU收到信号自动调用中断向量表中的进程调度函数处理信号。

  后来人们发现,时钟中断可以直接集成在CPU里面,不用走中断控制器,这样效率更高。时钟中断的触发频率就是你计算机的主频。

  有了以上两点的认知,我们很轻易的就得出两个结论

  • 操作系统就在硬件时钟中断的驱动下进行调度的。
  • 操作系统的本质:就是基于中断,进行工作的软件

2.4.3补充内容:为什么时钟中断被集成到CPU内部了

  中断控制器同时接收到多个控制信号,是否要在内部维护什么结构来保存这些信号?既然保存了信号是否应当有一种优先级处理机制来给CPU发送信号?

  是的,中断控制器内部确实要维护"哪些中断正在等待处理"的信息。但它一般不是一个简单的FIFO队列,而是一组位图(Bitmap)+ 优先级仲裁逻辑。

  这个优先级仲裁逻辑不必细讲,不过值得一提的是"时钟中断信号"的优先级往往更高。

于是引发了一个问题:时钟源容易和硬件发生竞争并且往往时钟源获胜。导致硬件访问的效率低下。

  因此人们在发现问题的根源后把时钟源集成到了CPU内部

2.4.4时钟中断与时间片/时间戳

  为了讲解方便,我上面对这一点讲的并不清楚,时钟中断一来就一定会切换进程------这是错误的。

真正流程

  1. 触发 :时钟中断触发 → \rightarrow → 进入内核。
  2. 处理 :更新系统时间 → \rightarrow → 当前进程时间片递减 (时间片--)。
  3. 判断 :检测时间片是否耗尽?
    • 未耗尽:直接返回原进程继续执行。
    • 已耗尽 (时间片 == 0) 或 触发以下其他条件 时,才会真正调用调度器进行上下文切换:
      • 有更高优先级进程变为就绪状态
      • 当前进程主动阻塞
      • 当前进程主动让出 CPU (yield)
      • 当前进程结束

  比如我一个进程有10单位的个时间片。每一个时间片的长度是1纳秒。时钟中断的触发频率是1纳秒一次,那么时钟中断每触发一次就会让时间片--。直到时间片==0,调用shedule 切换进程。

  计算机第一次开机,从网络中读取时间戳x。知道自己的时钟中断的频率y。1秒中内触发的时钟中断的次数:1/y 。于是我们计算出"历史总频T"= x* (1/y)。 于是让我们的计算机在离线的时候也能知道现在几点了。

2.5从源代码看时钟中断

  在main函数中,我们发现有很多的初始化函数,其中就有调度程序初始化。

  设置中断门,注册时钟中断。

  在时钟中断中调用了do_timer函数。

  进入do_timer函数我们发现这样的处理逻辑:时钟中断发生,查看current(task_struct重命名)里面的counter(task_struct中的成员变量,时间片)。如果大于0,时间片没完,时间片--,并退出。否则执行调度函数。

  调度函数内部,switch_to函数切换到任务号为next的任务。

  再看点别的:硬件中断处理函数初始化

  许多的注册函数被注册进中断向量表。

2.6 因为软件原因触发中断处理流程

  有没有可能因为软件因素触发如上的中断处理流程?有可能的。

2.6.1 被动中断------异常

  异常是一种由CPU内部条件触发的同步中断 。与时钟中断、硬件中断等外部设备产生的异步中断不同,异常一定发生在CPU执行某条指令的过程中,并且与当前正在运行的程序相关。

  例如,程序执行除0操作:

c 复制代码
int a = 10 / 0;

  CPU执行对应的除法指令时,会检测到除数为0,此时CPU内部产生除法错误异常 。随后CPU根据异常编号查询异常向量表入口,并跳转到操作系统注册的异常处理程序。

  除了除法错误之外,CPU在执行指令过程中还可能检测到其他异常情况,例如:

  • 缺页异常(Page Fault):程序访问的虚拟地址没有对应的物理页面,CPU中的MMU检测到页表无效后产生异常,操作系统随后负责加载页面或者终止进程。
  • 非法指令异常(Invalid Opcode):CPU无法识别当前执行的指令。
  • 保护异常(General Protection Fault):程序违反CPU权限保护机制,例如用户态访问内核空间。
  • 断点异常(Breakpoint Exception):调试程序主动设置断点导致CPU进入异常处理流程。

  需要注意的是,异常虽然通常由程序行为引起,但是异常的产生者是CPU硬件,而不是软件本身。例如:

c 复制代码
int *p = NULL;
*p = 100;

  程序访问空指针后,并不是程序主动发送一个"错误中断",而是CPU执行内存访问指令时,由MMU发现该地址不可访问,于是产生缺页异常,操作系统收到异常后进一步处理,最终可能向进程发送 SIGSEGV 信号。

2.6.4 主动中断------陷阱

  触发指令 :用户态程序通过硬件指令主动陷入内核。x86(32位)环境下使用 int 中断号 指令,通过指定中断号触发;x86_64(64位)环境下则直接使用专为内核跳转优化的 syscall 指令。

  0x80 中断号:在 32位 Linux 中,0x80(十进制 128)是内核专为系统调用注册的软件中断号。执行 int 0x80 即可触发系统调用流程。

我为什么要讲int和syscall?为理解系统调用的执行原理做准备。

2.7系统调用如何执行

  补充内容:系统调用函数的指针被放在一张函数指针数组中存储。

  "在内核初始化(如 sched_init)期间,内核通过 set_system_gate 宏,将软中断号 0x80 与系统调用总入口函数 system_call 进行绑定 ,将其注册为系统调用的中断处理程序。当用户态发起 int 0x80 指令时,CPU 就会通过中断向量表跳转到 system_call 执行具体的系统调用逻辑。"

  讲句人话就是!就是在中断向量表中注册了一个新的函数。然后触发该中断的中断号会去调用这个函数。

  system_call是用汇编写的。看!用表地址+4个寄存器值大小,调用系统调用。 你现在看不懂没关系,我下面画了一张图,看完这张图再回来你就明白了。

  #define SYS_ify(syscall_name) _NR##syscall_name :是一个宏定义,用于将系统调用的名称转换为对应的系统调用号。比如:SYS_ify(open) 会被展开为 __NR_open

  这里$SYS_ify (vfork) 是将vfork这个系统调用转化成系统调用号,然后再movl放入寄存器。 然后直接调用系统调用。

  而系统调用号,不是 glibc 提供的,是内核提供的,内核提供系统调用入口函数 man 2 syscall ,或者直接提供汇编级别软中断命令 int or syscall ,并提供对应的头文件或者开发入口,让上层语言的设计者使用系统调用号,完成系统调用过程。

  glibc通过include这个头文件获取宏并转化的:
linux-2.6.18\linux-2.6.18\include\asm-x86_64\unistd.h

  最后,我们把以上的理解拼起来放在一张图中。

  系统调用也是C语言的glibc库里面的 。操作系统只提供系统调用号宏,于是这个世界上所有的语言都和C语言有关,C生万物,有其一定道理。


好的本期内容就到这里,如果对你有帮助,还不要忘记点赞三联支持。我是此方,我们下期再见。bye! Linux、C++、算法持续连载中,欢迎关注WeChat Official Account 【此方的技术栈】。

相关推荐
无人生还别怕2 小时前
如何制作一个linux标准iso镜像
linux·iso·opencloudos·openanolisos
Yana.nice10 小时前
Linux 只保留 30 天内日志(find命令删除日志文件)
linux·运维·chrome
DFT计算杂谈14 小时前
无 Root 权限在 Tesla K80 零门槛部署 DeepSeek 大模型
linux·服务器·网络·数据库·机器学习
Zhang~Ling15 小时前
从 fopen 到 struct file:从零开始拆解 Linux 文件 I/O
linux·运维·服务器
DeeplyMind15 小时前
Linux 深入 per-VMA lock:Linux 缺页路径如何摆脱 mmap_lock
linux·per-vma lock
爱写代码的森15 小时前
蒙三方库 | harmony-utils之FileUtil文件重命名与属性查询详解
linux·运维·服务器·华为·harmonyos·鸿蒙·huawei
XMAIPC_Robot16 小时前
软硬协同实时控制|RK3588业务调度+FPGA硬件时序,ethercat实现半导体设备微秒级响应(125us)
linux·arm开发·人工智能·fpga开发
重生的黑客17 小时前
Linux 进程优先级、切换与调度:从孤儿进程到 O(1) 调度模型
linux·运维·服务器·进程优先级·nice
骑上单车去旅行18 小时前
MD5校验对比脚本
linux·服务器·windows