Linux:信号

1.生活中的信号

你在网上买了很多件商品,再等待不同商品快递的到来。但即便快递没有到来,你也知道快递来临时,你该怎么处理快递。也就是你能 "识别快递"

当快递员到了你楼下,你也收到快递到来的通知,但是你正在打游戏,需 5min 之后才能去取快递。那么在在这 5min 之内,你并没有下去去取快递,但是你是知道有快递到来了。也就是取快递的行为并不是一定要立即执行,可以理解成 "在合适的时候去取"。

在收到通知,再到你拿到快递期间,是有一个时间窗口的,在这段时间,你并没有拿到快递,但是你知道有一个快递已经来了。本质上是你 "记住了有一个快递要去取"

当你时间合适,顺利拿到快递之后,就要开始处理快递了。而处理快递一般方式有三种:1. 执行默认动作(幸福的打开快递,使用商品)2. 执行自定义动作(快递是零食,你要送给你你的女朋友)3. 忽略快递(快递拿上来之后,扔掉床头,继续开一把游戏)

快递到来的整个过程,对你来讲是异步的,你不能准确断定快递员什么时候给你打电话。

或者说,你想要一定时间之后去做一件事情,但是怕自己忘记了,于是给自己定了一个闹钟,当闹钟响起时,你就知道自己要去做事情了。

而闹钟铃声、快递员的电话、红绿灯.......这些都是信号

因此我们可以得到一个结论,信号在产生之后,我们一定是知道该如何处理,并且处理方法在信号产生之前,就已经准备好了。

不过在处理信号之前,一定要对该信号有一定认识,能明白这个信号的含义。上述的信号都是我们在日常生活中潜移默化养成的概念,但对于进程来说,识别信号是程序员编写的进程的内置特性,能够使进程自动识别特定信号。

并且如果当信号产生了,但是当前我们还有更高优先级的事情,就会先将信号的处理放在一边,但不会忘记,只是过一会儿再来处理,这个过程分为:信号到来、信号保留、信号处理 ,三个过程,对于信号处理有三种处理方式:默认处理、自定义处理、忽略信号 ,这三个方式到后面统称为信号捕捉

2. 基本概念

Linux 信号是操作系统内核提供的异步进程通信手段,用于向进程递送各类事件通知,是系统层面轻量级的事件通知机制。既可由内核触发,也能由其他进程、用户操作发起,用来告知进程外部发生了特定事件。

它的本质是以整型数字作为唯一标识的简短通知指令,仅传递事件类型编号,不附带额外业务数据。内核会将信号标记在进程 PCB 中,进程调度时检测信号状态,进而中断原有执行逻辑,响应对应事件,实现无需预先约定时机的事件交互。

2.1 查看信号

我们之前在讲进程等待的时候就提到过信号的概念,并且指出了查看信号的命令。现在再来详细的说一说,其中每个信号都有对应的号码,称为信号码,在代码中是以 #define SIGHUP 1 这样的形式呈现的。其中 1 ~ 31 号是普通信号,34 ~ 64 号是实时信号。我们后续对信号的讲解主要集中在普通信号当中,因为实时信号几乎用不到,所以我们不做讲解。

2.2 signal 函数

我们想要对信号有更深的了解,就必须先明白 signal 函数的使用:

signal 是 Linux 下专门用来处理信号 的核心函数,作用就是:告诉进程 "收到某个信号时,要做什么事"。其中 sighandler_t 是一个函数指针,这里的作用是为了回调函数。

其中第一个参数 signum 是 int 类型,用来指定要处理的信号编号,既可以直接传信号对应的数字(如 2 代表SIGINT),也可以传系统定义的宏(如SIGINT、SIGTERM),但 SIGKILL 和 SIGSTOP 这两个信号无法被捕获或忽略 ;第二个参数 handler 是函数指针类型,用来指定收到信号后的处理方式,支持三种传值:1. 传 SIG_DFL 表示恢复信号的系统默认处理动作; 2. 传 SIG_IGN 表示忽略该信号; 3. 传自定义函数名(函数格式需为void func(int sig))表示收到信号后执行自定义的处理逻辑。

我们用这段代码来演示一下:

其中signal函数的两行代码,是告诉操作系统:当这个进程收到 SIGINT 或 3 号信号时,不要执行默认行为,而是去执行 handler 函数。

