【Linux系统】进程间的信号
代码语言:javascriptAI代码解释。
信号概念
信号是OS发送给进程的异步机制!所谓异步指的是,发送信息这个动作,并不会干扰进程本身!
对于信号的基本认识:
|
1.什么样的信号应该如何处理,是在信号产生之前早就得知了的 |
|---|
|
2.信号的处理并不是立即处理,而是等待合适的时间去处理 |
|
3.对于进程来说,其内部是以及内置了对于信号的识别以及处理方式 |
|
4.产生信号的方式很多,也就是说信号源非常多 |
信号的产生
信号的产生有很多方式
1.键盘产生信号
之前我们常见的:Ctrl + c就是信号,用于终止进程!
信号都有那些:

其中,我们只需要关注信号1~31(普通信号),信号的名字本身是宏,其真正的值就是前面的编号。
处理信号
进程收到信号之后,进程会在合适的时候,进程处理!其中处理的方式有三种
|
1.执行默认的处理动作!(相当一部分的信号默认动作都是终止进程) |
|---|
|
2.执行自定义动作! |
|
3.忽略信号,继续做自己的事! |
自定义处理
代码语言:javascript
AI代码解释
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
代码语言:javascript
AI代码解释
#include <signal.h>
#include <iostream>
using namespace std;
void sighandler(int i)
{
cout << "收到一个信号:" << i << endl;
}
int main()
{
// 将1~31的信号全部自定义
for (int i = 0; i < 32; i++)
{
signal(i, sighandler);
}
while (1)
{
}
}
代码语言:javascript
AI代码解释
hyc@hyc-alicloud:~/linux/进程信号$ ./test
^C收到一个信号:2
^C收到一个信号:2
^C收到一个信号:2
^C收到一个信号:2
^C收到一个信号:2
可以看到,我们发送了多次的Ctrl + c信号,可见Ctrl + c信号发送的就是2号信号:SIGINT
当然并不是说有的信号都可以被自定义,那不然进程就无法停止了!
前后台

当我们运行可执行程序时,我们发现Linux指令不起作用了?!这就是前后台的问题了
|
在OS中,进程分为:前台进程、后台进程 |
|---|
|
前台进程:有且仅有一个!并且只有前台进程才能接收输入的数据! |
|
后台进程:可以有多个! |
|
虽然输入的数据只有前台进程可以接收,但是输出的数据可以由前后台共同进行的! |
所以,当我们运行我们的程序时,当前这个程序就处于前台了!那么负责接收解析指令的shell程序就会退出前台!而后台程序是不能接收输入进来的数据的,所以这才导致我们输入的指令没有反应!
发送信号的本质
信号发送给进程后,进程需要在合适的时间再进行处理!那么这就意味着进程需要先将信号保存下来!后续再读取执行。
那么保存在哪里呢?答案是保存在task_struct的sigs变量中!其中sigs采用的是位图结构!比特位的位置表示信号的编号,比特位的内容(1表示收到、0表示没有收到)表示是否收到。
所以,发送信号的本质就是,向目标进程写信号 -> 修改位图!
但是task_struct中的数据属于OS内核数据!所以想要修改其数据,就只能让OS自己来修改!所以信号只能让OS来发送!
2.系统调用产生信号
kill接口
代码语言:javascript
AI代码解释
#include <signal.h>
int kill(pid_t pid, int sig);
作用:向指定的进程发送信号!
pid 参数:
pid > 0:向指定进程 ID 的进程发送信号
pid = 0:向与调用进程同进程组的所有进程发送信号
sig 参数:
代表信号编号
看看效果:
代码语言:javascript
AI代码解释
#include <signal.h>
#include <sys/types.h>
#include <unistd.h>
#include <iostream>
using namespace std;
void sighandler(int i)
{
cout << "收到一个信号:" << i << endl;
}
int main()
{
// 将1~31的信号全部自定义
for (int i = 0; i < 32; i++)
{
signal(i, sighandler);
}
kill(getpid(), 2);
}

也可以通过kill来验证一下,上面说的“并不是所有信号都可以被自定义!”
代码语言:javascript
AI代码解释
#include <signal.h>
#include <sys/types.h>
#include <unistd.h>
#include <iostream>
using namespace std;
void sighandler(int i)
{
cout << "收到一个信号:" << i << endl;
}
int main()
{
// 将1~31的信号全部自定义
for (int i = 0; i < 32; i++)
{
signal(i, sighandler);
}
for (int i = 1; i < 32; i++)
{
kill(getpid(), i);
}
}

