Linux进程信号

目录

  • 信号预备知识
  • 信号的产生
  • 信号的保存
  • 信号的捕捉

信号的预备知识

信号我们从四个方面进行讲解,第一个是前置知识,第二个是信号的产生,第三个是信号的保存,第四个是信号的捕捉

在我们的现实世界里,有哪些是信号的体现?
信号弹,下课上课的铃声,红绿灯,快递发取件码短信,旗语,狼烟,发令枪,军训时哨声,闹钟,外卖电话,冲锋号。。。
a.那你是怎么意识(识别)到这些是信号的?有人教(告诉)你
b.即便我们现在没有收到信号,我也知道信号产生之后,我该干什么
c.信号产生了,我们可能并不会立即处理这个信号,在合适的时候,因为我们可能正在做更重要的事情。所以信号产生后,在到你处理信号期间,你的脑海里一定记住了这个信号。

同理
进程能够识别信号+没有收到信号也知道信号到来时应该怎么做–由程序员编码实现的
进程收到了信号,可能不会立即处理这个信号,会在合适的时候处理。所以这个信号一定被保存到了某个地方。合适的时候是什么时候?后面会讲


int main()
{
    while (1)
    {
        cout << "I am carzy process" << endl;
        sleep(1);
    }
    return 0;
}

在这里插入图片描述

信号的产生

信号的产生有哪几种方式?
1.ctrl + c
2.ctrl + \
3.kill 命令
4.系统调用

从里里外外了解为什么ctrl+c能够杀掉前台进程呢?

讲解前台和后台
linux中,一次登录中的终端,只允许一个进程是前台进程,可以允许多个进程是后台进程。前台和后台进程的本质区别:谁能拿到键盘输入谁就是前台
在这里插入图片描述
输入指令没效果
我们再将这个进程让它在后台进行执行。运行时指令+&
在这里插入图片描述
可以看到在后台运行的进程,你仍然可以断断续续的输入指令,你输入的L、S只不过是回显,重要的是你输入的内容能够被bash进程拿到
在这里插入图片描述

ctrl+c本质是被进程解释成为收到了2号信号,而信号的处理方式有三种。1.默认动作(有的信号是终止自己,有的是忽略该信号)2.忽略(该忽略是主动忽略,不是默认动作的忽略)3.自定义动作
2号信号的默认动作就是终止自己
在这里插入图片描述
1-31号为普通信号、34-64为实时信号(不对实时信号进行讲解)

如何验证ctrl + c就是让进程收到了2号信号呢?
验证方法:对2号信号进行捕捉
在这里插入图片描述
signal函数。第一个参数signum:你要对哪个信号进行捕捉,第二个参数为函数指针(返回值为void,参数为int,传参的是信号)该函数就是信号的自定义动作
那我们就可以将2号信号捕捉,让进程不再执行默认动作,而执行自定义动作

void handler(int signo)//参数为收到了哪个信号
{
    //自定义动作设置为打印一条信息
    cout << "process get a signal" << endl;
}

int main()
{
	//代码是走到这一行signal函数时就执行方法吗?不是,时在后续条件发生的时候再执行
	//对标闹钟,不是设置闹钟的时候就执行这个动作,是这个闹钟响了再执行这个动作
    signal(2, handler);//只设置一次,在此代码往后都有效
    while (1)
    {
        cout << "I am carzy process" << endl;
        sleep(1);
    }
    return 0;
}

执行代码
在这里插入图片描述
可以看到按组合键ctrl + c确实是收到了2号信号


信号的产生有哪几种方式?
1.ctrl + c
2.ctrl + \
3.kill 命令
4.系统调用

ctrl + \是收到的哪个信号呢?收到的是3号信号
如何验证呢?捕捉三号信号

void handler(int signo)
{
    //自定义动作设置为打印一条信息
    cout << "process get a signal" << signo << endl;
}

int main()
{
    signal(3, handler);
    while (1)
    {
        cout << "I am carzy process" << endl;
        sleep(1);
    }
    return 0;
}

在这里插入图片描述

如果把所有的进程设置为自定义,那是不是没有人能kill掉我了?

void handler(int signo)//参数为收到了哪个信号
{
    //自定义动作设置为打印一条信息
    cout << "process get a signal" << signo << endl;
}

