【Linux】进程信号的产生与捕捉、核心转储

目录

一、信号的引入

二、信号捕捉

三、核心转储

四、系统调用发送信号

五、软件条件产生信号

六、硬件异常产生信号


一、信号的引入

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

例如,当用户输入命令,在Shell下启动一个前台进程后,我们便可以通过信号的方式终止进程。

【Linux】进程信号的产生与捕捉、核心转储_第1张图片

接下来我们运行一下test.c程序,因为是一个死循环,然后我们使用Ctrl+C停止该进程。

【Linux】进程信号的产生与捕捉、核心转储_第2张图片

当我们按下Ctrl+C时,实际上就是给进程发送了一个信号。

  • 当用户按下Ctrl+C时,这个键盘输入产生一个硬件中断,被OS获取,解释成信号,发送给目标前台进程,前台进程收到信号,进而引起进程退出。

那系统是如何将组合键编程信号的呢?

  • 实际上当用户按Ctrl+C时,这个键盘输入会产生一个中断,被操作系统获取并解释成信号(Ctrl+C被解释成2号信号),然后操作系统将2号信号写入对应的信号到进程PCB内部的位图结构中,然后进程检测到位图中数据的变化,做出相应的操作。
  • 信号发送的本质:OS 向目标进程写信号,OS直接修改PCB中的指定位图结构,就完成了”发送“信号的过程。

我们可以使用 kill -l 命令 查看 Linux 中的信号列表。

【Linux】进程信号的产生与捕捉、核心转储_第3张图片

 一共有62个信号(没有32、33号信号),其中白色区域【1-31】的为普通信号。【35-64】为实时信号。


二、信号捕捉

信号的可选动作有以下三种:

  1. 忽略此信号。
  2. 执行该信号的默认处理动作。
  3. 提供一个信号处理函数,要求内核在处理该信号时切换到用户态执行这个处理函数,这种方式被称为捕捉(catch)一个信号。

即信号的处理方式有三种:1.默认处理,2.忽略,3.自定义捕捉

接下来我们先了解一下第三种方式——自定义捕捉,OS为我们提供了一个系统调用 signal

【Linux】进程信号的产生与捕捉、核心转储_第4张图片

 第一个参数:要传入信号编号或宏(即62个信号中的其中一个),用于捕捉该信号, 部分如下:

【Linux】进程信号的产生与捕捉、核心转储_第5张图片

第二个参数:要求传入一个函数指针,这个传入的函数是当信号产生时执行的自定义功能了。调用一个函数传入另一个函数的函数指针,这种操作在C语言中被称为回调函数

接下来写一段简单代码看一下:

当SIGINT(即Ctrl+C)信号产生时,不去执行该信号的默认操作,而是去执行我们的catchSig函数。

【Linux】进程信号的产生与捕捉、核心转储_第6张图片

 然后我们来看看运行结果,并从键盘按下Ctrl+C.

【Linux】进程信号的产生与捕捉、核心转储_第7张图片

 说明signal函数,修改了当前进程对特定信号的处理动作,转而执行我们指定的函数。


三、核心转储

我们可以输入man 7 signal 查看信号的默认处理行为。这里不同信号的Action不同,有Term、Core、Ign、Cont、Stop等状态行为。

【Linux】进程信号的产生与捕捉、核心转储_第8张图片

接下来就是了解一下Core动作——核心转储。

关于进程等待中,status 中如果是正常终止就保存返回值、错误码。

如果被信号所杀,第7位上保存的这个就叫做core dump,如果是0表示没有发生核心转储,为1则是发生了核心转储。【Linux】进程信号的产生与捕捉、核心转储_第9张图片

我们可以打印code_dump位的信息 (左移7位然后与上1即可)。

int main()
{
    pid_t id = fork();
    if (id == 0)
    {
        sleep(1);
        int a = 100;
        a /= 0;
        exit(0);
    }
    int status = 0;
    waitpid(id, &status, 0);
    cout << "父进程:" << getpid() << "子进程:" << getppid() << endl;
    //退出信号:
    cout << "exit sig" << (status & 0x7f) << endl;
    // 打印core dump位
    cout << "core dump" << (status > 7 & 1) << endl;
}

【Linux】进程信号的产生与捕捉、核心转储_第10张图片

如果使用的云服务器,其默认核心转储功能是关闭的

我们可以使用ulimit -a 进行查看【Linux】进程信号的产生与捕捉、核心转储_第11张图片

然后使用命令 ulimit -c 10240 就可以打开(这种打开方式仅针对当前会话)

【Linux】进程信号的产生与捕捉、核心转储_第12张图片

 打开之后,运行mysingal程序,然后使用kill -8 信号终止该进程。

