Linux 信号

目录

信号概述

信号的概念:

进程看待信号的方式:

查看信号:

信号的处理方式:

信号的产生

通过kill指令产生信号:

通过终端按键产生信号:

通过系统调用产生信号:

给任意进程发送任意信号:

给自己发送任意信号:

给自己发送固定信号:

通过软件条件产生信号:

设定闹钟:

通过硬件异常产生信号:

总结:

信号的默认处理动作:

[核心转储(Core Dump):](#核心转储(Core Dump):)

信号的保存

相关概念:

内核结构:

sigset_t:

信号集操作函数:

修改阻塞信号集:

获取未决信号集:

测试:

信号的捕捉

信号的检测与处理:

信号的捕捉过程:

​编辑

进程地址空间:

信号捕捉函数:

可重入函数

volatile

SIGCHLD信号

信号概述

信号的概念:

在日常生活中,信号是表示消息的物理量 ,信号是运载消息的工具 ,是消息的载体

在Linux中,信号同样有上述特征,同时,信号还是Linux系统提供的让一个进程给其他进程发送异步信息的一种方式,属于软中端

所谓异步即双方不需要共同的时钟,也就是接收方不知道发送方什么时候发送信号接收方在接收信号之前一直在做自己的事两者是并发运行的

进程看待信号的方式:

1.能识别并处理信号。

2.收到信号时,在做其他更重要的事情,不能及时处理时,可以暂存信号(通过位图)。

3.收到信号时,可以不立即处理,等到合适的时候再处理。

4.信号是随机产生的,无法准确预料,因此信号是异步发送的。

查看信号:

可以使用 kill -l 查看所有信号:

  • 每个信号都有一个编号和宏定义的名字 ,即使用信号名称实际上还是使用信号编号。如 3号新号的名成的定义为 #define SIGQUIT 3。
  • 没有0号、32号、33号信号,编号34以上的都是实时信号,本文不讨论实时信号。
  • 这些信号的产生条件和默认处理动作可以使用 man 7 signal 指令查看。

信号的处理方式:

对信号的处理动作有以下三种:

  • 忽略此信号,即不做任何处理,使用 signal(信号编号/信号名称,SIG_IGN)
  • 执行信号的默认处理动作。
  • 执行自定义的信号的处理动作,即捕捉,使用signal(信号编号/信号名称,函数指针)

以2号信号为例,2号信号可以直接使用 kill + -2 + 进程pid 给相应进程发送信号,也可以通过键盘输入 Ctrl+c 触发 。2号信号的默认处理动作是终止进程

cpp 复制代码
#include <iostream>
#include <signal.h>
#include <sys/types.h>
#include <unistd.h>

using namespace std;

int main()
{
    int count=10;
    while (count)
    { 
        count--;
        cout<<"count:"<<count<<endl;
        sleep(1);
    }

    cout<<"进程退出......"<<endl;
    return 0;
}

可以发现,程序运行起来后,给程序发送2号信号后,程序直接退出了。 这是执行默认处理动作

cpp 复制代码
#include <iostream>
#include <signal.h>
#include <sys/types.h>
#include <unistd.h>

using namespace std;
int main()
{
    signal(2, SIG_IGN);//忽略2号信号

    int count=10;
    while (count)
    { 
        count--;
        cout<<"count:"<<count<<endl;
        sleep(1);
    }

    cout<<"进程退出......"<<endl;
    return 0;
}

可以发现,程序运行起来后,给进程发送2号信号后,程序没有任何处理动作。这是忽略此信号

cpp 复制代码
#include <iostream>
#include <signal.h>
#include <sys/types.h>
#include <unistd.h>

using namespace std;

void task(int sig)
{
    cout << "I am " << getpid() <<",signal num:"<< sig << endl;
}

int main()
{
    signal(2, task);//自定义2号信号处理动作
    int count=10;
    while (count)
    { 
        count--;
        cout<<"count:"<<count<<endl;
        sleep(1);
    }

    cout<<"进程退出......"<<endl;
    return 0;
}

可以发现,程序运行起来后,给进程发送2号新号,进程会执行自定义的task指向的函数方法。这是捕捉信号。

信号的产生

通过kill指令产生信号:

可直接使用 kill + -信号编号/信号名称 + 进程pid 给相应进程发送相应信号。要注意信号编号/信号名称前边的 " - "不能省略。

cpp 复制代码
#include <iostream>
#include <signal.h>
#include <sys/types.h>
#include <unistd.h>

using namespace std;

void task(int sig)
{
    cout << "I am " << getpid() <<",signal num:"<< sig << endl;
}

int main()
{
    signal(2, task);//自定义2号信号处理动作
    signal(3, task);//自定义3号信号处理动作
    signal(4, task);//自定义4号信号处理动作
    int count=10;
    while (count)
    { 
        count--;
        cout<<"pid:"<<getpid()<<", count:"<<count<<endl;
        sleep(1);
    }

    cout<<"进程退出......"<<endl;
    return 0;
}

通过终端按键产生信号:

当通过键盘输入 Ctrl + c 的组合键时,系统会把它解释为2号信号,再发送给进程;

当通过键盘输入 Ctrl + \ 的组合键时,系统会把它解释为3号信号,再发送给进程;

当通过键盘输入 Ctrl + z 的组合键时,系统会把它解释为19号信号,再发送给进程。

cpp 复制代码
#include <iostream>
#include <cerrno>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>

using namespace std;

void task(int sig)
{
    cout << "Get a signal , signal num:"<< sig << endl;
}

int main()
{
    signal(2, task);//自定义2号信号处理动作
    signal(3, task);//自定义3号信号处理动作
    signal(19, task);//自定义19号信号处理动作
    int count=10;
    while (count)
    { 
        count--;
        cout<<"pid:"<<getpid()<<", count:"<<count<<endl;
        sleep(2);
    }

    cout<<"end......"<<endl;
    return 0;
}

观察运行结果可以发现,19号信号无法被自定义捕捉

通过系统调用产生信号:

给任意进程发送任意信号:

cpp 复制代码
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);