不过这里有个问题,大家看到代码里 signal(SIGINT, handler) 没有给 handler 传参数,但 handler 定义里有 int signo,那到时候是谁给 handler 函数传参呢?实际上操作系统内核会自动传参数 ,逻辑是这样的:因为 handler 是一个 "回调函数" : 你只是把 handler 的地址告诉了 signal 函数,并没有直接调用它。它不是由你的代码主动调用的,而是当信号发生时,由操作系统内核帮你调用 的。内核调用 handler 时,会自动传入信号编号 : 当进程收到信号(比如按 Ctrl+C 触发 SIGINT),内核会:暂停进程当前的执行(比如暂停 sleepcout),调用我们定义的 handler 函数,并且把当前触发的信号编号 作为参数传给 handlersigno。

接着运行试一下:

当我们通过键盘按下 Ctrl+C 的时候,会发现程序并没有停止执行,而是显示了我们handler函数里面的执行语句,这就是因为我们传入的是自定义函数,这个函数内部并没有设计接收到这个信号就终止程序的逻辑,因此会出现上面的情况。而我们之前按下 Ctrl+C 之所以会终止程序,就是通过键盘向进程发送了 2 号信号,同样的这里也是向 handler 函数中传入 2 号信号。

不过我们前面说了,SIGKILL和SIGSTOP是不可被自定义的,所以输入 kill -9 就会直接终止进程:

3. 硬件异常产生信号

所谓的硬件异常,并不是指硬盘坏了、CPU 烧了这种物理损坏,而是指 CPU 在执行指令时,遇到了违反硬件规则的情况导致程序无法正常执行, 比如非法访问内存、除零错误、未定义指令等,此时就会触发一个硬件级别的异常,然后操作系统内核会把这个硬件异常,转换成一个信号发送给导致错误的进程。

3.1 除 0 错误 和 段错误

最典型的硬件异常就是 除 0 错误 和 段错误,其中段错误指的是进程访问了不属于它的内存地址 ,也就是野指针问题

首先我们来看看除 0 错误:

我们会发现程序执行过后等待一秒会马上终止,并且会弹出一个错误信息:

而这对应的就是 8 号信号 Signal Floating-Point Exception,是浮点异常信号。

下面是段错误,对应野指针问题:

这对应的就是 11 号信号 Segmentation Violation 段错误。

我们除了可以通过弹出的错误信息,来判断对应的是哪个信号。还可以通过自定义处理方式来查看,是否确实是我们所说的这两个信号:

我们以除 0 问题举例,在这里我调用 signal 函数,即如果该进程收到了 8 号信号,就去执行 handler 的代码,果不其然结果正如我们所料。

3.2 三个问题

那大家要思考第一个问题,为什么代码在执行过程当中遇到了问题,就会直接发送信号?操作系统又是怎么识别到这些信号的呢?是怎么知道哪个问题对应的哪个信号呢?

首先大家要知道,CPU 有专门的硬件模块,在执行指令时会实时检查是否违规,比如:

  • 内存访问模块(MMU):检查进程是否访问了不属于自己的内存 → 违规就抛出「页错误 / 段错误异常」
  • 算术运算单元(ALU):检查除零、溢出、非法浮点操作 → 违规就抛出「算术异常」
  • 指令解码器:检查是否执行了非法 / 未定义指令 → 违规就抛出「非法指令异常」

以除 0 问题举例:当 CPU 执行除法指令,发现除数为 0,此时硬件算术单元直接报错 ,所以CPU 会立刻停止当前指令,触发一个硬件异常(Hardware Exception),因为操作系统内核预先注册了这个异常的处理函数,会捕获到这个异常。捕获到异常之后,内核会进行判断:"这是进程 xxx 搞出来的除零错误,对应 SIGFPE(8号信号)"

这个判断的逻辑,其实是操作系统启动时,会给 CPU 注册一张异常向量表(Interrupt Vector Table),里面写死了当 CPU 抛出第 N 号硬件异常时,跳转到内核的哪个处理函数。

接着,内核把这个信号标记到进程的 PCB里,等待进程调度时递送给它。当进程被调度执行时,内核会先检查它有没有未处理的信号,有的话就打断进程,执行对应的信号处理逻辑。

所以本质上,信号就是内核把硬件 / 系统错误,翻译成进程能理解的 "事件通知",让进程知道自己出了问题,要么退出,要么自己处理。


