目录
[1. 什么是信号](#1. 什么是信号)
[1.1 引入](#1.1 引入)
[1.2 概念](#1.2 概念)
[1.3 特性](#1.3 特性)
[1.4 信号的三个方面](#1.4 信号的三个方面)
[2. 信号的产生](#2. 信号的产生)
[2.1 键盘按键产生](#2.1 键盘按键产生)
[2.2 signal捕捉信号函数](#2.2 signal捕捉信号函数)
[2.3 发送信号原理](#2.3 发送信号原理)
[2.4 硬件中断](#2.4 硬件中断)
[2.5 指令和函数接口](#2.5 指令和函数接口)
[2.5.1 kill指令](#2.5.1 kill指令)
[2.5.2 kill函数](#2.5.2 kill函数)
[2.5.3 raise与abort函数](#2.5.3 raise与abort函数)
[2.6 软件条件](#2.6 软件条件)
[2.7 异常错误产生信号](#2.7 异常错误产生信号)
1. 什么是信号
1.1 引入
在我们的日常生活中,会遇到许多信号,比如红绿灯、铃声和乌云。看到红灯,我们知道需要停下来等待;绿灯亮起,我们可以安全通行。教室里的铃声响起,我们明白这是下课的信号。当看到天空中布满乌云,我们就知道应该把晾晒的衣服收回家了。这些信号帮助我们按照既定的规则和自然的指示来安排我们的行动。
1.2 概念
信号是用户、操作系统、其他进程,向目标进程发送异步事件的一种方式。
什么是异步事件?异步事件(Asynchronous Event)是指在一个系统或程序中,事件的发生和它的处理不是在同一个时间序列或流程中进行的。
比如,当你正在玩游戏时,突然快递到你家楼下,你需要暂时中断游戏去取快递。这两个活动各自独立,展现了异步事件的特点。
1.3 特性
为什么我们能识别信号呢?当我们看到红绿灯,知道这是交通信号。因为有人告诉我们,所以识别信号是内置的。
- 进程也是如此,识别信号的功能,是程序员内置的特性。
我们遇到红灯知道停下来,绿灯响才能过马路。这些都是别人规定的规则。
- 所以信号产生后,进程知道怎么处理,是程序员设定的。
当你在玩游戏时,突然快递到楼下了。你可能处在游戏的关键时刻,不能立即处理,需要过一会。
- 进程收到信号时,不一定要立即处理该信号,需要在合适的时候处理。
设定起床闹钟,是为了在特定时间提醒我们。当闹钟响时,有的人起床按掉闹钟,开始做计划之内的事情;有的人可能会按掉闹钟,继续睡觉;有的人可能会赖一会床,再起来。
- 同样的,进程在接受信号后也有不同的处理方式,分别是采取默认行为,忽略信号,或执行程序员设定的自定义动作。
1.4 信号的三个方面
信号的产生与处理是信号机制中不可或缺的两个环节。然而,还有一个重要的方面尚未提及,那就是信号的保存。
当进程在收到信号时恰好忙于处理其他任务,无法立即对信号作出响应,此时信号的保存机制便发挥了作用。它确保了信号能够被临时记录在案,以便进程在合适的时机对其进行后续处理。
因此,进程信号会从信号的产生,信号的保存和信号的处理这三方面着手讲起。
2. 信号的产生
2.1 键盘按键产生
cpp
#include <iostream>
#include <unistd.h>
int main()
{
while(true)
{
std::cout << "hello, world!" << std::endl;
sleep(1);
}
return 0;
}
我们执行一个无限循环打印helloworld的代码。可以按键盘上Ctrl+C,来终止该进程。其中这种直接运行的进程时前台进程。

如果在可执行程序后面加上"&"符号,表示将该进程转为后台进程。

前台进程会占用了前台终端,导致shell进程无法接收输入的命令行来执行命令。所以Ctrl+C按键是发送给前台进程,作用是终止前台进程。
而后台进程允许shell进程执行命令行输入的指令。如下图,启动循环打印的程序,中间输入ll指令,显示该目录的文件内容。

如果想杀掉后台进程,可以打开一个新会话,使用kill指令配合-9选项,终止后台进程。

还可以使用nohup指令,将启动程序输出结果重定向到一个默认文件,叫做nohup.out。nohup.out文件不断记录进程输出内容。


此时,可以使用fg文件加上第一次启动进程中括号的数字1,可以将后台进程转为前台进程,再按Ctrl+C按键终止进程。

2.2 signal捕捉信号函数

其中sighandler是函数指针类型,signal函数用于捕捉信号,并使用传进来的函数指针对象对应的方法处理信号。
使用"kill -l"指令可以查看Linux系统提供的常见信号。1号到31号中的信号,是普通信号,之后的信号统称为实时信号。我们重点关注普通信号。其中每个信号名称其实都是一个宏,表示其编号。

其中Ctrl+C按键会发送一个信号给进程,该信号被操作系统解释为2号SIGINT信号。我们自定义一个返回值类型为void,函数参数只有一个整型变量的函数,然后使用signal函数捕捉2号信号。捕捉到2号信号,进程会按照Handler方法打印一条语句。
cpp
#include <iostream>
#include <unistd.h>
#include <signal.h>
void Handler(int signo)
{
std::cout << "Received signal, signal number: " << signo << std::endl;
}
int main()
{
signal(SIGINT, Handler);
while(true)
{
std::cout << "hello, world!" << std::endl;
sleep(1);
}
return 0;
}
启动该程序,按Ctrl+C按键,执行Handler函数,打印一条语句。这就证明了Ctrl+C按键,实质上是2号信号。

通过man 7 signal指令,可以查询到普通信号的默认处理动作,其中Term和Core都是终止进程。

2.3 发送信号原理
当按下键盘中Ctrl+C按键,操作系统会识别该操作,发送2号信号给进程。使用kill -l指令,会显示所有信号,你会发现信号编号是从1开始的。
因为进程PCB中有许多字段,其中使用位图上的比特位0和1两个状态,来表示该位数信号是否有接受到。还有个字段是sighandler_t类型的数组,sighandler_t是函数指针类型,也就是说该字段是函数指针数组。
当操作系统发送2号信号给进程时,其实向进程PCB位图中的第二位比特位写入1,表示该进程接受2号信号,并通过函数指针数组中下标为2的函数指针,执行该函数。如果使用signal函数,则会将该数组对应下标的内容修改为Handler函数的地址。

2.4 硬件中断
当用户按下Ctrl+C组合键时,键盘硬件检测到这一动作,并将按键编码转换为相应的信号。此时,键盘准备就绪,将数据通过中断请求线(IRQ)发送给CPU的特定引脚,以此发起一个中断请求。这个中断信号即刻通知操作系统,有来自键盘的外部事件需要被关注和处理。
操作系统接收到中断信号后,随即响应,它会暂停当前正在执行的任务,并将控制权转交给专门处理键盘输入的中断服务例程(ISR)。该例程负责将键盘缓冲区中的数据读取出来,并将其安全地拷贝到系统的内存中,以便CPU可以进一步处理这些输入。

所以,操作系统不需要对外部设备进行轮询检测,当执行完当前指令,只要等外部设备发送中断请求。操作系统如公司老板一般,指挥手底下的员工干活,自己只需要等待员工汇报工作进展,从而达到外设与操作系统并行。
2.5 指令和函数接口
2.5.1 kill指令

kill指令可以向目标进程发送指定信号。我们写一份代码,使用signal函数捕捉1到31号信号,然后打个死循环,使用kill指令发送信号。
cpp
#include <iostream>
#include <unistd.h>
#include <signal.h>
void Handler(int signo)
{
std::cout << "Received signal, signal number: " << signo << std::endl;
}
int main()
{
for(int signo = 1; signo <= 31; signo++)
{
signal(signo, Handler);
}
while(true)
{
sleep(1);
}
return 0;
}
我们使用kill指令发送1号、2号和6号信号,都被sig进程处理为打印一条语句。有人会说把所有信号捕捉,采用自己自定义方法,那进程岂不是无法终止。其实操作系统设计者也想到了这个问题,所以有些信号是无法被捕捉的,只会使用默认方式处理,如9号信号,可以终止所有你可以终止的进程。


2.5.2 kill函数

kill也是系统调用函数,向指定进程发送信号。我们可以使用这个函数写一个自己的kill指令。
cpp
#include <iostream>
#include <string>
#include <sys/types.h>
#include <signal.h>
void Usage(std::string proc)
{
std::cout << "Usage: " << proc << "signumber processid" << std::endl;
}
// ./mykill 9 12345
int main(int argc, char* argv[])
{
if(argc < 3)
{
Usage(argv[0]);
exit(1);
}
int signumber = std::stoi(argv[1]);
pid_t pid = std::stoi(argv[2]);
int n = kill(pid, signumber);
if(n < 0)
{
perror("kill");
exit(2);
}
exit(0);
}
如下图,执行mykill进程需要输入三个参数,可以杀掉普通进程。


2.5.3 raise与abort函数

raise函数是发送信号给调用该函数的进程。写一个五秒后发送9号信号给自己的代码。
cpp
#include <iostream>
#include <unistd.h>
#include <signal.h>
int main(int argc, char* argv[])
{
int cnt = 0;
while(true)
{
std::cout << "I am alive!" << std::endl;
cnt++;
if(cnt > 4)
raise(SIGKILL);
sleep(1);
}
return 0;
}


abort函数作用是通过发送SIGABRT信号给调用该函数的进程,以此终止该进程。这算作异常退出。

cpp
#include <iostream>
#include <string>
#include <unistd.h>
#include <stdlib.h>
#include <signal.h>
void Handler(int signo)
{
std::cout << "Received signal, signal number: " << signo << std::endl;
}
int main(int argc, char* argv[])
{
signal(SIGABRT, Handler);
int cnt = 0;
while(true)
{
std::cout << "I am alive!" << std::endl;
cnt++;
if(cnt > 4)
abort();
sleep(1);
}
}

2.6 软件条件
软件没准备好,软件条件不具备引起信号的产生。常见的是管道的读端关闭,而某个进程向管道写入,操作系统会发送SIGPIPE信号给该进程,并终止进程。

alarm函数用来设置发送信号的闹钟,该信号是SIGALRM信号。
cpp
#include <iostream>
#include <unistd.h>
int main()
{
alarm(1); //1s后,会收到SIGALRM信号
int number = 0;
while(true)
{
printf("count: %d\n", number++);
}
return 0;
}
上面的代码实现了测试服务器1s能完成多少次IO的功能,差不多稳定在七万次左右。但是对于CPU每秒几亿执行次数来说,已经很慢了。


cpp
#include <iostream>
#include <string>
#include <unistd.h>
#include <signal.h>
int number = 0;
void Die(int signumber)
{
printf("get a signal %d, count: %d\n", signumber, number);
exit(0);
}
int main(int argc, char* argv[])
{
alarm(1);
signal(SIGALRM, Die);
while(true)
{
number++;
}
}
上面的代码,没有进行调用printf进行IO。运行结果如下,结果接近6亿次,相比于上面的结果,快了将近一万倍。说明IO操作十分耗时。

2.7 异常错误产生信号
cpp
#include <iostream>
#include <unistd.h>
#include <signal.h>
int main()
{
int *p = nullptr;
*p = 10;
while(true);
}
上面是野指针问题,程序启动后会,操作系统会发送11号SIGSEGV信号给进程,终止该进程。


cpp
#include <iostream>
#include <unistd.h>
#include <signal.h>
int main()
{
int a = 10;
a /= 0;
while(true);
}
上面代码是除零错误,会导致栈溢出,操作系统会发送8号SIGFPE信号给进程。


当CPU执行该进程"a /= 0"代码时,实际上被解释为"a = a / 0",CPU会用三个寄存器存储三个值。
- eax存储一开始a变量的值,ebx存储0,再通过计算把结果存储到ecx寄存器中。CPU还有一个状态寄存器Eflags,其中有个溢出标记位,专门记录结果是否正常。
- a变量除0后,结果会溢出,Eflags中的标记位会写入为1。
- CPU因溢出标记位为1,得知计算结果有问题,会触发硬件中断。操作系统就会知道CPU内部出错,会向执行此代码的进程发送终止信号。
- 即使使用signal函数捕捉SIGSEGV信号,不终止该进程。当进程被CPU执行时,该进程的上下文数据重新加载到CPU时,Eflags上的溢出标记位还是1,照样会引起硬件中断,操作系统就会不断发送信号来终止进程。

创作充满挑战,但若我的文章能为你带来一丝启发或帮助,那便是我最大的荣幸。如果你喜欢这篇文章,请不吝点赞、评论和分享,你的支持是我继续创作的最大动力!