参数:

  • pid :要发送信号的目标进程。
  • sig :要发送的信号编号/信号名字。

返回值:

  • 成功返回 0 ;失败返回 -1,并设置错误码。
cpp 复制代码
#include <iostream>
#include <cerrno>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>

using namespace std;

int main(int argc,char* argv[])//自定义实现kill指令
{
    if(argc<3)
    {
        cout << "Usage: " << argv[0] << " -signumber pid" << endl;
        return 1;
    }

    int sig=stoi(argv[1]+1);//把命令行第二个参数转换为整数,跳过 " - "
    int pid=stoi(argv[2]);//取出进程pid

    int n=kill(pid,sig);
    if(n==0)
    {
        cout<<"mykill success,pid:"<<pid<<endl;
    }
    else
    {
        cout<<"mykill falied,errno:"<<errno<<",errno message:"<<strerror(errno)<<endl;
    }

    return 0;
}

给自己发送任意信号:

cpp 复制代码
#include <signal.h>
int raise(int sig);

参数:

  • sig :要发送的信号编号/信号名称。

返回值 :

  • 成功返回 0 ;失败返回 非0。
cpp 复制代码
#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>

using namespace std;

void task(int sig)
{
    cout << "Get a signal , signal num:"<< sig << endl;
}

int main()
{
    signal(2,task);
    int count=10;
    while(count)
    {
        if(count%3==0)
        {
            raise(2);//给自己发送2号信号
        }
        else
        {
            cout<<"pid:"<<getpid()<<" ,count:"<<count<<endl;
        }
        count--;
        sleep(1);
    }
    cout<<"end......"<<endl;
    return 0;
}

上述代码中,当count为3的倍数时,调用一次raise函数。

给自己发送固定信号:

cpp 复制代码
#include <stdlib.h>
void abort(void);
  • 该函数无参也无返回值,给当前进程发送固定信号---6号信号。
  • 6号信号会终止程序。
cpp 复制代码
#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>

using namespace std;

void task(int sig)
{
    cout << "Get a signal , signal num:"<< sig << endl;
}

int main()
{
    signal(6,task);
    int count=10;
    while(count)
    {
        if(count==5)
        {
            abort();
        }
        else
        {
            cout<<"pid:"<<getpid()<<" ,count:"<<count<<endl;
        }
        count--;
        sleep(1);
    }
    cout<<"end......"<<endl;
    return 0;
}

上述代码运行5秒后调用abort函数。

可以发现6号信号即使被捕捉自定义了方法,执行完自定义方法后,还是会终止程序

通过软件条件产生信号:

进程间通信一文中有说到管道的四种情况之一,读端关闭后,写端再继续写已经没有意义了,会直接被操作系统终止,这是因为读端关闭后,操作系统给写端进程发送了13号信号(SIGPIPE),直接杀掉了进程。这里就不在赘述,相关验证代码在进程间通信一文中有,有兴趣请自行查看。

这里主要介绍alarm函数和14号信号(SIGALRM)。

