目录
一. 信号保存和阻塞的相关概念
二. 进程信号的表示
2.1 进程信号在内核中的表示
2.2 sigset_t 类型
三. 信号集操作相关函数
3.1 sigset_t 类型参数设置相关函数
3.2 sigprocmask 函数
3.3 sigpending 函数
四. 演示代码
4.1 将所有信号的处理方式都注册为不退出进程
4.2 显示未决信号集
4.3 所有信号设置为阻塞
五. 总结
一. 信号保存和阻塞的相关概念
- 信号递达:进程实际执行信号处理的动作叫做信号的递达(Delivery)
- 信号未决:信号从实际产生到递达之间的状态,称为信号未决(Pending)
- 进程可以阻塞某个信号(block),即:即使产生了某个信号也不执行相应的处理动作。
- 被阻塞信号在产生后,只要不接触阻塞状态,就一直不会递达。
- 信号阻塞和忽略不是一个概念,信号阻塞是不对进行处理,信号处于未决状态,而忽略信号是指进程接收到了信号,信号也会递达,只是没有实质性的处理动作。
二. 进程信号的表示
2.1 进程信号在内核中的表示
每一个进程的PCB中,都会存储三张用于表示信号状态的表(如图2.1所示),他们分别为block、pending和handler,其中:
- block:阻塞状态表,其底层实现是位图,如果设置某个进程信号阻塞,block中对应的二进制位就由0变1,block标志位如果为1,信号就不能够递达。
- pending:未决信号,底层实现也是位图,如果OS检测到了信号,但还没有对信号进行处理,那么在其pending位图中的对应bit位就会被设置为1,如果信号递达,pending位图就会由1变0,如果其block中的二进制位也为1,那么信号就不会递达,pending中的二进制位永远都会为1,直到阻塞状态解除信号递达。
- handler:是一个函数指针数组,指向其对应的信号处理方法的函数指针,图中SIG_DEF和SIG_IGN分别对应默认处理方式和忽略信号。
- SIG_DFL和SIG_IGN在源码中的定义为:#define SIG_DEF ((__sighandler_t ) 0) 和 #define SIG_IGN ((__sighandler_t ) 0),进程收到信号,在递达是判断用哪个函数对信号进行处理的流程大概为:先通过 if 和 else if 判断是否选取默认处理方法或者忽略,如果都不是,则走到else执行用于自定义的处理方法。
2.2 sigset_t 类型
OS不允许用于直接对block和pending位图中的二进制位进行修改,因此提供了一个OS类型sigset专门用于设置用于进程信号表示的相关位图,sigset_t 可以被称为信号集,sigset_t 的底层是位图结果,用 0/1 来表示状态。
阻塞信号集也叫做当前进程的信号屏蔽字(Signal Mask)。
三. 信号集操作相关函数
3.1 sigset_t 类型参数设置相关函数
- int sigemptyset(sigset_t *set) -- 让set中不包含任何信号,即全部二进制位都是0。
- int sigfillset(sigset_t *set) -- 让set包含全部信号,即让全部二进制位都是1。
- int sigaddset(sigset_t *set, int signo) -- 将set指定信号的状态设置为有效1。
- int sigdelset(sigset_t *set, int signo) -- 删除set指定信号的状态无效0。
- int sigismember(sigset_t *set, int signo) -- 判断set中某个信号的状态是否为有效1,如果有效,函数返回1,无效返回0。
上面的这些函数,都是成功执行返回0,失败返回-1。
3.2 sigprocmask 函数
sigprocmask函数 -- 读取或更改信号的阻塞状态
函数原型:int sigprocmask( int how, sigset_t *set, sigset_t *obset )
函数参数:
how -- 方法选择,添加阻塞、删除阻塞或设置阻塞
set -- 如果选择设置阻塞,则用set中的信息设置
obset -- 输出型参数,读取原来的阻塞信息
返回值:成功返回0,失败返回-1
在sigprocmask函数中,参数 how 用于方法的选择,how有三个可选项,分别为:SIG_BLOCK、SIG_UNBLOCK 和 SIG_SETMASK。
how | 功能 |
---|---|
SIG_BLOCK | 添加对特定信号的阻塞状态,相当于mask | set |
SIG_UNBLOCK | 取消对特定信号的阻塞状态,相当于mask & ~set |
SIG_SETMASK | 使用set设置信号阻塞状态,相当于mask = set |
3.3 sigpending 函数
sigpending函数 -- 获取进程当前的未决信号集
函数原型:int sigpending(sigset_t *set)
函数参数:set为输出型参数,用于获取当前进程的未决信号集
返回值:运行成功返回0,失败返回-1
四. 演示代码
4.1 将所有信号的处理方式都注册为不退出进程
如果我们在程序中将所有信号的处理方法都设置为不退出,并在之后执行死循环,那么,是不是进程就无法被杀死了呢?
答案显然是否定的,OS的设计者早就考虑到了这一点,9号信号SIGKILL为管理员权限信号,用户不可以重新注册其处理方法,代码和运行结果如下。
代码4.1:所有信号重新注册方法为不退出
#include <iostream>
#include <signal.h>
#include <unistd.h>
// 用户自定义的信号处理函数
void siganl_handler(int sig)
{
std::cout << "recieve a signal, signum:" << sig << std::endl;
}
int main()
{
std::cout << "this is a process, pid:" << getpid() << ", ppid:" << getppid() << std::endl;
// 1. 将全部普通信号处理函数重新注册
for(int sig = 1; sig <= 31; ++sig)
{
signal(sig, siganl_handler);
}
// 2. 执行死循环
while(true)
{ }
return 0;
}
4.2 显示未决信号集
代码4.2通过sigprocmask函数,在第5s设置2号SIGINT信号为阻塞状态,,在第8s通过raise函数向进程发送2号SIGINT信号,在第15s取消SIGINT信号的阻塞状态,并实时输出未决状态信息,代码运行结果如图4.2,2号信号的阻塞状态由0至1,并在阻塞状态取消后,执行用户自定义的2号SIGINT信号处理函数,退出进程。
代码4.2:未决信号集的打印
#include <iostream>
#include <signal.h>
#include <unistd.h>
// 用户自定义的信号处理函数
void siganl_handler(int sig)
{
std::cout << "recieve a signal, signum:" << sig << std::endl;
exit(0);
}
// 未决状态打印函数
void showPending(const sigset_t& pending)
{
for(int sig = 1; sig <= 31; ++sig)
{
if(sigismember(&pending, sig)) std::cout << 1;
else std::cout << 0;
}
std::cout << std::endl;
}
int main()
{
// 重新注册2号信号的处理方法
signal(SIGINT, siganl_handler);
// bset用于设置阻塞状态,obset用于接收原阻塞状态
sigset_t bset, obset;
// pending用于获取阻塞集
sigset_t pending;
//对信号集设置初始状态
sigemptyset(&bset);
sigaddset(&bset, SIGINT);
int count = 0;
while(true)
{
sleep(1);
std::cout << ++count << ": " << std::flush;
sigpending(&pending); // 接收阻塞状态
showPending(pending); // 阻塞状态打印
if(count == 5)
{
std::cout << "设置SIGINT信号处于阻塞状态 ..." << std::endl;
sigprocmask(SIG_BLOCK, &bset, &obset);
}
if(count == 8)
{
std::cout << "发送SIGINT信号" << std::endl;
raise(SIGINT);
}
if(count == 15)
{
std::cout << "取消对SIGINT信号的阻塞" << std::endl;
sigprocmask(SIG_SETMASK, &obset, nullptr);
}
}
return 0;
}
4.3 所有信号设置为阻塞
设置所有信号阻塞,然后死循环,是不是发送任何信号都无法终止进程的运行?
答案当然也不是,9号SIGKILL信号和19号SIGSTOP信号都无法被阻塞,在19号信号使进程终止运行期间,18号SIGCONT信号也无法被阻塞。
代码4.3:验证9号SIGKILL信号无法被阻塞
#include <iostream>
#include <signal.h>
#include <unistd.h>
// 未决状态打印函数
void showPending(const sigset_t& pending)
{
for(int sig = 1; sig <= 31; ++sig)
{
if(sigismember(&pending, sig)) std::cout << 1;
else std::cout << 0;
}
std::cout << std::endl;
}
int main()
{
// 设置所有信号阻塞
sigset_t bset, obset;
sigset_t pending;
sigfillset(&bset);
sigprocmask(SIG_BLOCK, &bset, &obset);
// 依次向进程发送1 - 31号信号,如何输出未决状态
for(int sig = 1; sig <= 31; ++sig)
{
std::cout << "kill -" << sig << std::endl;
raise(sig);
// 获取阻塞信号集并打印
sigpending(&pending);
showPending(pending);
sleep(1);
}
return 0;
}
五. 总结
- 进程处理信号的动作叫做信号递达、信号从产生到递达的状态叫做信号未决、被阻塞的信号无法递达。
- 进程PCB中有三张表:block、pending、handler,block和pending为阻塞和未决信号集,handler为指向信号处理函数的函数指针数组。
- Linux操作系统提供了内置信号集类型sigset_t,用于信号状态的设置,sigprocmask 函数用于读取或设置阻塞信号集,sigpending用于获取阻塞信号集。
- 9号SIGKILL信号具有管理员权限,用户无法自定义其处理动作,也无法被阻塞。