Linux信号三部曲:产生机制、处理方式与内核接口

前言
Linux中,信号(Signal)是一种进程间通信(IPC)的机制,它用来通知进程发生了特定的事件。进程接受到信号后会根据信号的类型结合自己的处理方式做出相应的处理。

一、背景知识铺垫
1.1 信号的基本概念
Linux信号是一种异步通信机制,用于在进程之间传递事件或在系统于进程之间进行交互。当发生某个特定事件时,如:用户输入特定组合建(Ctrl+c等)、进程异常终止,系统就会向相关进程发送信号。

1.2 进程对信号的处理
进程在被设计时,就内置了识别信号的方法以及默认处理不同信号方式,当进程接收到信号时,并不一定会立即处理,这也就要求进程需要具有保存信号的能力,当等到合适的时候,进程会根据信号的类型结合自己的处理方式法,做出处理。
进程在处理信号的方式:

默认处理方式(进程内置的)
忽略信号
自定义处理方式(捕捉信号后,使用用户设定的方法)
二、信号的产生
穿插了一部分拓展知识

2.1 前台进程和后台进程
首先我们先来看下面这个程序:

#include<iostream>
#include<unistd.h>
using namespace std;

int main()
{
while(true)
{
cout<<"I'm a crazy process,PID:"<<getpid()<<endl;
sleep(1);
}
return 0;
}

当我们执行该程序后,再输入ls、pwd,可以看到指令并不会执行,进程则一直运行,当我们使用Ctrl+c就可以将进程终止,这样的进程就是前台进程。再次执行该程序:

这次我们以后台进程的形式执行该进程./可执行程序 &,可以发现,当进程执行后,我们再输入指令,此时指令是可以成功执行的,当我们使用Ctrl+c时无法终止进程。这种进程为后台进程。

Linux中,一次登录,一个终端,一般配有一个bash,而每个终端只允许有一个前台进程,可有多个后台进程,当我们执行./process时,前台进程就由bash变为了./process而键盘输入是优先被前台进程获取的,所以指令无法被执行,但前台进程./peocess接收到Ctrl+c信号时就会终止。这样我们再来理解第二个现象,当我们以后台进程运行./process是,此时bash仍被视为前台进程,当用户输入指令是仍可被接收并执行,此时再输入Ctrl+c信号./process进程并没有接收,所以没有终止。

前台进程:会独占终端,直到进程执行完成或者被挂起,在这期间终端无法接受其他命令输入,用户只能与该进程进行交互。
后台进程:不会占用终端,终端可以继续接受用户输入的其他命令,用户可以在同一个终端中同时启动多个后台进程。

前台进程:其执行过程会受到用户操作的直接影响,比如用户可以通过键盘输入来中断或暂停进程。如果终端关闭,前台进程通常会被终止,除非进行了特殊的设置。
后台进程:通常是长时间运行的,不受终端关闭的影响,除非明确地对其进行停止或重启操作。它按照自身的逻辑和任务需求在后台持续运行,不会因为用户的一些常规操作而中断

Ctrl+c本质会解释为2号信号,后面我们会验证

2.2 键盘组合键
这是我们在学习Linux过程中,比较常用的一种向进程发送信号的方式,它通过一些特定的键盘组合键,来发送一些特殊的信号,如Ctrl+c终止进程。组合有很多种,都比较简单,这里我们想要介绍的是,Ctrl+c这类组合键是如何被转换为信号,又是如何被进程接收的?

我们可以确定的意见事是,CPU不能直接从键盘读取数据(冯诺依曼体系结构),那么这个工作只能交由操作系统来完成,操作系统又是如何得知键盘有数据了呢?我们根据下图来回答:

CPU上有很多针脚,每个针脚对应一个硬件设备(键盘、网卡),当用户按下Ctrl+c组合键时,键盘发生硬件中断,产生中断号,通过对于针脚发送(充放电)给CPU,通知CPU进行相关处理,操作系统从CPU读取到中断号,通过中断号在自己的中断向量表中索引到对应方法地址,执行该方法(读取键盘),操作系统识别如果是数据直接读取,如果是组合键就将他解释成对应信号,如ctrl+c解释为2号信号,并将它读入键盘缓冲区(一切皆文件),再拷贝至用户缓冲区,被进程接收,进程执行对应处理方法。我们的信号处理方式(异步通知、事件驱动等)就是模拟的硬件中断,因此信号又被叫做软件中断。

2.3 kill 命令
对于上面的后台进程,我们可以通过kill指令的形式,给它发送信号,终止它,可以通过kill -l查看信号种类:

0~31为普通信号,34~64为实时信号(我们不研究),这里有多种信号来都能达到终止,后台进程的要求,如:2号终止进程,9号杀掉进程。
使用格式:kill -信号编号 进程PID.

常用信号

信号编号 信号名称 触发方式 作用
2 SIGINT Ctrl+C 终止前台进程
3 SIGQUIT Ctrl+\ 终止进程并生成core文件
9 SIGKILL kill -9 pid 强制终止进程,不可被捕获或忽略
15 SIGTERM kill -15 pid 正常终止进程,进程可捕获并进行清理
1 SIGHUP 终端连接断开等 让进程重新初始化或终止
18 SIGCONT kill -18 pid 继续执行暂停的进程
19 SIGSTOP kill -19 pid 暂停进程,不可被捕获或忽略
20 SIGTSTP Ctrl+Z 暂停前台进程,将其放入后台
10 SIGUSR1 kill -10 pid 用户自定义信号,用于特定程序逻辑
12 SIGUSR2 kill -12 pid 用户自定义信号,用于特定程序逻辑
2.4 系统调用
2.4.1 signal()接口
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);