设定闹钟:

cpp 复制代码
#include <unistd.h>
unsigned int alarm(unsigned int seconds);

参数:

  • second:second秒,即second秒之后给进程发送 14 号信号。

返回值:

  • 返回最近一次设定的闹钟的剩余秒数 或者 0。

注意:

  • 若second为0,表示取消以前设定的闹钟,返回值仍然是以前设定的闹钟的剩余秒数,或者0。
  • 一般情况下,定义多个闹钟,只会有响一次(最近一次设定的那一个),除非通过自定义捕捉嵌套定义,才能响多次。
cpp 复制代码
#include <iostream>
#include <unistd.h>

int main()
{
    int a=0,b=0,c=0; 
    a=alarm(3);
    sleep(1);

    b=alarm(10);
    sleep(1);

    c=alarm(5);
    int count=0;
    while(1)
    {
        cout<<"count:"<<count<<" a="<<a<<" b="<<b<<" c="<<c<<endl;
        count++;
        sleep(1);
    }
    return 0;
}

上述代码中定义了三个闹钟,分别定时3秒、10秒、5秒,每定义一个闹钟程序暂停1秒钟,a、b、c分别记录三个闹钟的返回值,有一个count负责计时,每秒打印一次数据。

观察结果可以发现,后边设定的闹钟会把前边设定的闹钟覆盖掉 ,只有最后一次设定的5秒的闹钟是有效的,并且alarm函数的返回值是前一个设定的闹钟的剩余秒数,若前边没有设定闹钟,alarm函数返回值为0。

cpp 复制代码
int main()
{
    alarm(4);
    sleep(1);

    alarm(5);
    sleep(1);

    alarm(2);
    sleep(1);

    alarm(0);
    int count=10;
    while(count)
    {     
        cout<<"count:"<<count<<endl;
        
        count--;
        sleep(1);
    }
    cout<<"end......"<<endl;
    return 0;
}

上述代码中,设定了四个闹钟,分别定时4秒、5秒、2秒、0秒,每设定一个程序员暂停1秒钟,有一个count负责计时。

观察可以发现,最后设定的0秒的闹钟把以前设定的闹钟都取消了。

通过硬件异常产生信号:

硬件异常通常会被硬件以某种方式检测到并通知内核,然后内核向当前进程发送适当的信号。

例如当前进程执行了除以0的运算 ,CPU的运算单元会产生异常,内核将这个异常解释为SIGFPE信号发送给进程。再比如当前进程访问了非法内存地址 ,MMU会产生异常,内核将这个异常解释为SIGSEGV信号发送给进程。

cpp 复制代码
void Divide(int sig)
{
    cout << "Get a signal " << sig << " ,this signal is divided by 0 anomalies" << endl;
}

void Wildptr(int sig)
{
    cout << "Get a signal " << sig << " ,this signal is a field pointer anomaly" << endl;
}

int main()
{
    signal(SIGFPE, Divide);
    signal(SIGSEGV, Wildptr);
    int a = 0;
    int b = 10 / a;

    int *ptr = NULL;
    *ptr = 10;

    sleep(1);

    cout << "end......" << endl;
    return 0;
}

上述代码中,对SIGFPE信号和SIGSEGV信号进行了自定义捕捉,想要达到的目的是执行自定义的两个函数,然后退出程序,实际结果是怎样的呢?

实际结果却是一直执行除以零错误的捕捉,这是因为我们自定义捕捉了SIGFPE信号,但是并没有让程序终止,那么SIGFEP信号就会一直产生,进而就会一直捕捉SIGFEP信号。

总结:

信号的默认处理动作:

信号产生后,默认处理动作有TermIgnCoreStepCont

这五种默认处理动作的作用:

  • Trem:终止进程。
  • Ign:忽略这个信号。
  • Core:终止进程并进行核心转储(Core Dump)。
  • Step:暂停进程。
  • Cont:让暂停的进程继续执行。

大部分信号的默认处理动作是Trem和Core

Trem和Core的区别:

  • term:普通的终止进程不会做别的事情
  • core:除了终止进程,还会进行核心转储core dump),生成core文件,core文件中存储的是程序出错的相关信息,有助于找出问题所在。
  • 核心转储功能默认是关闭的(可使用 ulimit + -a 查看相关信息),需要手动开启 ulimit + -c + num , 其中num代表core文件的大小,当num为0时 ,即代表关闭核心转储功能

核心转储(Core Dump):

