Linux:进程信号(一.认识信号、信号的产生及深层理解、Term与Core)

上次结束了进程间通信的知识介绍:Linux:进程间通信(二.共享内存详细讲解以及小项目使用和相关指令、消息队列、信号量


文章目录


1.认识信号

**概念:**在Linux系统中,进程之间可以通过信号进行通信,实现异步信息的发送和接收。

信号是Linux系统中一种轻量级的通信机制,用于通知进程发生了某种事件或异常情况。进程可以发送信号给其他进程,也可以接收来自其他进程或系统的信号。

  • 异步:是一种编程模型或通信方式,指的是在进行操作或通信时,不需要等待前一个操作完成或响应返回,而是可以继续执行下一个操作或任务(二者是并发的,一个不用等另一个)。异步编程可以提高系统的并发性和响应性,使得程序能够更高效地利用资源和处理多个任务
  • 异步信息通常指的是在通信或交流过程中,信息的发送和接收是不同步的,即发送方和接收方的速度或时间不一致。这种情况下,接收方可能会在不同的时间点接收到发送方发送的信息

可以使用kill -l来查看信号

  • 1-31是普通信号,34-64是实时信号,没有0,没有32 33

  • 使用数字和名称都行(本质也是宏定义)

进程看待信号方式

  1. 在没有发生的时候,进程就已经知道如果发生了,怎么进行处理:这句话可能指的是预先设置好的信号处理方式。在Linux系统中,进程可以使用signal()或者sigaction()等系统调用来注册信号处理函数,这样当特定信号发生时,系统会调用相应的信号处理函数来处理该信号。

  2. 进程能认识信号:这句话指的是我们可以识别和处理特定的信号。Linux系统定义了一系列标准信号(如SIGINT、SIGTERM、SIGKILL等),每个信号都有特定的含义和默认处理方式,进程可以根据需要识别和处理这些信号。

  3. 信号到来的时候,如果进程正在处理更重要的事情,导致暂时不能处理到来的信号,那么进程必须要把到来的信号进行临时保存:这指的是信号的异步性。当进程正在执行某些重要任务时,如果接收到信号,可能无法立即处理,此时系统会将信号暂时保存,等到合适的时机再进行处理。

  4. 信号到了,可以不立即处理,选择在合适的时候处理:进程可以选择在合适的时机处理信号,而不是立即响应。这种灵活性使得进程能够根据自身状态和需求来处理信号。

  5. 信号的产生是随时产生的,我们无法准确预料,所以信号是异步发送的:信号是由其他用户、进程或系统事件产生的,进程无法准确预测信号的产生时机。因此,信号的发送是异步的,进程需要通过信号处理函数来处理这种异步事件。

异步发送指的是信号是由其他用户或进程产生的,而接收信号的进程在信号到达之前可能一直在处理自己的任务


2.信号的产生

2.1信号的处理的方式 --- signal()函数

signal()函数是Linux系统中用于注册信号处理函数的函数。它的原型如下:

c 复制代码
#include <signal.h>

typedef void (*sighandler_t)(int);

sighandler_t signal(int signum, sighandler_t handler);//sighandler_t是个函数指针

这个函数接受两个参数:signum表示要捕捉的信号编号,handler表示要注册的信号处理函数。

  1. 参数说明

    • signum:表示要捕捉的信号编号,可以是预定义的信号宏如SIGINTSIGTERM等,也可以是用户自定义的信号编号。
    • handler:表示要注册的信号处理函数,其**原型通常是void handler(int signal_number)(调用这样的函数)。**可以是函数指针(自定义),也可以是SIG_IGN(忽略信号)或SIG_DFL(默认处理)。
  2. 返回值

    • signal()函数的返回值是一个函数指针,指向之前注册的信号处理函数。如果之前未注册过该信号的处理函数,则返回SIG_DFL(默认处理)。
  3. 信号处理方式

    • 如果handler为函数指针,则表示注册自定义的信号处理函数,当收到指定信号时,系统会调用该函数进行处理。
    • 如果handlerSIG_IGN,表示忽略该信号,即当收到指定信号时不进行任何处理。
    • 如果handlerSIG_DFL,表示使用系统默认的处理方式,通常是终止进程或执行默认操作。
  4. 注意事项

    • 当使用signal()函数注册信号处理函数时,处理函数并不会立即执行,而是在未来收到对应的信号时才会执行
    • 如果注册了一个处理SIGINT信号的处理函数,但是进程从未收到SIGINT信号,那么注册的处理函数也就永远不会被调用。这种情况可能会发生
  • 定义信号处理函数 :指的是编写实际的处理信号的函数,即编写处理SIGINT信号的具体函数逻辑。这个函数通常具有特定的原型,如void handler(int signal_number)

  • 注册信号处理函数 :指的是使用signal()函数将定义好的信号处理函数与特定的信号关联起来。通过注册信号处理函数,系统会在收到对应的信号时调用这个函数来处理信号。

完整的表述应该是:定义一个处理SIGINT信号的处理函数,并通过signal()函数将这个处理函数注册到SIGINT信号上。当进程收到SIGINT信号时,系统会调用注册的处理函数来处理该信号。

2.2kill指令产生信号

kill指令是用于向进程发送信号的命令。通过kill命令,可以向指定进程发送不同类型的信号,例如SIGTERMSIGKILL等。这些信号可以触发进程中注册的信号处理函数,或者直接终止进程的执行。

kill命令的基本语法为:

复制代码
kill [options] <PID>

<PID>是要发送信号的目标进程的进程ID。可以通过ps命令或其他方式获取目标进程的进程ID。

c++ 复制代码
#include <iostream>
#include <unistd.h>
#include <sys/types.h>


using namespace std;

int main()
{
    while(true)
    {
        cout << "I'm a process, pid:" << getpid() << endl;
        sleep(2);//我们写个死循环,每隔两秒打印一下
    }
    return 0;
}

2.3键盘产生信号

  1. 我们之前使用的Ctrl+c:ctrl + c -> OS 解释成为2(SIGINT)号信号 -> 向目标进程进行发送 -> 进程收到-> 进程响应

    • 用户按下Ctrl+C组合键,操作系统会将这个操作解释为发送SIGINT(信号编号为2)信号给目标进程。
    • 目标进程收到SIGINT信号后,会执行与之关联的信号处理函数。通常情况下,SIGINT信号会导致进程终止执行,类似于用户主动输入exit或者点击关闭窗口。
  2. 之前使用的Ctrl+\:ctrl + c -> OS 解释成为3(SIGQUIT)号信号 -> 向目标进程进行发送 -> 进程收到-> 进程响应

    • 用户按下Ctrl+\组合键,操作系统会将这个操作解释为发送SIGQUIT(信号编号为3)信号给目标进程。
    • 目标进程收到SIGQUIT信号后,会执行与之关联的信号处理函数。与SIGINT不同的是,SIGQUIT信号通常用于请求进程终止,并且会生成core文件(如果core文件生成是启用的话)

验证:

c++ 复制代码
#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>

using namespace std;

void handler(int signum)
{
    cout << "got a signal, number is : " << signum << endl;
}

int main()
{
    signal(SIGINT, handler);  // 对2号信号SIGINT处理
    signal(SIGQUIT, handler); // 对号信号SIGQUIT处理
    while(true)
    {
        cout << "I'm a process, pid:" << getpid() << endl;
        sleep(2);
    }
    return 0;
}

2.4系统调用发送信号 ---kill系统调用、raise()和abort()库函数

kill是一个常见的系统调用,用于向指定的进程发送信号。通过kill系统调用,一个进程可以向另一个进程发送不同类型的信号,从而实现进程之间的通信和控制。

kill系统调用的原型如下:

c 复制代码
#include <sys/types.h>
#include <signal.h>

int kill(pid_t pid, int sig);
  • pid参数指定了要发送信号的目标进程的进程ID(PID)。如果pid为正数,则表示发送信号给进程ID为pid的进程;如果pid为0,则表示发送信号给与调用进程在同一进程组的所有进程;如果pid为-1,则表示发送信号给所有有发送权限的进程。
  • sig参数指定了要发送的信号的编号,可以是预定义的信号常量(如SIGKILLSIGTERM等),也可以是自定义的信号编号。

kill系统调用的返回值为0表示成功发送信号,-1表示发送信号失败,并且在这种情况下,可以通过errno全局变量获取具体的错误信息。

我们可以利用这个,来实现一个kill指令

c++ 复制代码
#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>
#include <cerrno>
#include <cstring>

using namespace std;

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

    int signum = stoi(argv[1] + 1); // 获取信号的数字
    int pid = stoi(argv[2]);        // 获取pid

    int n = kill(pid, signum);
    if (n == -1)
    {
        cerr << "kill failed: " << strerror(errno) << endl;
    }
    return 0;
}

