信号的概念

信号是用于进程间通信和进程控制的一种机制。
信号是一种异步事件,用于向进程发送通知,告知其发生了某些特定的事件或异常情况

查看信号

在这里插入图片描述

通过命令kill-l,列出所有可用信号名称和对应的编号;
通常可以对信号分为三类:标准信号,实时信号、专用信号
标准信号:编号为1-31之间都是标准信号,这些都是预定义信号,用于通知进程发生的各种事件。
实时信号:编号从32开始起均是实时信号,与标准信号相对应。
实时信号提供了更高精度和更灵活的通信机制,适用于需要实时处理的应用场景。实时信号比较复杂,多个实时信号可以同时发送给一个进程,而标准信号一次只能发送一个。实时信号可以附带额外的数据,以提供更多的信息。

信号的产生

利用键盘产生信号

有关函数:
在这里插入图片描述

void handler(int signal_number)
{
    cout<<"get a sig, number is: "<<signal_number<<endl;
}

int main()
{
    signal(SIGINT,handler);

    while(true)
    {
        cout<<"I am activing...,pid: "<<getpid()<<endl;
        sleep(1);
    }
    return 0;
}

在这里插入图片描述
执行程序后,按住ctrl+c将会执行到handler函数,并并且signal_number为2,因为SIGINT编号为2;
此时ctrl+c就是一个信号(SIGINT表示键盘中断,输入ctrl+c就是表示对进程的中断),我们调用signnal函数就是设置了接收到信号该调用对应函数;利用命令kill-9 pid才能终止进程;

改成 SIG_IGN:

在这里插入图片描述

特殊:
在这里插入图片描述

利用系统调用产生信号

有关系统调用:
在这里插入图片描述

int main(int argc,char* argv[])
{
    if(argc!=3)
    {
        cout<<"Usage: "<<argv[0]<< " - signNumberPid"<<endl;
        return 1;
    }

    int signumber=stoi(argv[1]+1);
    int pid =stoi(argv[2]);

    int n=kill(pid,signumber);
    if(n<0)
    {
        cerr<<"kill error, "<<strerror(errno)<<endl;
    }

    return 0;
}

解释:
在这里插入图片描述

有关系统调用:
在这里插入图片描述

void handler(int signumber)
{
    cout<<"get a signal, number is:"<<signumber<<endl;
}
int main()
{
    signal(2,handler);
    int cnt=0;
    while(true)
    {
        cout<<"cnt: "<<cnt++<<endl;
        sleep(1);
        if(cnt%5==0)
        {
            cout<<"sned 2 to caller"<<endl;
            //raise(2);
            abort();
        }
    }
}

在这里插入图片描述

abort:
在这里插入图片描述

coredump:
核心存储,当程序发生异常终止时,系统会将程序的内存数据保存到一个称为核心存储文件的文件中
会将错误的数据保存到磁盘当中,通过分析coredumped文件,开发人员可以了解程序崩溃时的上下文信息;从而更好的定位和修复程序的错误;
默认情况下coredump是关闭的,这是为了防止未知的coredump一直运行,导致服务器磁盘被打满;
在这里插入图片描述
在这里插入图片描述

软件产生信号

之前的SIGPIPE就是一种由软件产生的信号:当读端读一般关闭时,写端一直在写,系统会直接终止写入的进程,内核会向写端进程发送SIGPIPE信号。

有关函数:
在这里插入图片描述

int g_cnt=0;
int ret=0;
void handler(int sig)
{
    cout<<"sig: "<<sig<<" g_cnt: "<<g_cnt++<<endl;
    unsigned int n=alarm(5);
    cout<<"还剩多少时间"<<n<<endl;
}
int main()
{
    alarm(5);
    signal(SIGALRM,handler);
    int cnt=0;
    while(true)
    {
        sleep(1);
        cout<<"cnt: "<<cnt++<< " pid: "<<getpid()<<endl;

        if(cnt==5)
        {
            //int n=alarm(0);
            cout<<"alarm(0) ret: "<<n<<endl;
        }
    }

    return 0;
}

1s能循环多少次:
在这里插入图片描述
设定特定时间进入处理函数:
在这里插入图片描述
alarm函数的返回值:
在这里插入图片描述

异常产生信号

void handler(int sig)
{
    cout<<"sig: "<<sig<<endl;
    exit(1);
}
int main()
{
    //signal(SIGSEGV,handler);
    int a=10;
    a/=0;

    // int* p=nullptr;
    // *p=100;//野指针

    //while(true) sleep(1);

    return 0;
}

除0异常:
在这里插入图片描述
除0异常并进入处理函数:
在这里插入图片描述
野指针:
在这里插入图片描述
产生对应的信号:
在这里插入图片描述

信号的保存

在这里插入图片描述
在这里插入图片描述

所以这里信号的保存指的是接收到信号后,将其暂时保存起来以便后续处理;
信号的保存和进程的状态息息相关。这三种数据结构就是与保存有关

对有关函数的解释