当一个进程要异常终止 时,可以选择把进程的用户空间内存数据全部保存到磁盘上 ,文件名通常是core,这叫做Core Dump。进程异常终止通常是因为有Bug,比如非法内存访问导致段错误,事后可以用调试器检查core文件以查清错误原因,这叫做事后调试 (Post-mortem Debug )。一个进程允许产生多大的core文件取决于进程的Resource Limit (这个信息保存在进程PCB中)。默认是不允许产生core文件的 ,因为core文件中可能包含用户密码等敏感信息,不安全 。在开发调试阶段可以用ulimit命令改变这个限制,允许产生core文件,ulimit + -c + num ,其中num代表生成core文件的大小,当num为0时 ,即代表关闭核心转储功能

  • 是否进场核心转储,会在进程的退出信息中标识

正常退出的进程的退出信息的低16位中,低 8 位都为0 (这也是为什么没有0号信号的原因),表示正常退出,高 8 位标记退出状态

被信号杀掉的进程的退出信息的低16位中,低 7 位标记退出码第 8 位标记是否进行核心转储高 8 位未用

信号的保存

相关概念:

  • 实际执行信号的处理动作叫做 信号递达
  • 信号从产生到递达之间的状态叫做 信号未决 ,即保存信号
  • 进程可以选择阻塞(屏蔽)某一个信号。
  • 被阻塞的信号产生后将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达动作。

注意:

  • 这里的阻塞和前边说的忽略是不同的,被阻塞的信号不会被递达 ,而忽略是信号递达后可选择的处理动作 。类比到日常收发消息,阻塞----收不到消息忽略----已读不回

内核结构:

信号未决,需要被标识,这个标识都是通过位图实现的(pending)。这个位图是通过一个整型数据实现的,每一个比特位的位置表示信号编号比特位的内容表示是否收到该位置对应的信号

与信号未决类似,信号阻塞也是通过一个位图标识的(block)。这个位图是通过一个整型数据实现的,每一个比特位的位置表示信号编号比特位的内容表示是否阻塞该位置对应的信号

内核结构:

  • block即阻塞位图。
  • pending即信号未决位图。
  • handler是一个函数指针数组,其中存储的是对应信号的处理动作。

信号产生时,内核在进程控制块中设置该信号的未决标志,直到信号递达才清除该标志

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

当一个信号被阻塞时,一旦产生这个信号,它将被保存在pending位图 中,等待进程解除对该信号的阻塞,如果在进程解除对某信号的阻塞之前这种信号产生过多次 ,由于pending位图 是通过一个整型实现的,因此每个比特位只能标记是否产生了该信号不能标记产生了几次该信号 。因此,常规信号在递达之前产生多次只计一次,而实时信号 又有所不同**,** 在递达之前产生多次可以依次放在一个队列里

sigset_t:

每个信号只有一个比特位的未决标志,非0即1,不记录该信号产生了多少次,阻塞标志也是这样表示的。因此,未决和阻塞标志可以用相同的数据类型sigset_t来存储sigset_t称为信号集它是操作系统提供的一个数据类型 。这个类型可以表示每个信号的"有效"或"无效"状态,在阻塞信号集中"有效"和"无效"的含义是该信号是否被阻塞 ,而在未决信号集中"有效"和"无效"的含义是该信号是否处于未决状态阻塞信号集 也叫做当前进程的信号屏蔽字

信号集操作函数:

sigset_t类型是操作系统提供的数据类型,对于每种信号用一个比特位表示"有效"或"无效"状态,至于这个类型内部是如何存储数据的,则依赖于系统实现,用户是不必关心的,用户只能调用以下函数来操作sigset_ t变量,而不能对它的内部数据做任何解释,比如用printf函数直接打印sigset_t变量是没有意义的。

cpp 复制代码
#include <signal.h>
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset (sigset_t *set, int signo);
int sigdelset(sigset_t *set, int signo);
int sigismember(const sigset_t *set, int signo);
  • sigemptyset:用于初始化 set指向的信号集,使其中所有信号的对应比特位的数据清零,表示该信号集不包含任何有效信号。成功返回0,出错返回-1。
  • sigfillset:用于初始化set指向的信号集,使其中所有信号的对应比特位置1,表示该信号集的有效信号包括系统支持的所有信号。成功返回0,出错返回-1。
  • sigaddset:向set指向的信号集中添加signo号有效信号。成功返回0,出错返回-1。
  • sigdelset:向set指向的信号集中删除signo号有效信号。成功返回0,出错返回-1。
  • sigismember:用于判断set指向的信号集的有效信号中是否包含signo号信号。若包含返回1,不包含返回0,出错返回-1。

