Linux——进程信号(一)

本文详细介绍了信号在IT中的概念、产生方式(包括用户输入、系统函数和硬件异常)、记录机制以及处理方法,特别关注了Ctrl+C信号、CoreDump的使用和Linux资源限制。文章还探讨了信号如何由操作系统发送并被进程捕获或终止的过程。
摘要由CSDN通过智能技术生成

目录

1、信号入门

1.1、技术应用角度的信号

1.2、注意

1.3、信号概念

1.4、用kill -l命令可以查看系统定义的信号列表

1.5、信号处理常见方式概览

2、产生信号

2.1通过终端按键产生信号

Core Dump

2.2、调用系统函数向进程发信号

2.3、由软条件产生信号

3、总结思考一下



1、信号入门

通过自然世界中人对信号的基本理解:

接下来是对进程的分析:

我们可以看到信号都是宏

那么还有一个问题:信号是如何发送的以及如何记录的?

首先回答信号是如何记录的:普通信号的编号是从【1,31】,所以信号应该用位图来保存信号数据,信号的记录是进程的task_struct(PCB)->结构体变量,本质更多的是为了记录信号是否产生。

如何发送:进程收到信号,本质是进程内信号位图被修改了,也只有OS才有资格修改进程内的数据,因为操作系统是进程的管理者,所以绝对有资格修改进程数据,本质就是OS直接去修改目标进程task_struct中信号位图。(信号发送只有OS有资格,但是信号发送的方式可以有多种)

1.1、技术应用角度的信号

1、用户输入命令,在shell下启动一个前台进程。

用户按下Ctrl-C,这个键盘输入产生一个硬件中断,被OS获取,解释成信号,发送给目标前台进程

前台进程因为收到信号,进而引起进程退出

#include <stdio.h>
#include <unistd.h>
int main()
{
    while(1)
    {
        printf("hello world!\n");
        sleep(1);
    }
    return 0;
}

当我们用ctrl+c这个组合键结束这个进程的本质是,操作系统识别到ctrl+c这个组合键,操作系统将ctrl+c解释成了2号新号,也就是SIGINT。

这个就是处理信号三种方案中的默认动作,为了要能够让信号自定义,有下面这个接口:

#include <signal.h>
typedef void(*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);

第一个参数是信号编号,也就是信号中的1-31。

第二个参数的类型是一个函数指针,且是一个回调函数,相当于我们可以通过signal,提前向进程注册一个对信号的处理方法。

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>
#include <stdlib.h>

void sigcb(int signo)
{
    printf("get a sig: %d\n", signo);
}


int main()
{
    signal(2,sigcb);
    while(1)
    {
        printf("hello world!\n");
        sleep(1);
    }
    return 0;
}

可以看到这次ctrl+c的时候,操作系统没有终止进程,因为默认行为被我们改成了自定义行为。这里无论使用ctrl+c还是kill -2 id操作都是执行我们的自定义行为,程序不会被终止,如果要退出进程,只能发送其他的退出信号

1.2、注意

1、Ctrl+C产生的信号只能发给前台进程。一个命令后面加个&可以放到后台运行,这样Shell不必等待进场结束就可以接受新的命令,启动新的进程。

2、Shell可以同时运行一个前台进程和任意多个后台进程,只有前台进程才能接到像Ctrl+C这种控制键产生的信号。

3、前台进程在运行过程中用户随时可能按下Ctrl+C而产生一个信号,也就是说该进程的用户空间代码执行到任何地方都有可能收到SIGINT信号而终止,所以信号相对于进程的控制流程来说是异步的。

1.3、信号概念

信号是进程之间事件异步通知的一种方式,属于软中断。

1.4、用kill -l命令可以查看系统定义的信号列表

每个信号都有一个编号和一个宏定义名称,这些宏定义可以在signal.h中找到

编号34以上的是实时信号,暂不做讨论。其他信号各自在什么条件下产生,默认的处理动作是什么,在signal(7)中都有详细说明:man 7 signal

1.5、信号处理常见方式概览

可选的处理动作有以下三种:

1、忽略此信号

2、执行该信号的默认处理动作

3、提供一个信号处理函数,要求内核在处理该信号时切换到用户态执行这个处理函数,这种方式称为捕捉一个信号


2、产生信号

2.1通过终端按键产生信号

