Skip to content

LV026-进程等待信号

一、阻塞进程来等待信号

1. pause() 函数

1.1 函数说明

linux下可以使用man 2 pause 命令查看该函数的帮助手册。

c
/* 需包含的头文件 */
#include <unistd.h>

/* 函数声明 */
int pause(void);

函数说明】该函数用可以使得进程暂停运行、进入休眠状态,直到进程捕获到一个信号为止。

函数参数none

返回值int类型,只有执行了信号处理函数并从其返回时,pause()才返回,在这种情况,pause()返回-1,并且将errno设置为EINTR

使用格式】一般情况下基本使用格式如下:

c
/* 需要包含的头文件 */
#include <unistd.h>

/* 至少应该有的语句 */
pause();

注意事项

(1)如果信号的默认处理动作是终止进程,则进程终止,pause函数就没有机会返回了。

(2)如果信号的默认处理动作是忽略,进程继续处于挂起状态,pause函数不返回。

(3)如果信号的处理动作是捕捉,则调用完信号处理函数之后,pause返回-1

(4)pause收到的信号如果被屏蔽,那么pause就不能被唤醒 。

1.2 使用实例

这里是一个pause的基本使用实例。

c
/* 头文件 */
#include <stdio.h>  /* perror */
#include <unistd.h> /* sleep pause */
#include <signal.h> /* signal */

typedef void (*sighandler_t)(int);
sighandler_t oldact;
void sigintHandle(int sig);

/* 主函数 */
int main(int argc, char *argv[])
{
	int ret = 0;
	oldact = signal(SIGINT, sigintHandle);
	/* 阻塞进程 */
	ret = pause();/* 被中断后才会继续向下执行 */
	printf("After pause!ret=%d\n", ret);
	while(1)
	{
		printf("Please Enter: Ctrl+c \n");
		sleep(1);
	}
	return 0;
}

void sigintHandle(int sig)
{
	static int count = 0;
	printf("I catch the SIGINT![%d times] \n", ++count);
	signal(SIGINT, oldact);
}

在终端执行以下命令编译程序:

shell
gcc test.c -Wall # 生成可执行文件 a.out 
./a.out # 执行可执行程序

然后,就会发现程序一直阻塞,直到我们按下Ctrl+c,捕获到信号后,程序才开始运行,之后终端会有以下信息显示:

shell
^CI catch the SIGINT![1 times] 
After pause!ret=-1
Please Enter: Ctrl+c 
Please Enter: Ctrl+c 
^C

程序开始运行后吗,再按一次Ctrl+c,程序便会正常终止了,而这个时候,pause()后边的语句不再执行,pause根本就没有进行返回,程序就结束了,这是因为Ctrl+c后来被改回了系统默认操作,也就是直接终止进程,根本不会给pause返回的机会。

1.3 信号驱动任务

这个例子其实是为了给下边的另一个阻塞进程等待信号的函数做铺垫。

c
/* 头文件 */
#include <stdio.h>  /* perror */
#include <unistd.h> /* alarm sleep*/
#include <signal.h> /* sigaction */

void sigactionHandle(int sig);
void mytask(void);