注意:

  • 使用sigset_t类型的变量之前一定要 调用sigemptyset或者sigfillset做初始化操作保证信号集处于确定的状态
  • sigset_t只是系统提供的数据类型,与int、char等类似,单纯的对sigset_t类型的数据进行操作并不会影响内核中的数据,想要修改内核数据,还要借助系统调用函数sigprocmask

修改阻塞信号集:

cpp 复制代码
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);

参数:

  • how:有三个参数可选

    |-------------|-----------------------------------------------------------------------------------------|
    | SIG_BLOCK | 把在set指向的信号集中存在,而在当前阻塞信号集中没有的有效信号加入到阻塞信号集中,形成新的阻塞信号集。即新的阻塞信号集是当前的阻塞信号集和set指向的信号集的并集。 |
    | SIG_UNBLOCK | 把在set指向的信号集中存在的有效信号,从阻塞信号集中移除,即解除对应信号的阻塞,允许移除未被阻塞的信号。 |
    | SIG_SETMASK | 把阻塞信号集设置为set指向的信号集。 |

  • set:输入性参数,如果为非空,则根据how参数对进程的信号屏蔽字(阻塞信号集)进行 操作。

  • oset:输出型参数,如果为非空,则读取进程的当前信号屏蔽字,通过oset传出。

返回值:

  • 成功返回 0 ;失败返回 -1 ,并设置错误信息。

那么,能不能把所有信号都阻塞掉,创建一个"无敌"进程呢?上代码测试:

cpp 复制代码
void PrintSig(sigset_t &pending)//打印信号集
{
    cout << "pending bitmap: ";
    for (int signo = 31; signo > 0; signo--)
    {
        if (sigismember(&pending, signo))//判断信号是否在信号集中
        {
            cout << "1";
        }
        else
        {
            cout << "0";
        }
    }
    cout << endl;
}
int main()
{

    sigset_t Set,OSet;
    int a=sigemptyset(&Set);//初始化Set信号集全为0
    assert(a==0);
    a=sigemptyset(&OSet);//初始化OSet信号集全为0
    assert(a==0);

    for(int i=1;i<32;i++)
    {
        a=sigaddset(&Set,i);//把i号信号添加进Set中
        assert(a==0);
    }

    a=sigprocmask(SIG_SETMASK,&Set,&OSet);//把Set设置为进程的屏蔽字
    assert(a==0);

    cout<<"pid:"<<getpid()<<endl;
    while(1)
    {
        sigset_t pending;
        a=sigemptyset(&pending);//初始化pending信号集全为0
        assert(a==0);

        a=sigpending(&pending);//获取未决信号集
        assert(a==0);

        PrintSig(pending);//输出未决信号集

        sleep(1);
    }

    return 0;
}

上述代码,把31个信号都加入到了阻塞信号集中,运行程序观察:

可以发现当向进程发送9号信号时,进程依然被终止了。

接下来从10号信号开始继续:

可以发现当向进程发送19号信号时,还是执行了19号信号的默认处理动作。

接下来从20号信号开始:

可以发现,向进程发送20号信号,会把18号信号的屏蔽解除。

再倒着来一遍看看:

可以发现,向进程发送18号信号会把20、21、22号信号的屏蔽解除。

总结:

  • 9、19号信号无法被屏蔽,18、20号信号会把特定信号的屏蔽解除

获取未决信号集:

cpp 复制代码
#include <signal.h>
int sigpending(sigset_t *set);

参数:

  • set:输出型参数,若为非空,则读取当前进程的未决信号集,通过set传出。

返回值:

  • 成功返回 0 ;失败返回 -1 ,并设置错误信息。

测试:

有了上述基础,我们就可以对阻塞信号集进行修改了,并且还可以获取当前进程的未决信号集。

下面写一段代码进行测试:

cpp 复制代码
#include <iostream>
#include <cerrno>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>
using namespace std;

void task(int sig)
{
    cout << "Get a signal , signal num:" << sig << endl;
}

void PrintSig(sigset_t &pending)//打印信号集
{
    cout << "bitmap: ";
    for (int signo = 31; signo > 0; signo--)
    {
        if (sigismember(&pending, signo))//判断信号是否在信号集中
        {
            cout << "1";
        }
        else
        {
            cout << "0";
        }
    }
    cout << endl;
}

void task(int sig)
{
    cout << "Get a signal , signal num:" << sig << endl;
}