int main()
{
    for (int i = 1; i <= 31; ++i)
    {
        signal(i, handler);
    }
    while (1)
    {
        cout << "I am carzy process" << endl;
        sleep(1);
    }
    return 0;
}

执行代码
在这里插入图片描述
在这里插入图片描述
说明9号信号无法捕捉
在这里插入图片描述
在这里插入图片描述
说明19号信号无法捕捉
在这里插入图片描述在这里插入图片描述
综上,只有9号和19号信号不可以被捕捉。为什么不能捕捉这两个信号呢?如果能够捕捉,那就是操作系统的bug了,很容易被不法分子进行损害计算机的行为

硬件中断

键盘数据是如何输入给操作系统的,ctrl + c又是如何变成信号的?
键盘被摁下,肯定是操作系统先知道,操作系统怎么知道键盘上有数据了?难道是操作系统定期向所有外设进行检测吗?那OS的效率太低了。
硬件中断,如果硬件有数据了会主动告知
在这里插入图片描述
软件的信号就是参考硬件中断的

信号的产生有哪几种方式?
1.ctrl + c
2.ctrl + \
3.kill 命令
4.系统调用

系统调用

kill函数
在这里插入图片描述
第一个参数pid表示给哪个进程发送信号,第二个参数sig表示给该进程发送哪个信号

#include <iostream>
#include <unistd.h>
#include <stdlib.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>
using namespace std;

void handler(int signo)//参数为收到了哪个信号
{
    //自定义动作设置为打印一条信息
    cout << "process get a signal" << signo << endl;
}
int main()
{
    int pid = fork();
    if (pid == 0)
    {
        while (1)
        {
            //signal(3, handler);
            cout << "I am crazy process" << endl;
            sleep(1);
        }
        exit(0);
    }
    sleep(3);
    cout << "child send 3 signal to father" << endl;
    kill(pid, 3);
    int status = 0;
    int n = waitpid(pid, &status, 0);
    if (n > 0)
    {
        printf("waitpid sucess exit code: %d, signal: %d\n", (status >> 8) & 0xFF, status & 0x7F);
    }
    return 0;
}

执行结果
在这里插入图片描述

raise函数
在这里插入图片描述
给自己进程发送一个指定信号

void handler(int signo)//参数为收到了哪个信号
{
    //自定义动作设置为打印一条信息
    cout << "process get a signal" << signo << endl;
}
int main()
{
    int pid = fork();
    if (pid == 0)
    {
        int i = 0;
        while (1)
        {
            //signal(3, handler);
            cout << "I am crazy process" << endl;
            i++;
            if (i == 3)
            {
                raise(3);
            }
            sleep(1);
        }
        exit(0);
    }
    int status = 0;
    int n = waitpid(pid, &status, 0);
    if (n > 0)
    {
        printf("waitpid sucess exit code: %d, signal: %d\n", (status >> 8) & 0xFF, status & 0x7F);
    }
    return 0;
}

执行结果
在这里插入图片描述

给自己的进程发送指定SIGABRT,6号信号
在这里插入图片描述
如何证明发送了SIGABRT信号?捕捉它

void handler(int signo)//参数为收到了哪个信号
{
    //自定义动作设置为打印一条信息
    cout << "process get a signal" << signo << endl;
}
int main()
{
    signal(SIGABRT, handler);
    int i = 0;
    while (1)
    {
        if (i == 3)
        {
            abort();
        }
        cout << "I am crazy process" << endl;
        sleep(1);
        i++;
        
    }
    return 0;
}

在这里插入图片描述
可以看出即使被捕获,任然会执行退出,这是该函数的特点,因为是封装的函数,所以内部实现了即使SIGABRT被捕获任然会退出


信号的产生不仅仅有
1.ctrl + c
2.ctrl + \
3.kill 命令
4.系统调用
还有硬件条件、软件条件产生的异常

硬件异常

除0的理解

int main()
{
    cout << "div before" << endl;
    sleep(1);
    int a = 10;
    a /= 0;
    cout << "div after" << endl;
    return 0;
}

在这里插入图片描述

执行错误的本质是进程收到了信号,收到了8号SIGFPE信号
如何证明呢?捕捉该信号

void handler(int signo)//参数为收到了哪个信号
{
    //我只打印了该信息,其他什么也没干
    cout << "process get a signal" << signo << endl;
}
int main()
{
    signal(8, handler);
    cout << "div before" << endl;
    sleep(1);
    int a = 10;
    a /= 0;
    cout << "div after" << endl;
    return 0;
}