可见,信号9并不能被“自定义”!当然不仅仅编号9,还有其他信号也不能被自定义。
abort接口
代码语言:javascript
AI代码解释
#include <stdlib.h>
void abort(void);
作用:强制终止当前的进程!
看看效果:
代码语言:javascript
AI代码解释
#include <signal.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
#include <iostream>
using namespace std;
void sighandler(int i)
{
cout << "收到一个信号:" << i << endl;
}
int main()
{
// 将1~31的信号全部自定义
for (int i = 0; i < 32; i++)
{
signal(i, sighandler);
}
abort();
for (int i = 1; i < 32; i++)
{
kill(getpid(), i);
}
}

显然,进程并没有执行for循环!这说明:即使自定义了处理函数,abort 最终仍会强制终止进程!
alarm接口
代码语言:javascript
AI代码解释
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
作用:向当前进程发送SIGLRM信号
参数 seconds:指定定时器的超时时间(单位:秒)
若 seconds > 0:内核会在 seconds 秒后向当前进程发送 SIGALRM 信号
若 seconds = 0:取消当前进程中已设置的所有 alarm 定时器(如果存在)
返回值:
若之前已设置过 alarm 定时器且未超时:返回剩余的秒数(即距离上次设置的超时时间还剩多久)
若之前未设置过 alarm 或已超时:返回 0
看看效果:
代码语言:javascript
AI代码解释
void sighandler(int i)
{
cout << "收到一个信号:" << i << endl;
}
int main()
{
// 将1~31的信号全部自定义
for (int i = 0; i < 32; i++)
{
signal(i, sighandler);
}
alarm(5);
sleep(5);
}

可见,确实发送了信号!
3.命令产生信号
代码语言:javascript
AI代码解释
killall -9 chrome # 发送SIGKILL(9),强制终止所有chrome进程
很简单就不过多说明了
4.异常产生信号
程序异常:出现“除0错误”、“野指针” 等错误!
代码语言:javascript
AI代码解释
#include <signal.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
#include <iostream>
using namespace std;
void sighandler(int i)
{
cout << "收到一个信号:" << i << endl;
}
int main()
{
// 将1~31的信号全部自定义
for (int i = 0; i < 32; i++)
{
signal(i, sighandler);
}
int a = 1;
a /= 0;
}

除零错误,发送信号8!
代码语言:javascript
AI代码解释
void sighandler(int i)
{
cout << "收到一个信号:" << i << endl;
}
int main()
{
// 将1~31的信号全部自定义
for (int i = 0; i < 32; i++)
{
signal(i, sighandler);
}
int *p = nullptr;
*p = 10;
}

同样的,野指针也发送了信号!
上面我们说到所有的信号都是由OS来进行发送的!那OS是如何得知程序出现错误的呢?
因为,OS是所有软硬资源的管理者!通过硬件协作和自身监控机制,实时捕获程序运行中的异常状态
信号的保存
信号的产生我们知道了,下面我们来看信号是如何保存的

核心概念
信号从产生到递达之间的状态,称作信号未决(Pending)
进程可以选择阻塞(Block)某个信号
被阻塞的信号会处于未决状态,直到进程解除对该信号的阻塞,才会执行递达动作
信号递达后分别有3个动作:默认动作、自定义动作、忽略!
注:忽略是递达后的动作!而阻塞是未递达的动作!
保存

