Skip to content

LV025-信号集与信号阻塞

本文主要是进程通信——信号的相关笔记,若笔记中有错误或者不合适的地方,欢迎批评指正😃。

一、概述

在上边,当我们的信号来临的时候,马上就开始执行信号处理函数了,这个时候,我们的主进程就会被打断,但是,有的时候,主进程正在执行一些很重要的事情,不希望被打断,就像我们正在跟女朋友视频电话,但是好基友发来消息说“王者?”,啊,,,这,,,当然是女朋友更重要啦,我们可以等跟女朋友视频完毕再跟好友打游戏,好友发来的消息就像信号,我们不希望马上去执行,但是又希望这个信号不会消失,暂时忽略掉,当我们做完一些事后再去处理这个信号,这一部分就是关于这样如何实现的一些笔记啦。

1. 相关的概念

1.1 信号的状态

信号产生后有三种状态,分别是信号递达状态、信号未决和信号阻塞状态:

  • 信号递达delivery

实际信号执行的处理过程(3种状态:忽略,执行默认动作或者捕获)。

  • 信号未决pending

从产生到递达之间的状态。

  • 信号阻塞block

被阻塞的信号产生时将一直保持在未决状态,直到进程解除对此信号的阻塞, 才执⾏递达的动作。我们有时候不希望在接到信号时就立即停止当前执行,去处理信号,同时也不希望忽略该信号,而是延时一段时间去调用信号处理函数。这种情况就可以通过阻塞信号实现。信号的”阻塞“是一个开关动作,指的是阻止信号被处理,但不是阻止信号产生。所以信号被阻塞,实际上是当该信号产生后,进程将此信号的状态保持为未决(pending)状态,直到对该信号解除了阻塞或将该信号的动作改为忽略。

注意事项】信号阻塞和忽略的区别:

(1)忽略是进程对信号的一种处理方式,它属于信号递达状态。而阻塞是跟信号递达同级的概念。

(2)只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作。

1.2 信号的存储

信号的不同状态在进程的PCB中对应不同的表,三种状态就有三个表对应

image-20220608163829684

前两张表都是位图(BitSet)来存储的。信号被阻塞就将相应位置1,否则置0。而pending表中,若置1则表示信号存在,0则相反。也就是说,pending表中的数据是判断信号是否存在的唯一依据。上图中的三个信号状态说明如下:

  • SIGHUP信号未阻塞也未产生过,但当它递达的时候就会执行默认处理动作。
  • SIGINT信号产生过,但已被阻塞。所以暂时不能递达,虽然它的处理动作是忽略,但在没有解除阻塞之前不能忽略这个信号,这是因为进程仍有机会改变处理动作之后再解除阻塞。
  • SIGQUIT信号未产生过,一旦产⽣将被阻塞,它的处理动作是用户自定义的捕捉函数sigHandler

如果在进程解除对信号的阻塞之前,该信号产生过多次,将会如何处理呢?

  • POSIX.1允许系统递送该信号一次或多次。
  • Linux是这样规定的:常规信号(1-31)在递达之前产生多次只记一次,而实时信号(34-64)在递达之前产生多次,并可以依次放在一个队列中。

1.3 信号集

信号未决和信号阻塞标志都可以用相同的数据结构(位图)存储,所以它们可以用同一数据类型来表示,在linux中这个数据类型就是sigset_t。这个结构体在哪里定义的呢?我们还是要通过locate命令来查找一些文件的位置,

首先是signal.h文件中有如下定义:

c
typedef __sigset_t sigset_t;

额,按理说应该再去找__sigset的定义,但是回到开头一看,有这么一条语句:

c
#include <bits/sigset.h>                /* __sigset_t, __sig_atomic_t.  */

于是我们立刻可以定位到bits/sigset.h文件中,打开该文件并查找__sigset_t的定义如下:

c
/* A `sigset_t' has a bit for each signal.  */

# define _SIGSET_NWORDS (1024 / (8 * sizeof (unsigned long int)))
typedef struct
  {
    unsigned long int __val[_SIGSET_NWORDS];
  } __sigset_t;

sigset_t就被称为信号集,这个类型可以表示每个信号的“有效”或“无效”状态。在阻塞信号集其含义是该信号是否被阻塞;在未决信号集中就代表该信号是否处于未决状态。

另外,阻塞信号集也可以叫做当前进程的信号屏蔽字(Signal Mask),而这里的屏蔽我们应该理解为阻塞而不是忽略。

【注意事项】虽然信号的未决和阻塞状态都是用位图来表示的,但是不可以通过移位操作来改变信号状态,统对于信号集有特定的信号集操作函数,我们只能调用这些操作函数来改变信号状态。

二、信号集

1. 初始化信号集

1.1 sigemptyset()

1.1.1 函数说明

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

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

/* 函数声明 */
int sigemptyset(sigset_t *set);

函数说明】该函数初始化一个信号集,使其不包含任何信号,也就是将一个信号集初始化为空。

函数参数

  • setsigset_t类型指针变量,表示需要进行初始化的信号集变量。