【Linux】进程信号的产生与捕捉、核心转储_第13张图片

此时当前目录下就多了一个 core.4530 文件。

这个文件就叫做核心转储:

  • 当进程出现某种异常的时候,是否由OS将当前进程在内存中的相关核心数据,转存到磁盘中。大白话就是将内存中的重要数据保存起来,主要作用是用于调试。

所以在信号的默认处理行为中action我们就知道是什么意思了。(ign表示忽略,Cont表示继续)

【Linux】进程信号的产生与捕捉、核心转储_第14张图片

那这个核心转储生成的文件有什么用呢?
其主要作用是用于调试,接下来我演示core文件进行调试。

接下来我编写一段代码,其中有句代码执行了整数除以0,会触发8号信号生成core文件。我们加上-g选项生成可执行文件。

#include 
#include 
using namespace std;
int main()
{
    cout<<"i am a process,pid:"<

【Linux】进程信号的产生与捕捉、核心转储_第15张图片

 然后使用gdb打开core文件。

【Linux】进程信号的产生与捕捉、核心转储_第16张图片


四、系统调用发送信号

操作系统提供了许多系统调用接口让我们产生并发送信号:

  • kill 接口
  • raise 接口
  • abort 接口

接下来让我们先来认识 kill 接口

【Linux】进程信号的产生与捕捉、核心转储_第17张图片

 第一个参数为指定的进程pid,第二个参数为对应的信号编码。

其实本质kill命令就是调用的系统调用kill,所以我们也可以实现一个kill命令:

int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        usage(argv[0]);
        exit(1);
    }
    int procid = atoi(argv[1]);
    int signumber = atoi(argv[2]);
    // 系统调用kill
    kill(signumber, procid);
    return 0;
}

检验步骤步骤如下:

1. 让当前会话的前台进程中直接sleep 10000秒,然后在会话2中查看该进程的pid

2.调用 mykill ,传入信号和sleep 10000的pid,观察结果。

结果:

raise命令:

kill 是给指定进程发送信号,而如果想让自己给自己发信号,可以使用 raise 命令

【Linux】进程信号的产生与捕捉、核心转储_第18张图片

接下来我们写一段程序,让进程本身给自己发送8号信号。 

举例使用如下:

int main()
{
    cout << "pid: " << getpid() << "run......" << endl;
    raise(8);
    return 0;
}

【Linux】进程信号的产生与捕捉、核心转储_第19张图片

abort接口:

给自己发送abort信号,也就是6号信号。相当于代码:raise(6) 或  kill(getpid(),6)

【Linux】进程信号的产生与捕捉、核心转储_第20张图片

 举例代码如下:

int main()
{
    cout << "pid: " << getpid() << "  run......" << endl;
    abort();    //raise(6) 或 kill(getpid(),6)
    return 0;
}

【Linux】进程信号的产生与捕捉、核心转储_第21张图片


五、软件条件产生信号

举一个例子:

当管道,读端不进行读取,还关闭了文件描述符,而写端一直写入,会发生什么问题?

操作系统会自动终止对应写端进程,通过发送信号的方式,发送SIGPIPE信号。

验证步骤:

1.创建匿名管道

2.让父进程进行读取,子进程进行写入

3.让父进程关闭读端 && waitpid(),子进程一直进行写入

4.子进程退出,父进程waitpid拿到子进程的退出status。

5.提取退出信号。

SIGPIPE便是一种软件条件产生的信号,除了管道中会发出SIGPIPE信号,接下来我们学习其它软件产生的信号—— alarm 函数与SIGALRM 信号。

系统调用中的 alarm 函数会产生 SIGALRM  信号。

接下来让我们了解一下 alarm 接口。

【Linux】进程信号的产生与捕捉、核心转储_第22张图片

调用 alarm 函数可以设定一个闹钟,也就是告诉内核再 seconds 秒之后给当前进程发 SIGALRM 信号,该信号的默认处理动作是终止当前进程。

有了 alarm 这个计时接口,我们可以写一段代码来验证1秒内,服务器一共能进行多少次count++;