接着是第二个问题:大家要注意的是在handler函数里面我还调用了sleep函数,因为如果我不调用的话,就会持续刷屏的打印 cout 的语句。这是为什么呢?

这是因为,当我们使用 signal 函数捕获 SIGFPE 并进入 handler 时,CPU 的错误状态并没有被清除 !此时信号处理函数handler里的打印语句执行完后,内核以为你已经处理了这个信号问题,所以就会让进程**重新执行刚才出错的那条指令,**但是错误根本没修复,指令一执行,又立刻触发硬件异常,所以陷入循环。


接着是第三个问题,我们在命令行终端查看到,当进程报错的时候,会出现一个 core dumped 的提示,这是什么东西?

core dumped(核心转储) 是类 Unix/Linux 系统里,程序收到致命信号 崩溃时,操作系统把进程当时的完整运行状态保存到磁盘的过程,生成的文件叫 core 文件,相当于程序崩溃瞬间的 "内存快照"。 系统将进程崩溃瞬间的虚拟内存、CPU 寄存器、函数调用栈、错误信息等运行状态保存为 ELF 格式的 core 文件的机制,该文件如同程序崩溃现场的内存快照,可借助 gdb 工具加载文件并查看调用栈来定位崩溃问题。

这种情况和我们平时使用 Ctrl+c 去终止进程的结果不同,Ctrl+C 是用户主动、正常终止进程,系统认为是安全退出,所以不会引发存储信息的操作。

系统默认通常关闭核心转储,可通过 ulimit -a 查看状态,执行 ulimit -c 就可以在当前终端临时开启核心转储,同时还需保证对应目录拥有写入权限、磁盘空间充足。

操作系统之所以默认关闭核心转储,是因为 core 文件体积大、占用磁盘空间与 IO 资源,日常运行中频繁生成会造成磁盘冗余、读写卡顿,且普通用户大多不需要崩溃现场文件来调试问题;另外 core 文件还可能泄露进程内存里的账号、密钥等敏感数据,存在安全风险。

4. 函数产生信号

4.1 kill函数

除了硬件异常产生信号,我们还可以通过函数去传递信号,要介绍的第一个函数就是 kill 函数:

kill函数是 Linux/Unix 系统中用于向指定进程或进程组发送信号的系统调用函数,作用是让一个进程主动给另一个进程发送信号。它的两个参数分别是:

1. pid :目标进程 / 进程组 ID(pid_t 是进程 ID 类型)