task_sturct中存在三张表,信号由三张表负责保存。
handler表,保存信号的处理方法,其本质的函数指针数组。SIG_DFL表示默认方法,SIG_IGN表示忽略,使用接口sighandler(int sigon)表示自定义方法!(SIG_DFL本质是宏,其内容是被强转的整数0:(_sighandler_t) 0。)
pending表,保存信号是否被接收,其本质是位图。0表示没有接收到,1表示接收到了。
block表,保存信号是否被阻塞,其本质是位图。0表示没有被阻塞,1表示被阻塞了。
一行信息才是一个信号的完整信息!从上往下,依次表示信号1~31!
sigset_t
sigset_t是一个数据类型,表示信号集!用于记录每个信号的“有效”“无效”状态。
从上图来看,我们发现每个信号的block、pending都只使用一个bit位来表示!而并不记录这个信号产生了多少次!所以这两个信号都用sigset_t来存储,分别叫做未决信号集、阻塞信号集(屏蔽信号集)。
信号集操作函数
sigprocmask
代码语言:javascript
AI代码解释
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
返回值:若成功则为0,若出错则为-1
参数:
sigprocmask的行为由how决定,set和oldset分别用于指定新的信号集和保存旧的信号集
|
how |
控制信号屏蔽字的修改方式,仅支持 3 个预定义值(核心参数)。 |
|---|---|
|
set(输入型参数) |
指向新的信号集: 若 how 非 0,此参数指定要操作的信号集; 若为 NULL,表示不修改屏蔽字(仅用于获取旧屏蔽字)。 |
|
oldset(输出型参数) |
用于保存修改前的旧信号屏蔽字: 若为 NULL,表示不保存旧屏蔽字。 |
how的取值:

sigpending
代码语言:javascript
AI代码解释
#include <signal.h>
int sigpending(sigset_t *set);
作用:获取当前进程未决信号集。
参数:set是指向sigset_t类型的指针,用于存储未决信号集合。
返回值:成功时返回0;失败时返回-1,并设置errno。可能的错误包括EFAULT(set指向非法地址)。
演示:
代码语言:javascript
AI代码解释
#include <signal.h>
#include <iostream>
using namespace std;
void Print(sigset_t pending)
{
cout << "当前进程:" << getpid() << "pending:" << endl;
for (int i = 31; i >= 1; i--)
{
// 检查信号编号i是否在pending中
if (sigismember(&pending, i))
{
cout << 1;
}
else
{
cout << 0;
}
}
cout << endl;
}
void handler(int signo)
{
cout << "信号递达!" << endl;
sigset_t pending;
sigpending(&pending);
Print(pending);
}
int main()
{
// 捕捉2号信号
signal(2, handler);
// 屏蔽2号信号
sigset_t set, oldset;
sigemptyset(&set);
sigemptyset(&oldset); // 将信号集初始化为空!
sigaddset(&set, SIGINT); // 向指定信号集,添加信号
// 向进程阻塞信号集添加信息,让SIGINT信号被阻塞
sigprocmask(SIG_BLOCK, &set, &oldset);
int cnt = 10;
while (true)
{
// 获取当前进程的pending信号集
sigset_t pending;
sigpending(&pending);
// 打印pending信号集
Print(pending);
cnt--;
// 解除对2号信号的阻塞
if (cnt == 0)
{
cout << "解除对2号信号的阻塞\n";
sigprocmask(SIG_SETMASK, &oldset, &set);
}
sleep(1);
}
}

我们可以看到:在还没有解除2号信号阻塞时,信号确实接收到了!但没有递达。当信号阻塞解除时,信号立马递达了!
注意:
当信号准备抵达时,会先将pending表中信号对应的1修改为0!避免同一个信号被反复递达。
补充:
在Linux中信号中止的方式有两种:Core、Term
其唯一区别就在于是否会“核心转储”! Core:在进程异常退出时,会在当前路径下形成一个文件,将进程的核心数据拷贝至文件中,然后将进程退出!
而Term则会直接进行进程退出!核心转储的目的是为了实现debug!开启core dump,程序运行崩溃时,gdb core-file core,可以直接帮我们定位到错误的地方!
当然也可以通过 ulimit -a 查看,ulimit -c打开core dump功能。

信号处理
信号的保存我们知道了是如何进行的,下面我们来讲一讲信号的如何处理的
上面我们讲到了信号的处理,进程收到了信号不是立即处理!而是在合适的时间进行处理。
先直接给出结论:
适合的时间:进程从内核态,返回至用户态的时候。此时会进行信号检查(检查spending若发现接收到了信号,则再去检查block,若block没有显示阻塞,则去执行信号对应的方法!反之不满足任何一点)
我们在执行自定义方法时,OS也必须进行用户身份的转化!:用户态与内核态的转化。(因为用户身份是无法访问操作系统的内核数据的)。当然仅执行默认动作(完全由内核态完成,这是系统预定义好的)或忽略动作是不需要的用户身份转化的!
更多推荐


所有评论(0)