一、信号的概念
1.信号的基本概念
软中断信号(signal)用来通知进程发生了异步事件。进程之间可以互相通过系统调用kill发送软中断信号。内核也可以因为内部事件而给进程发送信号,通知进程发生了某个事件。信号只是用来通知进程发生了什么事件,并不给进程传递任何数据。
收到信号的进程对信号的处理方法有三种:
• 类似中断的处理程序,进程可以指定处理函数,回调处理
• 忽略某个信号,对此信号不做任何处理
• 对此信号的处理保留系统默认值,对大部分信号的缺省操作时使得进程终止。
在进程表的表项中有一个软中断信号域,在域中每一位对应一个信号,当有信号发送给进程时,对应位置位,所以进程对不同信号可以同时保留,但是无法保留信号触发次数。
2.信号的类型
(1)与进程终止相关的信号。当进程退出,或者子进程终止时,发出此类信号
(2)与进程例外事件相关的信号。如进程越界,或企图写一个只读的内存区域,或执行一个特权指令及其他各种硬件错误。
(3)与在系统调用期间遇到不可恢复条件相关的信号。如执行系统调用exec时,原有资源已经释放,而目前系统资源又已经耗尽
(4)与执行系统调用时遇到非预测错误条件相关的信号。如执行一个并不存在的系统调用。
(5)在用户态下的进程发出的信号。如进程调用系统调用kill项其他进程发送信号
(6)有终端交互的信号,如用户关闭一个终端,或按下break键等情况
(7)跟踪进程执行信号。
Arm Linux - signal(非实时信号)列表(arch/arm/include/uapi/asm/signal.h):
信号 | 值 | 执行动作 | 信号发出的原因 |
---|---|---|---|
SIGHUB | 1 | A | 终端的挂端或控制进程终止 |
SIGINT | 2 | A | 来自键盘的中断信号(ctrl+c组合键) |
SIGQUIT | 3 | C | 终端退出(ctrl+\组合键) |
SIGILL | 4 | C | 非法指令 |
SIGTRAP | 5 | C | 断点或陷阱指令(debuger使用) |
SIGABRT | 6 | C | 来自abort(3)发出的退出指令 |
SIGBUS | 7 | C | 总线错误 |
SIGFPE | 8 | C | 浮点运算错误 |
SIGKILL | 9 | AEF | Kill信号 |
SIGUSR1 | 10 | A | 用户自定义信号1 |
SIGSEGV | 11 | C | 段非法错误(无效的内存引用) |
SIGUSR2 | 12 | A | 用户自定义信号2 |
SIGPIPE | 13 | A | 管道损坏:写一个没有读端口的管道 |
SIGALARM | 14 | A | 由alarm(2)发出的信号 |
SIGTERM | 15 | A | 终止信号 |
SIGSTKFLT | 16 | 栈溢出 | |
SIGCHLD | 17 | B | 子进程结束信号 |
SIGCONT | 18 | B | 进程继续(曾被停止的进程) |
SIGSTOP | 19 | DEF | 停止进程的执行,只是暂停 |
SIGTSTP | 20 | D | 停止进程的运行(Ctrl+z组合键) |
SIGTTIN | 21 | D | 后台进程需要从终端读取数据 |
SIGTTOU | 22 | D | 后台进程需要向终端写数据 |
SIGURG | 23 | B | I/O有紧急数据到达当前进程 |
SIGXCPU | 24 | A | 查过CPU资源限制 |
SIGXFSZ | 25 | A | 文件大小超出上限 |
SIGVTALRM | 26 | A | 虚拟时钟超时 |
SIGPROF | 27 | A | Profile时钟信号描述 |
SIGWINCH | 28 | B | 窗口大小改变 |
SIGIO | 29 | B | I/O相关 |
SIGPWR | 30 | B | 关机 |
SIGSYS | 31 | 非法的系统调用 |
处理动作的字母含义:
A 缺省动作时终止进程(进程退出)
B 缺省动作时忽略此信号(将信号丢弃,不做处理)
C 缺省动作是终止进程并进行内核映像转储(dump core)
D 缺省动作是停止进程(挂起程序,但是还可以重新唤醒执行)
E 信号不能被捕获
F 信号不能被忽略
3.signal API
1.signal
设置一个函数来处理信号,即带有参数的信号处理程序
void (*signal(int sig,void (*func)(int)))(int);
实例程序:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <signal.h>
void sighandler(int);
int main(){
signal(SIGINT, sighandler);
while(1)
{
printf("开始休眠一秒钟...\n");
sleep(1);
}
return(0);
}
void sighandler(int signum){
printf("捕获信号 %d,跳出...\n", signum);
exit(1);
}
执行结果:
开始休眠一秒钟...
开始休眠一秒钟...
开始休眠一秒钟...
开始休眠一秒钟...
开始休眠一秒钟...
捕获信号 2,跳出...
2.sigaction
sigaction函数用来查询和设置信号处理方式。
函数原型:
#include <signal.h>
int sigaction(int signum,const struct sigaction *act,struct sigaction *oldact);
函数说明:
sigaction()会依参数signum指定的信号编号来设置该信号的处理函数
函数参数:
signum:指定信号的编号,出SIGKILL和SIGSTOP编号以外
act :
struct sigaction
{
void (*sa_handler)(int);
void (*sa_sigaction)(int,siginfo_t *,void *);
sigset_t sa_mask;
int sa_flag;
void (*sa_restorer)(void);
};
(1)sa_handler : signal()的参数handler相同
(2)sa_sigaction : 处理函数被调用时,不但可以得到信号编号,还可以获取被调用原因以及产生问题的上下文相关消息
(3)sa_mask:用来设置在处理该信号时暂时将sa_mask指定的信号搁置
(4)sa_restorer:此参数没有使用
(5)sa_flags:用来设置信号处理的其他相关操作
SA_NOCLDSTOP:如果参数signum为SIGCHLD,则当子进程暂停时并不会通知父进程
SA_ONESHOT/SA_RESETHAND:如果参数signum为SIGCHLD,则当子进程暂停时并不会通知父进程
SA_RESTART:被信号中断的系统调用会自行重启
SA_NOMASK/SA_NODEEFER:在处理此信号未结束前不理会此信号的再次到来
SA_SIGINFO:信号处理函数是带有三个参数的sa_sigaction
oldact : 如果参数oldact不是NULL指针,则原来的信号处理方式会由此结构sigaction返回函数返回值:成功返回0,出错则返回-1,错误原因存于error中。
实例程序:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <signal.h>
void show_handler(int sig)
{
printf("I got signal %d\n", sig);
int j;
for (j = 0; j < 5; j++)
{
printf("j = %d\n", j);
sleep(1);
}
}
/*
开启终端1,终端2
在终端1中执行程序,终端2发送命令
*/
int main()
{
int i = 0;
struct sigaction act, oldact;
act.sa_handler = show_handler;
sigaddset(&act.sa_mask, SIGQUIT);
act.sa_flags = SA_RESETHAND | SA_NODEFER;
sigaction(SIGINT, &act, &oldact);
while (i < 50)
{
sleep(1);
printf("sleeping %d, pid=%d\n", i, getpid());
i++;
}
}
运行结果:
终端1:
sleeping 5, pid=18449
I got signal 2
j = 0
j = 1
j = 2
j = 3
j = 4
sleeping 6, pid=18449
sleeping 7, pid=18449----> 之后停止运行
终端2:
kill -n SIGINT 18449 ---> main 函数捕获SIGINT ,即进入show_handler
kill -n SIGINT 18449----> 再次捕获SIGINT, 处理方式是默认方式,因为设定了SA_RESETHAND
4.实时信号和非实时信号
查询Linux支持的信号列表(Ubuntu下测试)
$ 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
列表中,编号为1~31
的信号为传统UNIX支持的信号(表1),即不可靠信号(非实时),编号34~64
的信号是后来扩充的,称为可靠信号(实时信号)。
标准信号的局限性
1.阻塞状态下信号可能会丢失,当一个信号阻塞时,这个信号即使多次发送给线程,也只会执行一次信号句柄
2.信号递送没有携带与信号有关的信息。接受到信号的进程无法区分同种信号的不同情况,也不知道信号从哪里来
3.信号的交付没有优先级。当有多个信号悬挂于一个进程时,递送的顺序不确定
实时信号的特点
- 实时信号也没有明确的含义,是由使用者自己来决定如何使用
- 进程可以接受多个同样的实时信号,而标准信号在处理时,多个标准信号会被合为一个
- 实时信号使用sigqueue发送的时候,可以携带数据(int或者pointer)
- 实时信号有事件顺序,所以实时信号会按次序被处理
- 实时信号具有优先级概念,数值越低的信号优先级越高。而实时信号和标准信号的优先级,在posix标准未定义,但是一般来说优先处理标准信号。
- 实时信号的默认行为都是结束当前进程,而标准信号的默认行为是不同的
- 实时信号是从SIGTMIN到SIGRTMAX,在使用时应该使用SIGRTMIN+n、SIGRTMAX-n的方式,而不是直接使用数值。
实时信号的使用
// 用来设置signal的处理函数
int sigaction(int signum,const struct sigaction *act,struct sigaction *oldact);
// 发送给signal给对应的pid,sig是实时信号值,value参数是传递的data参数
int sigqueue(pid_t pid,int sig,const union sigval value);
5.Kill命令
Kill命令用于向进程发送一个信号,可将指定的信号送至程序。默认的信号为SIGTERM(15),可将指定程序终止,可使用SIGKILL(9)信号强制删除程序。
kill的使用方法:
// 杀死进程
kill 12345
// 发送SIGHUP信号
kill -HUP pid
// 彻底杀死进程
kill -9 12345
6.alarm()和pause()函数
使用alarm函数可以设置一个定时器(闹钟),当定时器定时时间到达,内核会向进程发送SIGALARM信号。
函数原型:
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
函数参数和返回值:
seconds:设置定时时间,以秒为单位,如果设置为0则表示取消之前设置的alarm闹钟。
返回值:如果调用alarm()时,之前已经为该进程设置alarm的闹钟还没有超时,则该闹钟剩余值作为返回值,之前闹钟则被新的替代;否则返回0
alarm闹钟并不能循环触发,只能触发一次,若想要实现循环触发,可以在SIGALARM信号处理函数中再次调用alarm()函数设置定时器。
pause()系统调用可以使进程暂停运行,进入休眠状态,直到进程捕获到一个信号为止,只有执行了信号处理函数并返回时,pause()才返回。
函数原型:
#include <unistd.h>
int pause(void);
二、信号的执行机制
1.内核对信号的基本处理方法
内核给一个进程发送软中断信号的方法,是在进程所在的进程表项的信号域设置对应于该信号的位。
如果信号发送给一个正在睡眠的进程,那么要看该进程进入睡眠的优先级,如果进程睡眠在可被中断的优先级上,则唤醒进程;否则仅设置进程表中信号域相应的位,而不唤醒进程。进程检查是否收到信号的时机是:一个进程在即将从内核态返回到用户态时。
内核处理一个进程收到的信号的时机是在一个进程从内核态返回用户态时。所以当一个进程在内核态下运行时,软中断信号并不立即起作用,要等到将返回用户态时才处理。进程只有处理完信号才会返回用户态,进程在用户态下不会有未处理完的信号。
内核处理一个进程收到的软中断信号是在该进程的上下文中,因此进程必须处于运行状态。当进程接收到一个忽略的信号,进程丢弃该信号。如果进程收到一个要捕捉的信号,那么从内核态返回用户态时执行用户定义的函数。执行用户定义的函数的方法是,内核在用户栈上创建一个新的层,该层中将返回地址的值设置成用户定义的处理函数的地址,这样进程从内核返回弹出栈顶时就返回到用户定义的函数处,从函数返回再弹出栈顶时,才返回原来进入内核的地方。这样做的原因是用户定义的处理函数不能且不允许在内核态下执行。
三、信号集及相关操作函数
信号集数据类型如下:
typedef struct
{
unsigned long int __val[_SIGSET_NWORDS];
} __sigset_t;
信号集用来描述信号的集合,每个信号占用一位(64位),Linux所支持的所有信号可以全部或部分的出现在信号集中,主要与信号阻塞相关函数配合使用。
信号集相关定义的操作函数:
// 初始化由set指定的信号集,信号集里面的所有信号被清空,相当于64位置零
int sigemptyset (sigset_t *__set);
// 调用后,set指向的信号集中将包含linux支持的64种信号,相当于64位都置1
int sigfillset (sigset_t *__set);
// 在set指向的信号集中加入__signo信号,相当于给指定信号所对应的位置1
int sigaddset (sigset_t *__set, int __signo);
// 在set指向的信号集中删除__signo信号,相当于给定信号锁对应的位置0
int sigdelset (sigset_t *__set, int __signo);
// 判定信号signum是否在set指向的信号集中,相当于检查给定信号所对应的位是0还是1
int sigismember (const sigset_t *__set, int __signo);
实例程序(demo1):
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <stdlib.h>
void print_sigset(__sigset_t *set);
int main()
{
__sigset_t myset;
sigemptyset(&myset);
__sigaddset(&myset,SIGINT);
__sigaddset(&myset,SIGQUIT);
__sigaddset(&myset,SIGUSR1);
print_sigset(&myset);
return 0;
}
void print_sigset(__sigset_t *set)
{
int i;
for(i=1;i<_NSIG;i++)
{
if(__sigismember(set,i))
{
printf("1");
}else
{
printf("0");
}
}
printf("\n");
}
运行结果:
zx@zx-PC:~/Desktop/codes/$ cc sigset_demo1.c
zx@zx-PC:~/Desktop/codes/$ ./a.out
0110000001000000000000000000000000000000000000000000000000000000
四、信号的阻塞及未决
执行信号的处理动作称为信号递达(Delivery),信号从产生到递达之前的状态,称为未决。进程可以选择阻塞(block)某个信号,被阻塞的信号产生时保持在未决状态,直至进程解除对此信号的阻塞,才执行递达的动作。
阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作。每个进程都有一个用来描述哪些信号递送到进程时将被阻塞的信号集,该信号集中的所有信号在递送到进程后都将被阻塞。
信号在内核中的结构:
Block集(阻塞集、屏蔽集):一个进程所要屏蔽的信号,在对应要屏蔽的信号位 置1
Pending集(未决集):如果某个信号在进程的阻塞集中,则也在未决集中对应位 置1,表示该信号不能被递达,不会被处理
handler集(信号处理函数集):表示每个信号所对应的信号处理函数,当信号不在未决集中时,将被调用
信号阻塞及未决相关的函数操作:
// 根据参数how来实现对信号集的操作
int sigprocmask(int how,const sigset_t *set,sigset_t *oldset);
// 获得当前已递送到进程,却被阻塞的所有信号,在set指向的信号集中返回结果
int sigpending(sigset_t *set);
// 用于在接收到某个信号之前,临时用mask替换进程的信号掩码,并暂停进程执行,直到收到信号为止。
int sigsuspend(const sigset_t *mask);
// 阻塞信号,检测到有sigprocmask屏蔽的set中有任意信号发生,即返回此信号
int sigwaitinfo(sigset_t * set , siginfo_t * info) ;
sigprocmask支持的how操作:
- SIG_BLOCK在进程当前阻塞信号集中添加set指向信号集中的信号,相当于:mask=mask|~set
- SIG_UNBLOCK如果进程阻塞信号集中包含set指向信号集中的信号,则解除对该信号集的阻塞,相当于mask=mask|~set
- SIG_SETMASK更新进程阻塞信号集为set指向的信号集,相当于mask=set
sigsuspend返回后将恢复调用之前的信号掩码。信号处理函数完成后,进程将继续执行。该系统调用始终返回-1,并将errno设置为EINTR。
实例程序(demo2):
#include <unistd.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <fcntl.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <signal.h>
#define ERR_EXIT(m) \
do \
{ \
perror(m); \
exit(EXIT_FAILURE); \
}while(0)
void handler(int sig);
void printsigset(__sigset_t *set)
{
int i;
for (i=1; i<_NSIG; ++i)
{
if (sigismember(set, i))
putchar('1');
else
putchar('0');
}
printf("\n");
}
int main(int argc,char *argv[])
{
__sigset_t pset;
__sigset_t bset;
// 创建bset信号集
sigemptyset(&bset);
// 加入INT信号
sigaddset(&bset,SIGINT);
if (signal(SIGINT, handler) == SIG_ERR)
ERR_EXIT("signal error");
if (signal(SIGQUIT, handler) == SIG_ERR)
ERR_EXIT("signal error");
// 将bset信号集加入进程阻塞中
sigprocmask(SIG_BLOCK,&bset,NULL);
for(;;)
{
//
sigpending(&pset);
printsigset(&pset);
sleep(1);
}
}
void handler(int sig)
{
if (sig == SIGINT)
printf("recv a sig=%d\n", sig);
else if (sig == SIGQUIT)
{
__sigset_t uset;
sigemptyset(&uset);
sigaddset(&uset, SIGINT);
sigprocmask(SIG_UNBLOCK, &uset, NULL);
}
}
执行结果:
程序分析:
程序首先将SIGINT信号加入进程阻塞集中,开始没有发送SIGINT信号,所以进程未决集中没有处于未决态的信号,当按下ctrl+c时,会向继承发送SIGINT信号,因为信号在阻塞集中,所以不能递达,也就是处于未决状态,所以打印未决集合时可以看到SIGINT对应位为1,而程序并没有被中断,当按下ctrl+\
,发送SIGQUIT信号,此信号没有被进程阻塞,所以可以直接递达,执行对应的处理函数,在该处理函数中解除进程对SIGINT信号的阻塞,所以之前发送的SIGINT信号递达了,执行了对应的处理函数,但是SIGINT信号是不可靠信号,不支持排队,所以只有一个信号递达。
实例程序(demo3):
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <error.h>
#include <string.h>
#include <unistd.h>
#define MYSIGNAL SIGRTMIN+5
//#define MYSIGNAL SIGTERM
void sig_handler(int signum)
{
psignal(signum,"catch a signal");
}
int main(int argc,char **argv)
{
__sigset_t block,pending;
int sig,flag;
// 设置信号的handler
signal(MYSIGNAL,sig_handler);
// 屏蔽此信号
sigemptyset(&block);
sigaddset(&block,MYSIGNAL);
printf("block signal\n");
sigprocmask(SIG_BLOCK,&block,NULL);
// 发送两次信号,看信号会被触发多少次
printf("---> send a signal --->\n");
kill(getpid(), MYSIGNAL);
printf("---> send a signal --->\n");
kill(getpid(), MYSIGNAL);
/* 检查当前的未决信号 */
flag = 0;
sigpending(&pending);
for (sig = 1; sig < _NSIG; sig++) {
if (sigismember(&pending, sig)) {
flag = 1;
psignal(sig, "this signal is pending");
}
}
if (flag == 0) {
printf("no pending signal\n");
}
/* 解除此信号的屏蔽, 未决信号将被递送 */
printf("unblock signal\n");
sigprocmask(SIG_UNBLOCK, &block, NULL);
/* 再次检查未决信号 */
flag = 0;
sigpending(&pending);
for (sig = 1; sig < _NSIG; sig++) {
if (sigismember(&pending, sig)) {
flag = 1;
psignal(sig, "a pending signal");
}
}
if (flag == 0) {
printf("no pending signal\n");
}
return 0;
}
执行结果:
程序分析:
两次执行结果不同:
第一次连续发送两次可靠信号,解除阻塞后,都递达,说明可靠信号支持排队
第二次连续发送两次不可靠信号,解除阻塞后,只有一个递达,说明不可靠信号不支持排队
参考文章:
https://zhuanlan.zhihu.com/p/61882365
https://www.cnblogs.com/mickole/p/3191281.html#:~:text=Linux%E6%89%80%E6%94%AF%E6%8C%81,%E5%85%B3%E5%87%BD%E6%95%B0%E9%85%8D%E5%90%88%E4%BD%BF%E7%94%A8%E3%80%82