进程信号很常见,比如前台运行进程时终止进程按下的快捷键
Ctrl+C
就会产生一个进程信号。这个信号会记录到该进程的PCB结构体中,当系统从内核态切换到该进程执行时就会先检查信号进行处理。
介绍
使用kill -l
查看所有的信号,值) 宏名称的结构,只用关注1~31号信号,后面的信号都是实时信号。
使用命令man 6 signal
可以找到每个宏信号代表的具体含义和动作。
信号展示
键盘产生停止信号SIGSTOP
按下Ctrl+Z
停止了sleep进程,可以看到该进程停止了,但是进程并未被杀死,只是在后台停止执行。
键盘产生中断信号SIGINT
一般来说按下Ctrl+C
中断进程,这里使用signal函数捕获该信号不执行默认操作,而执行其他函数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
static void sighandler(int arg){
printf("Get SIGINT signal\n");
}
int main(int argc,char *argv[]){
signal(SIGINT,sighandler);
printf("pid is:%d\n",getpid());
while(1);
return 0;
}
此外signal(SIGINT, SIG_DFL);
可以恢复默认的信号操作;signal(SIGINT, SIG_IGN);
可以忽略对信号处理。
软件产生报警信号SIGALRM
例子一
调用alarm(2);
函数在2s后内核给当前进程发送一个报警信号,该信号默认操作是终止当前进程,所以可以使用signal函数进行重定义操作。
1
2
3
4
5
6
7
8
9
#include <stdio.h>
#include <unistd.h>
int main(int argc,char *argv[]){
alarm(2);
printf("pid is:%d\n",getpid());
while(1);
return 0;
}
可以看到2s后闹钟响起,进程结束。
例子二
以下例子配合使用pause();
函数挂起进程,得到纯延时sleep(2);
效果:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
static int flag = 0;
static void sighander(int arg){
flag = 1;
}
int main(int argc,char *argv[]){
//以下三个组合,效果类似sleep(2)
signal(SIGALRM,sighander);
alarm(2);
pause(); //挂起当前进程,收到信号会终止挂起
if(flag == 1)
printf("pid is:%d\n",getpid());
return 0;
}
例子三
以下例子配合使用int kill(pid_t pid, int sig);
函数发送信号,这里由子进程发送警报信号kill(ppid,SIGALRM);
给父进程。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>
static int alarm_fired = 0;
static void sighander(int arg){
alarm_fired = 1;
}
int main(int argc,char *argv[]){
pid_t pid;
pid = fork();
switch(pid)
{
case -1:
perror("fork failed\n");
exit(1);
case 0:
//子进程
sleep(5);
//向父进程发送信号
kill(getppid(), SIGALRM);
exit(0);
default:;
}
//设置处理函数
signal(SIGALRM, sighander);
while(!alarm_fired)
{
printf("Hello World!\n");
sleep(1);
}
if(alarm_fired)
printf("\nI got a signal %d\n", SIGALRM);
return 0;
}
上面的程序中子进程做了休眠5s,向父进程发送信号以及结束进程,所以程序后半段只有父进程执行到了,因为警报在5s后故会输出5次“Hello world!\n”。由下图结果看到和分析一致。
信号集操作信号
前面我们看到了signal函数对信号的处理,但是一般情况下我们可以使用一个更加健壮的信号接口——sigaction函数。
其原型为int sigaction(int sig, const struct sigaction *act, struct sigaction *oact);
-
sig参数是目标信号;
-
act参数是包含执行函数指针的结构体指针;
struct sigaction结构体定义包含如下关键成员:
void (*sa_handler)(int);
执行函数指针;sigset_t sa_mask;
需要屏蔽的信号,可添加多个,通过信号集函数设置,必须使用sigprocmask函数才能写入进程控制中,其他函数只改变了该结构体的值;int sa_flags;
指定一些行为:- SA_RESETHAND:当调用信号处理函数时,将信号的处理函数重置为缺省值SIG_DFL,也就是执行一次自定义函数后重置为执行默认操作;
- SA_RESTART:如果信号中断了进程的某个系统调用,则系统自动启动该系统调用;
- SA_NODEFER :一般情况下, 当信号处理函数运行时,内核将阻塞该给定信号。但是如果设置了 SA_NODEFER标记, 那么在该信号处理函数运行时,内核将不会阻塞该信号。
-
oact是备份结构体指针,可使用NULL。
以下就是我们常用的信号集操作函数:
- sigaction设置信号的处理函数;
- sigemptyset清空信号集;
- sigaddset加入信号到信号集;
- sigprocmask将信号集应用到进程,包括应用空的信号集(即解除屏蔽);
- sigpending备份未处理的信号到结构体;
- sigismember判断信号是否在结构体中,常和sigpending联合使用,判断信号是否未处理(即是否被屏蔽);
- sigsuspend挂起,结构体中的信号被忽略,只有其他信号能唤醒进程。注意,被忽略的信号不会出现在未处理的信号中。
例子一
下列sigaction函数使得进程收到SIGINT信号后执行自定义的ouch函数,又因为sa_flags成员的值为SA_RESETHAND,所以调用一次ouch函数后会重置SIGINT信号的处理行为。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <unistd.h>
#include <stdio.h>
#include <signal.h>
void ouch(int sig)
{
printf("\nOUCH! - I got signal %d\n", sig);
}
int main()
{
struct sigaction act;
act.sa_handler = ouch;
//创建空的信号屏蔽字,即不屏蔽任何信息
sigemptyset(&act.sa_mask);
//使sigaction函数重置为默认行为
act.sa_flags = SA_RESETHAND;
sigaction(SIGINT, &act, 0);
while(1)
{
printf("Hello World!\n");
sleep(1);
}
return 0;
}
结果是按下第一次Ctrl+C
会打印信息,第二次Ctrl+C
会执行默认行为终止进程。
例子二
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
void ouch(int sig)
{
printf("OUCH! - I got signal %d\n", sig);
}
int main()
{
struct sigaction act;
sigset_t s;
pid_t pid;
int i = 100000000;
//设置处理函数为ouch,不屏蔽任何信号
act.sa_handler = ouch;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
sigaction(SIGUSR1, &act, 0);
sigaction(SIGUSR2, &act, 0);
//发送自定义信号1、2,会发现两个信号均未屏蔽,执行自定义函数打印
printf("\n<--- First send signal --->\n");
kill(getpid(),SIGUSR1);
kill(getpid(),SIGUSR2);
//准备发送自定义信号1、2,会发现两个信号均被屏蔽即不会执行自定义函数打印
printf("\n<--- Second send signal --->\n");
//屏蔽信号
printf("Block signals!\n");
sigaddset(&act.sa_mask,SIGUSR1);//将屏蔽SIGUSR1加入到结构体
sigprocmask(SIG_BLOCK, &act.sa_mask,0);//BLOCK方式:新增一个屏蔽
//下面操作为了删除结构体中的信号1,清空屏蔽结构体,起到对比验证的效果
//或者使用sigdelset(&act.sa_mask,SIGUSR1);将屏蔽SIGUSR1从结构体删除
sigemptyset(&act.sa_mask);
sigaddset(&act.sa_mask,SIGUSR2);//将屏蔽SIGUSR2加入到结构体
sigprocmask(SIG_BLOCK, &act.sa_mask,0);//BLOCK方式:新增一个屏蔽
//发送信号
kill(getpid(),SIGUSR1);
kill(getpid(),SIGUSR2);
sigpending(&s);//获取未处理的信号集备份到s结构体中
//判断成员是否在未处理的信号中,如果是说明此信号被屏蔽
if(sigismember(&s, SIGUSR1))
printf("The SIGUSR1 signal has ignored.\n");
if(sigismember(&s, SIGUSR2))
printf("The SIGUSR2 signal has ignored.\n");
//解除屏蔽(用空替换屏蔽),会发现被阻塞的信号开始响应
printf("Unblock all signals!!!\n");
sigemptyset(&act.sa_mask);//清空屏蔽结构体
sigprocmask(SIG_SETMASK, &act.sa_mask,0);//SETMASK方式:替换所有屏蔽
while(i--);//简单延时
pid = fork();
switch(pid){
case -1:
perror("fork");
exit(1);
case 0://子进程
sleep(1);
//发送唤醒信号2,此时父进程忽略信号1挂起
printf("\n<--- Third send signal --->\n");
kill(getppid(),SIGUSR2);
sleep(1);
//此时已唤醒,正常发送信号1,2
printf("\n<--- Fourth send signal --->\n");
kill(getppid(),SIGUSR1);
kill(getppid(),SIGUSR2);
exit(1);
default:;
}
sigemptyset(&act.sa_mask);//清空屏蔽结构体
sigaddset(&act.sa_mask,SIGUSR1);//将屏蔽SIGUSR1加入到结构体
sigsuspend(&act.sa_mask);//挂起,结构体中的信号被忽略,其他才放行
sleep(3);
printf("end!\n");
return 0;
}