目录标题
- sigset_t
- sigset_t的操作函数
- sigprocmask
- sigpending
- 信号的屏蔽测试
- sigaction
sigset_t
为了能够让操作系统更好的使用信号,操作系统提供了sigset_t的数据类型,操作系统中存在pending表和block表,但是这两张表是内核数据结构,用户是没有办法直接修改的,而且我要是想同时修改一个表中的10个信号或者两个表的十几个信号呢?操作系统是不可能给你提供10几个参数的函数的,所以他给我们统一定义了一个sigset_t的数据类型,因此,未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t称为信号集,这个类型可以表示每个信号的“有效”或“无效”状态,在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞,而在未决信号集中“有效”和“无效”的含义是该信号是否处于未决状态,所以sigset_t是操作系统为了让用户可以更好的修改两张表而提供的数据结构,我们一般把sigset_t的数据结构称之为信号集,信号集又分为两种,一种是pending信号集,一种事block信号集,我们一般把block信号集称为信号屏蔽字,sigset_t本质上就是一个位图,但是不同的操作系统实现的这个位图的方法是不一样的,这里的位图不是一个简单的整形,因为他要保证信号的扩展和兼容实时信号就不能简单的使用整形来实现,所以就不能简单使用安位与安位或来实现,所以操作系统提供了一些列的接口来对sigset_t的数据进行操作,那么接下来我们就来一一介绍对应的函数接口。
sigset_t的操作函数
这两个函数的声明如下:
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
这两个函数都有相同类型的参数,传递一个信号集的指针进去,sigemptyset函数可以将该信号集的每个信号都置为0,而sigfillset函数就可以将信号集中的每个信号都置为1,所以我们可以认为这两个函数的作用就是初始化,一个是将所用的信号初始化为0,一个是将所有的信号初始化为1。
int sigaddset (sigset_t *set, int signo);
int sigdelset(sigset_t *set, int signo);
这两个函数的参数也是相同的,第一个参数是一个信号集的指针,第二个参数是一个整型表示是信号集中的第几个信号,所以sigaddset函数就是将set指向的信号集中的第signo信号置为1,sigdelset函数的作用就是将set指向的信号集中的第signo信号置为0
int sigismember(const sigset_t *set, int signo);
这个参数的作用就是判断signo信号是否在信号集set里面。这5个函数不需要记忆用的时候查一下就行知道有这回事就行。
sigprocmask
int sigprocmask(int how,const sigset_t*set, sigset_t *oset)
这个函数的作用就是修改内核中的block表,第一个参数是一个标记位表示如何修改信号的屏蔽字,修改所用到的数据就来自于set,标记位有3个其中SIG_BLOCK表示将set中的信号屏蔽字添加到当前信号屏蔽字里面相当于mask=mask|set,SIG_UNBLOCK表示解除屏蔽字也就是从当前信号屏蔽字中删除set中包含的信号屏蔽字相当于mask=mask&~set,SIG_SETMASK表示将屏蔽字设计成跟set一样,set的作用就是将set的数据重置进进程的block,因为使用该函数后会修改进程屏蔽字所以oset参数的作用就是将原来block的数据放到oset里面。
sigpending
这个函数的作用就是获取当前进程的pending信号集,函数的参数是一个输出他的参数是一个输出型参数,哪个进程调用这个函数就输出哪个进程的pending表。
信号的屏蔽测试
那么有了上面的这些函数我们就可以写一个函数用来测试信号屏蔽的效果比如说我们屏蔽了某个信号然后不停的打印进程的pending位图,这时当我们发送对应信号的时候就可以看到打印的pending位图上的信号由0变成了1,但是没有执行该信号的功能,等过了一会我们解除了对信号的屏蔽时又可以看到pending位图上的信号由1变成了0并且执行了信号的功能,那么这就是我们要实现的测试,首先我们得创建两个信号集,一个用来设置新的信号,一个用来接收原来的老的信号,在使用信号集之前先使用sigemptyset对两个信号集进行初始化:
#include<iostream>
#include<signal.h>
#include<unistd.h>
using namespace std;
int main()
{
sigset_t block oblock;
sigemptyset(&block);
sigemptyset(&oblock);
return 0;
}
然后我们就可以使用sigaddset函数将你想要屏蔽的信号添加进信号屏蔽字里面,比如说想要屏蔽2号信号,那么我们就可以传递2给sigaddset函数的第二个参数,然后就可以调用sigprocmask函数将想要屏蔽的信号添加进内核的block表里面,那么这里的代码如下:
#include<iostream>
#include<signal.h>
#include<unistd.h>
using namespace std;
int main()
{
sigset_t block oblock;
//初始化
sigemptyset(&block);
sigemptyset(&oblock);
//添加屏蔽的信号
sigaddset(&block,2);
//将屏蔽信号添加进内核的block表里面
sigprocmask(SIG_SETMASK,&block,&oblock);
return 0;
}
然后我们就可以创建一个循环不停的打印pending表里面的内容,因为要查看pending表所以要使用sigpending函数,因为要使用sigpending函数所以我们还得创建一个名为pending的sigset_t对象,那么这里我们就可以创建一个函数,每次循环都对pending进行初始化,然后使用spending获得内核中表的数据,因为要打印pending对象里面的数据所以这里我们可以创建一个函数来实现这样的功能,那么这里的代码如下:
#include<iostream>
#include<signal.h>
#include<unistd.h>
using namespace std;
void showpending(const sigset_t* pending)
{
}
int main()
{
sigset_t block oblock pending;
//初始化
sigemptyset(&block);
sigemptyset(&oblock);
//添加屏蔽的信号
sigaddset(&block,2);
//将屏蔽信号添加进内核的block表里面
sigprocmask(SIG_SETMASK,&block,&oblock);
while(true)
{
sigemptyset(&pending);
spending(&pending);
showpending(&pending);
sleep(1);
}
return 0;
}
那么接下来我们只用实现一下showpending函数就行,因为一共右31个信号,所以我们可以创建一个循环让其循环31次,因为我们不能使用按位或和按位与来获取pending中的数据,所以我们得循环使用sigismember函数来一个一个的判断信号是否存在,如果存在我们就打印1如果不存在我们就打印0,那么完整的代码如下:
#include<iostream>
#include<signal.h>
#include<unistd.h>
using namespace std;
void showpending(const sigset_t* pending)
{
for(int i=31;i>0;i--)
{
if(sigismember(pending,i))
{
cout<<'1';
}
else
{
cout<<'0';
}
}
cout<<endl;
}
int main()
{
sigset_t block,oblock, pending;
//初始化
sigemptyset(&block);
sigemptyset(&oblock);
//添加屏蔽的信号
sigaddset(&block,2);
//将屏蔽信号添加进内核的block表里面
sigprocmask(SIG_SETMASK,&block,&oblock);
while(true)
{
sigemptyset(&pending);
sigpending(&pending);
showpending(&pending);
sleep(1);
}
return 0;
}
然后我们就可以运行一下代码进行测试,运行的结果如下:
一开始没有收到任何的信号所以这里打印的结果都是0,当我们在键盘上面输入ctrl c发送2号信号给进程时就可以看到2号位子上的数据由0变成了1,2号信号本来会终结进程的但是由于我们将其屏蔽了,所以程序此时依然会继续运行:
那么这就是将信号屏蔽的过程,那么这是由0变成了1,我们还可以通过解除信号的阻塞将其由1再变成0,那么这里我们就可以在循环外面定义一个变量将其初始化位0,每次循环都对这个变量加一,等该变量的值等于10的时候外面就对2号信号进行解锁,然后打印一句话说我们将信号的阻塞回复到了之前的状态,那么这里的代码就如下:
int main()
{
sigset_t block,oblock, pending;
//初始化
sigemptyset(&block);
sigemptyset(&oblock);
//添加屏蔽的信号
sigaddset(&block,2);
//将屏蔽信号添加进内核的block表里面
sigprocmask(SIG_SETMASK,&block,&oblock);
int cnt=1;
while(true)
{
sigemptyset(&pending);
sigpending(&pending);
showpending(&pending);
cnt++;
if(cnt==10)
{
sigprocmask(SIG_SETMASK, &oblock, &block);
cout << "恢复对信号的屏蔽,不屏蔽任何信号\n";
}
sleep(1);
}
return 0;
}
代码的运行结果如下:
可以看到这里确实只打印了9次,但是为什么没有打印if语句里面的那句话呢?原因很简单一旦对特定信号进行解除屏蔽,一般OS要至少立马递达一个信号!所以当我们对2号信号进行解封的时候立马就会抵达2号信号,而2号信号的作用就是终止进程,所以还没来得及打印这句话操作系统就将这句话终止了,那么要想解决这个问题就可以将打印的话放到解封的前面:
if(cnt==10)
{
cout << "恢复对信号的屏蔽,不屏蔽任何信号\n";
sigprocmask(SIG_SETMASK, &oblock, &block);
}
再运行一下代码就可以看到下面这样的场景:
但是这里依然存在一个问题,虽然我们看到了if语句打印出来的一句话,但是我们没有看到信号由1变成0的现象啊,那么要想看到对应的现象我们就得对2号信号的方法进行重定义,那么这里就得使用signal函数完整代码如下:
#include<iostream>
#include<signal.h>
#include<unistd.h>
using namespace std;
void showpending(const sigset_t* pending)
{
for(int i=31;i>0;i--)
{
if(sigismember(pending,i))
{
cout<<'1';
}
else
{
cout<<'0';
}
}
cout<<endl;
}
void handler(int signal)
{
cout<<"收到了信号:"<<signal<<endl;
}
int main()
{
signal(2,handler);
sigset_t block,oblock, pending;
//初始化
sigemptyset(&block);
sigemptyset(&oblock);
//添加屏蔽的信号
sigaddset(&block,2);
//将屏蔽信号添加进内核的block表里面
sigprocmask(SIG_SETMASK,&block,&oblock);
int cnt=1;
while(true)
{
sigemptyset(&pending);
sigpending(&pending);
showpending(&pending);
cnt++;
if(cnt==10)
{
cout << "恢复对信号的屏蔽,不屏蔽任何信号\n";
sigprocmask(SIG_SETMASK, &oblock, &block);
}
sleep(1);
}
return 0;
}
代码的运行结果如下:
那么这就是我们测试的完整内容。
sigaction
之前我们学过一个跟函数信号有关的signal函数,他可以实现对信号行为的重定义,那么接下来我们还要介绍一个与信号捕捉有关的函数sigaction,函数的声明如下:
这个函数的作用就是给特定的信号设定特定的方法,act是一个结构体并且该结构体的名字和函数的名字一模一样,结构体的成员如下:
因为这个结构体未来还会用来处理实时信号,所以里面的有些成员我们现在先不关心,第一个成员sa_handler就是一个函数指针也就是之前要设定的信号处理的方法,sa_sigaction不用管它,sa_flags设置为0不用管,sa_restorer也设置为null不用管,sa_mask是一个信号集这个具体有什么用我们待会再说,所以对于这个结构体我们只需要关系sa_mask成员和sa_handler就可以了。
函数中的act是一个输入型参数用于把我们给的信号设置进内核里面,oldact是输出型参数用来获取之前设定的老的信号,那么接下来我们就要用一端代码来理解这个函数的作用,首先创建两个sigaction结构体对象,一个用来设置进内核信号,一个用来接收内核信号原来的性质
#include<iostream>
#include<signal.h>
using namespace std;
int main()
{
struct sigaction act ,oact;
return 0;
}
然后就对act对象的内部成员进行初始化,因为这里需要函数指针,所以我们得创建对应的handler函数,在函数里面我们就打印一句话表示接收到了对应的信号,然后倒计时10秒为了让这里的现象更加的明显我们还可以添加一个倒计时的功能,因为sa_mask是一个sigset_t类型所以我们还得使用sigemptyset函数对其进行初始化,那么这里的代码就如下:
#include<iostream>
#include<signal.h>
#include<unistd.h>
using namespace std;
void handler(int signo)
{
cout<<"接收到了信号:"<<signo<<endl;
int cnt=10;
while(cnt)
{
printf("cnt:%2d\r",cnt--);
fflush(stdout);
sleep(1);
}
cout<<endl;
}
int main()
{
struct sigaction act ,oact;
act.sa_handler=handler;
act.sa_flags=0;
act.sa_restorer=nullptr;
sigemptyset(&act.sa_mask);
sigaction(SIGINT,&act,&oact);
while(true)
{
sleep(1);
}
return 0;
}
那么接下来我们就可以对其进行测试,运行程序可以看到这里没有任何的反应:
对其发送一个2号信号就可以看到这里出现了信号处理的动作
我们这里一下子只发送了一个2号信号,那如果我们一下子发送多个2号信号会出现什么样的场景呢?会不会递归处理我们发送的信号呢?那么这里的运行结果如下:
可以看到这里就处理了两次信号,并没有递归式的除了发送次数的信号,因为当我们正在递达某一个信号期间,同类信号是无法被递达的,当当前信号正在被捕捉时,系统就会自动将当前信号加入到进程的信号屏蔽字block里面,当信号完成捕捉动作之后,系统又会自动恢复对该信号的捕捉,当发送了多个信号,信号处理完毕时只会再执行一次信号的动作,原因就是处理信号的时候会讲pending中的1变成0,而多次发送信号又会0变成1,因为只有一个比特位所以无法递归多次,所以这里就会出现两次信号执行的现象,一般一个信号被解除屏蔽的时候会自动进行递达当前屏蔽信号,如果信号已经被pending的话就执行该信号,没有的话就不执行任何的操作。我们进程处理信号的原则是串行的处理同类型的信号,不允许递归,而sa_mask的作用就是:当我们正在处理某一种信号的时候,如果我们要想顺便屏蔽其他的信号的话就可以将对应信号添加到这个sa_mask中,比如说下面的操作:
当前正在处理2号信号,但是我们可以通过发送3号信号的方式结束真正处理2号信号的进程:
但是我们将3号信号放进sa_mask之后就可以看到,处理2号信号的时候发送3号信号不会终止进程:
int main()
{
struct sigaction act ,oact;
act.sa_handler=handler;
//act.sa_sigaction=nullptr;
act.sa_flags=0;
act.sa_restorer=nullptr;
sigemptyset(&act.sa_mask);
sigaddset(&act.sa_mask,3);
sigaction(SIGINT,&act,&oact);
while(true)
{
sleep(1);
}
return 0;
}
测试的结果如下:
只有当2号信号处理完了才会接着处理3号信号:
那么这就是sa_mask的作用