返回值int类型,成功返回0,失败将返回-1,并设置errno

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

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

/* 至少应该有的语句 */
sigset_t sigset;
sigemptyset(&sigset);

注意事项none

1.1.2 使用实例

暂无。

1.2 sigfillset()

1.2.1 函数说明

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

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

/* 函数声明 */
int sigfillset(sigset_t *set);

函数说明】该函数初始化一个信号集为满,就是将所有信号加入该信号集。

函数参数

  • setsigset_t类型指针变量,表示需要进行初始化的信号集变量。

返回值int类型,成功返回0,失败将返回-1,并设置errno

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

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

/* 至少应该有的语句 */
sigset_t sigset;
sigfillset(&sigset);

注意事项none

1.2.2 使用实例

暂无。

2. 向信号集添加信号

2.1 sigaddset()

2.1.1 函数说明

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

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

/* 函数声明 */
int sigaddset(sigset_t *set, int signum);

函数说明】该函数向信号集中添加一个信号。

函数参数

  • setsigset_t类型指针变量,表示已经初始化过的信号集变量。
  • signumint类型,表示要添加到信号集中的信号。

返回值int类型,成功返回0,失败将返回-1,并设置errno

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

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

/* 至少应该有的语句 */
sigset_t sigset;
sigemptyset(&sigset);

sigaddset(sigset, signum);

注意事项none

2.1.2 使用实例

暂无。

3. 从信号集删除信号

3.1 sigdelset()

3.1.1 函数说明

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

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

/* 函数声明 */
int sigdelset(sigset_t *set, int signum);

函数说明】该函数从信号集中删除一个信号。

函数参数

  • setsigset_t类型指针变量,表示已经初始化过的信号集变量。
  • signumint类型,表示要从信号集中删除的信号。

返回值int类型,成功返回0,失败将返回-1,并设置errno

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

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

/* 至少应该有的语句 */
sigset_t sigset;
sigfillset(&sigset);

sigdelset(sigset, signum);

注意事项none

3.1.2 使用实例

暂无。

4. 测试信号是否在信号集

4.1 sigismember()

4.1.1 函数说明

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

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

/* 函数声明 */
int sigismember(const sigset_t *set, int signum);

函数说明】该函数检测一个信号是否在指定的信号集中。

函数参数

  • setsigset_t类型指针变量,表示已经初始化过的信号集变量。
  • signumint类型,表示要需要测试的信号。

返回值int类型,如果信号在信号集中,则返回1;如果不在信号集中,则返回0,失败则返回-1,并设置errno

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

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

/* 至少应该有的语句 */
sigset_t sigset;
sigfillset(&sigset);

if (sigismember(&sig_set, SIGINT) == 1)
    ...

注意事项none

4.1.2 使用实例

暂无。

三、阻塞信号

1. 信号掩码

Linux内核为每一个进程维护了一个信号掩码(其实就是一个信号集,严格来说就是前边提到的信号屏蔽字或者叫阻塞信号集),即一组信号。当进程接收到一个属于信号掩码中定义的信号时,该信号将会被阻塞、无法传递给进程进行处理,那么内核会将其阻塞,直到该信号从信号掩码中移除,内核才会把该信号传递给进程从而得到处理。

那么,如何将一个信号添加到信号掩码中去呢?大概有三种途径:

(1)当应用程序调用signal()sigaction()函数为某一个信号设置处理方式时,进程会自动将该信号添加到信号掩码中,这样保证了在处理一个给定的信号时,如果此信号再次发生,那么它将会被阻塞;对于sigaction()而言,是否会如此,需要根据sigaction()函数是否设置了SA_NODEFER标志而定;当信号处理函数结束返回后,会自动将该信号从信号掩码中移除。

(2)使用sigaction()函数为信号设置处理方式时,可以额外指定一组信号,当调用信号处理函数时将该组信号自动添加到信号掩码中,当信号处理函数结束返回后,再将这组信号从信号掩码中移除;通过sa_mask参数进行设置。

(3)使用sigprocmask()系统调用,可以显式地向信号掩码中添加或者移除信号。

2. sigprocmask() 函数

2.1 函数说明

linux下可以使用man命令查看该函数的帮助手册。大概会有两种函数声明形式,但是其实是一样的,形式二已经弃用,但是有些地方可能还是会看到,因此还是使用形式一比较好,形式二仅作了解吧。

c
// 声明形式一:使用的命令为`man sigprocmask`或者`man 2 sigprocmask`查询到的系统调用的函数声明形式。
#include <signal.h>
/* Prototype for the glibc wrapper function */
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);

// 声明形式二: 使用`man 3 sigprocmask`查询到的库函数中的函数声明形式。
#include <signal.h>
/* Prototype for the legacy system call (deprecated) */
int sigprocmask(int how, const old_kernel_sigset_t *set, old_kernel_sigset_t *oldset);

Tips:不过在man手册中有说明,该函数声明二已经弃用,使用形式一即可,形式二仅作了解。

函数说明】该函数向信号掩码中添加或者删除信号,并设定对信号掩码内的信号的处理方式(阻塞或不阻塞)。