sigset_t: sigset_t是一个信号集的数据类型。信号集用于表示每个信号的状态,即该信号是有效的(未被阻塞)还是无效的(被阻塞)。通过使用sigset_t类型的变量,可以进行信号集的操作,例如添加信号、删除信号、检查信号是否存在等操作。

sigemptyset_t:
在这里插入图片描述
这是一个用于清空信号集的函数。它的作用是将指定的信号集设置为空集,即将所有信号都标记为无效(初始化)。

参数说明:

  • set:指向要清空的信号集的指针。

返回值:

  • 成功:返回0。
  • 失败:返回-1,并设置errno为相应的错误码。

sigaddset:
这是一个用于向信号集中添加信号的函数。它的作用是将指定的信号加入到信号集中,使得该信号成为有效的(未被阻塞)。

函数原型:

int sigaddset(sigset_t *set, int signum);

其中,set是一个指向sigset_t类型的指针,表示要操作的信号集;signum是要添加的信号编号。

如果成功添加了信号,则该函数返回0;如果出错,则返回-1。

sigprocmask:
在这里插入图片描述
这是一个用于设置进程的信号屏蔽字(signal mask)的函数。信号屏蔽字用来指定哪些信号在给定的上下文中被阻塞,即不会引起相应信号处理程序的执行。

其中,how表示信号屏蔽字的设置方式,有三种取值:

  • SIG_BLOCK: 将set中的信号添加到当前信号屏蔽字中;
  • SIG_UNBLOCK: 从当前信号屏蔽字中移除set中的信号;
  • SIG_SETMASK: 设置当前信号屏蔽字为set中的值。

set是一个指向sigset_t类型的指针,表示要设置的新的信号屏蔽字。oldset是一个指向sigset_t类型的指针,用于保存之前的信号屏蔽字。

如果成功设置了信号屏蔽字,则该函数返回0;如果出错,则返回-1。

sigpenging:

在这里插入图片描述

sigisnumber:
在这里插入图片描述

使用例子

#include<iostream>
using namespace std;
#include<signal.h>
#include<unistd.h>
#include<assert.h>
#include<sys/types.h>
void PrintSig(sigset_t& pending)
{
    cout<<"pending Sig: ";
    for(int i=31;i>0;i--)
    {
        if(sigismember(&pending,i))
        {
            cout<<"1";
        }
        else
        {
            cout<<"0";
        }
    }
    cout<<endl;
}

void handler(int signo)
{
    sigset_t pending;
    sigemptyset(&pending);
    int n = sigpending(&pending); // 正在处理2号信号
    assert(n == 0);

    // 3. 打印pending位图中的收到的信号
    std::cout << "递达中...: ";
    PrintSig(pending); 
    std::cout << signo << " 号信号被递达处理..." << std::endl;
}
int main()
{
    //对信号进行捕捉,进入到处理函数中;
    signal(SIGINT,handler);

    cout<<"pid:"<<getpid()<<endl;
    //1。对2号信号的屏蔽
    sigset_t block,oblock;
    sigemptyset(&block);//对信号集进行初始化
    sigemptyset(&oblock);
    sigaddset(&block,2);//将信号添加到信号集中
    
    //如果对所有信号进行屏蔽呢
    // for(int signo=1;signo<32;signo++)
    //     sigaddset(&block,signo);
     int n=sigprocmask(SIG_SETMASK,&block,&oblock);//对信号开始屏蔽,设置到内核中;
    assert(n==0);//n为0时表示返回成功
    (void)n;
    cout<<"block 2 on success"<<endl;

    int cnt=0;
    while (true)
    {
        sleep(1);
        //2.获取pending的位图
        sigset_t pending;
        sigemptyset(&pending);
        n=sigpending(&pending);
        assert(n==0);
        (void)n;
        
        //3.打印pending位图
        PrintSig(pending);
        cnt++;
        
        //4.对2号信号解除屏蔽
        if(cnt==20)
        {
            cout<<"将2号屏蔽解除"<<endl;
            n=sigprocmask(SIG_UNBLOCK,&block,&oblock);
            assert(n==0);
        }
    }
    
    
}

测试:
在这里插入图片描述

是否所有信号都可以屏蔽?
在这里插入图片描述
达到一定时间后对信号解除屏蔽:
在这里插入图片描述

利用处理函数来清0与递达哪个优先:
在这里插入图片描述

信号的处理

信号什么时候被处理?

  1. 进程从内核态切换回用户态的时候,会检查是否有未处理的信号,并在此时对信号处理
  2. 当进程接受一个信号时,如果该信号的处理函数已经注册,那么在合适的时机(通常是下一次进入内核态之前)会调用该信号的处理函数进行处理。
  3. 当一个被阻塞的信号被解除阻塞时,立即进入信号处理
  4. 进程调度时,操作系统可能会选择挂起正在执行的进程并转而执行其他进程。被挂起的进程再次运行时之前会处理一些未处理的信号。

用户态和内核态

在这里插入图片描述

信号如何被处理?