可以对任意进程发送任意的信号

  1. raise()函数:raise()函数用于向当前进程发送一个信号。它的原型如下:

    c 复制代码
    int raise(int sig);

    sig是要发送的信号编号。

    成功时返回0,失败时返回非零值

    对自己发送任意信号

    c++ 复制代码
    int main()
    {
        int count = 0;
        while (true)
        {
            cout << "cnt: " << count++ << endl;
            sleep(1);
            if (count == 3)
            {
                cout << "send 9 to itself" << endl;
                raise(9);
            }
        }
        return 0;
    }
  2. abort()函数:abort()函数用于异常终止程序的执行。当调用abort()函数时,程序会立即终止,并向操作系统发送SIGABRT信号。abort()函数的原型如下:

    c 复制代码
    void abort(void);

    abort()函数会导致程序生成一个core文件,用于调试。一般来说,abort()函数被用于发现程序中的严重错误,并且需要立即终止程序执行。

给自己放指定信号(6号SIGABRT)

2.5软件条件产生信号

  1. 管道

读端关闭其文件描述符 并且不再读取数据 时,如果写端继续向管道写入数据 ,操作系统会发送一个SIGPIPE信号给写端进程。默认情况下,这个信号会终止写端进程SIGPIPE信号是一个用于处理管道写端在写操作时无读端接收的情况的信号。

  1. alarm()函数

    #include <unistd.h>

    unsigned int alarm(unsigned int seconds);

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

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

