Linux知识点 -- 进程信号(一)

Linux知识点 – 进程信号(一)

文章目录

  • Linux知识点 -- 进程信号(一)
  • 一、理解信号
    • 1.理解Linux信号
    • 2.信号的产生与处理
    • 3.常见的信号
    • 4.如何理解组合键变成信号
    • 5.如何理解信号被进程保存
  • 二、信号的产生
    • 1.键盘产生
    • 2.核心转储
    • 3.系统调用接口产生信号
    • 4.由软件条件产生信号
    • 5.硬件异常产生信号


一、理解信号

1.理解Linux信号

  • Linux信号:
    本质是一种通知机制,用户or操作系统通过发送一定的信号,通知进程,某些事件已经发生,你可以在后续进行处理;

  • 信号结论:
    (1)进程要处理信号,必须要具备识别的能力(看到 + 处理动作);
    (2)进程通过程序员编写的代码逻辑,能够识别信号;
    (3)信号的产生是随机的,进程可能在忙自己的事情,所以,信号的后续处理,可能不是立即处理的;
    (4)进程会临时记录下对应的信号,方便后续进行处理;
    (5)进程会再合适的时候处理信号;
    (6)一般而言,信号的产生相对于进程而言是异步的;

2.信号的产生与处理

  • 信号的产生:
    当一个进程正在运行时,我们通过键盘上的ctrl + c可以杀掉进程,这本质就是通过键盘的组合键向目标进程发送2号信号;

  • 信号处理常见的方式:
    (1)默认(进程自带的,程序员写好的逻辑)
    (2)忽略
    (3)自定义动作(捕捉信号)

3.常见的信号

在Linux下输入kill -l指令,可以查看系统的信号:
Linux知识点 -- 进程信号(一)_第1张图片
其中,前31个是常用的信号;

4.如何理解组合键变成信号

键盘的工作方式是通过中断方式进行的,当然能够识别组合键;

  • 操作系统处理信号的流程
    OS解释组合键 -> 查找进程列表 -> 前台运行的进程 -> OS写人对应的信号到进程内部的位图结构中

5.如何理解信号被进程保存

信号的数据主要有两类:信号种类以及是否产生;

在进程PCB中的task_struct中有保存信号的相关数据结构 – 位图(unsigned int),每一位都表示不同信号,0和1代表有没有接收该信号;

信号发送的本质:OS向目标进程写信号,OS直接修改目标进程PCB中的指定的位图的结构,完成发送信号的过程;

二、信号的产生

1.键盘产生

我们知道信号可以从键盘产生,当我们产生信号后,信号的处理方式有三种,接下来我们使用自定义捕捉来处理键盘产生的信号;

  • signal函数:
    Linux知识点 -- 进程信号(一)_第2张图片
    参数:
    sighandler:函数指针,作为回调函数的参数使用;
    signum:信号名称;
    handler:处理信号的函数的指针;
    这个函数的功能是:我们一旦收到了指定的信号,signal函数会将信号的编号传回给回调函数的参数;
#include
#include
#include

using namespace std;

void catchSig(int signum)
{
    cout << "进程捕捉到了一个信号,正在处理中:" << signum << "pid: " << getpid() << endl;
}


int main()
{
    //signal(2, catchSig);//signal第一个参数可以传信号名,也可以传对应的序号
    signal(SIGINT, catchSig);

    while(true)
    {
        cout << "我是一个进程,我正在运行,pid: " << getpid() << endl;
        sleep(1);
    }


    return 0;
}

上面的代码在捕捉2号信号,当捕捉到后,会调用自己的处理函数catchSig;
运行结果:
Linux知识点 -- 进程信号(一)_第3张图片
(1)进程不断在运行,当我们按下键盘组合键ctrl + c时,触发2号信号,进程就会执行我们自定义捕捉的处理方式,但是进程并没有结束;
(2)这是因为特定信号的处理方式,一般只有一个,我们将2号信号的处理方式改为自定义的处理方式,系统默认的处理方式就失效了;
(3)signal函数仅仅是修改了进程对特定信号的后续处理动作,不是直接调用对应的处理动作;只有当对应信号触发的时候,signal函数会回调catchSig函数,对信号进行处理;
(4)如果后续没有任何SIGINT信号产生,那么catchSig永远不会被调用;