2. sig :要发送的信号编号 (如 SIGKILLSIGTERM

它和我们平时使用的命令行命令的kill不一样。我们用一段代码来验证一下它的作用:

4.2 raise 函数

cpp 复制代码
#include <signal.h>

int raise(int sig);

刚刚的kill函数的主要作用,是可以接收PID去达到给不同进程发送信号的效果,但是raise函数就是只给自己发送信号。其中的参数 sig 就是信号,我们就不过多阐述。

4.3 abort 函数

cpp 复制代码
#include <stdlib.h>

void abort(void);

大家可以看到abort函数甚至没有参数,它的作用是强制终止当前程序,并生成核心转储(core dump),并且它无法被阻塞、无法被忽略,一定会让进程终止。

5. 软件条件产生信号

5.1 alarm 函数

cpp 复制代码
#include <unistd.h>

unsigned int alarm(unsigned int seconds);

alarm() 是 Linux/Unix 下专门用来设置定时器信号 的函数,核心作用:让内核在 N 秒之后,给当前进程发送一个 SIGALRM 信号 。它是异步定时 最基础、最常用的函数,几乎所有 Linux 定时基础功能都用它。大家要注意的是 SIGALRM 信号的默认动作是终止进程, 并且 alarm 是单次触发,不是循环定时器。

它的参数 second 表示定时秒数(单位:秒),如果传入的是 0 ,代表取消之前的定时器。

cpp 复制代码
alarm(5);
alarm(0);  // 取消,不会再发送 SIGALRM

它的返回值默认返回上一次 alarm 剩余的秒数 。如果之前没有定时器,返回 0 。

cpp 复制代码
alarm(5);
int left = alarm(2); 
// left = 5(上一次还剩5秒)
// 新定时器变成2秒

alarm 函数有一个特性:一个进程同一时间只能有一个 alarm 。 如果你再次调用 alarm,新的时间会覆盖旧的

cpp 复制代码
alarm(5);  // 5秒后发信号
alarm(3);  // 覆盖成3秒,之前的5秒作废

因为多个进程可能会调用不同的 alarm 函数,因此操作系统会将其进行管理,同一存放在进程的PCB中,由 itimerval 这个结构体存储:

cpp 复制代码
struct task_struct 
{
    // 大量进程信息...
    struct itimerval real_timer;  //这就是存放 alarm 的地方!
    // ......
};
cpp 复制代码
struct itimerval 
{
    struct timeval it_interval;  // 循环定时(alarm 不用)
    struct timeval it_value;     // 剩余时间(alarm 核心!)
};

6. 信号保存

6.1 概念铺垫

在正式介绍信号保存之前,我们先要向大家引入几个关于信号的常见概念:

  1. 实际执行信号的处理动作称为信号递达 (Delivery)

  2. 信号从产生到递达之间的状态,称为信号未决 (Pending)。

  3. 进程可以选择阻塞 (Block) 某个信号。

  4. 被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递送的动作.

注意,阻塞和忽略是不同的,只要信号被阻塞就不会递送,而忽略是在递送之后可选的一种处理动作。

6.2 在内核中的表示

每个信号都有两个标志位分别表示阻塞(block)和未决(pending),还有一个函数指针表示处理动作。

这张图中表示在PCB中关于信号有三个表,首先是 block 表,又称信号屏蔽字,用来标记: 哪些信号当前被 "屏蔽 / 阻塞" 了 。 它采用位图形式记录进程当前需要阻塞的信号,某位置 1 表示对应信号被阻塞,置 0 则表示不阻塞。block 表属于进程级别的数据结构,系统中每个进程都拥有独立的 block 表,彼此互不干扰。无论是常规的标准信号,还是支持排队的实时信号,都可以通过 block 表进行阻塞控制。

要注意的是 SIGKILL 与 SIGSTOP 这两个信号无法被阻塞,用户态可通过 sigprocmask 等函数修改 block 表内容。

另一个是 pending 表,它是 Linux 内核为每个进程维护的未决信号集合,它记录了「已经产生、但还没被进程处理的信号」。

每个进程的 task_struct 里,都有一个叫 pending 的结构体,里面包含了未决信号位图

cpp 复制代码
struct task_struct {
    // ...
    struct sigpending pending;  // 未决信号表
    // ...
};

struct sigpending {
    struct list_head list;      // 信号链表(记录信号来源信息)
    sigset_t signal;            // 未决信号位图(核心)
};

其中 sigset_t 就是位图 ,本质是一个大整数(通常是 64 位或更多位),每一位对应一个信号:位为 1:表示该信号处于「未决状态」(已产生但未处理),位为 0:表示该信号无未决状态。

pending表还具有一些特性:标准信号的 pending 表不支持排队,同一种信号多次产生时,pending 表只会标记一次(对应位仍为 1),比如连续发送 3 次 SIGUSR1,pending 表中只会记录一次,处理时也只会执行一次 handler;而只有实时信号(RT 信号)支持排队,内核会保留多个信号实例。

信号递达时,pending 位会被自动清除,当信号被递达(处理)后,内核会把 pending 表中对应的位清为 0,除非信号被阻塞,否则不会一直留在 pending 表中。pending 表对用户态是只读的,进程可以用 sigpending () 系统调用查看 pending 表的状态,但不能直接修改 pending 表,只能通过解除阻塞、处理信号来改变它的状态。

因此,信号从产生到被处理的过程是这样的:当信号产生后,内核会先访问目标进程的 block 表判断该信号是否被屏蔽,若信号被阻塞,则会一直停留在 pending 表中处于未决状态,暂时不会被处理;若未被阻塞,该信号同样会在 pending 表完成标记。等到进程从内核态切换回用户态的时机,内核会主动扫描 pending 表,发现未决信号后先清空对应标记,再按照预设方式执行信号处理逻辑,处理完成后,进程便回到此前被中断的代码位置继续正常运行。

6.3 sigset_t

从上面的讲解来看,每个信号只有一个bit的未决标志,非0即1,不记录该信号产生了多少次,阻塞标志也是这样表示的。因此,未决和阻塞标志可以用相同的数据类型 sigset_t 来存储。

sigset_t称为信号集 ,本质是一块专门用来存放信号位图 的内存结构。 你可以把它理解成: 专门用来表示 "一堆信号" 的容器 ,里面用每一位代表一个信号

这个类型可以表示每个信号的"有效"或"无效"状态,在阻塞信号集中"有效"和"无效"的含义是该信号是否被阻塞,而在未决信号集中"有效"和"无效"的含义是该信号是否处于未决状态。下一节将详细介绍信号集的各种操作。阻塞信号集也叫做当前进程的信号屏蔽字(Signal Mask),这里的"屏蔽"应该理解为阻塞而不是忽略。

其中对于 sigset_t 类型的数据还有五个操作函数,这 5 个函数目的是设置、修改、检查位图里的每一位(每一位对应一个信号)。

它们的头文件都是:

cpp 复制代码
#include <signal.h>

第一个函数:sigemptyset

cpp 复制代码
int sigemptyset(sigset_t *set);

作用是把信号集里所有位都设为 0 ,表示这个集合里不包含任何信号。它可以用来初始化一个空的信号集。

cpp 复制代码
sigset_t s;
sigemptyset(&s);   // s = 00000000...

第二个函数:sigfillset:

cpp 复制代码
int sigfillset(sigset_t *set);

作用是把信号集里所有位都设为 1 ,表示这个集合里包含所有信号。它可以用来一次性阻塞全部信号(很少用)。

cpp 复制代码
sigset_t s;
sigfillset(&s);    // s = 11111111...

第三个函数:sigaddset:

cpp 复制代码
int sigaddset(sigset_t *set, int signum);

它的作用是把指定信号对应的位设为 1 ,把这个信号加入集合。它可以用来阻塞某个信号、或对某个信号做批量操作。

第四个函数:sigdelset:

cpp 复制代码
int sigdelset(sigset_t *set, int signum);

它能把指定信号对应的位设为 0 ,把这个信号从集合中移除。它可以用来解除对某个信号的阻塞。

cpp 复制代码
sigdelset(&s, SIGALRM);   // 从集合删除 SIGALRM

第五个函数:sigismember:

cpp 复制代码
int sigismember(const sigset_t *set, int signum);

它一般用来查看指定信号的位是 1 还是 0 。它的返回值有三个:1 :表示在集合中(位 = 1);0 :表示不在集合中(位 = 0);-1:出错。

cpp 复制代码
if (sigismember(&s, SIGINT) == 1) 
{
    // SIGINT 在集合里
}

7. 信号捕捉

7.1 内核态和用户态

在介绍信号捕捉之前,我觉得有必要向大家介绍一下用户态和内核态,标准的对于用户态和内核态的定义是这样的:用户态是进程以受限权限运行的状态,仅能执行普通指令、访问受限资源,无法直接操作硬件或执行特权指令,是应用程序默认的运行模式;内核态则是操作系统内核运行的特权状态,拥有访问所有硬件资源、执行特权指令、管理系统资源的全部权限,进程需通过系统调用、中断或异常等方式切换到内核态,才能完成如文件读写、内存分配等需要内核权限的操作。

但我认为,对于这两个的概念可以这样去理解:用户态:进程执行代码,访问数据,都在访问 0,3GB 地址空间的时候,就是访问用户自己的代码、自己的数据;内核态:都在访问 3GB, 4GB 地址空间的时候,就是访问 OS 的过程

7.2 信号捕捉的流程

关于信号捕捉我想用这张图来带大家深刻理解:

这张图表明了,进程在用户态执行主控制流程时,会因中断、异常或系统调用第一次进入内核态;内核处理完相关操作、准备返回用户态前,会调用do_signal()检测进程的未决信号,若存在可递达的自定义信号,便会第二次切换回用户态执行信号处理函数;信号处理函数执行完毕后,会通过sigreturn()系统调用第三次进入内核态,内核完成上下文恢复后,再第四次切换回用户态,让进程从主流程被中断的位置继续执行,这一过程对应了信号自定义捕捉时的四次用户态与内核态状态切换。

用更加详细的图解释就是这样的:

7.3. 穿插--操作系统是怎么运行的

7.3.1 硬件中断

这张图是 CPU 底部特写图,展示的是 LGA 封装的金色触点排针,每一根排针都是独立的电气引脚,通过与主板 CPU 插座上的弹性针脚紧密接触形成完整电气通路,其中专门的控制总线类排针承担着外设与 CPU 之间中断信号的物理传输职责,与下方中断流程图共同构成了硬件中断从物理信号到软件处理的完整链路。

当外部设备完成任务或需要 CPU 介入时(如键盘输入、网卡收包),其内部的硬件电路会产生一个符合电气规范的电脉冲信号,这个电脉冲通过主板上的铜箔走线(传输线),以接近光速的速度传输到中断控制器,以发起中断请求。

中断控制器作为外设与 CPU 之间的调度枢纽,内部有 24 个独立的中断输入引脚,每个引脚对应一个硬件寄存器,所以它可以为每个请求分配全局唯一的中断号,并通过对应的 CPU 排针将中断通知发送给 CPU。

CPU 收到中断信号后,会立即暂停当前正在执行的用户程序,先将程序计数器、通用寄存器等运行上下文保存到内核栈中完成现场保护,随后根据获取到的中断号查询中断向量表 ------ 这是操作系统启动时预先在内核内存中初始化的一张映射表,它以中断号为索引,每个表项存储着对应中断服务程序的内存入口地址,CPU 通过该表可快速定位到内核中的中断处理代码并执行;待中断服务程序处理完毕,CPU 会从内核栈中恢复之前保存的程序上下文,回到被中断的位置继续执行原程序。

7.3.2 时钟中断

大家首先要思考两个问题:进程可以在操作系统的指挥下,被调度,被执行,那么操作系统自己被谁指挥,被谁推动执行呢?外部设备可以触发硬件中断,但是这个是需要用户或者设备自己触发,有没有自己可以定期触发的设备?

这两个问题的答案指向同一个核心 ------时钟中断 。它既是那个能完全自主、周期性触发的特殊硬件设备,是整个计算机系统最基础的 "心跳"。

时钟中断是由计算机内部的可编程定时器(如 CPU 集成的 APIC 定时器、主板的 HPET 高精度事件定时器)产生的硬件中断。它不需要任何外部用户或设备触发,操作系统启动时会将其配置为每隔固定时间间隔(通常为 1 毫秒)自动产生一个符合电气规范的电脉冲信号。

这个电脉冲会通过中断控制器分配固定的全局中断号,再通过 CPU 的控制总线排针发送给 CPU;CPU 收到信号后会立即暂停当前正在执行的用户进程,将程序计数器、通用寄存器等运行上下文保存到内核栈,随后查询中断向量表跳转到内核预先注册的时钟中断服务程序执行。

正是时钟中断的周期性触发,让操作系统获得了 "主动执行" 的能力:它会在每次中断中检查当前进程的时间片是否用完,若用完则调度器会暂停当前进程、切换到下一个就绪进程,从而实现抢占式多任务调度;同时它还负责维护系统全局时间、管理所有用户态和内核态的软件定时器、统计 CPU 和进程的资源使用情况。

如果没有时钟中断,操作系统就只能被动等待外部事件触发,无法主动指挥和调度任何进程,一个陷入死循环的进程就会永远占用 CPU,导致整个系统彻底卡死。

为了能让大家更好的理解,我们可以把整个计算机系统比作一个只有一张桌子的大型自习室

各个用户进程就是自习室里的学生,他们都需要用这张唯一的桌子(CPU)来学习(执行代码),操作系统内核就是自习室的管理员。

如果没有任何管理机制,一个学生可能会一直霸占着桌子,其他学生根本没法学习;甚至如果这个学生睡着了(进程陷入死循环),整个自习室就会彻底瘫痪。

时钟中断就是自习室里的一个自动打铃器: 它被设置成每隔 10 分钟(对应 1 毫秒)就会自动响一次铃,完全不需要任何人去按。每次铃响的时候,管理员(操作系统)就会立刻走过来:

  1. 先让当前正在用桌子的学生暂停学习,把他的书本、笔记都整理好放在一边(对应 CPU 保存进程上下文到内核栈)
  2. 检查这个学生已经用了多久的桌子,如果时间到了(时间片用完),就叫他先回到座位上,然后叫下一个排队的学生过来用桌子(对应进程调度)
  3. 顺便看一下墙上的钟表,更新一下当前时间(对应维护系统时间)
  4. 再检查一下有没有学生预约了某个时间要做什么事(对应处理到期的软件定时器,比如 alarm、sleep)
  5. 最后记录一下每个学生用了多久的桌子(对应统计 CPU 资源使用情况)

等管理员做完这些事,就让新的学生(或者原来的学生,如果时间片还没用完)继续用桌子学习。这样一来,所有学生都能轮流使用桌子,不会出现一个人霸占的情况,整个自习室就能有序运行。

很多人会把时间中断和时间片的概念给搞混,这两个概念本质上是 "触发信号" 和 "被触发的规则" 的关系。一个时间片里,通常会包含多个时钟中断 。比如时间片是 10ms,时钟中断是 1ms 一次,那么一个时间片里会触发 10 次时钟中断,前 9 次都只是检查,第 10 次才会真正触发进程切换。所以时钟中断是检查时间片是否用完的唯一时机

7.3.3 软中断

软中断是操作系统内核为高效处理硬件中断而引入的延迟处理机制,它将硬件中断的工作拆分为两部分:紧急的部分在硬中断上下文执行,不紧急的部分则延迟到软中断上下文执行,以此提升系统的响应速度和并发处理能力。

硬件中断发生时,CPU 必须立即响应并进入硬中断上下文,此时不能被其他中断打断,也不能调用可能睡眠的函数,如果把所有处理逻辑都放在硬中断里,会导致中断响应时间过长,甚至丢失后续中断请求,而软中断的出现正是为了解决这一问题。

硬中断上下文只做最紧急、耗时最短的操作,比如把网卡收到的数据包放到内存缓冲区,随后立即开中断,让 CPU 能继续响应其他硬件中断;而剩下的、耗时较长的操作,比如解析数据包、协议栈处理、交给用户态进程等,则被放到延迟执行的软中断里,在中断返回前或内核线程中处理。

软中断并非硬件产生,而是内核在软件层面实现的中断机制,其优先级低于硬中断、高于普通进程,处理时机主要有两个:一是硬中断处理完毕、准备返回用户态之前,二是内核专门创建的ksoftirqd内核线程调度时。

以 Linux 内核为例,预定义了多种软中断类型,包括高优先级的HI_SOFTIRQ、定时器相关的TIMER_SOFTIRQ、网络数据包收发相关的NET_TX_SOFTIRQNET_RX_SOFTIRQ,以及块设备相关的BLOCK_SOFTIRQ等。

通俗来说,硬件中断就像外卖小哥敲门,你必须立刻开门把外卖拿进来,这个动作必须快,不然小哥会等不及,后面的外卖也送不进来;而软中断就像你把外卖拿进来后,不用立刻吃,可以先放一边,等不忙的时候再慢慢处理,这样就能快速给小哥开门,让他继续送下一个外卖。

从核心区别来看,硬中断由硬件设备触发,响应优先级最高,执行于不可抢占的硬中断上下文,且绝对不能睡眠;而软中断由内核软件触发,优先级次高,可被硬中断抢占,同样不能睡眠,二者配合实现了中断处理的高效与灵活。

本文到此结束,感谢各位读者的阅读,如果有讲解的不到位或者错误的地方,欢迎各位读者进行批评或指正。

相关推荐
tongluowan0071 小时前
负载均衡之硬件与软件层面的异同
运维·nginx·负载均衡·f5
小此方1 小时前
Re:Linux系统篇(二十五)进程篇·十:深度硬核!Linux 进程等待,从 task_struct 源码到位图状态解构
linux·运维·驱动开发
隔窗听雨眠1 小时前
CentOS Stream 9 服务器 Docker 部署 KaiwuDB 实战
服务器·docker·centos
会Tk矩阵群控的小木1 小时前
企业级iMessage群发系统实战:单主机管控多iPhone设备完整实现
运维·ios·开源软件·个人开发
z202305081 小时前
RDMA之DCQCN (14)
linux·服务器·网络·人工智能·ai
zh路西法1 小时前
【ROS2相机标定】基于棋盘格的单目标定法
linux·c++
m0_737302581 小时前
读懂OpenClaw:开源自主AI智能体的革新与价值
服务器
用户2367829801682 小时前
Linux killall 命令详解:按进程名批量终止进程的原理与实践
linux
无限进步_2 小时前
【Linux】进度条:行缓冲区、\r 与 fflush 的实战
linux·服务器·开发语言·数据结构·后端