在Linux系统中,SIGALRM信号的默认行为是终止进程。当程序设置一个定时器并在定时器到期时产生SIGALRM信号时,如果程序没有显式地捕获和处理这个信号,那么默认情况下操作系统会终止该进程。

alarm(0),代表取消闹钟:alarm(0)函数会清除之前设置的定时器,并返回剩余的定时器时间(如果有的话)而且不会再触发SIGALRM信号

怎么理解软件条件:

软件条件是指软件层面上的一种异常情况或特定的条件,通常由软件中断信号触发,用来通知进程某种特定的事件已经发生

结构体与堆等数据结构都是软件,也有条件触发

2.6异常产生信号

  1. 代码除0了,收到8号信号SIGFPE

    c++ 复制代码
    void handler(int signum)
    {
        cout << "got a signal, number is : " << signum << endl;
        exit(0);
    }
    
    int main()
    {
        signal(8, handler); // 8号信号SIGFPE
        int a = 10;
        int b = a / 0;
        cout << b << endl;
        return 0;
    }
  2. 访问野指针指向的空间,收到11号信号SIGSEGV(段错误)

    c++ 复制代码
    void handler(int signum)
    {
        cout << "got a signal, number is : " << signum << endl;
        exit(0);
    }
    
    int main()
    {
        signal(11, handler); // 11号信号SIGSEGV
        int* pa=nullptr;
        *pa=10;
        return 0;
    }

3.信号产生的深层理解

键盘产生信号

那么现在又有问题了,什么叫做解释成为信号,什么叫做发送给进程?

信号临时保存在哪里呢?

进程的PCB中,使用位图结构来存1到31号的信号:比特位的位置来表示信号编号,比特位的01来表示是否收到指定的信号

那么发送信号本质上是写入信号:


task_struct是内核数据结构,只有OS有能力写入。我们用户只能使用系统调用。所以,无论信号产生的方式有多少种,最终都是OS在进程中写入信号的

异常产生信号

  • 除0异常

但如果我们自定义处理里,没有进行exit()退出,那么就会一直打印

因为,寄存器中的数据都是进程的上下文,CPU一直在进行进程的调度,那么就涉及到进程上下文的保存和恢复,因为我们没有进行退出操作,所以每次恢复后,异常还是存在。

  • 野指针异常

最终信号一定都是OS进行写入进程中的信号位图中

总结一下:

  • 上面所说的所有信号产生,最终都要有OS来进行执行,为什么?OS是进程的管理者

  • 信号的处理是否是立即处理的?在合适的时候

4.Term与Core

  1. Term(Termination)
    • 当进程接收到一个默认处理动作为Term的信号时,操作系统会立即终止该进程的执行。
    • 进程在接收到这样的信号后,会立即停止运行,并释放其所占用的系统资源。
    • 除非进程已经捕获了该信号并定义了自己的信号处理函数,否则进程会按照默认的Term动作被终止。
  2. Core(Core Dump)
    • 当进程接收到一个默认处理动作为Core的信号时,操作系统不仅会终止该进程的执行,还会生成一个核心转储文件(core dump file)
    • 核心转储文件是进程在异常终止时的内存映像,它包含了进程在终止时的状态信息,如变量值、函数调用栈等。
    • 这个文件对于程序员来说非常有用,因为它可以帮助他们分析进程崩溃的原因,进行调试和修复。
    • Term不同,Core动作在终止进程的同时还会生成一个额外的文件。

