信号 —— Linux 编程
一、简介
A 给 B 发送信号,B 收到信号之前执行自己的代码,收到信号后,不管执行到程序的什么位置都要暂停运行,去处理信号,处理完毕再继续执行,与硬件中断类似 —— 异步模式 ,但信号是软件层面上实现的中断,早期被称为“软中断”。
每个进程收到的所有信号,都是由内核负责发送的,内核处理。
1、信号的产生
(1)、按键产生,如: Ctrl+c :2) SIGINT (终止/中断) Ctrl + z : 20)SIGTSTP(暂停/停止) Ctrl+\ : 3)SIGQUIT(退出)
(2)、系统调用产生,如: kill 、raise、abort
(3)、软件条件产生,如: 定时器 alarm
(4)、硬件异常产生,如: 非法访问内存(段错误)、除 0 (浮点数除外)、内存对齐(总线错误 )
(5)、命令产生, 如: kill 命令
2、信号的状态
- 信号递达(Delivery),我们将实际执行信号的处理动作称为信号递达。
- 信号未决(Pending),我们将信号从产生到递达之间的状态称为信号未决。
- 信号阻塞(Block),进程可以选择阻塞某个信号,也可以理解为屏蔽某个信号。就像我们将讨厌的人拉入黑名单,某一天我们不讨厌了,还可以拉出来。
注意,如果收到被阻塞的信号,那么该信号将一直保持在未决状态,不会被递达。还有,阻塞和忽略是不同的,阻塞是进程没有收到该信号,而忽略是进程收到信号后的一种处理方式。
进程将收到的信号存放在PCB中,PCB中有三个与上面三个状态相对应的位图表。(因为用于表示信号的状态,并且状态只有是与否两个概念,我们中0,1来表示方便且节省空间,所以用位图。)
每个信号都在阻塞表(block)和未决表(pending)中有一个0或1的状态。还有一个handler表,类似于一个函数指针数组,每个指针都指向指定信号的处理方式。
3、信号的处理方式:
①忽略,将pending表中的信号位置由1置0,返回用户态,恢复main函数上下文。
②执行默认动作,默认动作一般为终止程序(stop或destroy),此时不返回,执行终止进程流程。
③执行自定义动作(信号捕捉),这个最重要,如果信号的处理动作是用户自定义的动作,内核会先切换到用户态执行信号处理函数,信号处理函数返回时执行系统调用sigreturn再次返回内核态。然后再从内核态返回用户态从上次异常或中断的地方继续执行。此种方式最为繁琐,共有四次内核用户之间的切换。可以参照下图理解:
4、阻塞信号集(信号屏蔽字)
将某些信号,假如集合,对它们设置屏蔽,当屏蔽 x 信号后,在收到该信号,该信号的处理将退后(解除屏蔽后)
5、未决信号集
(1)、信号产生,未决信号集中描述该信号的位立刻翻转为1,表信号处于未决状态。当信号别处理对应位翻转会为 0 。这一时刻往往非常短暂。
(2)、信号产生后由于某些原因(主要阻塞)不能抵达,这类信号的 集合称之为未决信号集。在屏蔽解除之前,信号一直处于未决状态。
6、信号的编号
kill -L
1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP
6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1
11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM
16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP
21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ
26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO 30) SIGPWR
31) SIGSYS 34) SIGRTMIN 35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3
38) SIGRTMIN+4 39) SIGRTMIN+5 40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8
43) SIGRTMIN+9 44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9 56) SIGRTMAX-8 57) SIGRTMAX-7
58) SIGRTMAX-6 59) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2
63) SIGRTMAX-1 64) SIGRTMAX
不存在编号为 0 的信号,其中 1 - 31 称之为常规信号(也叫普通信号或标准信号),34-64 称之为实时信号,驱动编程与硬件 相关。名字上区别不大。而前 32 个名字不尽相同。
可通过 man 7 signal 查看帮助文档获取。也可查看 /usr/src/linux-headers-4.4.0-31/arch/s390/include/uapi/asm/signal.h
二、信号操作函数
1、信号集的设定
#include <signal.h>
sigset_t set;
int sigemptyset(sigset_t *set); // 将某个信号集清 0 成功:0 失败 :-1
int sigfillset(sigset_t *set); // 将某个信号集置 1 成功:0 失败 :-1
int sigaddset(sigset_t *set, int signum); // 将某个信号加入信号集 成功:0 失败 :-1
int sigdelset(sigset_t *set, int signum); // 将某个信号清除信号集 成功:0 失败 :-1
int sigismember(const sigset_t *set, int signum); // 判断某个信号是否在信号集中 返回值:在集合 1; 不在集合:0; 出错: -1
sigset_t 类型的本质是位图,但不应该直接使用位操作,而应该使用上述函数,保证跨系统操作有效。
2、sigprocmask函数
一个进程的Block的信号屏蔽字(阻塞信号集,后面我们都称信号屏蔽字)规定了此进程屏蔽的信号,我们可以调用函数sigprocmask对信号屏蔽字进程检测和修改。
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
参数:
how参数指定了这个函数工作的方式,有以下三种:
SIG_BLOCK:使当前的信号屏蔽字与参数set指针指向的信号屏蔽字组成并集,构成新的信号屏蔽字。set包含了希望阻塞的新信号。
SIG_UNBLOCK:使当前的信号屏蔽字与参数set指针指向的信号屏蔽字补集的交集,构成新的信号屏蔽字。set包含了希望希望解除阻塞的信号。
SIG_SETMASK:将当前进程的信号屏蔽字设置成set所指向的值。
参数:
set指针,指向一个合适的信号屏蔽字。
参数:
oldset指针,当我们修改了当前的信号屏蔽字之后,需要保存之前的信号屏蔽字,以便回复之前的工作状态。
如果调用sigprocmask解除了对当前若干个未决信号的阻塞,则在sigprocmask函数返回前,至少有一个信号递达(非实时信号,最后一个到达的信号将被处理,实时信号具有排队机制,都会被处理)。
3、sigpending 函数
读取当前进程的未决信号集。
#include <signal.h>
int sigpending(sigset_t *set);
set 传出参数
返回值: 成功 0; 失败: -1 ,设置 errno
/*
打印未决信号集
*/
#include<signal.h>
#include<stdio.h>
#include<string.h>
void printped(sigset_t *ped)
{
int i;
for( i=1;i<32;i++ )
{
if( sigismember(ped,i) == 1 )
{
putchar('1');
}
else
{
putchar('0');
}
}
printf("\n");
}
int main(void)
{
sigset_t myset,oldset,ped;
sigemptyset(&myset);
sigaddset(&myset,SIGQUIT);
// sigaddset(&myset,SIGINT);
// sigaddset(&myset,SIGTSTP);
sigprocmask(SIG_BLOCK,&myset,&oldset);
while(1)
{
sigpending(&ped);
printped(&ped);
sleep(1);
}
return 0;
}
4、信号捕捉
1)signal 函数
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
2)sigaction 函数
此函数的功能是检查或修改指定信号的处理动作。与我们之前讲的signal功能相似
#include <signal.h>
int sigaction(int signum, const struct sigaction *act,
struct sigaction *oldact);
返回值:
成功:0 失败: -1 ,设置 errno
参数:
act : 传入参数,新的处理方式。
oldact : 传出参数,旧的处理方式。
struct sigaction {
void (*sa_handler)(int);
void (*sa_sigaction)(int, siginfo_t *, void *);
sigset_t sa_mask;
int sa_flags;
void (*sa_restorer)(void);
};
(1)、sa_handler : 指定信号捕捉后的处理函数名(即注册函数)。也可赋值为 SIG_IGN表忽略 或 SIG_DFL 表示执行默认动作。该函数可有一个int参数,通过此参数可以获得当前信号的编号。
(2)、sa_mask : 调用信号处理函数时,所要屏蔽的信号集合(信号屏蔽字)。注意:仅在处理函数被调用期间屏蔽生效,是零时性设置。
(3)、sa_flags : 通常设置为 0 ,表使用默认属性。
//信号捕获处理。
#include<stdio.h>
#include<unistd.h>
#include<signal.h>
#include<stdlib.h>
void sig_proc(int signo)
{
printf("%d siganl is catched\n",signo);
sleep(10);
printf("finish\n");
}
int main(void)
{
int ret;
struct sigaction act;
act.sa_handler = sig_proc;
sigemptyset(&act.sa_mask);
sigaddset(&act.sa_mask,SIGQUIT);
act.sa_flags = 0; // 默认属性信号捕捉函数执行期间,自动屏蔽本信号
ret = sigaction(SIGINT,&act,NULL);
// signal(SIGINT,sig_proc);
if( ret < 0 )
{
perror("sigaction error");
exit(1);
}
while(1);
return 0;
}
1、进程正常运行时,默认 PCB 中有一个信号屏蔽字,假定为 *,它决定了进程自动屏蔽哪些信号。当注册了某个信号捕捉函数,捕捉到该信号以后,要调用该函数。而该函数有可能执行很长时间。在这期间所屏蔽的信号。不由 * 来指定。而是用 sa_mask 来指定。调用完信号处理函数,再恢复为 * 。
2、xxx 信号捕捉函数执行期间,XXX 信号被自动屏蔽。
3、阻塞的常规信号不支持排队,产生多次只记录一次。(后 32 个实时信号支持排队)
三、测试举例
1. 阻塞非实时信号和实施信号,然后各发送三次。最后发送自定义信号,解除阻塞。观察非实时信号和实时信号的特点。
/*************************************************************************
> File Name: signal.c
> Author:
> Mail:
> Created Time: Sun 29 Jul 2018 04:02:53 PM CST
************************************************************************/
#include<stdio.h>
#include <unistd.h>
#include <signal.h>
//自定义信号处理程序
int myhandle(int num)
{
printf("myhandle running..\n");
printf("signal num is = %d \n",num);
//解除阻塞
if(num==SIGUSR1)
{
//解除阻塞非实时信号和实时信号
sigset_t set;
sigemptyset(&set);
sigaddset(&set,SIGINT);
sigaddset(&set,SIGRTMIN);
sigprocmask(SIG_UNBLOCK,&set,NULL);
printf("收到自定义信号,解除阻塞:%d\n",num);
}
if(num==SIGINT)
{
printf("收到非实时信号:%d\n",num);
}
if(num==SIGRTMIN)
{
printf("收到实时信号:%d\n",num);
}
}
int main()
{
pid_t pid;
int ret = 0;
struct sigaction act;
act.sa_handler = myhandle;
//act.sa_flags = SA_SIGINFO;
//注册非实时信号
if(sigaction(SIGINT,&act,NULL)<0)
{
perror("sigaction err:");
exit(-1);
}
//注册实时信号 kill -l 显示所有信号
if(sigaction(SIGRTMIN,&act,NULL)<0)
{
perror("sigaction err:");
exit(-1);
}
//注册用户自定义信号
if(sigaction(SIGUSR1,&act,NULL)<0)
{
perror("sigaction err:");
exit(-1);
}
//阻塞非实时信号和实时信号
sigset_t set;
sigemptyset(&set);
sigaddset(&set,SIGINT);
sigaddset(&set,SIGRTMIN);
sigprocmask(SIG_BLOCK,&set,NULL);
pid = fork();
if(pid==0)
{
//子进程向父进程发送信号
printf("子进程发送信号\n");
union sigval value;
//参数是子进程的pid
value.sival_int = getpid();
int i=0;
for(i=0;i<3;++i)
{
//发送3次非实时信号
if(sigqueue(getppid(),SIGINT,value)<0)
{
perror("sigqueue SIGINT error:");
exit(-3);
}
printf("发送非实时信号\n");
}
for(i=0;i<3;++i)
{
//发送3次实时信号
if(sigqueue(getppid(),SIGRTMIN,value)<0)
{
perror("sigqueue SIGINT error:");
exit(-3);
}
printf("发送实时信号\n");
}
//发送自定义信号解除阻塞,kill同sigqueue
kill(getppid(),SIGUSR1);
}
else if(pid > 0)
{
//父进程
while(1)
{
sleep(1);
}
}
else
{
//error
perror("fork error:");
exit(-2);
}
return 0;
}