void PrintSig(sigset_t &pending)//打印信号集
{
    cout << "bitmap: ";
    for (int signo = 31; signo > 0; signo--)
    {
        if (sigismember(&pending, signo))//判断信号是否在信号集中
        {
            cout << "1";
        }
        else
        {
            cout << "0";
        }
    }
    cout << endl;
}

int main()
{
    signal(2,task);//自定义2号信号处理动作

    sigset_t Set,OSet;
    int a=sigemptyset(&Set);//初始化Set信号集全为0
    assert(a==0);
    
    a=sigemptyset(&OSet);//初始化OSet信号集全为0
    assert(a==0);
    
    cout<<"Set:"<<endl;
    PrintSig(Set);//由于sigset_t是系统提供的类型,无法直接打印输出,只能通过自定义函数打印输出
    cout<<"OSet:"<<endl;
    PrintSig(OSet);

    a=sigaddset(&Set,2);//把二号信号添加进Set中
    assert(a==0);
    
    cout<<"Add signal number 2 to the Set:"<<endl;
    PrintSig(Set);

    a=sigprocmask(SIG_SETMASK,&Set,&OSet);//把Set设置为进程的屏蔽字
    assert(a==0);
    
    cout<<"block 2 signal success"<<endl;
    cout<<"pid:"<<getpid()<<endl;

    int count=0;
    while(1)
    {
        sigset_t pending;
        a=sigemptyset(&pending);//初始化pending信号集全为0
        assert(a==0);

        a=sigpending(&pending);//获取未决信号集
        assert(a==0);

        PrintSig(pending);//输出未决信号集
        count++;

        if(count==10)//解除对2号信号的屏蔽
        {
            cout<<"解除2号信号的屏蔽"<<endl;
            a=sigprocmask(SIG_UNBLOCK,&Set,&OSet);
            assert(a==0);
        }

        sleep(1);
    }

    return 0;
}

上述代码中, 先是自定义2号信号的处理动作,然后将2号信号设置进进程的信号屏蔽字,10秒后再解除对2号信号的屏蔽,期间通过键盘 Ctrl+c 不断给进程发送2号信号,观察现象:

可以发现,未决信号集中,只有一个比特位标记是否收到2号信号,即使发送多次2号信号,在未决信号集中也只有一个1,表示有一个2号信号没有处理。并且当解除对2号信号的屏蔽后会立即执行2号信号的处理动作,后续再发送2号信号,将直接处理。

这里还有个小问题,当使用sigprocmask解除对某个信号的屏蔽时,是在sigprocmask返回之前,pending位图就清零了还是sigprocmask返回之后,pending位图才清零?再来看下边一段代码:

cpp 复制代码
#include <iostream>
#include <cerrno>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>
#include <cassert>

using namespace std;

void PrintSig(sigset_t &pending)//打印信号集
{
    cout << "bitmap: ";
    for (int signo = 31; signo > 0; signo--)
    {
        if (sigismember(&pending, signo))//判断信号是否在信号集中
        {
            cout << "1";
        }
        else
        {
            cout << "0";
        }
    }
    cout << endl;
}

void task(int sig)
{
    sigset_t pending;
    int n =sigemptyset(&pending);
    assert(n==0);
    n = sigpending(&pending);
    assert(n == 0);
    
    // 打印pending位图中的信息
    cout << "递达中...: "<<endl;
    PrintSig(pending); 
    cout << sig << " 号信号被递达处理..." << endl;
}

int main()
{
    signal(2,task);//自定义2号信号处理动作

    sigset_t Set,OSet;
    int a=sigemptyset(&Set);//初始化Set信号集全为0
    assert(a==0);
    
    a=sigemptyset(&OSet);//初始化OSet信号集全为0
    assert(a==0);
    
    a=sigaddset(&Set,2);//把2号信号添加进Set中
    assert(a==0);
    
    cout<<"Add signal number 2 to the Set:"<<endl;
    PrintSig(Set);

    a=sigprocmask(SIG_SETMASK,&Set,&OSet);//把Set设置为进程的屏蔽字
    assert(a==0);
    
    cout<<"block 2 signal success"<<endl;
    cout<<"pid:"<<getpid()<<endl;

    int count=0;
    while(1)
    {
        sigset_t pending;
        a=sigemptyset(&pending);//初始化pending信号集全为0
        assert(a==0);

        a=sigpending(&pending);//获取未决信号集
        assert(a==0);

        PrintSig(pending);//输出未决信号集
        count++;

        if(count==10)//解除对2号信号的屏蔽
        {
            cout<<"解除2号信号的屏蔽"<<endl;
            a=sigprocmask(SIG_UNBLOCK,&Set,&OSet);
            cout<<"解除屏蔽成功"<<endl;
            assert(a==0);
        }

        sleep(1);
    }

    return 0;
}