信号处理函数是在用户空间执行的,因此可以执行各种用户态操作,包括修改数据、发送信息、改变进程状态等。
进程可以注册信号的处理函数,用于接收到特定信号时执行自定义的操作。signal()就是;
还有另一个sigaction();

在这里插入图片描述
sigaction结构体

  • sahandler:指定处理函数的地址
  • sa_mask:指定在信号处理函数执行期间需要屏蔽的信号集合;
  • sa_flags: 字段用于修改信号的行为。

使用sigaction函数

void Print(sigset_t &pending)
{
    std::cout << "curr process pending: ";
    for(int sig = 31; sig >= 1; sig--)
    {
        if(sigismember(&pending, sig)) std::cout << "1";
        else std::cout << "0";
    }
    std::cout << "\n";
}

void handler(int signo)
{
    cout<<"signo: "<<signo<<endl;
    sigset_t pending;
    sigemptyset(&pending);
    while(true)
    {
        sigpending(&pending);
        Print(pending);
        sleep(1);
    }

}
int main()
{
    struct sigaction act,oact;
    act.sa_handler=handler;
    act.sa_flags=0;
    sigemptyset(&act.sa_mask);

    sigaddset(&act.sa_mask,3);
    sigaddset(&act.sa_mask,4);
    sigaddset(&act.sa_mask,5);
    sigaction(2,&act,&oact);
    while(true) sleep(1);
    return 0;
}

最简单的使用:
在这里插入图片描述
在这里插入图片描述

多增加几个屏蔽信号:
在这里插入图片描述
结果:
在这里插入图片描述
在这里插入图片描述

可重入函数

可重入函数是指在多任务环境中能够被中断并且安全地同时被多个任务调用的函数。这些函数具有以下特点:

  1. 不依赖于任何环境:可重入函数除了使用自己栈上的变量以外,不依赖于全局变量、静态数据结构等共享资源。这样可以保证在被中断后,函数的执行状态不会受到其他任务的影响。

  2. 没有数据竞争:可重入函数通过使用本地变量或者适当的同步机制,避免了数据竞争的问题。在多任务环境下,多个任务同时访问同一个函数时,不会导致数据不一致或者其他未定义行为的情况发生。

  3. 可以被递归调用:虽然不是所有的可重入函数都是递归的,但所有递归函数都应该是可重入的。因为递归函数可能在嵌套调用时,被同一个任务中断并进入其他代码执行,而返回时不会出错。

现象

#include<iostream>
using namespace std;
#include<signal.h>
#include<unistd.h>

volatile int g_flag=0;
void changeflag(int signo)
{
    (void)signo;
    printf("将g_flag,从%d->%d\n", g_flag, 1);
    g_flag = 1;
}

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

    while(!g_flag);

    printf("process quit normal\n");
    return 0;
}

结果:
在这里插入图片描述
编译可执行程序时加上-O1:
在这里插入图片描述
-O1:表示使用优化级别1进行编译;优化界别是指编译器在生成目标代码时所采用的优化策略的级别;
执行程序后,按ctrl+c后没有从循环出来,g_flag为1仍然在循环中;这是因为正常情况下,CPU读取代码时是从寄存器上拿的,内存上g_flag会被引I用到对应寄存器中,当按ctrl+c后g_flag在对应内存位置就改为了1,CPU读取后发现为1就退出程序了;
而设置为优化级别1后,会将g_flag映射到对应的寄存器中,当按ctrl+cg_flag在对应内存位置就改为了1,而此时寄存器上的g_flag没有改变,仍为0,CPU仍然读取这个寄存器,没有改变,所以仍然循环。
在这里插入图片描述

SIGCHLD

程序

void handler(int signo)
{
    cout<<"child quit,signo: "<<signo<<endl;
}
void CleanUpchild(int signo)
{
    // if(signo==SIGCHLD)
    // {
    //     //利用函数waitpid()让父进程对子进程进行等待结束
    //     pid_t rid = waitpid(-1, nullptr, 0);
    //     if (rid > 0)
    //     {
    //         std::cout << "wait child success: " << rid << std::endl;
    //     }
    // }

    if(signo==SIGCHLD)
    {
        //利用函数waitpid()让父进程对子进程进行等待结束
        
        while(true)
        {
            pid_t pid=waitpid(-1, nullptr, WNOHANG);
            if(pid>0)
            {
                cout << "wait child success: " << pid << std::endl;
            }
            else if(pid<=0)
            {
                break;
            }
        }    
    }

}
int main()
{
    signal(SIGCHLD,CleanUpchild);
    for(int i=0;i<100;i++)
    {
        //chlid
        pid_t id =fork();
        if(id==0)
        {
            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);
    return 0;
}

一般情况:
在这里插入图片描述

子进程的退出会向父进程发送一个信号:
在这里插入图片描述

利用SIGCHLD来解决资源的回收

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述
优化:
在这里插入图片描述

Logo

一起探索未来云端世界的核心,云原生技术专区带您领略创新、高效和可扩展的云计算解决方案,引领您在数字化时代的成功之路。

更多推荐