/* 主函数 */
int main(int argc, char *argv[])
{
	
	struct sigaction act; /* 定义一个处理信号行为的结构体变量 */
	sigset_t set;         /* 定义一个信号集变量 */

	/* 1.捕获信号 */
	/* 1.1初始化处理信号行为的结构体变量 */
	act.sa_handler = sigactionHandle;  /* 指定信号处理函数 */
	act.sa_flags = 0;                  /* a_flags指定了一组标志,这些标志用于控制信号的处理过程 */
	sigemptyset(&act.sa_mask);         /* 将信号集初始化为空 */
	/* 1.2设置捕捉信号 */
	sigaction(SIGINT, &act, NULL);     /* Ctrl + c 发送该信号 */
	sigaction(SIGHUP, &act, NULL);     /* kill -1 <PID> 发送该信号*/
	/* 2.信号阻塞设置 */
	sigemptyset(&set);     /* 信号集清空 */
	sigaddset(&set,SIGHUP);/* 向信号集添加信号 */
	sigaddset(&set,SIGINT);/* 向信号集添加信号 */
	/* 阻塞进程 */
	pause();
	while(1)
	{
		/* 以下连续发两次信号操作,任务只会执行一次,但是信号处理函数会处正常处理两次 */
		/* 若不屏蔽信号,则在任务执行期间的有信号到来的话,任务函数会被直接打断 */
		sigprocmask(SIG_BLOCK, &set, NULL);  /* 屏蔽信号,以保证任务函数执行时不会被打断 */
		mytask();/* 执行任务期间所获取的信号不会被pause接收,在到pause前就会处理掉了 */
		sigprocmask(SIG_UNBLOCK, &set, NULL);/* 取消屏蔽 */
		pause();
	}
	return 0;
}

void sigactionHandle(int sig)
{
	static int count = 0;
	printf("I catch the signal [%d] %d times!\n", sig, ++count);
}

void mytask(void)
{
	static int count = 0;
	int i = 1;
	count++;
	printf("My task start![%d times]\n", count);
	for(i = 1; i < 4; i++)
	{
		printf("i = %d\n", i);
		sleep(1);
	}
	printf("My task end![%d times]\n\n", count);
}

在终端执行以下命令编译程序:

shell
gcc test.c -Wall # 生成可执行文件 a.out 
./a.out # 执行可执行程序

然后,就会发现程序一直阻塞,直到我们按下Ctrl+c,捕获到信号后,程序才开始运行,之后我们在程序执行期间多按几次Ctrl+c按键,最后按ctrl+\退出进程,然后我们就会得到以下信息:

shell
^CI catch the signal [2] 1 times!
My task start![1 times]
i = 1
i = 2
^Ci = 3
My task end![1 times]

I catch the signal [2] 2 times!
^CI catch the signal [2] 3 times!
My task start![2 times]
i = 1
i = 2
i = 3
^CMy task end![2 times]

I catch the signal [2] 4 times!
^\退出 (核心已转储)

观察输出信息,我们发现,我们一共按下Ctrl+c组合键四次,并且自定义的信号处理函数也成功捕获并执行了四次,但是我们的任务函数仅仅是执行了两次,这是为什么呢?我们来分析一下,当我们在执行任务函数的过程中,时间长达3s,这段时间内我们按下Ctrl+c组合键,这个时候由于信号是被屏蔽的,所以任务函数并不会被打断,任务函数结束后,解除了信号的屏蔽,这个时候会直接进入我们自定义的信号处理函数,执行信号处理函数,处理完毕后,这个信号就没有了,所以后边的pause函数并没有接收到该信号,所以会一直停留在pause处继续等待信号的到来,才会继续回到循环开始,再执行一次任务函数。

2. sigsuspend() 函数

上边的pause()函数的信号驱动任务例子中,信号会被提前处理,导致pause()函数接收不到信号,想要解决这个问题,就需要将恢复信号掩码和pause()挂起进程这两个动作封装成一个原子操作,这样信号处理函数便无法在恢复信号掩码后直接执行了,这样信号就会被pause()接收到了。

2.1 函数说明

linux下可以使用man 2 sigsuspend命令查看该函数的帮助手册。

c
/* 需包含的头文件 */
#include <signal.h>

/* 函数声明 */
int sigsuspend(const sigset_t *mask);

函数说明】该函数会将参数mask所指向的信号集替换进程的信号掩码,也就是将进程的信号掩码设置为参数mask所指向的信号集,然后挂起进程,如果捕捉到一个信号并从信号处理函数返回,sigsuspend()返回,并将进程的信号掩码恢复成调用前的值;如果捕获的信号是mask信号集中的成员,将不会唤醒、会继续挂起,直到有非mask信号集中成员信号的到来。

函数参数

  • masksigset_t类型指针变量,指向一个已经初始化过的信号集。