可以发现,在递达信号的时候,pending位图就已经被清零了,即在sigprocmask返回之前,pending位图就已经清零了

信号的捕捉

信号的检测与处理:

  • 信号技术是通过软件的方式,模拟硬件中断。
  • 当程序中存在系统调用或者有中断、异常时,进程就会进入内核态去处理,当进程从内核态切换到用户态 时,信号会被检测并处理 。因为进程调度时间片的存在,使得程序中即使没有系统调用、中断、异常,进程也会存在从内核态到用户态的切换。

信号的捕捉过程:

在信号处理(捕捉)过程中,会出现4次用户态和内核态之间的转换

以2号信号为例:

如果信号的处理动作是用户自定义函数,在信号递达时就调用这个函数,这称为捕捉信号。 当前正在执行main函数,这时发生中断/异常/系统调用切换到内核态。 在处理完毕后要返回用户态的main函数之前检查到有2号信号递达。由于2号信号的自定义处理动作的代码在用户空间,内核决定返回用户态后不是恢复main函数的上下文继续执行,而是执行sighandler函数,sighandler和main函数使用不同的堆栈空间,它们之间不存在调用和被调用的关系,是两个独立的控制流程。 sighandler函数返回后自动执行特殊的系统调用sigreturn再次进入内核态。 如果没有新的信号要递达,这次再返回用户态就是恢复main函数的上下文继续执行了。

进程地址空间:

  • 操作系统也是一个软件,它是机器开机后第一个加载进机器的。
  • 每个进程在创建时,会把操作系统相关数据通过内核级页表加载到自己进程地址空间的[3,4]GB的空间中,当进程执行系统调用或访问系统数据时,都是在进程自己的地址空间中完成的。
  • 进程无论如何切换,总能找到操作系统、访问系统数据,本质就是去进程自己的地址空间的[3,4]GB空间中访问数据。(此时操作系统会进行身份识别,只有在内核态才能访问系统数据。)
  • 操作系统是一个死循环的程序,不断地接受外部的硬件中断,不断被刺激响应。

信号捕捉函数:

除了前边说的signal函数可以捕捉信号外,sigaction函数也可以捕捉信号。

cpp 复制代码
#include <signal.h>
int sigaction(int signo, const struct sigaction *act, struct sigaction *oact);

参数:

  • signo:指定信号的编号。
  • act:一个sigaction类型的结构体指针,若act非空,则根据act修改该信号的处理动作。
  • oact:一个sigaction类型的结构体指针,若oact非空,则通过oact传出该信号原来的处理动作。

返回值:

  • 成功返回0;失败返回 -1,并设置错误码。
  • sigaction结构体:
cpp 复制代码
struct sigaction
{
    void (*sa_handler)(int);
    void (*sa_sigaction)(int, siginfo_t *, void *);
    sigset_t sa_mask;
    int sa_flags;
    void (*sa_restorer)(void);
};
  • sa_sigaction和sa_restorer是与实时信号相关的,这里不做处理。
  • sa_handler:将sa_handler赋值为SIG_IGN表示忽略信号;赋值为SIG_DFL表示执行系统默认动作;赋值为一个函数指针表示用自定义函数捕捉信号,该函数返回值为void,可以带一个int型参数,通过参数可以得知当前信号的编号,这样就可以用同一个函数处理多种信号。显然,这也是一个回调函数,只不过不是被main函数调用,而是被系统所调用。
  • sa_flags:默认设为0.
  • sa_mask:一个信号集。在调用信号处理函数时,除了当前信号被自动屏蔽之外,若还希望自动屏蔽另外一些信号,则用sa_mask字段说明这些需要额外屏蔽的信号。
cpp 复制代码
void handler(int sig)
{
    cout<<"get a signal , signal number:"<<sig<<endl;
}

int main()
{
    struct sigaction act,oact;
    act.sa_flags=0;//默认设为0
    act.sa_handler=handler;//自定义处理动作
    int n=sigaction(2,&act,&oact);
    assert(n==0);

    while(1)
    { 
        sleep(1);
    }

    return 0;
}

上述代码中,只自定义捕捉了2号信号。没有额外屏蔽其他信号。

可以发现成功捕捉了2号信号。

可重入函数

若某个函数被多个执行流重复进入,导致出现了问题,这样的函数叫做不可重入函数。反之则叫做可重入函数。