SIGINT的默认动作是终止进程,SIGQUIT的默认动作是终止进程并且Core Dump,现在我们来验证一下:

可以看到,CTRL+\对应的是三号信号:SIGQUIT,3号信号默认的动作是Core,表示在结束的时候它有一个动作叫核心转储。

使用ulimit -a指令查看系统资源

看到core file size的大小为0,意味着它的核心转储是关闭的,那么我们为它设置一个值:

接着再运行一遍程序:

可以看到,进程也退出了,而且后面还多了一个(core dumped),用ls查看的时候也多了一个core.1751这个临时文件,这个1751数字叫做发生这次核心转储进程的id。

一个进程在终止的时候有很多终止方式,其中Terminal一般是直接退出,也可以理解成是我们手动的让它退出了,但不做任何转储文件的dump(转储),而我们如果自己打开了核心转储,并且我们收到了信号(不同的信号又不同的作用,不同的信号是一种不同的错误类别),而有些信号是需要进行和核心转储的。

比方说,代码运行的时候出错了,我们关心的是代码为什么出错了,我们之前讲的代码的三种退出方式:1、代码跑完结果对,2、代码跑完结果不对,3、代码运行中的时候出错。前两个最起码跑完了,最后根据退出码就能判断哪里有问题,那么第三种:代码运行中的时候出错了,我么也要有办法判定是什么原因出错了。

我们在平时出现第三种情况的时候,我们一般式通过调试来判断哪里出了问题,但其实还有Linux中的一种方法就是通过核心转储功能:把进程在内存中的核心数据转储到磁盘上,core.pid->核心转储文件。目的是为了调试、定位问题。一般云服务器是属于线上生产环境,默认是关闭的。

打开的状况我们上面那也进行了演示。那么还有一个问题:

为什么在云服务器上核心转储功能默认是关闭的呢?

比方说我们在服务器上写一个网络服务或者定期执行的一个任务,这个服务可能因为某种异常而挂掉,如果你打开了核心转储,那么挂掉之后会在本地的磁盘文件中生成corn文件,这个无可厚非,但是一般大的互联网公司在服务挂掉的时候,最重要的事情不是在乎是因为什么原因挂掉的,重要是的想尽快的让它恢复正常。因为BUG不是经常时间,而是偶尔的事情。所以重要的是先让服务跑起来,不要让公司收到太大的影响。当服务回复之后再对故障进行排除工作。

如果是小问题的话那么就先让服务恢复出来,然后再进行检查,但是如果出了大问题,而且有一个一崩就重启的功能,那么已重启就崩,崩了就重启,如此往复。就会出现大量的core file文件:

而且我们可以看到,这种文件一个都要1MB多,每个都不小,要说重启很长时间,那么我们去排查的时候会发现core文件将某个分区或者磁盘文件都占满了,最终导致服务想重启都没法重启,甚至操作系统都挂了,所以默认是关闭的。

Core Dump

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

下面通过一个实例来观察一下:

可以发现出现了一个浮点型异常,而且多出了一个core.27575这个core dumped文件。

通过gdb和core.27575文件找到了问题在20行,这样我们就快速定位到了刚刚的代码是因为什么原因出错的,这个调试叫做事后调试,也就是程序崩溃了再进行调试。

为什么C/C++进程会崩溃?

本质就是因为收到了信号。

那为什么会受到信号?

首先我们要知道,信号都是又OS发送的,那么OS又怎么识别到有进程触发了问题呢?

OS在进行正常运行的时候发现CPU内有一个计算机状态标志位发生了除0错误,然后操作系统就立马定位当前运行的那个进程,所以就来进行终止。

所以操作系统识别到了硬件错误,然后将这个硬件错误解释(包装)成信号发送给目标进程。

其实本质就是找到这个进程的PCB,向目标的位图比特位由0置1,然后这个进程在合适的时候处理8号信号时默认就给“自己终止了”。

所以错误最终一定会在硬件层面上有所表现,进而被OS识别到,所以进场最后才会崩溃。

2.2、调用系统函数向进程发信号

这里我们要用到的接口是kill

#include <sys/types.h>

#include <signal.h>

int kill(pid_t pid, int sig);

我们写了一个重复打印的mytest,然后通过系统调用kill掉了mytest进程,可以看到已经成功的使mytest退出。