int main()
{
    alarm(1);
    int count=0;
    while(1)
    {
        cout<<"count:"<

运行程序,看看结果是多少:

【Linux】进程信号的产生与捕捉、核心转储_第23张图片

观察结果:1秒钟为什么count只能++到6万左右,为什么这么慢?

因为1.对屏幕进行IO浪费了太多时,

2. 云服务器是网络传输,消耗太多时间。

如果想单纯计算算力,我们对代码进行改写,让其在服务器上进行计算,然后将结果返回给我们。

int count = 0;

void catchSig(int signum)
{
    cout << "final count: " << count << endl;
}
int main()
{
    // 1秒后发送消息
    alarm(1);
    signal(SIGALRM, catchSig);
    while (1)
    {
        ++count;
    }
    return 0;
}

【Linux】进程信号的产生与捕捉、核心转储_第24张图片

这时结果就是5亿多。

因为singal的原因,如果alarm时间到了,就会执行catchSig函数。所以我们可以设置一个周期程序,每一秒种打印计算机累计算了多少,改动如下:

【Linux】进程信号的产生与捕捉、核心转储_第25张图片

 所以每隔一秒,就自动打印count的值,这样,我们就实现了一个定时器的功能。

【Linux】进程信号的产生与捕捉、核心转储_第26张图片

有了这个定时器的功能,我们就可以实现定时打印信息。

每隔一秒钟就打印我们想要检测的内容


typedef function func;
vector callbacks;
long long count = 0;
void showCount()
{
    cout << "final count: " << count << endl;
}

void showLog()
{
    cout << "打印日志……" << endl;
}
// 打印登录的用户
void logUser()
{
    if (fork() == 0) // 子进程进行程序替换执行who命令
    {
        execl("/usr/bin/who", "who", nullptr);
        exit(1);
    }
    wait(nullptr);
}

void catchSig(int signum)
{
    // 定时执行以下任务:
    for (auto &f : callbacks)
    {
        f();
    }
    alarm(1);
}
int main()
{
    // 1秒后发送消息
    alarm(1);
    signal(SIGALRM, catchSig);
    callbacks.push_back(showCount);
    callbacks.push_back(showLog);
    callbacks.push_back(logUser);
    while (1)
        ++count;
    return 0;
}

运行如下:【Linux】进程信号的产生与捕捉、核心转储_第27张图片

那如何理解软件条件给进程发送信号?

  • OS先识别到某种软件条件触发或不满足。
  • OS构建信号,发送给指定的进程。

六、硬件异常产生信号

硬件怎么产生信号呢?我们现在先写一段整数除以0的代码进行引入:

void handler(int signum)
{
    sleep(1);
    cout << "signal is : " << signum << endl;
}
int main()
{
    signal(SIGFPE, handler);
    int a=100;
    a/=0;
    while (1)
        sleep(1);
    return 0;
}

【Linux】进程信号的产生与捕捉、核心转储_第28张图片


那如何理解整数除以0这个操作呢?

  1. 因为计算的是CPU,如果CPU计算出现错误,会将错误信息放入到状态寄存器中,状态寄存器中有对应的状态标记位(类比成 位图),其中会存在溢出标记位,OS会自动进行计算完毕之后的检查。
  2. 如果OS识别到有溢出问题,根据 current指针(指向当前正在运行的进程) 找到进程,然后提取出 PID,O S再进行信号发送到该进程,进程则会再合适的时候,进行信号的处理。
  3. 立即找到当前 task_struct中有一个current指针,当程序进行执行时,current内的内容也会被加载到CPU的寄存器中。
  4. 所以,整数除以零是一个硬件异常的问题

那一旦出现硬件异常,进程一定会退出吗?

  • 不一定,一般默认是退出,但是如果我们不进行退出,我们也不能进行任何操作,因为无权访问CPU中的寄存器数据。

为什么会发生死循环?

  • 因为寄存器中的异常一直没有被解决。
  • 所以一般我们出现除0等错误,一般就直接exit()退出了。

指针越界、野指针一般被称为段错误 (11号信号SIGSEGV)

【Linux】进程信号的产生与捕捉、核心转储_第29张图片

那如何理解野指针或越界问题?

  1. 都必须通过地址,找到目标位置,
  2. 语言上的地址,全部都是虚拟地址
  3. 将虚拟地址转化为物理地址
  4. 页表+MMU(Memmory Manager Unit——硬件)
  5. 野指针,越界->非法地址->MMU转化的时候,一定会报错。因为MMU这个硬件其中也有寄存器,注意,外设也有寄存器的,不只是CPU有寄存器。

结论:

  • 所以说,硬件也能产生信号。所有的信号,都有其来源,但最终全部都是被OS被识别、解释、发送的。

最后就是几个信号的常见问题:

  • 为什么所有的信号产生,最终都要由OS来执行?

        因为OS是进程的管理者。

  • 信号的处理是否是立即处理的?

        由OS在合适的时机进行处理。

  • 信号如果不是被立即处理,那么信号是否需要暂时被进程记录下来?记录在哪里?

        需要被记录下来,记录在进程PCB中对应的信号记录位图。

  • 如何理解OS向进程发送信号?

        本质是OS直接修改PCB中的信号位图,根据信号编号修改特定的比特位(由0置1)。

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