函数参数

  • howint类型,用于指定信号修改的方式,可能选择有三种。how 可能的取值如下:
SIG_BLOCK 将参数set所指向的信号集内的所有信号添加到进程的信号掩码中。
SIG_UNBLOCK将参数set指向的信号集内的所有信号从进程信号掩码中移除。
SIG_SETMASK进程信号掩码直接设置为参数set指向的信号集。
- `set`:`sigset_t`类型指针变量,将参数`set`指向的信号集内的所有信号添加到信号掩码中或者从信号掩码中移除;如果参数`set`为`NULL`,则表示无需对当前信号掩码作出改动。 - `oldset`:`sigset_t`类型指针变量,如果参数`oldset`不为`NULL`,在向信号掩码中添加新的信号之前,获取到进程当前的信号掩码,存放在`oldset`所指定的信号集中;如果为`NULL`则表示不获取当前的信号掩码。

返回值int类型,成功返回0,失败将返回-1,并设置errno

2.2 一般用法

2.2.1 添加信号到信号掩码
c
/* 需要包含的头文件 */
#include <signal.h>

/* 至少应该有的语句 */
int ret = 0;
sigset_t sig_set;      /* 定义信号集 */
sigemptyset(&sig_set); /* 将信号集初始化为空 */
sigaddset(&sig_set, SIGINT);/* 向信号集中添加SIGINT信号 */
ret = sigprocmask(SIG_BLOCK, &sig_set, NULL); /* 向进程的信号掩码中添加信号 */
if (ret == -1)
{
	perror("sigprocmask error");
	exit(-1);
}
2.2.2 从信号掩码删除信号
c
/* 需要包含的头文件 */
#include <signal.h>

/* 至少应该有的语句 */
int ret = 0;
sigset_t sig_set; /* 定义信号集 */
sigemptyset(&sig_set); /* 将信号集初始化为空 */
sigaddset(&sig_set, SIGINT);/* 向信号集中添加SIGINT信号 */
ret = sigprocmask((SIG_UNBLOCK, &sig_set, NULL); /* 向进程的信号掩码中删除信号 */
if (ret == -1)
{
	perror("sigprocmask error");
	exit(-1);
}

2.3 使用实例

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

void sigactionHandle(int sig);

/* 主函数 */
int main(int argc, char *argv[])
{
	int i = 1;
	struct sigaction act;/* 定义一个处理信号行为的结构体变量 */
	sigset_t set;/* 定义一个信号集变量 */
	
	/* 1.设置捕捉信号 */
	act.sa_handler = sigactionHandle;  /* 指定信号处理函数 */
	act.sa_flags = 0;                  /* sa_flags指定了一组标志,这些标志用于控制信号的处理过程 */
	sigemptyset(&act.sa_mask);         /* 将信号集初始化为空 */
	sigaction(SIGINT, &act, NULL);     /* 捕捉信号 */
	/* 2.信号集清空 */
	sigemptyset(&set);
	/* 3.向信号集添加信号 */
	sigaddset(&set, SIGINT);
	/* 4.设置信号处理方式 */
	sigprocmask(SIG_BLOCK,&set,NULL);
	for(i = 0; i < 5; i++)/* 先为阻塞,5s后设置为不阻塞 */
	{
		printf("i = %d\n",i);
		sleep(1);
	}
    sigprocmask(SIG_UNBLOCK, &set, NULL);/* SIG_UNBLOCK: 从信号屏蔽字中删除参数set中的信号 */
	
	while(1)
	{
		printf("i = %d\n", i++);
		sleep(1);
	}
	return 0;
}

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

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

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

然后,终端会有以下信息显示:

shell
i = 0
i = 1
^Ci = 2
^Ci = 3
^Ci = 4
Get sig = 2[1 times]
i = 5
i = 6
^CGet sig = 2[2 times]
i = 7
^CGet sig = 2[3 times]
# 后边的省略  ... ...

我们会发现,在i不大于4的时候,按下Ctrl+c按键并没有效果,这是因为我们在这段时间阻塞了信号,当i>4的时候,循环结束了,这个时候之前收到的信号立刻执行一次信号处理函数,即便我们按下多次,也只捕获了一次,之后信号被从信号掩码中移除,后边就可以正常接收信号了。

3. 获取处于阻塞状态的信号

3.1 sigpending()

3.1.1 函数说明

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

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

/* 函数声明 */
int sigpending(sigset_t *set);

函数说明】该函数可以获取进程中处于等待的信号也就是处于信号未决状态的信号,也可以说是可以获取进程的信号掩码(信号屏蔽字)。

函数参数

  • setsigset_t类型,处于等待状态的信号会存放在参数set所指向的信号集中。

返回值int类型,成功返回0,失败将返回-1,并设置errno

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

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

/* 至少应该有的语句 */
sigset_t sig_set;
sigemptyset(&sig_set);
// ......
sigpending(sig_set);
/* 判断SIGINT信号是否处于等待状态 */ 
if (sigismember(&sig_set, SIGINT) == 1)
    ... ...

注意事项none

3.1.2 使用实例

暂无。