2.核心转储

在上面的代码增加对3号信号的捕捉(3号信号快捷键是ctrl + \ ,也是让进程退出):

int main()
{
    //signal(2, catchSig);//signal第一个参数可以传信号名,也可以传对应的序号
    signal(SIGINT, catchSig);
    signal(SIGQUIT, catchSig);//3号信号

    while(true)
    {
        cout << "我是一个进程,我正在运行,pid: " << getpid() << endl;
        sleep(1);
    }


    return 0;
}

Linux知识点 -- 进程信号(一)_第4张图片
man 7 signal指令打开手册,查看信号的处理动作:
Linux知识点 -- 进程信号(一)_第5张图片
发现2号指令后面的动作是term,而3号的是core,这是进程的核心转储功能
我们在学习进程等待时,子进程的status中,被信号所杀的子进程的status会有一个core dump位:
Linux知识点 -- 进程信号(一)_第6张图片
这个位就是标志该进程是否发生了核心转储;

  • 核心转储:
    当进程出现某种异常的时候,是否由OS将当前进程在内存中的相关核心数据,转存到磁盘中;
    核心转储主要为了调试,这是它的主要应用场景;
    一般而言,云服务器(生产环境)的核心转储功能都是关闭的;

  • 打开核心转储:
    Linux知识点 -- 进程信号(一)_第7张图片
    ulimit -a:查看,可以看到core file size是0;
    ulimit -c 大小:设置core file的大小;

    Linux知识点 -- 进程信号(一)_第8张图片
    再次查看可以发现,core file size变为了10240;
    这仅仅是在自己的终端中打开了核心转储,如果重启云服务器,又会恢复回去;

只要是信号动作是core的,都会触发核心转储,在进程运行时触发核心转储,会报错core dumped,并产生一个core文件,后缀是进程的pid;
触发3号信号终止进程,并产生相应的core文件:
Linux知识点 -- 进程信号(一)_第9张图片
8号信号也是core信号,在进程中触发除0错误就会触发8号信号:
Linux知识点 -- 进程信号(一)_第10张图片

int main()
{
    while(true)
    {
        sleep(1);
        int a = 10;
        a /= 0;
        cout << "我是一个进程,我正在运行,pid: " << getpid() << endl;
        sleep(1);
    }

    return 0;
}

Linux知识点 -- 进程信号(一)_第11张图片
core文件是能够用来调试的,用gdb加载core文件:
Linux知识点 -- 进程信号(一)_第12张图片
core文件会告诉我们在哪里发生了错误,触发的是什么信号;

3.系统调用接口产生信号

  • kill函数:是向进程发送信号的系统调用接口
    Linux知识点 -- 进程信号(一)_第13张图片
    功能:向pid进程发送sig信号;
    能够发送系统信号,与kill指令的功能一致;

模拟kill指令代码:

#include
#include
#include
#include
#include

using namespace std;

static void Usage(string proc)
{
    cout << "Usage:\r\n\t" << proc << " signumber processid" << endl;
}

// ./system_signal 2 pid
int main(int argc, char* argv[])
{
    if(argc != 3)//不满足格式要求
    {
        Usage(argv[0]);
        exit(1);
    }

    int signumber = atoi(argv[1]);
    int procid = atoi(argv[2]);

    kill(procid, signumber);

    return 0;
}

运行结果:
将sleep 10000这个进程,通过我们自己模拟的kill指令,向进程发送信号,终止该进程;
Linux知识点 -- 进程信号(一)_第14张图片

  • raise函数:给自己发送sig信号
    Linux知识点 -- 进程信号(一)_第15张图片
int main(int argc, char* argv[])
{

    cout << "运行中" << endl;
    sleep(1);
    raise(8);

    return 0;
}

运行结果:
进程向自己发送了8号信号;
Linux知识点 -- 进程信号(一)_第16张图片

  • abort函数:给自己发送和abort信号,就是自己终止自己
    Linux知识点 -- 进程信号(一)_第17张图片
    Linux知识点 -- 进程信号(一)_第18张图片

  • 系统调用接口的流程:
    用户调用系统接口 -> 执行OS对应的系统调用代码 -> OS提取参数或者设置特定的数值 -> OS向目标进程写信号 -> 修改对应的信号标记位 -> 进程会处理信号 -> 执行对应的处理动作