返回值int类型,始终返回-1,并设置errno来指示错误(通常为EINTR),表示被信号所中断。如果调用失败,将errno设置为EFAULT

使用格式】一般情况下基本使用格式如下:

c
/* 需要包含的头文件 */
#include <signal.h>

/* 至少应该有的语句 */
sigset_t set_mask;
sigsuspend(&set_mask);

注意事项】调用sigsuspend()就等价于以不可中断的方式执行以下操作:

c
sigprocmask(SIG_SETMASK, &mask, &old_mask); 
pause(); 
sigprocmask(SIG_SETMASK, &old_mask, NULL);

2.2 使用实例

c
/* 头文件 */
#include <stdio.h>  /* perror */
#include <unistd.h> /* alarm sleep*/
#include <signal.h> /* sigaction */

void sigactionHandle(int sig);
void mytask(void);

/* 主函数 */
int main(int argc, char *argv[])
{
	struct sigaction act;   /* 定义一个处理信号行为的结构体变量 */
	sigset_t set, set_mask; /* 定义两个信号集变量 */

	/* 1.捕获信号 */
	/* 1.1初始化处理信号行为的结构体变量 */
	act.sa_handler = sigactionHandle;  /* 指定信号处理函数 */
	act.sa_flags = 0;                  /* a_flags指定了一组标志,这些标志用于控制信号的处理过程 */
	sigemptyset(&act.sa_mask);         /* 将信号集初始化为空 */
	/* 1.2设置捕捉信号 */
	sigaction(SIGINT, &act, NULL);     /* Ctrl + c 发送该信号 */
	sigaction(SIGHUP, &act, NULL);     /* kill -1 <PID> 发送该信号*/
	/* 2.信号阻塞设置 */
	sigemptyset(&set);     /* 信号集清空 */
	sigemptyset(&set_mask);/* 信号集清空 */
	sigaddset(&set,SIGHUP);/* 向信号集添加信号 */
	sigaddset(&set,SIGINT);/* 向信号集添加信号 */
	/* 阻塞进程 */
	pause();
	while(1)
	{
		/* 屏蔽信号,以保证任务函数执行时不会被打断 */
		sigprocmask(SIG_BLOCK, &set, NULL);
		mytask();
		sigsuspend(&set_mask);/* set_mask 为空,意思是不屏蔽信号,这样在任务函数执行期间收到的信号会使这里返回 */
	}
	return 0;
}

void sigactionHandle(int sig)
{
	static int count = 0;
	printf("I catch the signal [%d] %d times!\n", sig, ++count);
}

void mytask(void)
{
	int i = 0;
	static int count = 0;
	count++;
	printf("My task start![%d times]\n", count);
	for(i = 1; i < 4; i++)
	{
		printf("This is task function![i = %d]\n", i);
		sleep(1);
	}
	printf("My task end![%d times]\n", count);
}

在终端执行以下命令编译程序:

shell
gcc test.c -Wall # 生成可执行文件 a.out 
./a.out # 执行可执行程序

然后,就会发现程序一直阻塞,直到我们按下Ctrl+c,捕获到信号后,程序才开始运行,之后我们在程序执行期间多按几次Ctrl+c按键,最后按ctrl+\退出进程,然后我们就会得到以下信息:

shell
^CI catch the signal [2] 1 times!
My task start![1 times]
This is task function![i = 1]
^CThis is task function![i = 2]
^CThis is task function![i = 3]
My task end![1 times]
I catch the signal [2] 2 times!
My task start![2 times]
This is task function![i = 1]
This is task function![i = 2]
This is task function![i = 3]
My task end![2 times]
^\退出 (核心已转储)

可以看到,在任务函数执行期间,发出的信号在任务函数执行完毕后被捕获,并且返回,使任务函数再次执行。这样,在任务函数执行期间到来的信号就可以使任务函数再运行一次了,但是任务函数运行期间,收到多个相同信号,信号就相当于只被捕获了一次。