在这里插入图片描述
我并没有循环打印啊,为什么给我打印了这么多条信息?但可以初步推出是收到了这么多次信号,为什么会一直发送信号呢?
在这里插入图片描述
那我们能不能修正CPU的状态寄存器呢?不行,这是CPU自己维护的

为什么野指针解引用就会崩溃呢?OS会给当前进程发送11号信号
怎么验证呢?捕获该信号

void handler(int signo)//参数为收到了哪个信号
{
    //我只打印了该信息,其他什么也没干
    cout << "process get a signal" << signo << endl;
}
int main()
{
    signal(11, handler);
    sleep(1);
    int* a;
    *a = 10;
    return 0;
}

在这里插入图片描述
为什么也发送了这么多次信号呢?
在这里插入图片描述

信号的意义:进程收到大部分的信号,默认处理动作都是终止进程。信号的不同,代表着不同的事件,但是对事件发生之后处理动作可以一样,这样就可以追溯信号产生的原因,从而更容易让程序员去发现问题而去修正它

信号异常只能由硬件产生吗?还能由软件条件产生
如管道,读端关闭,写端任然在写入
在这里插入图片描述
闹钟函数
在这里插入图片描述
参数为设置闹钟的秒数,返回值为上次设置闹钟时,还剩余的时间
闹钟时间到了就会向进程发送指定SIGALRM、14号信号
如何证明?将该信号捕捉

void handler(int signo)//参数为收到了哪个信号
{
    cout << "process get a signal" << signo << endl;
}
int main()
{
    signal(SIGALRM, handler);
    alarm(5);
    while (1)
    {
        cout << "I am crazy process" << endl;
        sleep(1);
    }
    
    return 0;
}

在这里插入图片描述
为什么闹钟只响了一次?因为该闹钟不是异常,而且只设置了一次

可以用闹钟设置一个定时任务,执行的任务就是handler方法

void handler(int signo)//参数为收到了哪个信号
{
    cout << "process get a signal" << signo << endl;
    alarm(3);
}
int main()
{
    signal(SIGALRM, handler);
    alarm(3);
    while (1)
    {
        cout << "I am crazy process" << endl;
        sleep(1);
    }

    return 0;
}

在这里插入图片描述

进程退出时的核心存储问题
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
进程core退出和Term退出我们暂时看不到明显现象,只有将上面的打开限制打开才能看到core的核心转储现象
命令ulimit -c 1024
在这里插入图片描述
waitpid等待子进程的status第8位表示:信号退出是否是core

int main()
{
    pid_t pid = fork();
    if (pid == 0)
    {
        int a = 10;
        a /= 0;
        exit(0);
    }
    int status = 0;
    int n = waitpid(pid, &status, 0);
    if (n > 0)
    {
        printf("waitpid sucess, exit code: %d, core dump: %d, signal: %d\n", (status >> 8) & 0xFF, (status >> 7) & 1, status & 0x7F);
    }
    return 0;
}

执行结果
在这里插入图片描述
在这里插入图片描述
可以看到多了个core文件,后缀为引起信号异常的进程pid
我们想知道这个进程为什么崩溃,在哪里崩溃。所以将该进程的上下文数据dump下来,就有了core文件,这个core文件支持我们调试
在这里插入图片描述

为什么coredump的选项默认是关闭的?
从上面我们也可以看到起始core的文件也不小。一些大公司有自动运维,服务器挂了会自动重启,重启后又有可能挂,而导致可能将磁盘打满的情况,过一会儿就全是core文件,所以此选项一般是关闭的

信号发送与保存

对于普通信号而言,对于进程而言,信号是给谁发的?是给进程PCB发的
struct task_stuct
{
int signal; // 0000 0000 0000 0000 0000 0000 0000 0100 //表示收到了2号信号,因为它是第二位。一共有32位,31个普通信号,第0位没有使用


}
所以信号的发送的本质是操作系统将进程的PCB内的signal的位由0置1

信号的保存

介绍一些概念
1.实际执行信号的处理动作称为信号递达(Delivery)
2.信号从产生到递达之间的状态,称为信号未决(Pending)
3.进程可以选择阻塞(Block)某个信号
4.被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作
5.阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作

