【Linux系统篇】:什么是信号以及信号是如何产生的---从基础到应用的全面解析
✨感谢您阅读本篇文章,文章内容是个人学习笔记的整理,如果哪里有误的话还请您指正噢✨
✨ 个人主页:余辉zmh–CSDN博客
✨ 文章所属专栏:Linux篇–CSDN博客
文章目录
- 一.信号概念
- 1.生活角度的信号
- 2.技术应用角度的信号
- 3.补充内容
- 前台进程和后台进程
- 硬件中断
- 二.信号产生
- 1.键盘组合键
- 2.kill命令
- 3.系统调用
- 4.异常
- 5.软件条件
- 三.补充内容--核心转储
一.信号概念
1.生活角度的信号
在平常的生活中,信号无处不在,比如说上下课铃声,红绿灯,快递取件码,外卖电话,闹钟等等。
1.对于这些生活中的信号,我们是如何认识这些信号的?
当然是有人教我们怎么识别信号,以及信号到来后怎么处理信号!
2.即便是当前没有信号产生,我们也知道信号产生后应该做什么。
比如点外卖,我们平常点外卖都是下单之后,过一段时间骑手才能给我们送过来,当我们收到骑手的电话后,我们就知道外卖到了要下楼取餐。但是我们在等待的这个过程中,骑手还没有给我们打电话之前,我们依然知道收到电话之后要做什么。
3.信号产生之后,我们可能并不会立即处理这个信号,而是选择在合适的时候处理,因为我们可能正在做其他更重要的事情。所以,信号产生之后直到信号处理时,存在一个时间窗口,在这个时间窗口这一时间段内,我们必须记住信号到来!
还是点外卖这个例子,我们在收到骑手的电话后,可能并不是立即下楼取餐,比如此时我们还在打游戏,中间不能断,直到游戏结束后,我们才下楼取餐。在接收到骑手电话直到游戏结束,这个时间段就是一个时间窗口,在这个时间窗口中我们需要记住外卖到了,然后游戏结束后再处理。
2.技术应用角度的信号
如果站在技术应用角度看待信号,同样可以根据上面的内容得出结论:
在计算机中,程序也就是进程代表的就是我们用户的身份。
1.进程必须可以识别以及处理信号。
2.进程即便是没有收到信号,也能知道哪些信号该怎么处理。信号的处理能力,属于进程内置功能的一部分。
3.当进程真的收到了一个具体的信号的时候,进程可能并不会立即处理这个信号,而是选择在合适的时候处理。
信号的处理方式:
- 默认动作
- 忽略
- 自定义动作
4.进程在收到信号后,到信号开始被处理的时候,也一定会有一个时间窗口,进程具有临时保存哪些信号已经发生的功能。
举个例子:
写一个死循环的程序,在命令行上采用前台进程运行;当用户在进程运行的过程中,按下Ctrl+c
键,该进程就会终止退出
#include <iostream>
#include <unistd.h>
using namespace std;int main(){while(true){cout << "I am a process" << endl;sleep(1);}return 0;
}
直接说明结论:Ctrl+c
键的本质是被进程解释成为收到了2信号,前面提到过进程对信号的处理方式有三种,如果没有设置自定义动作,进程就会采用默认动作处理信号,而2号信号的默认动作就是终止退出。
下面图中表示的是所有的信号,其中1~31
个是普通信号,而34~64
则是实时信号(需要理解处理),后面讲解信号时主要围绕前31个普通信号来讲解。
信号以宏定义的形式表示,本质上就是数字编号
验证上面的结论,Ctrl+c
组合键本质上是发送2号信号,并且默认处理动作是终止退出:
- signal函数:
#include <signal.h>typedef void (*sighandler_t)(int);sighandler_t signal(int signum, sighandler_t handler);
功能:用于修改特定进程对于指定信号的处理动作,也就是自定义动作,这个过程叫做信号捕捉。
参数:
sig
:信号编号或者信号名。handler
:自定义处理函数指针,或特殊值SIG_IGN
(忽略信号,表示的是数字1)和SIG_DFL
(恢复默认行为,表示的是数字0)。
返回值:成功时返回之前的处理函数指针,失败时返回 SIG_ERR
。
测试代码:
#include <iostream>
#include <unistd.h>
#include <signal.h>
using namespace std;void handler(int signo){cout << "process get a signal: " << signo << endl;
}int main(){signal(2, handler);while (true){cout << "I am a process, pid: " << getpid() << endl;sleep(1);}return 0;
}
用signal函数设置自定义动作,捕捉2号信号,因为自定义动作只是输出一条语句,所以处理完后(函数调用结束),并没有终止退出,因此可以验证2号信号的默认动作就是终止退出。
- signal函数的注意事项:
1.信号处理的三种方式只能三选一,使用自定义动作,就不能再执行默认动作;
2.signal函数只需要设置一次,在进程整个生命周期往后都会有效;
3.signal函数中设置的自定义动作函数handler
是在执行后续代码的过程中收到信号才调用,没有收到就不会调用;
4.对于普通信号,除了9号和19号信号不能自定义动作捕捉外,其他都可以(可以将所有普通信号使用signal函数捕捉,然后依次发送每个信号进行测试);
5.多个信号可以设置同一个自定义动作,所以handler
函数需要带一个参数,用来指定具体的目标信号。
3.补充内容
前台进程和后台进程
前台进程和后台进程是操作系统中两种不同的进程运行方式,在Linux中,一次登录,一个终端,一般会配上一个命令行解释器bash
,每一个登录,只允许一个进程是前台进程,但是可以允许多个进程是后台进程。
-
前台进程:
-
在终端中直接启动命令时默认为前台运行;
-
一旦前台进程启动后,bash进程变成后台进程,就不再接收任何指令了,需要等待其完成才能执行其他指令
-
前台进程直接与用户交互,可以接受用户的输入(比如键盘输入)
-
可以通过
Ctrl+c
组合键终止
-
- 后台进程:
- 在Linux中,可以通过在命令行后添加
&
符号来启动后台进程(比如./test &
) - 一旦后台进程启动后,bash进程还是前台进程,可以继续执行用户输入的指令
- 后台进程不与用户直接交互,通常无法接受用户输入
- 无法通过
Ctrl+c
组合键终止
- 在Linux中,可以通过在命令行后添加
前后台进程的区分就是谁来获取键盘的输入,键盘输入首先是被前台进程收到的!
因为前台进程可以接收到用户输入的Ctrl+c
组合键,通过硬件中断转变为接收到2号信号,执行默认动作处理从而使进程终止退出。
硬件中断
在上面的讲解例子中,键盘数据是如何输入给内核的,Ctrl+c
组合键又是如何变成信号的?这就需要来了解一下关于硬件部分了。
进程是由CPU来执行的,在数据层面上,CPU不和外设直接交互;但是在控制层面上,CPU可以和外设直接交互,当外设某种事件完成后,就可以发起硬件中断,经过中断单后,向CPU发送中断信号,CPU根据中断请求线的电平变化来接收中断信号。
通过中断机制,外设可以需要时获得CPU的注意力,而不需要CPU不断检查外设状态,这样设计可以使CPU能够高效管理多个外设,而不必浪费时间在不需要服务的设备上。
此外在内存中存储了一张中断向量表,中断向量表中存放的是中断服务程序的入口地址(也就是函数指针),系统通过CPU收到的中断信号,以中断信号为索引到表中找到对应的处理中断方法,然后执行对应的操作。
当我们从键盘中输入时,键盘被按下,存有数据时,键盘立刻会发起硬件中断,通过中断单元向CPU发送中断信号,中断信号被CPU保存下来,同时操作系统会立刻读取中断信号,以此为索引到中断向量表中找到对应的处理中断方法(从键盘读取到内存中),键盘上的数据在读取到内存上时,系统会进行判断,如果是控制类型的数据,比如Ctrl+c
,系统就会将2号信号发送给进程。进程收到2号信号后,如果没有自定义捕捉,就会执行默认动作终止退出。
操作系统就是不断接受中断信号来处理外设的。
对于外设和CPU的直接交互也仅仅局限于控制层面上的。
除了有硬件中断,当然还存在软件中断:信号就是用软件方式,对进程模拟的硬件中断,虽然两者没有任何关联,但设计方式却是很相似(后面信号保存的时候再讲解)
当程序正在执行代码时,可能会产生信号,信号的产生和我们自己写的代码运行是异步的。信号就是进程之间事件异步通知的一种方式—属于软中断。
二.信号产生
1.键盘组合键
常用的几个:
1.Ctrl+c
- 信号:
SIGINT
,信号编号为2 - 默认行为:终止进程
- 用途:强制中断前台进程的执行
2.Ctrl+\
- 信号:
SIGQUIT
,信号编号为3 - 默认行为:终止进程并生成核心转储(暂时了解即可,后面会讲什么是核心转储)
- 用途:强制终止进程并生成调试用的核心文件。
3.Ctrl+z
- 信号:
SIGSTOP
,信号编号为19 - 默认行为:暂停进程,将进程放入后台(无法被捕捉)
- 用途:暂停进程
2.kill命令
通过kill指令同样可以给指定进程发送信号:
kill [-信号编号或信号名] PID
注意:如果省略信号参数,或默认向指定进程发送SIGTERM
(编号为15),优雅的终止进程。
常用信号及其用途:
信号名 | 编号 | 默认行为 | 典型场景 |
---|---|---|---|
SIGTERM | 15 | 优雅终止进程 | 允许进程清理资源后退出 |
SIGKILL | 9 | 强制终止进程 | 立即杀死无响应的进程 |
SIGINT | 2 | 终止进程(同 Ctrl+C ) | 手动终止前台进程 |
SIGSTOP | 19 | 暂停进程(不可被捕获) | 调试时冻结进程状态 |
SIGCONT | 18 | 恢复已暂停的进程 | 继续运行被 SIGSTOP 暂停的进程 |
3.系统调用
1.kill
函数
#include <signal.h>int kill(pid_t pid, int sig);
功能:
向指定进程发送目标信号(类似于指令kill发送信号)。
参数:
pid
:发送信号的目标进程sig
:要发送的信号名或信号编号
返回值:
成功返回0,失败返回-1并设置errno。
通过kill函数实现一个自己的kill指令:
signal.cc
文件:
#include <iostream>
#include <unistd.h>
#include <signal.h>
using namespace std;void handler(int signo){cout << "process get a signal: " << signo << endl;
}int main(){// 所有普通信号全部捕捉for (int i = 1; i < 31; i++){signal(i, handler);}while (true){cout << "I am a process, pid: " << getpid() << endl;sleep(2);}return 0;
}
mykill.cc
文件:
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <string>
using namespace std;void handler(int signo){cout << "process get a signal: " << signo << endl;exit(0);
}int main(int argc, char *argv[]){if(argc!=3){cout << "Usage:\n\t" << "kill signal pid\n" << endl;exit(1);}int signum = stoi(argv[1]);pid_t pid = stoi(argv[2]);int n = kill(pid, signum);if(n == -1){perror("kill");exit(2);}return 0;
}
2.raise
函数
#include <signal.h>int raise(int sig);
功能:
向调用这个函数的当前进程发送目标信号(等价于kill(getpid(), sig)
)。
参数:
sig
:要发送的信号名或信号编号
返回值:
成功返回0,失败返回-1并设置errno。
raise函数发送的信号,被捕捉后,只会执行自定义动作:
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <string>
using namespace std;void handler(int signo){cout << "process get a signal: " << signo << endl;exit(0);
}int main(){signal(2, handler);int cnt = 5;while (true){cout << "I am a process, pid: " << getpid() << endl;sleep(1);cnt--;if(cnt==0){raise(2);} }return 0;
}
这里对2号信号自定义动作添加了退出语句,所以执行完后回退出:
3.abort
函数
#include <signal.h>int abort(void);
功能:
向调用这个函数的当前进程发送6号信号(等价于kill(getpid(), 6)
)。
无参数无返回值
abort函数和raise不一样,被捕捉后,如果自定义动作没有退出,执行完自定义动作,还会执行默认动作,终止退出:
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <string>
using namespace std;void handler(int signo){cout << "process get a signal: " << signo << endl;// exit(0);
}int main(){signal(6, handler);int cnt = 5;while(true){cout << "I am a process, pid: " << getpid() << endl;sleep(1);cnt--;if(cnt==0){abort();}}return 0;
}
对6号信号的自定义动作取消退出,但是最后依然会终止退出,就是因为被捕捉后,如果自定义动作没有退出,执行完自定义动作,还会执行默认动作,终止退出
4.异常
在刚开始学习进程的时候,在进程终止的那里讲解过进程有三种退出情况:1.代码执行完,结果正确退出;2.代码执行完,结果不正确退出;3.执行过程中出现错误,异常退出,代码没有执行完。
现在学到信号这里,就可以明白,进程在执行过程中出现错误,本质上是收到了对应的异常信号,因为信号的默认动作是终止退出,所以当前进程就结束执行退出了。
举例:
#include <iostream>
#include <unistd.h>
#include <signal.h>
using namespace std;int main(){// 除零错误// int a = 10;// int b = 0;// int c = a / b;// cout << c << endl;// 野指针错误// int *p = nullptr;// *p = 10;// cout << *p << endl;// return 0;
}
这两个错误分别对应信号SIGFPE
(浮点异常)信号编号为8,以及信号SIGSEGV
(段错误)信号编号为11.
任何异常只会影响当前进程本身,并不影响整个操作系统以及其它进程。
这是因为CPU执行当前进程时,CPU寄存器中的数据只属于当前进程的上下文;
异常信号可以来自硬件异常(比如除0或者野指针错误),通常由CPU指令执行失败触发中断,系统是软硬件的管理者,系统就会知道对应的硬件错误信息,然后向当前进程发送对应的异常信号,当前进程收到异常信号,就会执行默认动作终止退出;而CPU则会获取下一个进程的上下文开始执行,所以并不会受到影响。
如果进程捕获了硬件异常信号但未终止退出,CPU会恢复该进程的上下文,使其从触发异常的指令位置继续执行。如果错误原因未修复,CPU调度执行该进程时,重新执行同一指令会再次触发异常,导致信号循环发送,形成死循环。
以上面的除0错误,进行捕捉但不退出为例:
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <string>
using namespace std;void handler(int signo){cout << "process,pid: "<<getpid()<<" get a signal: " << signo << endl;// exit(0);
}int main(){// 硬件异常// 除零错误signal(8, handler);int a = 10;int b = 0;int c = a / b;cout << c << endl;return 0;
}
所以异常信号并不是为了让用户解决问题,而是提示用户当前进程终止退出的错误原因,尽管可以捕捉设置自定义动作,但是如果不退出就会一直报错陷入死循环,并不能解决什么问题,所以对于大部分处理最后都是要终止进程。
5.软件条件
异常不光只会由硬件产生,也可能会是软件。
比如之前讲解进程通信时的管道,当通过管道进行通信的两个进程,读端关闭,写端就不能再继续写入,系统就会给写入的进程发送异常信号然后终止退出。
进程由系统进行管理,管道也是由系统创建的共享资源,所以这种就属于软件上的异常;除了这个,还有向未打开的文件进行写入和读取时也是属于软件上的异常。
而软件上除了异常可以产生信号,还有特殊事件—软件条件,比如闹钟就是一种软件条件
在操作系统中,“闹钟”是一种定时器机制,主要通过SIGALRM
信号(编号为14)实现:
闹钟是进程可以设置的一种定时器,当指定时间到时,系统会向该进程发送14号信号,在Linux中通过alarm
函数实现。
#include <unistd.h>unsigned int alarm(unsigned int seconds);
参数:seconds
指定多少秒后向该进程发送14号信号
返回值:闹钟响时剩余的秒数,如果没有则返回0;如果连续设置多个闹钟,返回值就是上一次闹钟的剩余时间。
进程可以捕捉该信号并自定义处理函数,默认处理会终止进程;闹钟的存在可以实现超时处理机制或者定时执行某些操作。
示例:
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <string>
using namespace std;void handler(int signo){cout << "process,pid: "<<getpid()<<" get a signal: " << signo << endl;
}int main(){// 软件条件---闹钟signal(14, handler);int n = alarm(5);while(true){cout << "I am a process, pid" << getpid() << endl;sleep(1);}return 0;
}
闹钟不属于软件异常,是系统层面上的一种软件条件。
上面5种就是常见的产生信号方式,最后总结一下:无论是哪种方式产生信号,本质上都是由系统发送给进程的,因为操作系统是进程的管理者。
三.补充内容–核心转储
下面图中是所有的普通信号,其中大多数信号默认处理动作都是终止退出,但是有的是Term
类型的退出(比如2号,9号等),有的是Core
类型的退出(比如8号,11号等)
其中core
类型的终止退出表示具有核心转储功能。
1.什么是核心转储:
在父进程回收子进程,也就是调用waitpid
函数时,用一个输出型参数int status
用来表示子进程的退出信息。
其中有一个core dump
标志位,如果当前进程收到的异常信号是Trem
类型该标志位上就是0,Core
类型的就是1。
用一段代码进行测试,父进程创建子进程后,在子进程运行时,向子进程分别发送Trem
类型的2号信号和Core
类型的8号信号,然后父进程回收子进程打印出status,观察两个不同类型信号最后status的core dump
标志位。
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>using namespace std;int main(){pid_t id = fork();if(id==0){// childint cnt = 100;while(cnt--){cout << "I am a child process, pid: " << getpid() << endl;sleep(1);}exit(0);}// fatherint status = 0;pid_t rid = waitpid(id, &status, 0);if(rid==id){cout << "child quit info, rid: " << rid << " exit code: " << ((status >> 8) & 0xFF) <<" exit signal: " << (status & 0x7F) <<" core dump: " << ((status >> 7) & 1) << endl;}return 0;
}
因为我这里使用的是云服务器,而云服务器上的core
功能默认是关闭的,所以会看到两个都是0。
想要打开需要在命令行中输入下面的指令:
#查看特定的资源
ulimit -a#其中有一个core file size,大小为0,表示关闭
ulimit -c#开启core dump 设置对应的大小
ulimit -c 10240#关闭core dump 设置大小为0
ulimit -c 0
打开core dump
之后再重新发送2和8号信号:
发送8号信号,core dump
标志位变为1,并且生成了一个core.27353
的文件。
打开core dump
功能后,一旦进程出现异常,系统会将该进程在内存中的运行信息,dump(转储)到进程的当前目录(磁盘),并且形成core.pid
文件,这就是核心转储功能。
2.为什么要进行核心转储:
一旦进程出现错误而异常终止,前面提到过异常信号只是让用户知道进程终止退出的错误原因,而核心转储可以具体定位原始代码中,在运行过程中哪方面出错误。
用一个简单的除0错误代码来测试:
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>using namespace std;int main(){int a = 10, b = 0;int c = a / b; // 11行这里的除0错误cout << c << endl;return 0;
}
使用g++编译时带上-g
选项,编译后生成可执行文件并运行,最后除0错误而终止退出,生成一个core.1263
文件,打开gdb进入调试
直接输入core-file core.1263
后,就会自动打印出错误信息,收到8号信号而终止退出,出错原因在第11行。
核心转储功能加上调试,可以直接复现问题之后,直接定位到出错行,也就是先运行,再core-file
事后调试。
所以核心转储功能就是方便进行事后调试。
以上就是关于信号第一部分:信号概念以及信号产生的讲解,如果哪里有错的话,可以在评论区指正,也欢迎大家一起讨论学习,如果对你的学习有帮助的话,点点赞关注支持一下吧!!!