信号产生
对信号的概念进行一定的理解后,就可以从时间维度上讲解信号产生的话题。
信号产生方式
键盘产生
- Ctrl+C (SIGINT):已验证,不再重复。
- Ctrl+\(SIGQUIT):发送终止信号并生成 core dump 文件,用于事后调试。
- Ctrl+Z(SIGTSTP):发送停止信号,将当前前台进程挂起到后台。
至于键盘是如何产生信号的话题需要到后面进行揭晓。
kill 命令产生
我们之前演示过用 Kill 命令的 9 号信号可以杀死进程。这里让每一个信号都有自己的捕捉方式或者忽视该信号,验证每一个信号是否都会调用自定义函数。
#include <iostream>
#include <csignal>
#include <unistd.h>
void handler(int signo) {
std::cout << "我这个进程:" << getpid() << ",抓到了一个信号:" << signo << std::endl;
}
int main() {
for(int i = 1; i <= 31; i++) {
signal(i, handler);
}
while(true) {
std::cout << "我是一个进程:" << getpid() << std::endl;
sleep(1);
}
return 0;
}
验证后发现,有几个信号比较特别,9 号和 19 号信号不可被捕捉,不可被忽略。这是为了防止有人设置病毒,并且将每个信号都设置成自定义捕捉方法,导致无法杀掉该进程。
函数产生信号
kill 系统调用
kill 命令实际上底层一定调用了 kill 函数。kill 函数可以给一个指定的进程发送指定的信号。
int kill(pid_t pid, int sig);
- pid : 目标进程
- sig : 什么信号
自定义实现 kill 命令:
#include <iostream>
#include <cstdlib>
#include <sys/types.h>
#include <signal.h>
#include <cstring>
int main(int argc, char *argv[]) {
if (argc != 3) {
std::cout << "Usage:" << argv[0] << "signumber pid" << std::endl;
return 1;
}
// 会把命令参数切到命令行参数表里,是一个一个的字符串,因此要 stoi 转成数字
int signumber = std::stoi(argv[1]);
pid_t target = std::stoi(argv[2]);
int n = kill(target, signumber);
if (n < 0) {
perror("kill");
return 2;
}
return 0;
}
raise
raise 函数可以给当前进程发送指定的信号(自己给自己发信号)。
int raise(int sig);
#include <iostream>
#include <csignal>
#include <unistd.h>
void handler(int signal) {
std::cout << "收到了一个信号:" << signal << std::endl;
}
int main() {
signal(2, handler);
while(true) {
sleep(1);
raise(2);
}
return 0;
}
abort
abort 函数使当前进程接收到指定信号而异常终止。
void abort(void);
那么 abort 是给当前进程发送几号信号呢?给自己发送 6 号信号。
软件条件
SIGPIPE 是一种由软件条件产生的信号,在'管道'中已经介绍过了。坏掉的管道,就是软件不满足 --> 也是 OS 给目标进程发送信号!
本节主要介绍 alarm 函数和 SIGALRM 信号。
unsigned int alarm(unsigned int seconds); 调用 alarm 函数可以设定一个闹钟,也就是告诉内核在 seconds 秒之后给当前进程发 SIGALRM 信号,该信号的默认处理动作是终止当前进程。这个信号就是 14 号信号 (默认动作为 Term)。 函数的返回值是 0 或者是以前设定的闹钟时间还余下的秒数。闹钟响一次,默认终止进程。
alarm 调用,一次只运行一个进程,设置一个闹钟,以最新的为准第二次设置闹钟的新时间,会取消上一次的闹钟,返回上一次闹钟的剩余时间!闹钟为 0 则为取消闹钟。
验证 IO 效率问题
如下图所示可见 printf 函数这类 IO 操作会降低效率。
理解软件条件: 在操作系统中,信号的软件条件指的是由软件内部状态或特定软件操作触发的信号产生机制。这些条件包括但不限于定时器超时(如 alarm 函数设定的时间到达)、软件异常(如向已关闭的管道写数据产生的 SIGPIPE 信号)等。当这些软件条件满足时,操作系统会向相关进程发送相应的信号,以通知进程进行相应的处理。简而言之,软件条件是因操作系统内部或外部软件操作而触发的信号产生。
理解闹钟
系统闹钟,其实本质是 OS 必须自身具有定时功能,并能让用户设置这种定时功能,才可能实现闹钟这样的技术。 如果每一个进程都有一个闹钟,有上百个进程就有上百个闹钟。那么这些闹钟是否需要被 OS 管理?如何管理?提出一种对闹钟进行管理的方案 --> 先描述!再组织!
内核中定时器数据结构:定时器超时时间 expires 和处理方法 function。
struct timer_list {
struct list_head entry;
unsigned long expires;
void (*function)(unsigned long);
unsigned long data;
struct tvec_t_base_s *base;
};
操作系统管理定时器,采用的是时间轮的做法,但是我们为了简单理解,可以把它在组织成为'堆结构'。
模拟 OS 行为
通过 alarm 设定闹钟,使得每 1s 都会向该进程发送 14 号信号,14 号信号已经设置为自定义捕捉,去执行自定义函数,理解为硬件中断。在这个自定义函数中,可以做自己想做的事情,如进程调度、进程切换等等。就如同 OS 每固定一段时间都会去检测时间片是否到了,是否需要进行进程切换等等...
#include <iostream>
#include <functional>
#include <vector>
#include <csignal>
#include <unistd.h>
using func_t = std::function<void()>;
std::vector<func_t> cb;
void FlushDisk() {
std::cout << "我是一个刷盘的操作" << std::endl;
}
void sched() {
std::cout << "我是一个进程调度" << std::endl;
}
void handler(int signo) {
for(auto &f : cb) {
f();
}
(void)signo;
std::cout << "我是一个信号捕捉函数,我被调用了" << std::endl;
alarm(1);
}
// 写一段代码,理解 OS
int main() {
cb.push_back(FlushDisk);
cb.push_back(sched);
signal(SIGALRM, handler);
alarm(1);
while(true) {
pause(); // 等待信号到来,否则暂停
}
}
硬件异常
常见的异常如 /0 错误,野指针 --> 会导致我们的进程崩溃了。 但是我们从未想过为什么这些异常会导致进程崩溃呢? 因为软件问题,被操作系统识别,给目标进程发送了信号然后进程处理信号,默认终止了进程!
下面写两段程序,分别是 /0 错误以及野指针的异常,先是验证 /0 错误和野指针是向目标进程发送几号信号,再来从深层进行理解。
产生信号都是有操作系统产生的。那么是谁让 OS 产生的?用户,OS 内部的软件条件。
理解 /0
首先,我们的代码和数据会加载到物理地址空间,再通过页表和虚拟地址空间建立映射关系,CPU 执行代码的时候,一定是在调度某一个进程!假设此时 CPU 内的一个寄存器 eax 存放着 a 为 10 的值,ebx 存放着 0,此时会有 /0 错误,而 CPU 里有一个寄存器 EFLAGS(控制盒状态寄存器),简单理解为一个位图,对应着一些状态标记位、溢出标记位,记录 CPU 单次运算时所对应的状态。此时当 CPU 执行 /0 这一行代码时,由 EFLAGS 检测到是非法操作,CPU 触发硬件异常,那么 OS 需不需要知道呢?OS 是软硬件资源的管理者,因此告诉 OS'我出错了,快来帮我' --> OS 靠 struct task_struct *current(进程描述结构体),它永远指向'当前正在被 CPU 调度的进程',知道了是这个进程的代码在搞事情,就需要对这个进程进行某种操作,即发送信号。CPU 内部的寄存器本质是:当前进程的硬件上下文。一旦该进程被默认杀掉了,进程的硬件上下文就不存在了,从而使 CPU 的报错就没有了!
因此本质是:OS 是为了恢复 CPU 的正常运行。
那为什么当我对 8 号信号执行自定义捕捉时,会被 OS 一直调度啊? 当 CPU 告诉 OS'我报错了',OS 会向目标进程发送信号,又因为 8 号信号有了自定义捕捉方法,并没有清理内存,关闭进程打开的文件,切换进程等操作,所以 CPU 中还保留上下文数据以及寄存器内容,当执行完这自定义方法时,此时又会恢复硬件上下文,让 CPU 继续执行这行数据,又检测到是非法操作,依次循环下去。看起来也是在被一直调度,但这其实是异常的情况,占用着 CPU 的资源!!!
理解野指针
int *p = nullptr;
*p = 10;
OS 是如何识别野指针问题,并终止进程的? 野指针 --> 硬件报错 --> OS 写信号 --> 进程终止!!!
如何理解键盘产生信号?
那么键盘具体是如何让 OS 知道键盘外设有数据了?举 ctrl + c 来说: 当我们键盘上按下 ctrl + c 时,会向 CPU 发送硬件中断,此时 CPU 通过里面内置的针脚识别发出硬件中断的是键盘 (实际上就是高低电频),告诉 CPU 要键盘被按下了,此时 OS 会去识别是普通数据还是组合键 (即将数据从外设读入内存),而 OS 内部内置了一套中断向量表 (之后会扒出源码提供),执行键盘处理方法。
总结
本文详细介绍了 Linux 系统中的信号产生方式及其原理。主要内容包括:
- 键盘组合键产生信号(如 Ctrl+C、Ctrl+\、Ctrl+Z);
- 通过 kill 命令和函数产生信号,重点分析了 9 号和 19 号信号的不可捕捉特性;
- 软件条件产生的信号(如 alarm 函数和 SIGALRM 信号);
- 硬件异常产生的信号(如除零错误和野指针访问)。
文章深入探讨了信号产生的底层机制,包括 CPU 寄存器状态、中断处理等硬件层面原理,并解释了操作系统如何管理这些信号。最后通过代码示例展示了信号处理的实际应用场景,如模拟操作系统调度等。