需要注意的是云服务器默认关闭了core file的选项:因为如果程序崩溃是由于某种未知的错误或条件触发的,并且这个问题没有得到及时解决,那么核心转储(core dump)文件可能会不断生成,占用大量的磁盘空间

ulimit -a 是一个在 Linux中用于显示当前 shell 会话的资源限制的命令。ulimit 命令允许用户设置或查看各种 shell 和进程资源限制。这些限制可以帮助防止系统资源的滥用,如 CPU 时间、文件大小、打开的文件描述符数量等。

当你运行 ulimit -a 时,它会列出所有当前设置的资源限制。以下是一些常见的 ulimit 资源和它们的描述:

  • -c: core file size (blocks, -c unlimited disables core files)
  • -d: data seg size (kbytes, -d unlimited)
  • -e: scheduling priority (-e 0 to 20)
  • -f: file size (blocks, -f unlimited)
  • -i: max locked memory (kbytes, -i unlimited)

如果想要修改某个限制,可以使用 ulimit 命令加上相应的选项和新的限制值。

例如,要设置最大打开文件描述符数量为 4096,你可以运行 ulimit -n 4096。但是请注意,这些限制通常只影响当前 shell 会话和由该 shell 启动的子进程。它们不会永久地改变系统配置。

我们想要产生core文件的话:ulimit -c选项设置core file的大小

core文件

  • 为什么要有这个文件:我们想通过core来知道进程为什么退出,以及执行到哪行代码退出的
  • 是什么:将进程在内存中的核心数据(与调试有关的)转储到磁盘中形成core、core.pid的文件
  • 作用:最大的作用是方便我们调试了

Core文件是Linux系统下的内核转储文件,当程序崩溃时由操作系统生成,主要用于对程序进行调试

当程序出现内存越界、段错误(Segmentation Fault)或其他异常情况导致崩溃时,操作系统会中止该进程,并将当前内存状态、寄存器状态、堆栈指针、内存管理信息以及各个函数使用堆栈信息等保存到Core文件中。这样,程序员就可以通过读取和分析Core文件来找出程序崩溃的原因和位置,从而进行调试和修复。

Core文件的存在是为了帮助程序员更好地理解和解决程序崩溃的问题。由于Core文件包含了程序崩溃时的详细内存状态信息,因此它对于调试复杂的内存问题、并发问题以及系统调用等问题非常有用。同时,由于Core文件是在程序崩溃时自动生成的,因此它也可以作为一种自动记录程序崩溃信息的机制,方便程序员进行事后分析和排查。

但是,由于Core文件可能包含大量的内存数据,因此它可能会占用较大的磁盘空间。在不需要进行调试或分析的情况下,可以通过修改操作系统的配置来禁止生成Core文件或将其保存到其他位置。

c++ 复制代码
#include <iostream>
#include <sys/types.h>
#include <sys/wait.h>
#include <signal.h>

using namespace std;

int main()
{
    pid_t id = fork();
    if (id == 0)
    {
        // child
        int a = 10;
        a /= 0;
        exit(1);
    }
    // father
    int status = 0;
    pid_t rid = waitpid(id, &status, 0);
    cout << "exit code:" << ((status >> 8) & 0xff) << endl;
    cout << "exit signal:" << (status & 0x7f) << endl;
    cout << "core dump:" << ((status >> 7) & 0x1) << endl;
    return 0;
}

今天也是到这里了,(存货太多慢慢发了)。学了网络部分的要赶快做项目了

相关推荐
264玫瑰资源库18 分钟前
问道数码兽 怀旧剧情回合手游源码搭建教程(反查重优化版)
java·开发语言·前端·游戏
pwzs28 分钟前
Java 中 String 转 Integer 的方法与底层原理详解
java·后端·基础
东阳马生架构30 分钟前
Nacos简介—2.Nacos的原理简介
java
深夜情感老师40 分钟前
centos离线安装ssh
linux·centos·ssh
普if加的帕43 分钟前
java Springboot使用扣子Coze实现实时音频对话智能客服
java·开发语言·人工智能·spring boot·实时音视频·智能客服
我的作业错错错1 小时前
搭建私人网站
服务器·阿里云·私人网站
施嘉伟1 小时前
Oracle 11g RAC ASM磁盘组剔盘、加盘实施过程
数据库·oracle
爱喝一杯白开水1 小时前
SpringMVC从入门到上手-全面讲解SpringMVC的使用.
java·spring·springmvc
王景程1 小时前
如何测试短信接口
java·服务器·前端
2301_807611491 小时前
77. 组合
c++·算法·leetcode·深度优先·回溯