4.由软件条件产生信号

  • alarm:设定闹钟
    在这里插入图片描述

代码验证1s内CPU能执行多少次++:

#include
#include
#include

using namespace std;

int count = 0;

void catchSig(int signum)
{
    cout << "count: " << count << endl;
}

int main(int argc, char* argv[])
{
    //验证1s之内,CPU能进行多少次++
    alarm(1);
    signal(SIGALRM, catchSig);

    while(true)
    {
        count ++;
    }

    return 0;
}

运行结果:
Linux知识点 -- 进程信号(一)_第19张图片
这段代码是使用了alarm作为时钟,当这个函数运行之后,就会定时一段时间,当这段时间到了之后,就会向进程发送信号,这就是软件发送信号;
当我们设定一个闹钟之后,一旦触发闹钟,就自动移除了,如果想要定期做一件事情,可以在捕捉完信号后,再设一个闹钟;

#include
#include
#include

using namespace std;

int count = 0;

void catchSig(int signum)
{
    cout << "count: " << count << endl;
    alarm(1);//捕捉完闹钟信号后,再设一个闹钟
}

int main(int argc, char* argv[])
{
    //验证1s之内,CPU能进行多少次++
    alarm(1);
    signal(SIGALRM, catchSig);

    while(true)
    {
        count ++;
    }

    return 0;
}

运行结果:
Linux知识点 -- 进程信号(一)_第20张图片

设置一个定时器

#include
#include
#include
#include
#include
#include
#include

using namespace std;

typedef function<void ()> func;
vector<func> callbacks;
int count = 0;

void showLog()
{
    cout  << "日志功能" << endl;
}

void logUser()
{
    if(fork() == 0)
    {
        execl("/usr/bin/who", "who", nullptr);
        exit(1);
    }
    wait(nullptr);
}

void showCount()
{
    cout << "count : " << count << endl;
}

void catchSig(int signum)
{
    for(auto &f : callbacks)
    {
        f();
    }
    alarm(1);
}

int main(int argc, char* argv[])
{
    signal(SIGALRM, catchSig);
    alarm(1);

    callbacks.push_back(showCount);
    callbacks.push_back(showLog);
    callbacks.push_back(logUser);
    

    while(true)
    {
        count ++;
    }

    return 0;
}

运行结果:
Linux知识点 -- 进程信号(一)_第21张图片

5.硬件异常产生信号

捕捉8号信号:

#include 
#include 
#include 

using namespace std;

void catchSig(int signum)
{
    cout << "进程捕捉到了一个信号:" << signum << endl;
    sleep(1);
}

int main()
{
    signal(SIGFPE, catchSig);

    int a = 10;
    a /= 0;
    while (true)
    {
        sleep(1);
    }

    return 0;
}

运行结果:
Linux知识点 -- 进程信号(一)_第22张图片

  • 系统一直在捕捉8号信号,这是因为8号信号即除0信号是硬件触发的;
    (1)进行计算的CPU是硬件;
    (2)CPU内部是有寄存器的,状态寄存器(位图)有对应的状态标记位,溢出标记位,O3S会自动进行计算完毕之后的检测,如果溢出标记位是1,OS会识别到有溢出问题,立即找到当前谁在运行,并提取pid,OS完成发送信号的过程,进程会在合适的时候,进行处理;
    (3)一旦出现硬件异常,进程不一定会退出;
    (4)一直捕捉到8号信号的原因是:寄存器标志位异常一直没有被解决;

  • 遇到野指针问题,称之为段错误,11号信号就是野指针;
    Linux知识点 -- 进程信号(一)_第23张图片
    在这里插入图片描述
    野指针越界是硬件信号的原因:
    (1)进程必须通过地址,找到目标位置;
    (2)语言上面的地址,全都是虚拟地址;
    (3)将虚拟地址转换为物理地址 ,需要页表 + MMU(Memory Manager Unit,硬件);
    (4)野指针越界,即访问非法地址,MMU转化的时候,一定会报错;

所有的信号,无论来源,最终都是被OS识别,解释并发送的;

  • 问题
    Linux知识点 -- 进程信号(一)_第24张图片

你可能感兴趣的:(Linux,linux,运维,服务器)