目录
1. 信号概念
信号就是程序要执行某件事情之前发送的信号,通知进程要做什么事情,是一个软件中断,信号是进程之间事件异步通知的一种方式。
下面用一个具体的例子解释信号。在日常生活中有非常多种的信号,比如说在过马路时,红灯不通行,绿灯通行。这就是一个发送给行人与车辆的信号。
在linux种,写了一个程序,但是这个程序因为某种原因,没有办法正常结束了,可以通过 ctrl+c
强制的结束掉这个进程。在这个过程中,使用ctrl+c
产生了一个硬件的中断,给进程发送了一个结束的信号,进程接收到这个信号并且立即处理这个信号,结束进程。
2. 信号的种类
在linux中,共有62种信号可以通过 kill -l
命令进行查看.
1. 非可靠信号(非实时信号)
在信号分类中,1-31
号信号被称为非可靠信号,即受到上述信号后,进程不会立即处理该信号,信号有可能会丢失。
2. 可靠信号(实时信号)
34-64
号信号被称为可靠信号,信号绝对不会丢失。
3. 信号的处理流程
- 在信号处理信号时,当处理完某些功能,进入内核态后,处理完异常之后,一定会调用
do_signal
函数,在函数中会检测当前有没有要处理的信号,如果有则判断是否执行默认处理,还是自定义的处理方式,处理完成后才会返回用户态,如果没有则直接返回用户态。- 执行流从内核态切换到用户态之前一定会调用
do_signal
函数处理信号- 从用户态切换到内核态时,是调用了系统调用函数,或者进程异常
4. 信号的产生方式
4.1 软件产生
1. 命令产生
kill
命令: 可以通过kill -num
的方式向进程中发送指定的信号.
2. 函数产生
int kill(pid_t pid, int sig);
// 参数: pid要发送信号的进程pid
// sig发送信号的序号
/
int raise(int sig);
// 作用:向自身发送信号,内部封装了kill函数
/
unsigned int alarm(unsigned int seconds);
// 作用:参数传递一个秒数,在事件到达之后发送一个SIGALRM信号
代码测试
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
int main()
{
//kill(getpid(), 2);
raise(2);
// 如果没有打印这一句 说明上述语句没有发送成功
printf("hello");
return 0;
}
4.2 硬件产生
ctrl+c
: 发送的为2号信号即SIGINT
ctrl+z
: 发送的为3号信号即SIGQUIT
ctrl+|
: 发送的为20号信号即SIGSTP
5. 信号的注册
信号的注册是通过改变signal位图中的比特位进行信号的注册。
linux中信号的种类共有62个,但用于存储信号的变量只有2个long的长度,共128个字节,如果一个变量存储一个是远远不够的,所以采用 位图 的方式进行存储信号,数组中对应的每一位存储一个信号,有信号则置为1,无则保持为0。并且数组的每一位没有全部使用,含有预留位置。
并且signalpending
结构体中含有一个sigqueue队列
,信号接收到进行注册时,需要在sigqueue队列中添加结点
- 非可靠信号的注册
第一次: 将信号对应的比特位置为1,在sigqueue队列中添加sigqueue结点
第二次: 只会将信号对应的比特位从1改为1,不会添加sigqueue结点- 可靠信号的注册
第一次: 将信号对应的比特位置为1,并且在sigqueue队列中添加sigqueue结点
第二次: 将信号对应的比特位从1改为1,并且还会在sigqueue队列中添加sigqueue结点
6. 信号的注销
1. 非可靠信号的注销
将信号对应在signal位图中的比特位置为0,并且将sigqueue结点从sigqueue队列当中进行出队操作。
2. 可靠信号的注销
1.将可靠信号对应的sigqueue节点从sigqueue队列中进行出队操作。
2.判断sigqueue队列当中是否还有当前可靠信号的sigqueue节点。如果没有,则将signal位图当中可靠信号对应的比特位置为0。如果有,则不会将signal位图当中的可靠信号对应的比特位置为0。
7. 信号的处理方式
7.1 信号的处理方式及基本概念
默认处理方式:SIGDFL
,在操作系统内核有对该信号的默认处理动作。
忽略处理方式:SIGCHILD
,子进程在退出时会给父进程发送一个SIGCHILD信号,但是父进程不关心这个信号
自定义处理方式:更改信号的处理方式。
- 实际执行信号的处理动作称为信号递达(Delivery)
- 信号从产生到递达之间的状态,称为信号未决(Pending)。
- 进程可以选择阻塞 (Block )某个信号。被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作.
- 注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作
7.2 信号自定义处理方式
1. 接口函数
函数一
sighandler_t signal(int signum, sighandler_t handler);
// 作用:更改信号的处理方式
// 参数: signum 待自定义处理函数的信号
// handler: 函数指针,接收一个函数地址,该函数的返回值是void
// 参数为int,当进程受到某个信号时,就会触发调用该函数,
// 并将该信号的值,传递给回调函数
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
void sigcb(int signo)
{
printf("signo: %d\n", signo);
}
int main()
{
// 当收到一下三种信号时,会调用回调函数输出进程号
signal(SIGINT, sigcb);
signal(SIGTSTP, sigcb);
signal(SIGQUIT, sigcb);
while(1)
{
sleep(1);
}
return 0;
}
函数二
int sigaction(int signum, const struct sigaction *act,
struct sigaction *oldact);
// 参数: signum 待自定义处理信号
// act 想要将信号更改为什么处理动作,是一个结构体变量:
// 参数1: void(*sa_handler)(int); 默认保存信号处理函数的函数指针
// 参数2: void(*sa_sigaction)(int, siginfo_t*,void*)
// 参数3: int sa_flags; 与上一个参数连用,一般不使用
// 参数4: sigset_t sa_mask 当正在处理信号时,会将注册的信号先放入该变量中过度,
// 当处理完该信号后,会将刚才注册的信号放入signal位图中
// 参数5: void(*sa_restorer)(void); 预留信息
// oldact:信号之前的处理动作
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
void sigcb(int signo)
{
printf("signo:%d\n", signo);
}
int main()
{
struct sigaction act;
act.sa_handler = sigcb;
act.sa_flags = 0;
sigemptyset(&act.sa_mask); // 将mask中的比特位全部置为0
// 拦截SIGINT信号
sigaction(SIGINT, &act, NULL);
while (1)
{
sleep(1);
}
return 0;
}
8. 信号的阻塞
当准备处理信号时,会判断当前信号是否为阻塞,如果该信号为阻塞,则暂时不会处理该信号。
8.1 block位图
该位图的结构与signal位图的类型相同,处理信号之前,会先查看block位图当中对应信号为的位图值是否为1,如果为1,则暂时不会去处理该信号。
8.2 接口函数设置阻塞状态
int sigprocmask(int how, const sigset_t* set, sigset_t* oldset);
// 作用:设置信号为阻塞状态
// 参数:
// how: 告诉sigprocmask函数以什么方式进行工作
// SIG_BLOCK: 设置某个信号为阻塞状态 block(new) = block(old) | set
// SIG_UNBLOCK: 解除信号为阻塞状态 block(new) = block(old) (~set)
// SIG_SETMASK: 替换原来的block位图 block(new) = set
// set: 要修改新的位图
// oldset: 在没有更改之前老的block位图
代码验证可靠信号与非可靠信号的阻塞
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
void signalcallback(int signo)
{
printf("signo: %d\n", signo);
}
int main()
{
// 更改2 40号信号的处理方式
// 2: 非可靠信号
// 40: 可靠信号
signal(2, signalcallback);
signal(40, signalcallback);
// 阻塞所有信号
sigset_t set;
sigset_t oldblock;
sigfillset(&set); // 将set中所有比特位置为1
sigprocmask(SIG_SETMASK, &set, &oldblock);
// 程序运行后,不输入内容会发生阻塞
getchar();
sigprocmask(SIG_SETMASK, &oldblock, NULL);
return 0;
}
启动两个窗口,一个窗口进行进程阻塞时的等待,另外一个窗口发送信号。
现象总结
- 信号的阻塞并不会干扰信号的注册
- 可靠信号受到几次处理几次,非可靠信号收到多次只处理一次,非可靠信号可能会丢失
- 先处理可靠信号,最后处理非可靠信号,按照队列出队的顺序进行处理
- 4、9、19号信号不能被阻塞,保证程序退出的条件
9. volatile关键字
先看一段代码
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
int g_v = 1;
void signalcallback(int signo)
{
g_v = 0;
printf("signo: %d\n", signo);
}
int main()
{
signal(2, signalcallback);
while(g_v)
{
}
return 0;
}
在这段代码中,我们阻塞了2号信号,在回调函数内部,修改了
g_v
这个全局变量的值,main函数中的while循环依赖与这个值的运行去运行,当发送二号信号之后,在回调函数内部,修改g_v
之后,程序应该会停止运行
上图中可以看出,虽然进入了回调函数,并且修改了全局变量的值,但并没有跳出循环。
因为该程序的优化等级较高,效率较快,所以程序会从寄存器中将值取出,我们更改的值存在内存中,内存中的值发生了更改但是寄存器中的值没有发生更改,所以程序没有结束。
如果在全局变量前加上volatile
关键字,就会禁止程序从寄存器中取值,保持内存的可见性。
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
// 进程程序在寄存器中取值,而是从内存中取值,保持内存可见性
volatile int g_v = 1;
void signalcallback(int signo)
{
g_v = 0;
printf("signo: %d\n", signo);
}
int main()
{
signal(2, signalcallback);
while(g_v)
{
}
return 0;
}