在这里插入图片描述
sigemptyset,sigfillset,sigaddset,sigdelset,sigismember函数,并不对信号进行操作,只对32个位进行操作
在这里插入图片描述
sigemptyset函数,将该32位都置为0
sigfillset函数,将32位都置为1
sigaddset函数,将第signum位置为1
sigdelset函数,将第signums位置为0
sigismember函数,检测第signum位是否为1
sigprocmask函数
在这里插入图片描述
第一个参数how:
SIG_BLOCK 添加当前信号屏蔽字的信号
SIG_UNBLOCK 从当前信号屏蔽字中解除阻塞信号
SIG_SETMASK 设置当前信号屏蔽字为set所指向的值(全部重置)
第二个参数set:输入型参数
第三个参数oldset:输出型参数–保存以前的set参数
sigpending函数
在这里插入图片描述
输入型参数,将pending位图设置为set所指向的值

实现代码,观察pending位图的变化

void handler(int signo)
{
    cout << "recv signo:" << signo << endl;
}
void printPending()
{
    sigset_t pset;
    sigpending(&pset);//获取该进程的pending位图表
    for (int i = 31; i >= 1; --i)
    {
        if (sigismember(&pset, i))
        cout << "1";
        else 
        cout << "0";
    }
    cout << endl;
}

int main()
{
    signal(2, handler);
    sigset_t bset;
    //将这位图初始化
    sigemptyset(&bset);
    sigaddset(&bset, 2);//讲bset第二位置为1

    sigprocmask(SIG_BLOCK, &bset, nullptr);//将信号屏蔽字设置

    while (1)
    {
        printPending();//打印pending位图
        sleep(1);
    }

    return 0;
}

在这里插入图片描述

我们看到按ctrl + c后没有执行捕捉方法,pending位图表的第一位(即二号信号)由0置为1了,因为将二号信号提前阻塞了,使二号信号无法递达

信号的捕捉处理

信号的捕捉处理是什么时候发生的?
当我们的进程从内核态返回到用户态的时候,进行信号的检测和处理

什么是内核态,什么是用户态?
内核态允许你访问操作系统的代码和数据,用户态只能访问用户自己的代码和数据
信号的捕捉处理
在这里插入图片描述
简单记忆方法:
在这里插入图片描述

重谈地址空间
在这里插入图片描述
操作系统的本质:基于时间中断的一个死循环
时钟中断
在这里插入图片描述


信号的捕捉处理
signation函数
在这里插入图片描述
第一个参数signum,你要设置的信号
第二个参数的类型
在这里插入图片描述
sa_handler为函数指针,信号捕捉时的自定义方法
mask:处理handler方法时,同时能够屏蔽其他的信号
sa_sigaction设为NULL、sa_flag设为0、sa_restorer设为NULL。目前这些参数我们并不用关系

void handler(int signo)
{
	//捕获信号,打印一条信息
    cout << "recv a signo:" << signo << endl;
}

int main()
{
    struct sigaction act, oact;
    act.sa_handler = handler;//将方法设置为handler方法
    sigaction(2, &act, &oact);

    while (1)
    {
        cout << "I am a process:" << getpid() << endl;
        sleep(1);
    }
    return 0;
}

在这里插入图片描述

pending位图的某位信号,什么时候从1变为0呢?是在函数里面变为0的还是调用函数之后变为0的?如何验证呢?

void PrintPending()
{
    sigset_t set;
    sigpending(&set);
    for (int i = 31; i >= 1; --i)
    {
        if (sigismember(&set, i) == true) 
            cout << "1";
        else 
            cout << "0";
    }
    cout << endl;
}
void handler(int signo)
{
    cout << "recv a signo:" << signo << endl;
    
}


int main()
{
    struct sigaction act, oact;
    act.sa_handler = handler;
    sigset_t set;
    sigaddset(&set, 3);
    sigaddset(&set, 4);
    sigaddset(&set, 5);
    sigaction(2, &act, &oact);

    while (1)
    {
        PrintPending();
        sleep(1);
    }
    return 0;
}

在这里插入图片描述
进入自定义方法后打印了peding位图2号信号为0,说明进入函数的时候pending信号就变为了0

sigaction函数的mask参数的验证
当某个信号的处理函数被调用时,内核自动将当前信号加入进程的信号屏蔽字,当信号处理函数返回时自动恢复原原来的信号屏蔽字