还有两个给自己发送信号的接口

#include <signal.h>

int raise(int sig);

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>

void handler(int signo)
{
    printf("get a sig: %d\n", signo);
}

int main()
{
    signal(2, handler);
    while(1)
    {
        printf("I am a process, pid: %d\n", getpid());
        sleep(1);
        raise(2);
    }
    return 0;
}

#include <stdlib.h>

void abort(void);

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>
#include <stdlib.h>

void handler(int signo)
{
    printf("get a sig: %d\n", signo);
}

int main()
{
    signal(6, handler);
    while(1)
    {
        printf("I am a process, pid: %d\n", getpid());
        sleep(1);
        abort();
    }
    return 0;
}

可以看到,运行起来后,接收到的是6号信号,而且接收到一次之后就退出了,可是上面明明对6号信号进行了捕捉。

这是因为有些信号是可以被捕捉,有些信号不可以被捕捉,6号信号既被捕捉了,也被终止了,这就是6号信号,abort的作用很像我们一直用的exit(),但是exit()是正常终止,而abort()的本质是通过信号来终止,是自己终止自己,但是要说明的是,exit()本质上是函数,只要是函数就说明它可能会失败,而abort函数总是会成功(函数无返回值)。

2.3、由软条件产生信号

我们之前的异常本质上是由软件引起的,但最终引起的问题是在硬件上,也就是CPU的状态寄存器出了问题,MMU转化出了问题,所以最后我们就看到操作系统识别硬件出了错误,然后转化成信号发送给进程。

软件条件产生信号:在我们写管道那里的时候说,有一端是读端,有一端是写端,如果将读端关闭,写端一只写,那么写端就会被立刻终止。这样的原因就是写入的软件条件不满足,也就是当前管道式不允许你写入的,所以我们当时就收到了一个SIGPIPE这个信号,这个信号就是由于软件条件产生的信号,所以就是我们写入的条件不成熟,这就是软件条件。

当然还有其他的软件条件,就是alarm(闹钟)函数。

这个函数的返回值是0或者是以前设定的闹钟时间余下的秒数。

#include <unistd.h>

unsigned int alarm(unsigned int seconds);

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

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>
#include <stdlib.h>

void handler(int signo)
{
    printf("get a sig: %d\n", signo);
}

int main()
{
    alarm(1);
    int count = 0;
    while(1)
    {
        printf("count is: %d\n", count++);
    }
    return 0;
}

这个代码的意思是,1秒后发送14号信号SIGALRM,然后在这1秒内看能进行多少次count++并打印出来,我们可以看到,在五万次左右,但是这其实不代表真实的速度,因为我们这里是在外设打印了就会慢很多。而且也会有网络的原因,我们在网络上计算,然后再发送过来,就会慢。

这里我们改一改:

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>
#include <stdlib.h>

int count = 0;

void handler(int signo)
{
    printf("count is: %d\n", count);
    exit(1);
}

int main()
{
    signal(14, handler);
    alarm(1);
    while(1)
    {
        count++;
    }
    return 0;
}

可以看到,我们直接让它累加,最后再打印,就可以看到会加到很大,这就是因为在累加的时候没有进行IO,所以我们得知,如果计算机在进行IO的时候,效率非常低。

2.4、硬件异常产生信号

硬件异常被硬件以某种方式被硬件检测到并通知内核,然后内核向当前进程发送适当的信号。例如当前进程执行了除以0的指令,CPU的运算单元会产生异常,内核将这个异常解释为SIGFPE信号发送给进程。再比如当前进程访问了非法内存地址,MMU会产生异常,内核将这个异常解释为SIGSEGV信号发送给进程。


3、总结思考一下

1、上面所说的所有信号产生,最终都要由OS来进行执行,为什么?

答:OS是进程的管理者

2、信号的处理是否是立即处理的?

答:在合适的时候

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

答:需要。记录在进程的PCB中,有对应的PCB位图

4、一个进程在没有收到信号的时候,能否能知道,自己应该对合法信号作何处理呢?

答:知道:默认、自定义、捕捉

5、如何理解OS向进程发送信号?能否描述一下完整的发送处理过程?

答:本质就是OS根据某种信号类别,直接去修改PCB位图中的0 1序列,进而达到发送信号的目的

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

双葉Souyou

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值