平常使用的大部分库函数都是不可重入的。最常见的就是链表的插入函数。

若某一个函数满足以下条件之一,则是不可重入的:

  • 调用了malloc、free。因为malloc是调用全局链表实现的。
  • 调用了标准I/O库函数的。标准I/O库中的很多实现都是以不可重入的方式使用全局数据结构。

volatile

  • volatile作用主要是保持内存的可见性。在读取volatile修饰的数据时,必须去内存中读取

有如下一段代码:

cpp 复制代码
#include<stdio.h>
#include<signal.h>

int g_flag = 0;

void changeflag(int signo)
{
    g_flag++;
    printf("g_flag:%d\n", g_flag);
}

int main()
{
    signal(2, changeflag);

    while(!g_flag); // 故意写成这个样子, 编译器默认会对代码进行自动优化
    printf("process quit normal\n");
    return 0;
}

使用gcc编译时,在最后边加上 -O3 强制以最高优化等级优化代码,再运行:

可以发现,本应该发送一次2号信号就结束的程序,却一直没有结束。这是因为g_flag是一个全局变量,强制优化编译时,编译器会把g_flag在寄存器上保存一份 ,在使用时,直接使用寄存器上的数据,没有去内存中读取数据,导致虽然内存中的数据改变了,但是程序不读取,就会一直陷入死循环。

给g_flag加上volatile修饰,再测试:

cpp 复制代码
#include<stdio.h>
#include<signal.h>

volatile int g_flag = 0;
//int g_flag = 0;

void changeflag(int signo)
{
    //(void)signo;
    g_flag++;
    printf("g_flag:%d\n", g_flag);
}

int main()
{
    signal(2, changeflag);

    while(!g_flag); // 故意写成这个样子, 编译器默认会对代码进行自动优化
    printf("process quit normal\n");
    return 0;
}

可以发现,即使使用最高优化等级优化代码编译,程序还是会正常退出。

SIGCHLD信号

通常使用wait和waitpid函数清理僵尸进程,父进程可以阻塞等待子进程结束,也可以非阻塞地查询是否有子进程结束等待清理(也就是轮询的方式)。采用第一种方式,父进程阻塞了就不能处理自己的工作了;采用第二种方式,父进程在处理自己的工作的同时还要记得时不时地轮询一下,太过复杂。

其实,子进程在终止时会给父进程发SIGCHLD信号,该信号的默认处理动作是忽略,父进程可以自定义SIGCHLD信号的处理函数,这样父进程只需专心处理自己的工作,不必关心子进程了,子进程终止时会通过信号通知父进程,父进程在信号处理函数中调用wait清理子进程即可。

cpp 复制代码
void CleanupChild(int signo)
{
    if (signo == SIGCHLD)
    {
        pid_t rid = waitpid(-1, nullptr, 0);
        if (rid > 0)
        {
            cout << "wait child success: " << rid << endl;
        }
    }
    cout << "wait sub process done" << endl;

}
int main()
{
    signal(SIGCHLD, CleanupChild);//自定义SIGCHLD信号处理动作

    pid_t id = fork();
    if (id == 0)
    {
        // child
        int cnt = 5;
        while (cnt--)
        {
            cout << "I am child process: " << getpid() << endl;
            sleep(1);
        }
        cout << "child process died" << endl;
        exit(0);
    }
    // father
    while (true)
        sleep(1);
}

可以发现,自定义捕捉函数确实可以等待回收子进程。

相关推荐
紅色彼岸花几秒前
第六章:DNS域名解析服务器
运维·服务器
Spring_java_gg4 分钟前
如何抵御 Linux 服务器黑客威胁和攻击
linux·服务器·网络·安全·web安全
✿ ༺ ོIT技术༻5 分钟前
Linux:认识文件系统
linux·运维·服务器
恒辉信达6 分钟前
hhdb数据库介绍(8-4)
服务器·数据库·mysql
会掉头发33 分钟前
Linux进程通信之共享内存
linux·运维·共享内存·进程通信
我言秋日胜春朝★35 分钟前
【Linux】冯诺依曼体系、再谈操作系统
linux·运维·服务器
饮啦冰美式1 小时前
22.04Ubuntu---ROS2使用rclcpp编写节点
linux·运维·ubuntu
wowocpp1 小时前
ubuntu 22.04 server 安装 和 初始化 LTS
linux·运维·ubuntu
wowocpp1 小时前
ubuntu 22.04 server 格式化 磁盘 为 ext4 并 自动挂载 LTS
服务器·数据库·ubuntu