文章目录
- 一、信号的产生
- 二、产生信号的系统调用
-
- [2.1 kill------给指定的进程发送指定的信号](#2.1 kill——给指定的进程发送指定的信号)
- [2.2 模拟实现指令 kill](#2.2 模拟实现指令 kill)
- [2.3 raise------给调用的进程发送指定的信号](#2.3 raise——给调用的进程发送指定的信号)
- [2.4 abort------给调用者发送 6 号信号](#2.4 abort——给调用者发送 6 号信号)
- 三、验证哪些信号不可以被捕捉
- 四、为什么除0和解引用空指针会给进程发信号呢?
- 五、alarm------设置闹钟
- 六、结语
一、信号的产生
-
键盘组合键 :
ctrl+c
给进程发送2号信号;ctrl+\
给进程发送3号信号;ctrl+z
给进程发送19号信号(该信号无法被signal
信号捕捉)。 -
指令 :
kill -signo pid
-
系统调用 :
kill
、raise
、abort
-
(硬件)异常 :例如常见的除0(
Floating point exception
),当程序中出现除0异常,操作系统就会给对应的进程发送 8 号信号,来终止进程(这是 8 号信号的默认动作);对空指针解引用(Segmentation fault
),当程序中出现对空指针解引用的时候,操作系统会给对应的进程发送 11 号信号。 -
软件条件 :管道通信中,写端正常,读端关闭,操作系统会给写端进程发送 13 号信号,终止掉正在进行向管道中写入的进程。
alarm
闹钟 。
无论信号是如何产生的,最终一定是由操作系统发送给进程的,因为操作系统是进程的管理者。
二、产生信号的系统调用
2.1 kill------给指定的进程发送指定的信号
2.2 模拟实现指令 kill
cpp
#include <iostream>
#include <sys/types.h>
#include <signal.h>
#include <stdlib.h>
#include <string>
using namespace std;
void Manual(char *directives)
{
cout << "\n\t" << directives << " signum pid\n\n";
}
int main(int argc, char *argv[])
{
if(argc != 3)
{
Manual(argv[0]);
exit(1);
}
int signum = stoi(argv[1]);
int pid = std::stoi(string(argv[2]));
cout << signum << ' ' << pid <<endl;
pid_t ret = kill(pid, signum);
if(ret == -1)
{
perror("kill");
exit(1);
}
return 0;
}
2.3 raise------给调用的进程发送指定的信号
raise
就相当于:kill(getpid(), signum)
2.4 abort------给调用者发送 6 号信号
abort
函数内部,不仅会执行自定义捕捉(前提是捕捉了 6 号信号),在执行完自定义捕捉之后,还要去执行 6 号信号默认的终止动作。通过 kill
指令去给进程发送 6 号信号,进程只会执行捕捉动作(前提是对 6 号进行了捕捉)或者只会执行默认动作(前提是没有对 6 号信号做捕捉)。
三、验证哪些信号不可以被捕捉
cpp
#include <iostream>
#include <signal.h>
#include <unistd.h>
using namespace std;
void myhandler(int signo)
{
cout << "process get a signal: " << signo << endl;
// exit(1);
}
int main()
{
// signal(2, myhandler);
signal(19, myhandler);
for(int i = 1; i <= 31; i++)
{
signal(i, myhandler);
}
while(true)
{
cout << "Hello Linux" << ' ' << getpid() << endl;
sleep(1);
}
return 0;
}
先通过循环将31个信号都捕捉,然后在命令行通过 kill
给进程发送信号,看是否被成功捕捉。经过验证 9、19 号信号都无法被捕捉。其中 9 号信号是杀进程,19 号信号是暂停进程。
四、为什么除0和解引用空指针会给进程发信号呢?
以除0为例,首先,程序中所有的指令是需要被 CPU 来执行的,计算任务也不例外。所以 CPU 里面有特定的寄存器来存储操作数,还有一个状态寄存器,里面都是比特位级别的标记位,其中有一个标记位就是用来表示当前运算是否发生溢出,当 CPU 在执行除 0 的时候,会发生溢出,此时 CPU 就会去修改溢出标记位,这些寄存器中的内容,都属于当前进程的上下文数据,当进程切换的时候,这些数据同时也会被其它进程的上下文数据替换,当该进程再一次被替换进 CPU 执行的时候,这些上下文数据又会恢复出来。**所以任何异常只会影响该进程本身,并不会波及到操作系统。**CPU 是也是硬件资源,操作系统作为硬件的管理者,它是必须要关心硬件的健康,所以当 CPU 中的溢出标志位被设置成溢出的时候,操作系统一定是能知道的,(本质上,发生溢出会给操作系统发送中断),操作系统知道后就会向该进程发送对应的信号。
空指针解引用、越界访问等本质都是因为虚拟到物理转化失败,转化失败还导致硬件报错,最终被操作系统识别到。
程序对于异常信号,默认动作是让程序立即终止。但是我们可以在程序中对异常信号进行捕捉,如果捕捉方法里没有做特殊处理,比如说让程序退出,那这导致的后果就是程序不会立即终止,而是一直被调用运行,硬件错误就一直存在,操作系统就会一直给进程发送异常信号。
总结:程序中出现的所有异常,最终一定会转化成硬件错误,操作系统能够识别这种硬件错误,最终给进程发送对应的信号。
信号捕捉并不是为了让我们来解决问题的,而是当收到这个信号后,程序可能要被立即终止,信号捕捉给了我们应对程序即将被终止的机会,我们可以在捕捉函数里做一些数据保存,打印日志等工作。
五、alarm------设置闹钟
-
seconds
:闹钟将在seconds
秒时候响起(给进程发送 14 号信号),如果seconds == 0
,则之前设置的闹钟会被取消,并将剩下的时间返回。 -
返回值:返回之前闹钟的剩余秒数,如果之前未设闹钟,或者上一次设置的闹钟已经响过了,那么返回的就是 0 。
小Tips :如果在上一次闹钟还没响的时候,再一次调用 alarm
函数设置闹钟,那么这一次调用的返回值就是上一次闹钟的剩余时间,并且闹钟的响应时间会被更新成这一次的,上一次那个还没响的闹钟就会被作废。
设置闹钟:
cpp
#include <iostream>
#include <unistd.h>
using namespace std;
int main()
{
int n = alarm(5);
while(true)
{
cout << "process is running..." << endl;
sleep(1);
}
return 0;
}
捕捉闹钟信号:
cpp
#include <iostream>
#include <signal.h>
#include <unistd.h>
using namespace std;
void handler(int signum)
{
cout << "get a signal: " << signum << endl;
}
int main()
{
signal(14, handler);
int n = alarm(5);
while(true)
{
cout << "process is running..." << endl;
sleep(1);
}
return 0;
}
alarm 返回值验证:
cpp
#include <iostream>
#include <signal.h>
#include <unistd.h>
using namespace std;
void handler(int signum)
{
cout << "get a signal: " << signum << endl;
int n = alarm(5); // 闹钟每五秒响一次
cout << n << endl;
}
int main()
{
signal(14, handler);
int n = alarm(50);
while(true)
{
cout << "process " << getpid() << " is running..." << endl;
sleep(1);
}
return 0;
}
第一次设置的闹钟是 50 秒,在前 50 秒内,通过命令行向进程发送 14 号信号,此时程序就会去执行 handler
方法,在该方法中又调用了一个 alarm
,这就是前一个闹钟还没响,就又设置了一个闹钟。
六、结语
今天的分享到这里就结束啦!如果觉得文章还不错的话,可以三连支持一下,春人的主页还有很多有趣的文章,欢迎小伙伴们前去点评,您的支持就是春人前进的动力!