基本概念:
生活中的信号:
一说到信号,我们可能立马想到的是信号枪(垂直打上去就有空投的那种,博主表示还没捡到过一次,Emmmm),抛开信号枪,我们的身边无时无刻不充斥着信号,比如红绿灯,看到红灯应该停下来,直到绿灯才可以往前走,你一看到就知道是什么意思,下一步该干嘛,但是遵不遵守就看你自己了,不过我们还是应该做一个尊重交通规则的乖宝宝,弘扬正能量。计算机中的信号:
计算机中的信号也是如此,计算机在收到一个信号后,并不会去立刻处理它,而是先把它保存起来,或者说标记起来,然后找一个合适的时机去处理它,下面我们就来详解Linux下的信号机制,包括信号是如何产生的,计算机如何处理信号,如何去捕捉信号等
Linux下查看所有信号的命令:kill -l
注意:信号表中是没有32和33号信号的,所以Linux下总共有62种信号
1~31 普通信号
34~64实时信号
本文主要讨论普通信号,对实时信号不做解释
大家可以从表中看出,每个信号都有一个编号和一个宏定义名称,这些宏定义可以在signal.h中找到。这些信号各自在什么条件下产生,默认的处理动作是什么,在signal(7)中都有详细说明:man 7 signal
信号的产生
通过终端按键产生信号
用户在终端按下某些键时,终端驱动程序会发送信号给前台进程,例如Ctrl-C产生SIGINT信号(只作用于前台进程),Ctrl-Z产生SIGTSTP信号(可使前台进程停止),Ctrl-\产生SIGQUIT信号前台进程:基本不用和用户交互,优先级稍微低一些(运行前台进程: ./test.c)
后台进程:需要和用户进行交互,需要较高的相应速度,优先级别高(运行后台进程: ./test.c &)
#include <stdio.h>
int main()
{
while(1)
{
printf("Wait for Ctrl-C\n");
sleep(3);//每隔3秒打印一句话,并且让它死循环
}
return 0;
}
在打印了两句话后,我按下了Ctrl-C,进程终止,但是如果将该进程放在后台执行时,Ctrl-C就没有用了,这时候就要用kill命令了,找到该进程的进程ID后,使用kill -9命令可以直接杀死该进程
- 硬件异常产生信号
这些异常由硬件检测到并通知内核,然后内核向当前进程发送适当的信号。例如当前进程执行了除以0的指令,CPU的运算单元会产生异常,内核将这个异常解释为SIGFPE信号发送给进程。再比如当前进程访问了非法内存地址,MMU会产生异常,内核将这个异常解释为SIGSEGV信号发送给进程
3.系统调用或者用户命令产生信号
一个进程调用kill()函数可以发送信号给另一个进程,也可以发送kill命令给某个进程,只要知道目标进程的进程ID(PID)就好, kill命令其实也是通过调用kill()函数实现的,如果不明确指定信号则发送SIGTERM信号,该信号的默认处理动作是终止进程。
kill函数可以给一个指定的进程发送指定的信号。raise函数可以给当前进程发送指定的信号。
#include<signal.h>
int kill(pid_t pid,int signo);
int raise(int signo);
这两个函数都是都是成功返回0,错误返回-1,这里就不做演示了
- 软件条件产生
当内核检测到某种软件条件发生时也可以通过信号通知进程,例如闹钟超时产生SIGALRM信号,向读端已关闭的 管道写数据时产生SIGPIPE信号。如果不想按默认动作处理信号,用户程序可以调用sigaction()函数告诉内核如何处理某种信号
演示alarm(闹钟)
#include <stdio.h>
#include<signal.h>
int main()
{
int count = 0;
alarm(1);//设置闹钟时间为1秒
while(1)
{
printf("count = %d\n",count);
count++;
}//这个函数实现1秒钟内不停地数数,1秒钟到了就被alarm发送的SIGALRM信号所终止
return 0;
}
说明当count数到154391时,接收到了SIGALRM信号,进程终止掉了
信号的存储
Linux一共有31中普通信号,也就是1~31号,看到这个数字大家有没有想到bit位呢?所以在Linux下每个进程的PCB中有专门位图来存储该进程接收到的信号信息,而且只需要一个字节的31个bit位就能有效的存储这些信号,bit位的位置就是信号的编号,每个bit位对应的值就是当前信号的信息(0–表示没有收到该信号,1–表示收到该信号),如果当前进程收到了某个信号,把对应的bit位的值由0改为1就行
实时信号不用位图存储,用的是链表,了解一下即可
信号的处理方式
- 忽略该信号
- 执行该信号的默认处理动作(部分信号的默认处理动作是终止进程)
- 提供一个信号处理函数,要求内核在处理该信号时切换到用户态执行这个处理函数(前两种处理方式是在内核态进行的),这种方式称为捕捉信号
信号的捕捉
上面说过,信号的捕捉大概意思就是当进程在处理某信号时,不按照信号的默认处理动作执行,而是执行用户自定义的函数
signal函数
#include<signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
参数:signum:信号编号 handler:指向怎样捕捉该信号的函数
返回值:signal函数的返回值是一个函数指针,成功返回返回以前的处理 配置,失败返回错误码对应的错误提示。
演示代码:
#include <stdio.h>
#include <signal.h>
void my_hander(int signum)
{
printf("got a %d signal\n",signum);//打印收到的信息编号
}
int main()
{
signal(2,my_hander);//收到2号信号后,执行用户自定义函数
while(1) //(Ctrl-C产生的SIGINT信号,原默认处理动作是终止进程)
{
sleep(1);
}
return 0;
}
因为Ctrl-C产生的信号默认处理动作已被我们在该进程内更改,所以最后使用Ctrl-Z来使进程终止
操作系统处理信号时并不是在用户代码的内部空间执行,而是在内核态转向用户态时处理所有接收到的信号,下面我用一张图来说明
第2部表明操作系统不是在进程接收到信号的第一时间就去处理该信号,而是在合适的时机进行处理,图里过程间的相互转换我们可以将其当做一个无穷的标志( ∞ ),便于记忆,而且整个过程共进行了四次用户态和内核态的转换