linux入门---信号的操作

目录标题

  • sigset_t
  • sigset_t的操作函数
  • sigprocmask
  • sigpending
  • 信号的屏蔽测试
  • sigaction
  • 可重入函数
  • volatile

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

linux入门---信号的操作_第1张图片

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

linux入门---信号的操作_第2张图片
这个函数的作用就是获取当前进程的pending信号集,函数的参数是一个输出他的参数是一个输出型参数,哪个进程调用这个函数就输出哪个进程的pending表。

信号的屏蔽测试

那么有了上面的这些函数我们就可以写一个函数用来测试信号屏蔽的效果比如说我们屏蔽了某个信号然后不停的打印进程的pending位图,这时当我们发送对应信号的时候就可以看到打印的pending位图上的信号由0变成了1,但是没有执行该信号的功能,等过了一会我们解除了对信号的屏蔽时又可以看到pending位图上的信号由1变成了0并且执行了信号的功能,那么这就是我们要实现的测试,首先我们得创建两个信号集,一个用来设置新的信号,一个用来接收原来的老的信号,在使用信号集之前先使用sigemptyset对两个信号集进行初始化:

#include
#include
#include
using namespace std;
int main()
{
    sigset_t block oblock;
    sigemptyset(&block);
    sigemptyset(&oblock);
    return 0;
}

然后我们就可以使用sigaddset函数将你想要屏蔽的信号添加进信号屏蔽字里面,比如说想要屏蔽2号信号,那么我们就可以传递2给sigaddset函数的第二个参数,然后就可以调用sigprocmask函数将想要屏蔽的信号添加进内核的block表里面,那么这里的代码如下:

#include
#include
#include
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
#include
#include
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
#include
#include
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;
}

然后我们就可以运行一下代码进行测试,运行的结果如下:
linux入门---信号的操作_第3张图片
一开始没有收到任何的信号所以这里打印的结果都是0,当我们在键盘上面输入ctrl c发送2号信号给进程时就可以看到2号位子上的数据由0变成了1,2号信号本来会终结进程的但是由于我们将其屏蔽了,所以程序此时依然会继续运行:
linux入门---信号的操作_第4张图片
那么这就是将信号屏蔽的过程,那么这是由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;
}

代码的运行结果如下:
linux入门---信号的操作_第5张图片
可以看到这里确实只打印了9次,但是为什么没有打印if语句里面的那句话呢?原因很简单一旦对特定信号进行解除屏蔽,一般OS要至少立马递达一个信号!所以当我们对2号信号进行解封的时候立马就会抵达2号信号,而2号信号的作用就是终止进程,所以还没来得及打印这句话操作系统就将这句话终止了,那么要想解决这个问题就可以将打印的话放到解封的前面:

if(cnt==10)
{
    cout << "恢复对信号的屏蔽,不屏蔽任何信号\n";
    sigprocmask(SIG_SETMASK, &oblock, &block);
}

再运行一下代码就可以看到下面这样的场景:
linux入门---信号的操作_第6张图片
但是这里依然存在一个问题,虽然我们看到了if语句打印出来的一句话,但是我们没有看到信号由1变成0的现象啊,那么要想看到对应的现象我们就得对2号信号的方法进行重定义,那么这里就得使用signal函数完整代码如下:

#include
#include
#include
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;
}

代码的运行结果如下:
linux入门---信号的操作_第7张图片
那么这就是我们测试的完整内容。

sigaction

之前我们学过一个跟函数信号有关的signal函数,他可以实现对信号行为的重定义,那么接下来我们还要介绍一个与信号捕捉有关的函数sigaction,函数的声明如下:
linux入门---信号的操作_第8张图片

这个函数的作用就是给特定的信号设定特定的方法,act是一个结构体并且该结构体的名字和函数的名字一模一样,结构体的成员如下:
linux入门---信号的操作_第9张图片
因为这个结构体未来还会用来处理实时信号,所以里面的有些成员我们现在先不关心,第一个成员sa_handler就是一个函数指针也就是之前要设定的信号处理的方法,sa_sigaction不用管它,sa_flags设置为0不用管,sa_restorer也设置为null不用管,sa_mask是一个信号集这个具体有什么用我们待会再说,所以对于这个结构体我们只需要关系sa_mask成员和sa_handler就可以了。
函数中的act是一个输入型参数用于把我们给的信号设置进内核里面,oldact是输出型参数用来获取之前设定的老的信号,那么接下来我们就要用一端代码来理解这个函数的作用,首先创建两个sigaction结构体对象,一个用来设置进内核信号,一个用来接收内核信号原来的性质

#include
#include
using namespace std;
int main()
{
    struct sigaction act ,oact;
    return 0;
}

然后就对act对象的内部成员进行初始化,因为这里需要函数指针,所以我们得创建对应的handler函数,在函数里面我们就打印一句话表示接收到了对应的信号,然后倒计时10秒为了让这里的现象更加的明显我们还可以添加一个倒计时的功能,因为sa_mask是一个sigset_t类型所以我们还得使用sigemptyset函数对其进行初始化,那么这里的代码就如下:

#include
#include
#include
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号信号会出现什么样的场景呢?会不会递归处理我们发送的信号呢?那么这里的运行结果如下:
linux入门---信号的操作_第10张图片

linux入门---信号的操作_第11张图片
可以看到这里就处理了两次信号,并没有递归式的除了发送次数的信号,因为当我们正在递达某一个信号期间,同类信号是无法被递达的,当当前信号正在被捕捉时,系统就会自动将当前信号加入到进程的信号屏蔽字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号信号:
linux入门---信号的操作_第12张图片
那么这就是sa_mask的作用

可重入函数

在之前的学习中我们知道链表头插节点的原理,比如说下面的图片:
linux入门---信号的操作_第13张图片
head是一个指针对象指向链表的第一个节点,那么当我们头插一个节点的时候就会采用下面这样的方法:首先让新节点指向头结点指向的对象
linux入门---信号的操作_第14张图片
然后再让头结点指向新创建的节点:
linux入门---信号的操作_第15张图片
那么这就是头插的过程一共有两步,但是这种方法存在这么一个现象,比如说main函数使用insert函数往链表的头部插入节点,当完成第一步的时候突然收到一个信号,那么这时main执行流就会先停止执行main函数中的代码,跑去执行信号对应的处理方法,如果这时信号的执行方法也是往链表的头部插入节点就会出现下面这样的场景,首先main函数中完成了第一步之后的图片是这样的:
linux入门---信号的操作_第16张图片
信号中再头插一个节点就会变成下面这样:
linux入门---信号的操作_第17张图片
当信号对应的代码执行完之后就又会接着执行main函数中的代码,那么这时就该完成插入元素的第二步也就是将头结点指向node1节点,所以图片就变成了下面这样:
linux入门---信号的操作_第18张图片
那么经过上面的几步我们就可以看到node2节点根本没有插入到链表里面,所以链表的插入函数在面对多执行流的时候是会出现问题的,一般我们认为main执行流和信号捕捉执行流是两个执行流,如果在main中和handler函数中该函数被重复进入出现了问题我们就把该函数称为不可重入函数,如果没有出现问题的话我们就称该函数为可重入函数,但是这种现象并不是函数实现的问题,他是一个特性所以不可重入只是一个中性词,如果一个函数符合以下条件之一则是不可重入的调用了malloc或free,因为malloc也是用全局链表来管理堆的,调用了标准I/O库函数,标准I/O库的很多实现都以不可重入的方式使用全局数据结构,那么这就是可重入函数的概念。

volatile

我们通过下面的代码来带着大家了解这个关键字的特性:

#include
#include
int quit=0;
void handler(int singo)
{
    printf("收到了信号: %d\n",singo);
    quit=1;
    printf("quit的值为: %d\n",quit);
}
int main()
{
    signal(2,handler);
    while(!quit);
    printf("程序正常的退出\n");
    return 0;
}

因为quit的值一开始为0,所以程序会进入while的死循环里面,因为这里给2号信号创建了自定义动作,在handler函数里面将quit的值变成了1,所以当收到了二号信号回到main函数的执行流之后死循环就会结束,然后就打印出后面的printf函数里面的话,那么程序的运行结果如下:
linux入门---信号的操作_第19张图片
可以看到程序的运行结果符合我们的预期,但是我们之前学习过一次词叫做编译器的优化,编译器的优化是有等级:比如说-O0,-O1,-O2,-O3等等,我们之前使用的编译器优化的力度都很小,所以在编译可执行程序的时候可以添加-O3来增加优化力度:

	g++ -o $@ $^ -O3

然后再执行可执行程序并发送2号信号就会出现下面这样的场景:
linux入门---信号的操作_第20张图片
可以看到这里虽然发送了2号信号将quit的值改成了1while循环依然没有结束,那为什么会这样呢?原因是cpu中存在许多寄存器,这里编译器在优化的时候会发现全局变量quit在后面的代码中只被用来作为检测没有被修改,所以他就将quit的值直接从内存加载进了寄存器,当while循环要用到quit的值时就不会到内存中进行读取,而是直接从寄存器中进行读取,如果我们发送了2号信号给了程序,他就会对quit的值进行修改,但是这里的修改是对内存中的值进行,将内存中的quit由0变成1并不修改寄存器中的值,因为编译器的优化即使内存中的quit发生了修改while中要用quit的值时也是从寄存器中获取而不是从内存中,那么这就是上述过程的原因,要想解决这个问题就可以得在quit变量前面加上volatile,这样每次在用到quit变量的时候就不会从寄存器中读取数据,而是从内存中读取数据,我们来看看下面的代码:

#include
#include
volatile int quit=0;
void handler(int singo)
{
    printf("收到了信号: %d\n",singo);
    quit=1;
    printf("quit的值为: %d\n",quit);
}
int main()
{
    signal(2,handler);
    while(!quit);
    printf("程序正常的退出\n");
    return 0;
}

代码的运行结果如下:
linux入门---信号的操作_第21张图片
那么这就是volatile关键字的作用。

你可能感兴趣的:(linux入门,linux,运维)