功能:捕捉指定信号,执行自定义功能

参数

signum: 要捕捉信号编号
handler:函数指针,用互自定义的方法
下面的程序我们使用signal函数捕捉二号信号,执行我们自定义的方法。

#include<iostream>
#include<unistd.h>
#include<signal.h>
using namespace std;

void handler(int num)
{
cout<<"I captured Signal No."<<num<<endl;
return ;
}
int main()
{
sighandler_t _handler=signal(2,handler);
while(true)
{
cout<<"I'm a crazy process,PID:"<<getpid()<<endl;
sleep(1);
}
return 0;
}

可以看到此时我们再使用Ctrl+c,信号程序就不会终止,而是执行我们的自定义方法。上面的场景也完美的呈现了,信号的异步性(程序先调用singnal,但是此时进程没有收到信号,所以这个函数不会执行,当进程接收到信号后,就会执行对于方法)。
需要注意的是,并不是所有信号,都可以被捕捉的,我们可以通过下面的方式来验证:

#include<iostream>
#include<unistd.h>
#include<signal.h>
using namespace std;

void handler(int num)
{
cout<<"I captured Signal No."<<num<<endl;
return ;
}
int main()
{
for(int i=0;i<=64;i++)
sighandler_t _handler=signal(i,handler);

while(true)
{
cout<<"I'm a crazy process,PID:"<<getpid()<<endl;
sleep(1);
}
return 0;
}

执行这个进程时,通过kill指令向进程发信号,这里不方便演示,大家可以自己尝试(9号和19号好像不能被捕捉)。

2.4.2 kill()接口
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);

功能:向指定进程发送信号

参数

pid:接收信号的进程PID
sig:信号编号
返回值
执行成功返回零,失败饭回-1并设置errno。

示例:

#include<iostream>
#include<signal.h>
#include<sys/types.h>
using namespace std;
int main(int argc,char*argv[])
{
if(argc!=3)
{
cout << "Usage:\n\t" << argv[0] << " signum pid\n\n";
exit(1);
}
int signum=stoi(argv[1]);
int pid=stoi(argv[2]);
int n=kill(pid,signum);
if(n==-1)
{
perror("kill");
exit(1);
}

return 0;
}

现在我们就可以使用上面这个进程,来对其他进程发送信号了。

当然你可以让你的进程直接使用kill自己给自己发信号。

2.4.3 raise()接口
#include <signal.h>
int raise(int sig);

功能:让当前进程给自己发送信号

参数

sig:信号编号
#include<iostream>
#include<unistd.h>
#include<signal.h>
using namespace std;

void handler(int num)
{
cout<<"I captured Signal No."<<num<<endl;
return ;
}
int main()
{
sighandler_t _handler=signal(2,handler);
int cnt=5;
while(true)
{
cout<<"I'm a crazy process,PID:"<<getpid()<<endl;
cnt--;
if(cnt==0)break;//跳出循环
sleep(1);
}

raise(9);

cout<<"111111"<<endl;
return 0;

 

循环执行五次后跳出,执行raise()完成“自杀”,程序终止,我们可以感受到raise()其实就是分装的kill()。

2.4.4 abort()接口
#include <stdlib.h>
void abort(void);

功能:向当前进程发送SIGABRT(6号)信号,默认情况下,进程收到该信号后会立即终止,即使被用户捕获,在执行过用户提供的方法后依然终止进程。

#include<iostream>
#include<unistd.h>
#include<signal.h>
using namespace std;

void handler(int num)
{
cout<<"I captured Signal No."<<num<<endl;
return ;
}
int main()
{
sighandler_t _handler=signal(6,handler);
while(true)
{
cout<<"I'm a crazy process,PID:"<<getpid()<<endl;
abort();
}
return 0;
}

如果不调用该函数,使用指令发送六号信号,当信号被捕获后,进程不会终止,abrot函数内部做了特殊处理,才会使进程终止,你可以测试看看。

总结
特点:
异步传递(随时可能中断进程)

SIGINT(2):由键盘输入Ctrl+C产生,用于中断正在运行的进程。
SIGKILL(9):强制终止进程,不能被捕获和忽略,用于紧急情况下终止进程。
SIGSTOP(19):暂停进程,不能被捕获和忽略,可使用SIGCONT信号恢复进程运行
不可捕获的信号:

SIGKILL(9)和SIGSTOP(19)无法被捕获、阻塞或忽略,用于强制控制进程。
信号发送函数

kill函数:可以向指定进程发送指定信号,例如 kill(pid, SIGINT) 向进程号为 pid 的进程发送 SIGINT 信号。
raise函数:用于向当前进程发送信号,如 raise(SIGABRT) 向当前进程发送 SIGABRT 信号。
处理方式:

默认处理:系统为每个信号定义了默认的处理行为,如终止进程、产生核心转储、忽略信号等。
捕获信号:进程可以通过 signal 函数当接收到指定信号时,执行自定义的处理逻辑。
忽略信号:进程可以使用 signal 函数将信号设置为忽略,使进程不响应该信号,但有些信号(如 SIGKILL 和 SIGSTOP )不能被忽略。
————————————————

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。

原文链接:https://blog.csdn.net/2301_80774875/article/details/147312252

阅读剩余
THE END