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时会自动传入信号编号 当进程收到信号比如按CtrlC触发SIGINT内核会暂停进程当前的执行比如暂停sleep和cout调用我们定义的handler函数并且把当前触发的信号编号作为参数传给handler的signo。接着运行试一下当我们通过键盘按下 CtrlC 的时候会发现程序并没有停止执行而是显示了我们handler函数里面的执行语句这就是因为我们传入的是自定义函数这个函数内部并没有设计接收到这个信号就终止程序的逻辑因此会出现上面的情况。而我们之前按下 CtrlC 之所以会终止程序就是通过键盘向进程发送了 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搞出来的除零错误对应SIGFPE8号信号”这个判断的逻辑其实是操作系统启动时会给 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 工具加载文件并查看调用栈来定位崩溃问题。这种情况和我们平时使用 Ctrlc 去终止进程的结果不同CtrlC 是用户主动、正常终止进程系统认为是安全退出所以不会引发存储信息的操作。系统默认通常关闭核心转储可通过 ulimit -a 查看状态执行 ulimit -c 就可以在当前终端临时开启核心转储同时还需保证对应目录拥有写入权限、磁盘空间充足。操作系统之所以默认关闭核心转储是因为core 文件体积大、占用磁盘空间与 IO 资源日常运行中频繁生成会造成磁盘冗余、读写卡顿且普通用户大多不需要崩溃现场文件来调试问题另外 core 文件还可能泄露进程内存里的账号、密钥等敏感数据存在安全风险。4. 函数产生信号4.1 kill函数除了硬件异常产生信号我们还可以通过函数去传递信号要介绍的第一个函数就是 kill 函数kill函数是 Linux/Unix 系统中用于向指定进程或进程组发送信号的系统调用函数作用是让一个进程主动给另一个进程发送信号。它的两个参数分别是1. pid目标进程 / 进程组 IDpid_t是进程 ID 类型2. sig要发送的信号编号如SIGKILL、SIGTERM它和我们平时使用的命令行命令的kill不一样。我们用一段代码来验证一下它的作用4.2 raise 函数#include signal.h int raise(int sig);刚刚的kill函数的主要作用是可以接收PID去达到给不同进程发送信号的效果但是raise函数就是只给自己发送信号。其中的参数 sig 就是信号我们就不过多阐述。4.3 abort 函数#include stdlib.h void abort(void);大家可以看到abort函数甚至没有参数它的作用是强制终止当前程序并生成核心转储core dump并且它无法被阻塞、无法被忽略一定会让进程终止。5. 软件条件产生信号5.1 alarm 函数#include unistd.h unsigned int alarm(unsigned int seconds);alarm()是 Linux/Unix 下专门用来设置定时器信号的函数核心作用让内核在 N 秒之后给当前进程发送一个 SIGALRM 信号。它是异步定时最基础、最常用的函数几乎所有 Linux 定时基础功能都用它。大家要注意的是SIGALRM 信号的默认动作是终止进程并且 alarm 是单次触发不是循环定时器。它的参数 second 表示定时秒数单位秒如果传入的是 0 代表取消之前的定时器。alarm(5); alarm(0); // 取消不会再发送 SIGALRM它的返回值默认返回上一次 alarm 剩余的秒数。如果之前没有定时器返回0 。alarm(5); int left alarm(2); // left 5上一次还剩5秒 // 新定时器变成2秒alarm 函数有一个特性一个进程同一时间只能有一个 alarm。 如果你再次调用 alarm新的时间会覆盖旧的。alarm(5); // 5秒后发信号 alarm(3); // 覆盖成3秒之前的5秒作废因为多个进程可能会调用不同的 alarm 函数因此操作系统会将其进行管理同一存放在进程的PCB中由 itimerval 这个结构体存储struct task_struct { // 大量进程信息... struct itimerval real_timer; //这就是存放 alarm 的地方 // ...... };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的结构体里面包含了未决信号位图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 次 SIGUSR1pending 表中只会记录一次处理时也只会执行一次 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 个函数目的是设置、修改、检查位图里的每一位每一位对应一个信号。它们的头文件都是#include signal.h第一个函数sigemptysetint sigemptyset(sigset_t *set);作用是把信号集里所有位都设为 0表示这个集合里不包含任何信号。它可以用来初始化一个空的信号集。sigset_t s; sigemptyset(s); // s 00000000...第二个函数sigfillsetint sigfillset(sigset_t *set);作用是把信号集里所有位都设为 1表示这个集合里包含所有信号。它可以用来一次性阻塞全部信号很少用。sigset_t s; sigfillset(s); // s 11111111...第三个函数sigaddsetint sigaddset(sigset_t *set, int signum);它的作用是把指定信号对应的位设为 1把这个信号加入集合。它可以用来阻塞某个信号、或对某个信号做批量操作。第四个函数sigdelsetint sigdelset(sigset_t *set, int signum);它能把指定信号对应的位设为 0 把这个信号从集合中移除。它可以用来解除对某个信号的阻塞。sigdelset(s, SIGALRM); // 从集合删除 SIGALRM第五个函数sigismemberint sigismember(const sigset_t *set, int signum);它一般用来查看指定信号的位是 1 还是 0。它的返回值有三个1表示在集合中位 10表示不在集合中位 0-1出错。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 的控制总线排针发送给 CPUCPU 收到信号后会立即暂停当前正在执行的用户进程将程序计数器、通用寄存器等运行上下文保存到内核栈随后查询中断向量表跳转到内核预先注册的时钟中断服务程序执行。正是时钟中断的周期性触发让操作系统获得了 主动执行 的能力它会在每次中断中检查当前进程的时间片是否用完若用完则调度器会暂停当前进程、切换到下一个就绪进程从而实现抢占式多任务调度同时它还负责维护系统全局时间、管理所有用户态和内核态的软件定时器、统计 CPU 和进程的资源使用情况。如果没有时钟中断操作系统就只能被动等待外部事件触发无法主动指挥和调度任何进程一个陷入死循环的进程就会永远占用 CPU导致整个系统彻底卡死。为了能让大家更好的理解我们可以把整个计算机系统比作一个只有一张桌子的大型自习室各个用户进程就是自习室里的学生他们都需要用这张唯一的桌子CPU来学习执行代码操作系统内核就是自习室的管理员。如果没有任何管理机制一个学生可能会一直霸占着桌子其他学生根本没法学习甚至如果这个学生睡着了进程陷入死循环整个自习室就会彻底瘫痪。而时钟中断就是自习室里的一个自动打铃器 它被设置成每隔 10 分钟对应 1 毫秒就会自动响一次铃完全不需要任何人去按。每次铃响的时候管理员操作系统就会立刻走过来先让当前正在用桌子的学生暂停学习把他的书本、笔记都整理好放在一边对应 CPU 保存进程上下文到内核栈检查这个学生已经用了多久的桌子如果时间到了时间片用完就叫他先回到座位上然后叫下一个排队的学生过来用桌子对应进程调度顺便看一下墙上的钟表更新一下当前时间对应维护系统时间再检查一下有没有学生预约了某个时间要做什么事对应处理到期的软件定时器比如 alarm、sleep最后记录一下每个学生用了多久的桌子对应统计 CPU 资源使用情况等管理员做完这些事就让新的学生或者原来的学生如果时间片还没用完继续用桌子学习。这样一来所有学生都能轮流使用桌子不会出现一个人霸占的情况整个自习室就能有序运行。很多人会把时间中断和时间片的概念给搞混这两个概念本质上是触发信号 和 被触发的规则 的关系。一个时间片里通常会包含多个时钟中断。比如时间片是 10ms时钟中断是 1ms 一次那么一个时间片里会触发 10 次时钟中断前 9 次都只是检查第 10 次才会真正触发进程切换。所以时钟中断是检查时间片是否用完的唯一时机。7.3.3 软中断软中断是操作系统内核为高效处理硬件中断而引入的延迟处理机制它将硬件中断的工作拆分为两部分紧急的部分在硬中断上下文执行不紧急的部分则延迟到软中断上下文执行以此提升系统的响应速度和并发处理能力。硬件中断发生时CPU 必须立即响应并进入硬中断上下文此时不能被其他中断打断也不能调用可能睡眠的函数如果把所有处理逻辑都放在硬中断里会导致中断响应时间过长甚至丢失后续中断请求而软中断的出现正是为了解决这一问题。硬中断上下文只做最紧急、耗时最短的操作比如把网卡收到的数据包放到内存缓冲区随后立即开中断让 CPU 能继续响应其他硬件中断而剩下的、耗时较长的操作比如解析数据包、协议栈处理、交给用户态进程等则被放到延迟执行的软中断里在中断返回前或内核线程中处理。软中断并非硬件产生而是内核在软件层面实现的中断机制其优先级低于硬中断、高于普通进程处理时机主要有两个一是硬中断处理完毕、准备返回用户态之前二是内核专门创建的ksoftirqd内核线程调度时。以 Linux 内核为例预定义了多种软中断类型包括高优先级的HI_SOFTIRQ、定时器相关的TIMER_SOFTIRQ、网络数据包收发相关的NET_TX_SOFTIRQ和NET_RX_SOFTIRQ以及块设备相关的BLOCK_SOFTIRQ等。通俗来说硬件中断就像外卖小哥敲门你必须立刻开门把外卖拿进来这个动作必须快不然小哥会等不及后面的外卖也送不进来而软中断就像你把外卖拿进来后不用立刻吃可以先放一边等不忙的时候再慢慢处理这样就能快速给小哥开门让他继续送下一个外卖。从核心区别来看硬中断由硬件设备触发响应优先级最高执行于不可抢占的硬中断上下文且绝对不能睡眠而软中断由内核软件触发优先级次高可被硬中断抢占同样不能睡眠二者配合实现了中断处理的高效与灵活。本文到此结束感谢各位读者的阅读如果有讲解的不到位或者错误的地方欢迎各位读者进行批评或指正。