void PrintPending()
{
    sigset_t set;
    sigpending(&set);//获取pending位图
    for (int i = 31; i >= 1; --i)
    {
        if (sigismember(&set, i) == true) 
            cout << "1";
        else 
            cout << "0";
    }
    cout << endl;
}
void handler(int signo)
{
    cout << "recv a signo:" << signo << endl;
    while (1)
    {
        PrintPending();
        sleep(1);
    }
}

int main()
{
    struct sigaction act, oact;
    memset(&act, 0, sizeof(act));
    memset(&oact, 0, sizeof(oact));
    act.sa_handler = handler;//设置自定义方法
    sigset_t set;
    sigemptyset(&set);
    sigaddset(&set, 3);
    sigaddset(&set, 4);
    sigaddset(&set, 5);
    act.sa_mask = set;

    sigaction(2, &act, &oact);

    while (1)
    {
        cout << "I am a process: " << getpid() << endl;
        sleep(1);
    }
    return 0;
}

在这里插入图片描述

可重入函数
在这里插入图片描述
因为iinsert函数被main和handler执行流重复进入,导致最后的node2节点丢失,导致了内存泄漏
如果该函数被重复进入出问题–该函数则称为不可重入函数,相反,若没有出问题,则该函数insert称为可重入函数
如果一个函数符合以下条件之一则是不可重入的:
1.调用了malloc或free,因为malloc也是用全局链表来管理堆的(STL的所有容器基本上都是不可重入的)
2.调用了标准IO库函数,很多实现都使用了全局数据结构

SIGCHLD信号

子进程退出的时候,不是静悄悄的退出,子进程在退出的时候,会主动的向父进程发生SIGCHLD、17号信号
怎么证明?捕获它

void handler(int signo)
{
    cout << "recv a signo:" << signo << endl;
}

int main()
{
    signal(SIGCHLD, handler);
    pid_t pid = fork();
    if (pid == 0)
    {
        for (int i = 0; i < 5; ++i)
        {
            cout << "I am child process" << endl;
            sleep(1);
        }
        exit(0);
    }
    waitpid(pid, nullptr, 0);
    return 0;
}

在这里插入图片描述
我们直到如果不等待子进程会造成僵尸进程,这样我们可以把waitpid函数写道自定义方法里面

void handler(int signo)
{
    cout << "recv a signo:" << signo << endl;
    waitpid(-1, nullptr, 0);//-1表示可以等待任意的子进程退出

}

int main()
{
    signal(SIGCHLD, handler);
    pid_t pid = fork();
    if (pid == 0)
    {
        for (int i = 0; i < 3; ++i)
        {
            cout << "I am child process" << endl;
            sleep(1);
        }
        exit(0);
    }
    while (1)
    {
        cout << "I am father prcoess" << endl;
        sleep(1);
    }
    return 0;
}

在这里插入图片描述
又会产生一个问题,如果有100个子进程同时退出呢?那这样必定会有信号丢失,也就会导致有子进程僵尸从而导致内存泄漏的问题

void handler(int signo)
{
    cout << "recv a signo:" << signo << endl;
    while (waitpid(-1, nullptr, WNOHANG) > 0)
    {
        cout << "waitpid a process success" << endl;
    }
}

int main()
{
    signal(SIGCHLD, handler);
    for (int i = 0; i < 6; ++i)
    {
        pid_t pid = fork();

        if (pid == 0)
        {
            for (int i = 0; i < 3; ++i)
            {
                cout << "I am child process" << endl;
                sleep(1);
            }
            exit(0);
        }
        
    }
    while (1)
    {
        cout << "I am father prcoess" << endl;
        sleep(1);
    }
    return 0;
}

在这里插入图片描述

必须得等待吗?必须调用wait吗?
可将17号信号设置为忽略

int main()
{
    signal(SIGCHLD, SIG_IGN);
    for (int i = 0; i < 6; ++i)
    {
        pid_t pid = fork();

        if (pid == 0)
        {
            for (int i = 0; i < 3; ++i)
            {
                cout << "I am child process" << endl;
                sleep(1);
            }
            exit(0);
        }
        
    }
    while (1)
    {
        cout << "I am father prcoess" << endl;
        sleep(1);
    }
    return 0;
}

在这里插入图片描述
在这里插入图片描述
可观测到没有僵尸进程退出

至此
信号的预备知识----------------------------->信号的产生----------------------------->信号的保存----------------------------->信号的捕捉
全部完成讲解

  • 27
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值