【Linux学习笔记】认识信号和信号的产生
🔥个人主页:大白的编程日记
🔥专栏:Linux学习笔记
文章目录
前言
哈喽,各位小伙伴大家好!上期我们讲了消息队列和信号量 今天我们讲的是认识信号和信号的产生。话不多说,我们进入正题!向大厂冲锋!
一. 信号快速认识
1.1 生活角度的信号
- 你在网上买了很多件商品,再等待不同商品快递的到来。但即便快递没有到来,你也知道快递来临时,你该怎么处理快递。也就是你能“识别快递”。
- 当快递员到了你楼下,你也收到快递到来的通知,但是你正在打游戏,需5min之后才能去取快递。那么在这5min之内,你并没有下去去取快递,但是你是知道有快递到来了。也就是取快递的行为并不是一定要立即执行,可以理解成“在合适的时候去取”。
- 在收到通知,再到你拿到快递期间,是有一个时间窗口的,在这段时间,你并没有拿到快递,但是你知道了有一个快递已经来了。本质上是你“记住了有一个快递要去取”。
- 当你时间合适,顺利拿到快递之后,就要开始处理快递了。而处理快递一般方式有三种:1. 执行默认动作(幸福的打开快递,使用商品)2. 执行自定义动作(快递是零食,你要送给你的女朋友)3. 忽略快递(快递拿上来之后,扔掉床头,继续开一把游戏)
- 快递到来的整个过程,对你来讲是异步的,你不能准确断定快递员什么时候给你打电话
基本结论:
你怎么能识别信号呢?识别信号是内置的,进程识别信号,是内核程序员写的内置特性。
信号产生之后,你知道怎么处理吗?知道。如果信号没有产生,你知道怎么处理信号吗?知道。所以,信号的处理方法,在信号产生之前,已经准备好了。
处理信号,立即处理吗?我可能正在做优先级更高的事情,不会立即处理?什么时候?合适的时候。
信号到来 | 信号保存 | 信号处理
怎么进行信号处理啊?a.默认 b.忽略 c.自定义,后续都叫做信号捕捉。
1.2 技术应用角度的信号
1.2.1 ⼀个样例
// sig.cc
#include <iostream>
#include <unistd.h>
int main()
{
while(true){
std::cout << "I am a process, I am waiting signal!" << std::endl;
sleep(1);
}
}
- 用户输入命令,在Shell下启动一个前台进程
- 用户按下 Ctrl+C,这个键盘输入产生一个硬件中断,被OS获取,解释成信号,发送给目标前台进程
- 前台进程因为收到信号,进而引起进程退出
NAME
signal - ANSI C signal handling
SYNOPSIS
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
参数说明:
signum:信号编号[后面解释,只需要知道是数字即可]
handler:函数指针,表示更改信号的处理动作,当收到对应的信号,就回调执行handler方法
而且其实, Ctrl+C 的本质是向前台进程发送 SIGINT 即 2 号信号,我们证明⼀下,这⾥需要引入一个系统调用函数
#include <iostream>
#include <unistd.h>
#include <signal.h>
void handler(int signumber)
{
std::cout << "我是:" << getpid() << ",我获得了一个信号:" << signumber << std::endl;
}
int main()
{
std::cout << "我是进程:" << getpid() << std::endl;
signal(SIGINT/*2*/, handler);
while(true){
std::cout << "I am a process, I am waiting signal!" << std::endl;
sleep(1);
}
}
$ g++ sig.cc -o sig
$ ./sig
我是进程:212569
I am a process, I am waiting signal!
^C我是:212569,我获得了一个信号:2
I am a process, I am waiting signal!
I am a process, I am waiting signal!
^C我是:212569,我获得了一个信号:2
I am a process, I am waiting signal!
1.2.2 一个系统函数
### NAME
signal - ANSI C signal handling
参数说明:
- signum: 信号编号 [后面解释,只需要知道是数字即可]
- handler: 函数指针,表示更改信号的处理动作,当收到对应的信号,就回调执行handler方法
- 参数说明:
signum:信号编号[后⾯解释,只需要知道是数字即可]
handler:函数指针,表⽰更改信号的处理动作,当收到对应的信号,就回调执⾏handler⽅法
而其实,Ctrl+C 的本质是向前台进程发送 SIGINT 即 2 号信号,我们证明一下,这里需要引入一个系统调用函数
开始测试
#include <iostream>
#include <unistd.h>
#include <signal.h>
void handler(int signumber)
{
std::cout << "我是: " << getpid() << ", 我获得了一个信号: " << signumber << std::endl;
}
int main()
{
std::cout << "我是进程: " << getpid() << std::endl;
signal(SIGINT, handler);
while(true){
std::cout << "I am a process, I am waiting signal!" << std::endl;
sleep(1);
}
}
$ g++ sig.cc -o sig
$ ./sig
我是进程: 212569
I am a process, I am waiting signal!
I am a process, I am waiting signal!
^C我是: 212569, 我获得了一个信号: 2
I am a process, I am waiting signal!
I am a process, I am waiting signal!
// 用本机编译运行
$ g++ sig.cc -o sig
$ ./sig
我是进程: 212569
I am a process, I am waiting signal!
I am a process, I am waiting signal!
^C我是: 212569, 我获得了一个信号: 2
I am a process, I am waiting signal!
I am a process, I am waiting signal!
思考:
- 这里进程为什么不退出?
- 这个例子能说明哪些问题?信号处理,是自己处理
- 请将生活例子和 Ctrl+C 信号处理过程相结合,解释一下信号处理过程:进程就是你,操作系统就是快递员,信号就是快递,发信号的过程就类似给你打电话
注意:
- 要注意的是,
signal
函数仅仅是设置了特定信号的捕捉行为处理方式,并不是直接调用处理动作。如果后续特定信号没有产生,设置的捕捉函数永远也不会被调用!! - Ctrl+C 产生的信号只能发给前台进程。一个命令后面加个
&
可以放到后台运行,这样 Shell 不必等待进程结束就可以接受新的命令,启动新的进程。 - Shell 可以同时运行一个前台进程和任意多个后台进程,只有前台进程才能接到像
Ctrl+C
这种控制键产生的信号。 - 前台进程在运行过程中用户随时可能按下 Ctrl+C 而产生一个信号,也就是说该进程的用户空间代码执行到任何地方都有可能收到 SIGINT 信号而终止,所以信号相对于进程的控制流程来说是异步(Asynchronous)的。
- 关于进程间关系,我们在网络部分会专门来讲,现在就了解即可。
- 可以渗透
&
和nohup
1.3 信号概念
信号是进程之间事件异步通知的⼀种方式,属于软中断。
1.3.1 查看信号
每个信号都有⼀个编号和⼀个宏定义名称,这些宏定义可以在signal.h中找到,例如其中有定义
#define SIGINT 2
编号34以上的是实时信号,本章只讨论编号34以下的信号,不讨论实时信号。这些信号各自在什么条件下产生,默认的处理动作是什么,在signal(7)
中都有详细说明: man 7 signal
1.3.2 信号处理
可选的处理动作有以下三种:
- 忽略此信号
#include <iostream>
#include <unistd.h>
#include <signal.h>
void handler(int signumber)
{
std::cout << "我是:" << getpid() << ",我获得了一个信号:" << signumber << std::endl;
}
int main()
{
std::cout << "我是进程:" << getpid() << std::endl;
signal(SIGINT/*2*/, SIG_IGN); // 设置忽略信号的宏
while(true){
std::cout << "I am a process, I am waiting signal!" << std::endl;
sleep(1);
}
}
- 执行该信号的默认处理动作
#include <iostream>
#include <unistd.h>
#include <signal.h>
void handler(int signumber)
{
std::cout << "我是:" << getpid() << ",我获得了一个信号:" << signumber << std::endl;
}
int main()
{
std::cout << "我是进程:" << getpid() << std::endl;
signal(SIGINT/*2*/, SIG_DFL);
while(true){
std::cout << "I am a process, I am waiting signal!" << std::endl;
sleep(1);
}
}
$ g++ sig.cc -o sig
$ ./sig
我是进程:212791
I am a process, I am waiting signal!
I am a process, I am waiting signal!
- 提供⼀个信号处理函数,要求内核在处理该信号时切换到用户态执行这个处理函数,这种⽅式称为自定义捕捉(Catch)⼀个信号。
#define SIG_DFL ((__sighandler_t) 0) /* Default action. */
#define SIG_IGN ((__sighandler_t) 1) /* Ignore signal. */
/* Type of a signal handler. */
typedef void (*__sighandler_t)(int);
// 其实SIG_DFL和SIG_IGN就是把0,1强转为函数指针类型
^C // 输入ctrl+c,进程退出,就是默认动作
二. 产生信号
当前阶段:
2.1 通过终端按键产生信号
2.1.1 基本操作
- Ctrl+C (SIGINT) 已经验证过,这里不再重复
- Ctrl+\ (SIGQUIT) 可以发送终止信号并生成core dump文件,用于事后调试(后面详谈)
#include <iostream>
#include <unistd.h>
#include <signal.h>
void handler(int signumber)
{
std::cout << "我是: " << getpid() << ", 我获得了一个信号: " << signumber << std::endl;
}
int main()
{
std::cout << "我是进程: " << getpid() << std::endl;
signal(SIGQUIT, handler);
while(true){
std::cout << "I am a process, I am waiting signal!" << std::endl;
sleep(1);
}
}
$ g++ sig.cc -o sig
$ ./sig
我是进程: 213056
^Z // 注释掉13行代码
$ ./sig
我是进程: 213146
I am a process, I am waiting signal!
I am a process, I am waiting signal!
I am a process, I am waiting signal!
^C //Quit
- Ctrl+Z (SIGTSTP) 可以发送停止信号,将当前前台进程挂起到后台等。
#include <iostream>
#include <unistd.h>
#include <signal.h>
void handler(int signumber)
{
std::cout << "我是: " << getpid() << ", 我获得了一个信号: " << signumber << std::endl;
}
int main()
{
std::cout << "我是进程: " << getpid() << std::endl;
signal(SIGTSTP, handler);
while(true){
std::cout << "I am a process, I am waiting signal!" << std::endl;
sleep(1);
}
}
2.1.2 理解OS如何得知键盘有数据
2.1.3 初步理解信号起源
注意:
- 信号其实是从纯软件角度,模拟硬件中断的行为
- 只不过硬件中断是发给CPU,而信号是发给进程
- 两者有相似性,但是层级不同,这点我们后面的感觉会更加明显
#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>
void handler(int signumber)
{
std::cout << "我是: " << getpid() << ", 我获得了一个信号: " << signumber << std::endl;
}
int main()
{
std::cout << "我是进程: " << getpid() << std::endl;
signal(SIGTSTP, handler);
while(true){
std::cout << "I am a process, I am waiting signal!" << std::endl;
sleep(1);
}
}
$ ./sig
我是进程: 213552
I am a process, I am waiting signal!
I am a process, I am waiting signal!
^Z // 多按一次回车
[1]+ Stopped ./sig
$ ./sig
我是进程: 213627
I am a process, I am waiting signal!
I am a process, I am waiting signal!
^Z
[1]+ Stopped ./sig
$ web: http://code/tests_jobs
2-2 调用系统命令向进程发信号
示例代码:
#include <iostream>
#include <unistd.h>
#include <signal.h>
int main()
{
while(true){
sleep(1);
}
}
$ g++ sig.cc -o sig // step 1
$ ./sig & // step 2
$ ps ajx | head -1 && ps ajx | grep sig // step 3
PPID PID PGRP SID TTY TPGID STAT UID TIME COMMAND
211805 213784 213784 211805 pts/0 213792 S 1002 0:00 ./sig
$ kill -SIGSEGV 213784
$ ./sig
[1]+ Segmentation fault ./sig
- 213784 是 sig 进程的pid。之所以要再次回车才显示
Segmentation fault
,是因为在213784
进程终止掉之前已经回到了Shell提示符等待用户输入下一条命令,Shell 不希望Segmentationfault
信息和用户的输入交错在一起,所以等用户输入命令之后才显示- 指定发送某种信号的 kill 命令可以有多种写法,上面的命令还可以写成
kill -11 213784,
11 是信号SIGSEGV
的编号。以往遇到的段错误都是由非法内存访问产生的,而这个程序本身没错误,给它发SIGSEGV
也能产生段错误。
2.3 使用函数产生信号
2.3.1 kill
kill 命令是调用 kill 函数实现的。kill 函数可以给一个指定的进程发送指定的信号。
NAME
kill - send signal to a process
SYNOPSIS
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);
RETURN VALUE
On success (at least one signal was sent), zero is returned. On error,
-1 is returned, and errno is set appropriately.
示例:实现自己的 kill 命令
#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>
// mykill -signumber pid
int main(int argc, char *argv[])
{
if(argc != 3)
{
std::cerr << "Usage: " << argv[0] << " -signumber pid" << std::endl;
return 1;
}
int number = std::stoi(argv[1]+1); // 去掉-
pid_t pid = std::stoi(argv[2]);
kill(pid, number);
return 0;
}
$ g++ sig.cc -o sig // step 1
$ ./sig & // step 2
$ ps ajx | head -1 && ps ajx | grep sig // step 3
$ kill -SIGSEGV 213784
$ ./sig
[1]+ Segmentation fault
2.3.2 raise
raise 函数可以给当前进程发送指定的信号(自己给自己发信号)。
NAME
raise - send a signal to the caller
SYNOPSIS
#include <signal.h>
int raise(int sig);
RETURN VALUE
raise() returns 0 on success, and nonzero for failure.
示例:
#include <iostream>
#include <unistd.h>
#include <signal.h>
void handler(int signumber)
{
// 整个代码就只有这一处打印
std::cout << "获取了一个信号: " << signumber << std::endl;
}
// mykill -signumber pid
int main()
{
signal(2, handler); // 先对2号信号进行捕捉
// 每隔1s,自己给自己发送2号信号
while(true)
{
sleep(1);
raise(2);
}
}
$ g++ raise.cc -o raise
$ ./raise
获取了一个信号: 2
获取了一个信号: 2
获取了一个信号: 2
2.3.3 abort
abort 函数使当前进程接收到信号而异常终止
NAME
abort - cause abnormal process termination
SYNOPSIS
#include <stdlib.h>
void abort(void);
RETURN VALUE
The abort() function never returns.
// 就像exit函数一样,abort函数总是会成功的,所以没有返回值。
示例:
#include <iostream>
#include <unistd.h>
#include <stdlib.h>
#include <signal.h>
void handler(int signumber)
{
// 整个代码就只有这一处打印
std::cout << "获取了一个信号: " << signumber << std::endl;
}
// mykill -signumber pid
int main()
{
signal(SIGABRT, handler);
while(true)
{
sleep(1);
abort();
}
}
$ g++ Abort.cc -o Abort
$ ./Abort
获取了一个信号: 6 // 实验可以得知,abort给自己发送的是固定6号信号,虽然捕捉了,但是还是要退出
Aborted
// 注释掉15行代码
$ ./Abort
Aborted
2.4 硬件异常产生信号
硬件异常被硬件以某种方式被硬件检测到并通知内核,然后内核向当前进程发送适当的信号。例如当前进程执行了除以0的指令,CPU的运算单元会产生异常,内核将这个异常解释为SIGFPE信号发送给进程。再比如当前进程访问了非法内存地址,MMU会产生异常,内核将这个异常解释为SIGSEGV信号发送给进程。
2.4.1 模拟除0
#include <stdio.h>
#include <signal.h>
void handler(int sig)
{
printf("catch a sig : %d\n", sig);
}
int main()
{
//signal(SIGFPE, handler); // 8) SIGFPE
sleep(1);
int a = 10;
a/=0;
while(1);
return 0;
}
2.4.2 模拟野指针
#include <stdio.h>
#include <signal.h>
void handler(int sig)
{
printf("catch a sig : %d\n", sig);
}
int main()
{
//signal(SIGSEGV, handler);
sleep(1);
int *p = NULL;
*p = 100;
while(1);
return 0;
}
[hblocalhost code_test]$ .(sig)
Segmentation fault (core dumped)
[hblocalhost code_test]$
[hblocalhost code_test]$ cat sig.c
#include <stdio.h>
#include <signal.h>
void handler(int sig)
{
printf("catch a sig : %d\n", sig);
}
int main()
{
//signal(SIGSEGV, handler);
sleep(1);
int *p = NULL;
*p = 100;
while(1);
return 0;
}
由此可以确认,我们在C/C++当中除零,内存越界等异常,在系统层面上,是被当成信号处理的。
注意: 通过上面的实验,我们可能发现:
- 发现一直有8号信号产生被我们捕获,这是为什么呢?上面我们只提到CPU运算异常后,如何处理后续的流程,实际上OS会检查应用程序的异常情况,其实在CPU中有一些控制和状态寄存器,主要用于控制处理器的操作,通常由操作系统代码使用。状态寄存器可以简单理解为一个位图,对应着一些状态标记位、溢出标记位。OS会检测是否存在异常状态,有异常存在就会调用对应的异常处理方法。
- 除零异常后,我们并没有清理内存,关闭进程打开的文件,切换进程等操作,所以CPU中还保留上下文数据以及寄存器内容,除零异常会一直存在,就有了我们看到的一直发出异常信号的现象。访问非法内存其实也是如此,大家可以自行实验。
后言
这就是认识信号和信号的产生。大家自己好好消化!今天就分享到这! 感谢各位的耐心垂阅!咱们下期见!拜拜~