为了理解信号,首先来看一个场景:
1.用户输入命令,在shell下启动一个前台进程
2.用户按下Ctrl+C键,此时会产生一个硬件中断
3.如果CPU当前正在执行这个进程的代码,则改进程的用户与空间代码暂停执行,CPU从用户态切换带内核态处理硬件中断
4.终端驱动程序将Ctrl+C键解释成一个SIGINT信号,记在该进程的PCB中(也可以说发送了一个SIGINT信号给该进程)
5.当某个时刻要从内核返回到用户空间代码继续执行之前,首先处理PCB中记录的信号,发现有一个SIGINT信号待处理,而这个信号的默认处理动作是终止进程,所以直接而终止进程而不返回它的用户空间代码执行
用kill -l 命令查看一下系统信号列表:
每个信号都有一个编号和宏定义名称,上述信号的产生条件和默认处理动作在signal(7)中有详细说明:
man 7 signal
产生信号的方式:
1.用户在终端按下一些按键(如Ctrl+C等)
2.硬件异常产生信号
3.一个进程调用kill(2)函数可以发送信号给另一个进程
4.软件条件产生
信号处理方式:
1.忽略该信号
2.执行该信号的默认处理动作
3.执行用户自定义动作(信号的捕捉)
下面详细介绍一下信号的产生:
1.通过终端按键产生信号
SIGINT的默认处理动作是终止进程,SIGQUIT的默认处理动作是终止进程Core Dump(当一个进程要异常终止时,可以选择把进程的用户空间内存数据全部保存到磁盘上,文件名通常是core,这叫做Core Dump。默认是不允许产生core文件的,因为core文件中可能包含用户密码等信息,在开发调试阶段可以使用ulimit命令改变这个限制,允许产生core文件,允许core文件最大为1024K)
写一个死循环验证一下:
运行上图程序,在终端输入Ctrl+\(注意这里Ctrl+C并不能展示出Core Dump)
2.调用系统函数向进程发信号
首先在后台执行死循环程序,然后用kill命令给它发SIGSEGV信号:
· 之所以要多按一次回车才显示Segmentation fault,是因为在7715进程中止掉之前已经回到了Shell提示符等待用户输入下一条命令,Shell不希望Segmentation fault信息和用户的输入交错在一起,所以等待用户输入之后才显示。
· 指定发送某种信号的kill命令可以有多种写法,上面的命令还可以写成 kill -11 7715 ,11是信号SIGSEGV的编号。
kill命令是调用kill函数实现的,kill函数可以给指定的一个进程发送指定的信号。raise函数可以给当前进程发送指定的信号(自己给自己发信号)
#include <signal.h>
int kill(pid_t pid,int signo);
int raise(int signo);
这两个函数都是成功返回0,错误返回-1
abort函数使当前进程接收到信号而异常终止:
#include <stdlib.h>
void abort(void)
就像exit函数一样,abort函数总是会成功,所以没有返回值
3.由软件条件产生信号
SIGPIPE是一种由软件条件产生的信号,在管道(https://blog.csdn.net/tangduobutian/article/details/79638121)中已经介绍过了,以下主要介绍alarm函数和SIGALRM信号:
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
调用alarm函数可以设定一个闹钟也就是告诉内核在second秒之后给当前进程发SIGALRM信号,该信号的默认处理动作是终止当前进程。
这个函数的返回值是0或者是以前设定的闹钟时间还余下的秒数
例:
#include <stdio.h>
#include <unistd.h>
int main()
{
int count=14;
alarm(3);
for(;1;count++){
printf("count == %d\n",count);
}
return 0;
}
此程序的作用是3秒钟之内不停地数数,3秒到了就被SIGALRM信号终止.
阻塞信号
1.信号的一些其他相关常见概念
· 实际执行信号的处理动作称为信号递达(Delivery)
· 信号从产生到递达之间的状态,称为信号未决(Pending)
· 进程可以选择阻塞(Block)某个信号
· 被阻塞的信号产生是将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作
· 阻塞和忽略是不同的,只要信号被阻塞就不会被递达,而忽略是在递达之后可选的一种处理动作
2.在内核中的表示
信号在内核中的表示示意图
从上图可知,每个信号只有一个bit的未决标志,非0即1,不记录该信号产生了多少次,阻塞标志也是这样表示的。因此,未决和阻塞标志可以利用相同的数据类型sigset_t来存储,sigset_t称为信号集,这个类型可以表示每个信号的“有效”或“无效”状态。
4.信号集操作函数
#include <signal.h>
int sigemptyset(sigset_t *set);//初始化set所指向的信号集,使其中所有信号的对应bit清零,表示该信号不包含任何有效信号
int sigfillset(sigset_t *set);//初始化set所指向的信号集使其中所有信号的对应bit置位,表示该信号集的有效信号包含系统支持的所有信号
int sigaddset(sigset_t *set,int signo);
int sigdelset(sigset_t *set,int signo);
int sigismember(const sigset_t *set,int signo);
这四个函数的返回值都是成功返回0,失败返回-1;
sigismember是一个布尔函数,用于判断一个信号集的有效信号中是否包含某种信号
注意:在使用sigset_t类型的变量之间,一定要调用sigemptyset或sigfillset做初始化,使信号处于确定的状态。
sigprocmask(调用该函数可以读取或更改进程的信号屏蔽字(阻塞信号集))
#include <signal.h>
int sigprocmask(int how,const sigset_t *set,sigset_t *oset);
返回值:成功返回0,失败返回-1
sigpending
#include <signal.h>
int sigpending(sigset_t *set);
读取当前进程的未决信号集,成功返回0,出错返回-1
例:
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
int printsigset(sigset_t *set)
{
int i = 0;
for(;i<32;i++){
if(sigismember(set,i)){ //判断指定信号是否在目标集合中
putchar('1');
}else{
putchar('0');
}
}
puts("");
}
int main()
{
sigset_t s,p;
sigemptyset(&s); //定义信号集对象,并清空初始化
sigaddset(&s,SIGINT);
sigprocmask(SIG_BLOCK,&s,NULL); //设置阻塞信号集,阻塞SIGINT信号
while(1){
sigpending(&p); //读取未决信号集
printsigset(&p);
sleep(3);
}
return 0;
}
测试结果:
由于阻塞了SIGINT信号,按Ctrl+C将会使SIGINT信号处于未决状态,按Ctrl+\仍然可以终止程序,因为SIGQUIT信号没有阻塞。