一、信号基本概念:
中断:就是终止当前代码转而执行其他代码,中断有软件中断与硬件中断。
信号的本质:是一系列非负整数(操作系统就是这么爱用整数)。UNIX常用信号1–48,Linux常用信号1–64。每个信号都有一个宏名称(宏变量),宏变量都是以SIG(signal)开头。信号的产生时间是无规律的,不知道什么时候回来,因此对于信号的处理采取异步处理方式(函数指针信号中断)。信号是不连续的;有些信号是不存在的;不同的操作系统,对应的信号的值时不同的(宏名是一样的),故在开发时应使用宏而不是使用数字以更好的兼容不同版本LINUX/UNIX。
eg:(LINUX)SIGINT是信号2,Ctrl+c产生的信号就是SIGINT,交由操作系统处理。
kill -l:查看信号
前31个(1-31)为UNIX经典信号,后31个(34-64)为硬件驱动开发的实时信号
man 7 signal查看信号章节对各信号的介绍
eg:(对应上图)
Ctrl + c:2 SIGINT 默认终止进程
Ctrl + z:20 SIGTSTP 停止(fg + id继续运行,dg + id后台运行)
Ctrl + \:3 SIGQUIT 默认终止进程
段错误:11 SIGSEGV 非法操作内存
总线错误:7 SIGBUS 非法操作文件映射
浮点数例外:8 SIGFPE CPU不能除0(致命错误)
管道信号:13 SIFPIPE 向一个没有读端的管道写操作
二、用kill函数实现kill命令:
1、kill()函数介绍:
kill函数:
int kill(pid_t pid, int signal);
pid > 0:指定pid进程
pid == 0:与发送信号同组的进程
pid < 0:发送给指定组gid = |pid|
pid == -1:向所有权限发送信号的进程发送信号
还用一种用法:(kill -0 pid测试)指定pid,并发送0,用来测试当前用户是否拥有给某个进程发送信号的权限,如图(普通用户不具备给init进程发送信号的权力,称作为:不允许的操作):
2、kill命令实现:
用kill函数实现简单的kill命令:
#include<stdio.h>
#include<stdlib.h>
#include<signal.h>
#include<sys/types.h>
int main(int argc, char *argv[])
{
if(argc < 3){
printf("./mykill signal pid");
exit(EXIT_FAILURE);
}
if(kill((pid_t)atoi(arv[2]), atoi(argv[1])) < 0){
perror("kill");
exit(EXIT_FAILURE);
}
return 0;
}
/*
Ctrl + z:进程暂停放到后台,或者启动时直接加&便启动到后台运行
fg num:从后台启动到前台运行
bg num:启动到后台运行
*/
3、测试结果如图:
我们向一个死循环的后台进程发送SIGSEGV(11)信号,并且fg重新调到前台时发现报了个“段错误(核心已转储)”,其实是因为我们发送的是SIGSEGV信号而已,并非是因为段错误。
三、信号发送与处理方式:
1、信号发送方式:
我们可以在键盘上Ctrl+c、Ctrl+\来发送一些信号,在程序出错(段错误、总线错误、浮点数例外)时也会发送一些信号,另外我们常用的发送命令的方式除了键盘还有使用kill命令/kill()函数发送,当然发送信号除了以上一些方式,还有一些函数也可以实现:
int raise(int sig);函数:只能向自己发送信号
void abort(void);函数:向自己发送指定信号SIGABRT(6)
alarm()函数:可以发送信号,但不是为了发送信号而诞生的函数。
#include<unistd.h>
unsigned int alarm(unsigned int seconds);
/*只能给当前进程(调用alarm()函数的进程)发送一个SIGALRM信号的函数。*/
alarm(n); n秒之后会发送一个闹钟信号SIGALRM。闹钟函数alarm()不阻塞,SIGALRM默认处理方式是打印“闹钟”并结束进程。如果多次调用alarm(),新的闹钟会替代原有闹钟。当参数为0,表示闹钟取消。
eg:
alarm(10); //从现在起10秒后发送一个闹钟信号,默认打印“闹钟”
alarm(2); //从现在起2秒钟后发送一个信号,取消原有10中后发送
alarm(0); //取消原有闹钟
//所以最后不会打印“闹钟”两字返回值:如果以前没有设置闹钟,或者以前设置闹钟已经结束,那么现在(新的)alarm()函数返回0,;如果以前设置alarm()没有结束,那么新的alarm()函数返回之前alarm()到发送信号剩余的时间(类似于sleep返回的剩余时间)。
2、信号处理的方式:
SIG_IGN:忽略
SIG_DEL:默认
a signal handling function:捕捉,自定义处理函数
(1)、默认处理方式:
默认处理五种方式:
①Term : Default action is to terminate the process.
默认动作是终止进程
②Ign : Default action is to ignore the signal.
默认动作是忽略信号(与SIG_IGN不是一个层级的)
③Core : Default action is to terminate the process and dump core (seecore(5)).
默认动作是终止这个进程并产生一个core文件(Core Dump用于GDB调试)
④Stop : Default action is to stop the process.
默认动作是暂停进程
⑤Cont : Default action is to continue the process if it is currently stopped.
默认动作是继续进程(如果当前这个进程被暂停)
(2)、自定义捕捉信号:
signal、sigaction函数处理(先说signal):
#incldue<signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum,sighandler_t handler);
参数:
参数与返回值都是自定义函数指针(函数名作为指针),signum是具体的信号值。handler(处理方式)除了可以设为自定义函数指针,还可以设定为SIG_IGN(忽略信号)、SIG_DFL(默认处理方式)。返回值:
成功返回信号处理的前值(return previous value of the signal handler),错误失败返回宏SIG_ERR。
3、alarm()与signal()测试:
alarm()函数在5秒后发送一个SIGALRM(14)信号,而死循环中注册了一个SIGALRM信号,并以自定义处理方式注册。循环中每打印一次alarm n就睡眠一秒,所以5秒钟打印了5次,之后就被信号中断执行自定义信号处理函数,并且直接退出,所以返回值为exit()退出码,而不是main函数中return值:
四、信号集与信号集操作函数:
1、PCB中维护的信号集:
PCB中为信号维护了两个信号集:PEND未决信号集与Block阻塞信号集。未决信号集初始值每一位均为0,如果产生一个信号则对应bit位置为1,产生的信号如果阻塞信号集对应位置为1,则阻塞。如果不再阻塞进行下一步处理(忽略、默认、自定义)时,处理完毕则未决信号对应bit位会被重新置为0。未决信号集对应位为1时,信号状态为未决态(信号产生,没有被响应);递达态:信号产生并且被相应(不阻塞)。未决信号集由内核自动设置,用户可读,而阻塞信号集用户可以自己设置一屏蔽某些信号。信号SIGKILL和SIGSTOP不能被忽略/阻塞。并且前三十一个信号不支持排队机制,后三十二个支持排队机制。
关于PEND未决信号集与Block阻塞信号集 的图解如图所示:
2、信号集处理函数:
sigset_t为信号集类型,sizeof(sifset_t)=128,每个信号站一个bit位,剩余的为预留位置,程序员可以操作的信号集为阻塞信号集,又叫做信号屏蔽字)。
信号屏蔽:在执行某些核心代码,我们不希望被信号意外中断,可采用屏蔽信号的方法(信号到了但是延时处理),防止写如等操作被意外终止。解除信号屏蔽以后再处理来过的被屏蔽的信号。
信号集的功能函数:
①清空、删除所有信号(全置为0):
int sigemptyset(sigset_t * set);
②将所有信号全加入(全置为1):
int sigfillset(sigset_t * set);
③增加1个信号:
int sigaddset(sigset_t * set,int signum);
④删除一个信号:
int sigdelset(sigset_t * set,int signum);
⑤查找元素(判断是否是现有成员):
int sigismember(const sigset_t * set,int signum);
有返回1、无返回0设置信号屏蔽字(屏蔽信号集):
int sigprocmask(int how,sigset_t * new,sigset_t * old);
参数:
how是屏蔽的方式,有三种:
SIG_BLOCK:相当于或运算,在原有的基础上加上新的屏蔽信号
SIG_UNBLOCK:相当于与运算,在原有的基础上去除屏蔽的信号
SIG_SETMASK:直接重新赋值,与原有的无关
new:新的设置的信号屏蔽字,old保存之前的信号屏蔽字,如果old为NULL就是不保存原有的信号屏蔽字。当保存了原有信号屏蔽字到old,核心代码执行完毕重新设置为old信号屏蔽字时,就是取消屏蔽的过程。获取未决信号集:
程序员虽然不能操作未决信号集,但是能够进行未决信号集的读取:
int sigpending(sigset_t * set);
3、信号屏蔽测试:
/*sigprocmask.c*/
#include<stdio.h>
#include<signal.h>
#include<sys/types.h>
void print_sigset(const sigset_t * sig_get)
{
int i;
for(i=1; i<32; i++){
if(sigismember(sig_get, i) == 1)
putchar('1');
else
putchar('0');
}
printf("\n");
}
int main(void)
{
sigset_t set, get;
printf("sizeof(sigset_t) = %d\n",sizeof(sigset_t));
sigemptyset(&set);//屏蔽字清空,即全部置为0
sigaddset(&set, SIGINT);//可以被阻塞
sigaddset(&set, SIGQUIT);//可以被阻塞
sigaddset(&set, SIGKILL);//不可以被阻塞,设置无效
sigprocmask(SIG_BLOCK, &set, NULL);
while(1){
sigpending(&get);
print_sigset(&get);
sleep(1);
}
return 0;
}
测试结果:
注意:一般,我们不对操作系统赋予了实际意义的信号进行自定义操作,而SIGUSR1与SIGUSR2这两个信号可用于用户自定义操作,因为这两个信号并没有实际意义,由程序员赋予他们意义,他们一般用于父子进程间通信,这点以后会提到。
五、可靠信号与不可靠信号:
信号分类:
1~64这62个信号分为两类:可靠信号与不可靠信号
①1–31都是不可靠信号,这种信号不支持排队,有可能丢失,是非实时信号
②34–64都是可靠信号,这种信号不支持排队,不会丢失,是实时信号
关于信号会丢失这点,我们可以做个测试:
/*
*模拟一个需要屏蔽某种信号的环境
*运行代码
*重新打开一个终端
*kill -sig pid给该进程发送信号测试
* */
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<signal.h>
void accept_signal(int sig){
printf("接收到信号%d\n",sig);
}
int main(void)
{
/*2,3为不可靠信号,会丢失一部分*/
signal(SIGINT,accept_signal);
signal(SIGQUIT,accept_signal);
/*50为可靠信号,自行排队等待*/
signal(50,accept_signal);
printf("pid = %d\n",getpid());
printf("执行普通代码,不屏蔽信号,但是未忽略的信号会中断sleep\n");
int ret = sleep(30);
printf("ret = %d\n",ret);
sigset_t set_new, set_old;
sigaddset(&set_new,SIGINT);
sigaddset(&set_new,SIGQUIT);
sigaddset(&set_new,50);
sigprocmask(SIG_SETMASK, &set_new, &set_old);/*设置屏蔽信号集*/
printf("执行关键代码,屏蔽信号,sleep不会被屏蔽的信号中断\n");
ret = sleep(30);
printf("ret = %d\n",ret);
printf("关键代码执行完毕,解除屏蔽\n");
sigprocmask(SIG_SETMASK, &set_old, NULL);/*解除屏蔽*/
return 0;
}
存放来过的阻塞信号的数据结构,类似于一个栈(当然这个数据结构不是栈,因为如果取消屏蔽的是2而不是3和50,那么2号信号也出不来),对于不可靠信号产生,如果还未处理(屏蔽阻塞状态),之后再产生的同类信号则会丢失;而可靠信号将所有未处理的信号都保存,当解除屏蔽时再取出来。结果如下: