Linux 进程信号的应用

介绍进程信号的使用

Posted by Jerry Chen on July 16, 2019

进程信号很常见,比如前台运行进程时终止进程按下的快捷键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。

以下就是我们常用的信号集操作函数:

  1. sigaction设置信号的处理函数;
  2. sigemptyset清空信号集;
  3. sigaddset加入信号到信号集;
  4. sigprocmask将信号集应用到进程,包括应用空的信号集(即解除屏蔽);
  5. sigpending备份未处理的信号到结构体;
  6. sigismember判断信号是否在结构体中,常和sigpending联合使用,判断信号是否未处理(即是否被屏蔽);
  7. 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;
}