一、相关概念
- 信号递达:实际执行信号的处理动作称为信号递达(Delivery),包括默认,忽略和自定义捕捉。
- 信号未决:信号从产生到递达之间的状态称为信号未决(Pending)。因此上面提到的信号位图又被称为Pending位图。
- 信号阻塞(屏蔽):进程可以选择阻塞 (Block )某个信号。被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作.
注意:阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作。
二、管理信号的数据结构
在进程PCB中,信号的pending位图、block位图和handler函数指针数组是用于管理信号处理的数据结构。
-
Pending位图:每个进程都有一个pending位图,用于记录当前已经被该进程接收但尚未处理的信号(未决信号)。当一个信号被接收时,对应的位会被设置为1,表示该信号处于未决状态,直到信号递达才清除该标志。进程可以通过检查pending位图来确定是否有未处理的信号。在Linux中,可以使用
sigpending
函数来获取当前进程的pending位图。 -
Block位图:每个进程都有一个block位图,用于指定当前被阻塞的信号。当一个信号被阻塞时,对应的位会被设置为1,表示该信号被阻塞,暂时不会被递达。进程可以通过设置block位图来控制哪些信号被阻塞,以避免进程在关键时刻被中断。在Linux中,可以使用
sigprocmask
函数来设置和获取当前进程的block位图。 -
Handler函数指针数组:每个进程都有一个handler函数指针数组,用于管理信号处理函数。该数组的索引对应于信号的编号,数组的元素是函数指针,指向相应信号的处理函数。当进程接收到一个信号时,会根据信号的编号在handler函数指针数组中查找对应的处理函数,并调用该函数来处理信号。在Linux中,可以使用
signal
和sigaction
函数来设置和获取信号处理函数。
提示:
- signal函数注册信号处理程序的原理:将信号处理函数的指针填入到handler数组对应信号编号的位置。
- pending位图、block位图和handler函数指针数组是针对每个进程而言的,每个进程都有自己独立的位图和数组。这样可以实现不同进程对信号的独立管理和处理。
信号的处理过程:
在上图的例子中:
- SIGHUP(1)信号未阻塞也未产生过,当它递达时执行默认处理动作。
- SIGINT(2)信号产生过,但正在被阻塞,所以暂时不能递达。虽然它的处理动作是忽略,但在没有解除阻塞之前不能忽略这个信号,因为进程仍有机会改变处理动作之后再解除阻塞。
- SIGQUIT(3)信号产生过,也未被阻塞,所以直接递达处理。它的处理动作是用户自定义程序。通过信号编号索引handler数组中的函数指针,并执行自定义处理程序。
三、信号集
3.1 sigset_t类型
Pending位图和Block位图又被称为信号集。在Linux系统中,sigset_t类型是一个用于表示信号集的数据类型。它是一个位向量,每个位表示一个特定的信号。这个类型可以表示每个信号的“有效”或“无效”状态。
在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞,而在未决信号集(Pending信号集)中“有效”和“无效”的含义是该信号是否处于未决状态。
相关概念:
- 阻塞信号集也叫做当前进程的信号屏蔽字(Signal Mask)。
- 未决信号集也叫做Pending信号集。
- Handler函数指针数组也叫做Handler处理方法表。
3.2 信号集操作函数
系统不允许用户直接对信号集进行位操作,而是提供了一套对应的信号集操作函数。
在Linux系统中,可以使用以下函数来操作信号集:
-
int sigemptyset(sigset_t *set)
:将信号集set初始化为空集,使其中所有信号对应的bit位清0,表示该信号集不包含任何有效的信号。成功返回0,失败返回-1。 -
int sigfillset(sigset_t *set)
:将信号集set初始化为包含所有信号的集合,使其中所有信号对应的bit位置1。成功返回0,失败返回-1。 -
int sigaddset(sigset_t *set, int signum)
:将信号signum添加到信号集set中。成功返回0,失败返回-1。 -
int sigdelset(sigset_t *set, int signum)
:将信号signum从信号集set中删除。成功返回0,失败返回-1。 -
int sigismember(const sigset_t *set, int signum)
:判断信号signum是否在信号集set中,如果在则返回1,不在则返回0,错误返回-1并设置errno。
这些函数都需要传入一个sigset_t类型的指针作为参数,用于指定要操作的信号集。其中,signum参数表示信号的编号,可以使用预定义的宏如SIGINT、SIGTERM等来表示具体的信号。
注意:在使用sigset_ t类型的变量之前,一定要调用sigemptyset或sigfillset做初始化,使信号集处于确定的状态。初始化sigset_t变量之后就可以在调用sigaddset和sigdelset在该信号集中添加或删除某种有效信号。
3.3 相关的系统调用接口
3.3.1 sigpending
sigpending函数用于获取当前被阻塞的未决信号集。未决信号是指已经发送给进程但尚未被处理的信号。
函数原型如下:
int sigpending(sigset_t *set);
参数:
- 参数set是一个指向sigset_t类型的指针,用于存储获取到的未决信号集。(输出型)
返回值:
- 成功:返回0
- 失败:返回-1,并设置errno来指示错误的原因。
通常,sigpending函数用于在信号处理函数中查询当前被阻塞的未决信号集,以便根据需要进行相应的处理。
3.3.2 sigprocmask
sigprocmask函数用于获取或更改进程的信号屏蔽字,即设置或修改进程当前阻塞的信号集。
函数原型如下:
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
参数:
-
参数how指定了信号屏蔽字的修改方式,可以是以下三个值之一:
- SIG_BLOCK:将set指向的信号集中的信号添加到当前信号屏蔽字中。(mask = mask | set)
- SIG_UNBLOCK:将set指向的信号集中的信号从当前信号屏蔽字中移除。(mask = mask & ~set)
- SIG_SETMASK:将当前信号屏蔽字设置为set指向的信号集。(mask = set)
-
参数set是一个指向sigset_t类型的指针,如果set是非空指针,则用于指定要设置的信号集。(输入型)
-
参数oldset是一个指向sigset_t类型的指针,如果oldset是非空指针,则用于存储修改前的信号屏蔽字。如果不需要保存旧的信号屏蔽字,可以将其设置为NULL。(输出型)
返回值:
- 成功:返回0
- 失败:返回-1,并设置errno来指示错误的原因。
通过调用sigprocmask函数,可以控制进程对特定信号的阻塞和解除阻塞。这对于在特定情况下暂时屏蔽某些信号的处理,或者恢复之前的信号屏蔽状态非常有用。
四、测试程序
4.1 尝试捕捉所有普通信号
测试代码:
mysignal.cc:尝试捕捉[1,31]号,所有的普通信号
void CatchSig(int signum)
{
cout << "捕获了一个信号:" << signum << endl;
}
void test1()
{
for (int i = 1; i <= 31; ++i)
{
signal(i, CatchSig);
}
while(1);
}
sendsig.sh:编写shell脚本,发送[1,31]号信号
#!/bin/bash
i=1
id=$(pidof mysignal) //获取mysignal进程的PID
while [ $i -le 31 ]
do
if [ $i -eq 9 ];then //跳过SIGKILL(9)信号
let i++
continue
fi
if [ $i -eq 19 ];then //跳过SIGSTOP(19)信号
let i++
continue
fi
kill -$i $id //kill命令发送信号
echo "kill -$i $id"
let i++
sleep 1
done
运行结果:
SIGKILL(9)信号 和 SIGSTOP(19)信号 不能被捕捉;当发送19号信号暂停进程后,再发送SIGCONT(18)信号,进程会继续运行。
4.2 先阻塞2号信号,再解除阻塞,打印观察pending信号集
测试代码:先阻塞2号信号,再向该进程发送2号信号,最后解除阻塞。过程中,打印观察pending信号集。
void ShowPending(const sigset_t *ppset)
{
for (int i = 1; i <= 31; ++i)
{
if (sigismember(ppset, i))
cout << "1";
else
cout << "0";
}
cout << endl;
}
void test2()
{
// 需要捕捉2号信号,否则解除阻塞后进程会立即退出。
signal(2, CatchSig);
// 定义信号集对象
sigset_t bset, obset, pset;
// 初始化信号集对象
sigemptyset(&bset);
sigemptyset(&bset);
sigemptyset(&bset);
// 将要进行屏蔽的信号添加到bset
sigaddset(&bset, SIGINT);
// 阻塞bset信号集 [默认情况进程不会对任何信号进行block]
cout << "阻塞2号信号!" << endl;
sigprocmask(SIG_BLOCK, &bset, &obset);
int cnt = 0;
// 打印观察pending信号集
while (true)
{
// 获取当前进程的pending信号集
sigpending(&pset);
// 显示pending信号集中的没有被递达的信号
ShowPending(&pset);
sleep(1);
++cnt;
// 10秒后解除阻塞
if (cnt == 10)
{
// 默认情况下,解除对于2号信号的block的时候,确实会进行递达
// 但是2号信号的默认处理动作是终止进程!
// 需要对2号信号进行捕捉
cout << "解除阻塞2号信号!" << endl;
sigprocmask(SIG_SETMASK, &obset, nullptr);
}
}
}
运行结果:
4.3 尝试阻塞所有信号
测试代码:尝试阻塞[1,31]号,所有的普通信号
void test3()
{
sigset_t bset, pset;
// 初始化信号集对象将所有位置1,阻塞所有信号
sigfillset(&bset);
cout << "阻塞所有信号!" << endl;
sigprocmask(SIG_BLOCK, &bset, nullptr);
while (true)
{
sigpending(&pset);
ShowPending(&pset);
sleep(1);
}
}
运行结果:
SIGKILL(9)信号 和 SIGSTOP(19)信号 不能被阻塞;当发送19号信号暂停进程后,再发送SIGCONT(18)信号,进程会继续运行。
4.4 小结
- SIGKILL(9)信号 和 SIGSTOP(19)信号 不能被捕捉,也不能被阻塞。
- 当发送19号信号暂停进程后,再发送SIGCONT(18)信号,也能成功递达使进程继续运行。
- 发送信号的本质就是将目标进程的pending信号集对应信号的bit位置为1,如果该信号未被阻塞则直接递达,然后清除该信号的pending标志。
- 当一个信号被接收且被阻塞时,pending信号集对应的位会被设置为1,表示该信号处于未决状态,直到解除阻塞,信号才能递达,最后清除该信号的pending标志。
- pending信号集的操作方法:
- 所有的信号产生方式都能修改pending信号集;
- 系统调用
sigpending
用于获取当前进程的pending信号集
- 信号屏蔽字的操作方法:
- 系统调用
sigprocmask
用于获取或修改当前进程的信号屏蔽字
- 系统调用
- Handler处理方法表的操作方法:
- 系统调用
signal
用于修改当前进程的Handler处理方法表 - 系统调用
sigaction
用于获取或修改当前进程的Handler处理方法表(下一章节介绍)
- 系统调用