1.进程与信号的关系
Unix信号是一种用于Unix信号是一种用于进程间通信的异步通知机制,用于通知进程某个事件已经发生,例如进程终止、中断等。
https://en.wikipedia.org/wiki/Signal_(IPC)
这说明进程需要识别+处理信号 ,但在接收到信号前,进程就应该知道该怎么处理信号,所以进程的信号处理机制应该属于信号的内置功能 ,在接收到信号后,进程会在合适的时间处理该信号,在"接收到处理"的这一时间段中,进程需要保存信号。
2.信号的预备知识
2.1 了解前台进程和后台进程
我们之前运行可执行文件,
都是在前台运行,如果我们在程序名后加上取地址(&),该进程就会在后台运行。
前台运行和后台运行有什么区别呢?(注意:程序是静态的,执行程序,一般就会创建进行)
我们可以用ctrl+c中断前台进程的运行,但中断不了后台进程。
这是因为后台进程不与用户直接交互,只有前台进程才会通过图形用户界面(GUI)或命令行界面(CLI)与用户进行交互,响应用户的输入和指令,且一个终端(会话/控制台窗口)一般只会有一个前台进程,可以有多个后台进程。
- 前台程序通常指的是与用户直接交互的应用程序或界面,它负责处理用户的输入和输出。
- 后台程序(也称为后台进程或服务)是在计算机系统中运行的,但不直接与用户交互的程序。它们通常在用户不知道或不需要直接干预的情况下执行任务。
2.1 Ctrl+C的本质
那么为什么ctrl+c可以中断前台进程呢?
因为键盘输入ctrl+c会被转换为前台进程识别的2号信号SIGINT,SIGINT信号的默认行为是终止自己。我们可以查看一下所有信号(本质上都是宏),一共62个。(没有32、33)
为什么进程收到2号信号SIGINT就会终断自己呢?
一般而言,进程收到信号有三类处理方式:
注意:自定义动作就是提供一个信号处理函数,要求内核在处理该信号时切换到用户态执行这个处理函数,这种方式称为信号的捕捉(Catch)。
SIGINT的默认动作就是终止自己。
我们可以编写程序验证一下,进程是因为收到信号2才退出的。
我们先认识一下signal函数,
在 C 语言中,signal() 函数用于设置一个信号处理程序,当指定的信号发生时,操作系统会调用这个信号处理程序来处理该信号。
- 参数:
- signum:要处理的信号编号(或者宏,SIGINT本质上是一个宏,其值就是2)。例如,SIGINT 表示用户中断信号(通常是 Ctrl+C),SIGSEGV 表示段错误信号,等等。
- handler:这是一个指向信号处理函数的指针,或者可以是两个特殊的宏 SIG_IGN(忽略信号)和 SIG_DFL(采用默认的信号处理行为)。
注意:函数指针
- 返回值
- 成功时,返回之前的信号处理程序(即旧的信号处理程序)。
- 失败时,返回 SIG_ERR,并设置 errno 以指示错误原因。
所以我们可以编写一个函数(自定义接收到SIGINT后进程的处理方式),用来替代SIGINT原来的默认行为(终止进程),当我们运行程序后,按下Ctrl+C,程序没有终止而是执行了我们编写函数的内容,就代表"SIGINT的默认行为确实是终止进程"。
cpp
#include <iostream>
#include <unistd.h>
#include <signal.h>
using namespace std;
//signo表示默认行为(旧的)
//函数体表示自定义的行为(新的)
void myhandler(int signo)
{
cout << "catch signal " << signo << endl;
}
int main()
{
signal(SIGINT, myhandler);//也可以用2替代SIGINT
while(true)
{
cout << "hello world" << endl;
sleep(1);
}
return 0;
}
运行结果:
验证无误。
可是这样我们就无法用"Ctrl+C"终止当前进程了,我们可以用"kill -9"杀掉这个进程,也可以在myhandler函数体中,加上进程退出语句exit(1),
这样输入一次Ctrl+C后,进程就会退出。
2.3 Ctrl+C如何变成信号的
Ctrl+C 组合键在 Linux 和其他类 Unix 系统中被用来发送一个特定的信号给正在运行的前台进程,这个信号通常被称为 SIGINT(中断信号)。以下是 Ctrl+C 如何变成信号并影响前台进程的详细过程:
1. 用户输入
- 当用户在终端或命令行界面中按下 Ctrl+C 组合键时,这是一个明确的用户操作,表示希望中断当前正在运行的前台进程。
2. 终端程序捕获
- 终端程序(如 bash、zsh 等 shell)会捕获这个按键组合。终端程序是用户与操作系统内核之间的一个接口,它负责接收用户的输入并将其转换为相应的操作或命令。
3. 发送信号给前台进程
- 终端程序识别到 Ctrl+C 后,会生成一个 SIGINT 信号,并将其发送给当前正在运行的前台进程。前台进程是指当前在终端中运行且与用户交互的进程。
4. 内核处理信号
- 操作系统内核接收到终端程序发送的信号后,会将其传递给目标前台进程。内核是操作系统的核心部分,负责管理硬件、内存、进程等系统资源。
5. 进程处理信号
- 前台进程收到 SIGINT 信号后,会根据信号的默认处理方式或自定义处理方式来做出相应的响应。
- 默认处理方式:大多数情况下,SIGINT 信号的默认处理方式是终止进程的执行。也就是说,如果进程没有捕获或忽略这个信号,那么它会被终止。
- 自定义处理方式:有些程序可能会捕获 SIGINT 信号,并进行自定义的处理,比如保存数据、释放资源等,然后再退出。这通常是通过在程序中注册一个信号处理函数来实现的。
6. 进程终止或继续
- 根据进程对 SIGINT 信号的处理方式,进程可能会被终止,也可能会继续执行(如果它捕获了信号并进行了自定义处理)。
注意事项
- Ctrl+C 发送的 SIGINT 信号是前台进程组信号,这意味着如果当前有多个进程组成了一个前台进程组,那么它们都会收到这个信号。
- 除了 SIGINT 外,Linux 系统中还有许多其他信号,如 SIGKILL(强制终止进程)、SIGTERM(请求终止进程)等,它们都可以通过不同的方式发送给进程,以实现对进程的控制和管理。
- 前台进程在运行过程中用户随时可能按下Ctrl-C而产生一个信号,也就是说该进程的用户空间代码执行到任何地方都有可能收到SIGINT信号而终止,所以信号相对于进程的控制流程来说是异步(Asynchronous)的
可参考
linux信号| 学习信号四步走 | 学习信号需要打通哪些知识脉络?
键盘输入流程
3.不是所有的信号都可以被signal()捕捉
3.1捕捉信号3(SIGQUIT)
我们用signal()来捕捉3号信号,
cpp
#include <iostream>
#include <unistd.h>
#include <signal.h>
using namespace std;
void myhandler(int signo)
{
cout << "catch signal " << signo << endl;
}
int main()
{
signal(3, myhandler);
while(true)
{
cout << "hello world" << endl;
sleep(1);
}
return 0;
}
重新编译并运行,
但我们向进程myprocess发送3号信号时,
旧的默认行为被替换成打印"catch signal:3",
3.2 捕捉信号19(SIGSTOP)
将上面代码中
cpp
signal(3, myhandler);
改成
cpp
signal(19, myhandler);
重新编译并运行,
向该程序发送19号信号后,进程直接终止,说明19号信号没有被signal捕获。
我们可以写一个循环,按照上面的方法一一测试所有的信号是否能被捕捉。
前31个信号中,只有9号信号和19号信号无法被捕捉。