[Linux打怪升级之路]-信号的产生-CSDN博客

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6

前言

作者小蜗牛向前冲

名言我可以接受失败但我不能接受放弃

  如果觉的博主的文章还不错的话还请点赞收藏关注支持博主。如果发现有问题的地方欢迎❀大家在评论区指正

目录

一、信号基础知识

1、信号是什么

2、信号的定义

3、信号的处理方式 

二、有关信号操作的函数

1、signal函数(捕捉信号)

2、kill函数

3、raise函数

4、abort函数

三、信号的产生 

1、通过终端按键产生信号

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

3、硬件异常产生信号 

4、由软件条件产生信号 


本期学习目标 了解什么是信号明白部分信号操作的相关函数理解信号产生的过程

一、信号基础知识

1、信号是什么

        在日常生活中我们在遇到十字路口过马路我们知道红灯停绿灯行。为什么我们能够知道红灯停绿灯行呢这不难理解这些规则都刻画在我们的脑海中了也就是说红灯其实就是一种信号当我们识别红灯信号大脑就会做出反应控制身体不动当然有时候会人闯红灯信号肯定是会传递了的也就是说其实信号也是可以被忽略的。

那在Linux下的信号又是指的是什么

下面我们见一个现象

这里我们运行程序当我们按下Ctrl+c将的时候我们发现进程退出。

 这也就说明操作系统肯定给进程发送了一个信号。

操作系统发送一个SIGINT信号给当前正在运行的进程。SIGINT信号表示一个中断请求它的默认行为是终止进程。

当你按下Ctrl+C时终端会捕获这个键盘事件并转发一个SIGINT信号给当前在前台运行的进程。这个信号告诉进程有一个中断请求通常意味着用户希望终止该进程。

2、信号的定义

信号是一个用来表示事件或消息的物理量。在操作系统中信号是一种软件中断用于通知进程发生了某个事件。这种事件可能是硬件异常、用户键入特殊的终端控制字符或者其他进程发送给该进程的消息。每个信号都有一个唯一的正数描述和一个符号名。例如SIGINT是用于表示中断的信号其编号通常是2。信号可以用于进程间的简单通信也可以用于处理异步事件

在操作系统下我们可以通过

//查看系统定义的命令
kill -l

在Linux中总共62共信号 

  • 每个信号都有一个编号和一个宏定义名称,这些宏定义可以在signal.h中找到,例如其中有定 义 #define SIGINT 2
  • 编号34以上的是实时信号,这里只讨论编号34以下的信号,不讨论实时信号。这些信号各自在什么条件下 产生,默认的处理动作是什么,在signal(7)中都有详细说明: man 7 signal
//查看7号手册
man 7 signal

3、信号的处理方式 

在Linux中信号处理的常见方式包括以下几种

  1. 忽略信号进程可以选择忽略某些信号即当这些信号发生时进程不会执行任何操作。这种方式可以用于屏蔽一些不必要的信号干扰。
  2. 默认行为对于每个信号系统都定义了一个默认行为。如果进程没有为某个信号设置自定义处理函数那么信号发生时将会执行默认行为。默认行为可以是终止进程、忽略信号、停止进程等。
  3. 自定义处理函数进程可以通过信号处理函数来捕获信号并在信号发生时执行自定义的操作。这种方式常用于实现一些特定的行为比如在接收到某个信号时进行日志记录、清理资源等。进程可以使用signal函数或者sigaction函数来设置信号处理函数。

二、有关信号操作的函数

1、signal函数(捕捉信号)

功能用于捕捉信号并执行相应的处理操作

原型  sighandler_t signal(int signum, sighandler_t handler);

参数

        signum要捕捉的信号编号

        handler指定一个函数指针它指向的信号处理函数将在接收到指定的信号时被调用

返回值signal函数返回之前为sig信号设置的处理函数的地址

typedef void (*sighandler_t)(int);

这里我们要特别  sighandler_t其实是一个函数指针。

signal函数的第二个参数handler是一个指向信号处理函数的指针也就是sighandler_t类型的函数指针。这个参数用于指定当信号发生时应该执行的函数。

使用handler参数的方式如下

  1. 自定义信号处理函数首先你需要定义一个符合sighandler_t类型的函数即接受一个整数参数信号编号并返回void的函数。这个函数将用于处理信号。例如

    void my_handler(int signal_num) {  
        // 处理信号的代码  
    }
  2. 设置信号处理函数使用signal函数来设置信号的处理函数。将信号编号和自定义的处理函数作为参数传递给signal函数。例如为了设置SIGINT信号通常由Ctrl+C发送的处理函数为my_handler

    signal(SIGINT, my_handler);

注意

  • 处理函数通常会根据信号编号采取不同的操作。因此在处理函数中你可以使用传递的信号编号来判断是哪个信号被接收。
  • 如果你将handler设置为SIG_IGN那么信号将被忽略。
  • 如果你将handler设置为SIG_DFL那么将采取信号的默认行为。

举例用法

#include<iostream>
#include<unistd.h>
#include<signal.h>

using namespace std;

void my_handler(int signal_num)
{
    cout<< "接收到的信号码: "<<signal_num << " "<<"my_pid: "<< getpid()<<endl;
}
//测试
int main()
{
    signal(SIGINT,my_handler);
    while(1)
    {
        cout<< "我是一个进程我正在运行" <<endl;
        sleep(1);
    }
    return 0;
}

这里我们发现当我们在按Ctrl+ c进程不再中止而是去执行my_handler函数中的操作singal也捕获了2号命令。(这里我们是用kill -9 pid中止进程的) 。

2、kill函数

功能用于向指定进程发送信号

原型 int kill(pid_t pid, int sig)

参数

       pid数指定要发送信号的进程ID

        sig参数指定要发送的信号类型

返回值当调用成功时kill函数返回0否则返回-1并设置errno来表示错误原因

 kill函数可以发送多种类型的信号例如

  • SIGINT中断进程。通常是通过用户按下Ctrl+C来产生的。
  • SIGTERM终止进程。这是一个终止进程的请求进程可以捕获该信号进行清理操作然后退出。
  • SIGKILL强制终止进程。这个信号会直接终止进程进程无法捕获或忽略它。 

3、raise函数

功能用于发送信号给当前进程

原型int raise(int sig)

参数

        sig参数指定要发送的信号类型

 当调用raise函数时它会向当前进程发送指定的信号。如果信号处理程序已经注册了对应的信号它将被调用进行处理。否则默认的信号处理动作将被执行可能会导致进程终止、停止或忽略信号。

举例

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

using namespace std;

void my_handler(int signal_num)
{
    cout<< "接收到的信号码: "<<signal_num << " "<<"my_pid: "<< getpid()<<endl;
}
//测试
int main()
{
    
    //注册信号处理程序
    signal(SIGINT,my_handler);

    cout<<"SIGINT to slef"<<endl;

    //发送SIGINT给当前进程
    raise(SIGINT);
    cout<<" signal after" <<endl;

    return 0;
}

 

4、abort函数

功能用于异常终止程序的函数

原型void abort(void);

调用abort函数将导致程序异常终止并返回一个非零的退出码给操作系统以指示程序异常结束。

由于abort函数会立即终止程序所以它通常会在发现严重错误的情况下使用例如内存泄漏、无效参数等。它向程序员提供了一种机制用于在无法恢复正常执行流程的情况下紧急停止程序

然而由于abort函数不进行任何清理操作因此不建议在正常情况下使用它来结束程序。在大多数情况下应该尝试通过其他方式恢复程序的执行或者使用更适当的函数来进行正常的程序终止如exit函数。

三、信号的产生 

1、通过终端按键产生信号

通过终端按键产生信号是指用户在终端命令行界面按下特定的按键组合向当前进程发送一种信号。这种信号可以是一种通知或请求告诉进程执行某种特定的操作

在Unix和类Unix系统中终端按键可以产生信号这些信号可以与进程进行交互。例如当用户按下 Ctrl+C 组合键时会向当前正在运行的进程发送一个 SIGINT 信号。这个信号的默认处理动作是终止进程

类似地还有其他按键组合可以发送不同的信号比如 Ctrl+\ 会发送 SIGQUIT 信号默认处理动作也是终止进程并生成 core dump 文件

这种通过终端按键产生信号的方式提供了一种用户与进程交互的手段用户可以通过这种方式控制进程的行为比如终止进程、暂停进程等。同时进程也可以根据自身需要对这些信号进行处理比如忽略信号、自定义处理函数等。

那么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 file size文件数量是0 

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

这里我们可以通过系统函数killraise,abort函数来向进程发送我们想要的信息

 int cnt = 0;
    while(cnt <= 10)
    {
        printf("cnt : %d ,pid: %d\n",cnt++,getpid());
        sleep(1);
        // if(cnt >= 5) abort();
        if(cnt >= 5) raise(9);
    }

 调用abort终止程序

 调用raise(9)终止程序

3、硬件异常产生信号 

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

除以0的指令

当我们在编写的程序进行除0操作为什么就会发送8好信号终止进程呢?

通过下图进行理解

当我们进行除0操作在CPU中有一个专门用来检测运算的状态寄存器里面有一个溢出标记位当除0时标记位就会溢出从而被操作系统知晓发送8好信号给进程。 

非法内存地址 

OS怎么知道呢我野指针了呢为什么程序中有野指针就会崩溃

我们知道其实指针指向的是虚拟地址通过页表映射到物理地址当指针越界访问的时候操作系统就可以察觉到从而对进程发送11号信号终止进程。

void my_handler(int signal_num)
{
    cout<< "接收到的信号码: "<<signal_num << " "<<"my_pid: "<< getpid()<<endl;
    sleep(1);
}
//测试
int main(int argc, char *argv[])
{

    //硬件异常产生信号

    signal(11,my_handler);
    while(true)
    {
       std::cout << "我在运行中...." << std::endl;
        sleep(1);
        int *p = nullptr; 
       //写一个野指针
        *p = 1;
    }
    return 0;
}

4、由软件条件产生信号 

在理解软件条件产生信号之前我们需要理解allarm函数

头文件#include unsigned

类型int alarm(unsigned int seconds);

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

这个函数的返回值是0或者是以前设定的闹钟时间还余下的秒数。打个比方,某人要小睡一觉,设定闹钟为30分钟之后 响,20分钟后被人吵醒了,还想多睡一会儿,于是重新设定闹钟为15分钟之后响,“以前设定的闹钟时间还余下的时间”就 是10分钟。如果seconds值为0,表示取消以前设定的闹钟,函数的返回值仍然是以前设定的闹钟时间还余下的秒数

void my_handler(int signal_num)
{
    cout<< "接收到的信号码: "<<signal_num << " "<<"my_pid: "<< getpid()<<endl;
    exit(1);
}
//测试
int main(int argc, char *argv[])
{
 
    //4、软件异常产生信号
    int cnt = 0;
    signal(SIGALRM,my_handler);
    //统计1秒数据能够累计到多少
    alarm(1);
    while(true)
    {
        cnt++;
        cout<< cnt <<endl;
    }
   
    return 0;
}

这里是没有用signal捕捉信号 

 这里是用singal捕捉了14号信号

 从现象上来看当调用alarm函数在经过设置的时间后会给进程发送14号信号。

总结思考

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

这是因为操作系统是计算机硬件和软件之间的中介负责管理计算机的资源并提供一个运行环境使得程序能够在计算机上正确、高效地运行。 

 OS是进程的管理者 信号的处理是否是立即处理的

信号的处理并不一定是立即的它取决于信号的类型以及接收进程的状态 

在合适的时候 信号如果不是被立即处理那么信号是否需要暂时被进程记录下来 

是的如果一个信号在适当的时候不能立即处理操作系统通常会将信号暂时记录下来以便稍后处理。这种记录信号的机制通常称为信号队列Signal Queue。

信号队列允许操作系统将接收到的信号排队而不会丢失它们。每个进程都有一个相关联的信号队列用于存储接收到的信号。当信号到达时它会被添加到接收进程的信号队列中。接收进程可以随后检查队列中的信号并决定如何处理它们。

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

一个进程可以预先定义信号的处理方式无论是否收到信号。这就是通过设置信号处理程序Signal Handler来实现的。信号处理程序是一段特定的代码它规定了进程在接收特定信号时应采取的操作。

进程可以使用操作系统提供的系统调用如signal()sigaction()等具体取决于操作系统和编程语言来注册信号处理程序。一旦设置了信号处理程序当进程接收到相应的信号时操作系统将执行信号处理程序中定义的操作。

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

操作系统OS可以向进程发送信号这是一种进程间通信的机制用于通知接收进程发生了某些事件或条件。下面是完整的发送信号的处理过程

  1. 信号产生信号的产生通常是由操作系统、其他进程或硬件事件引发的。信号可以表示不同的事件如用户按下终止键Ctrl+C进程除零错误或者定时器超时等。

  2. 信号的种类每个信号都有一个唯一的标识符如SIGTERM、SIGINT、SIGSEGV等用来表示不同的事件类型。操作系统和程序员可以根据需要定义自定义信号。

  3. 信号发送操作系统将信号发送给目标进程。这通常涉及到操作系统查找目标进程的进程标识符PID或进程组标识符PGID然后将信号发送给目标。

  4. 信号传递操作系统将信号传递给目标进程。目标进程的执行可能会被中断以便它可以处理接收到的信号。这意味着操作系统在进程执行期间会修改进程的执行流程以引发信号处理。

  5. 信号处理程序执行目标进程在接收到信号后会查找它为该信号注册的信号处理程序。信号处理程序是一段用户定义的代码用于定义在接收信号时应执行的操作。处理程序可以是默认的操作用户自定义的操作或者忽略信号。

  6. 信号处理根据信号处理程序的定义进程采取相应的操作。这可以包括终止进程、忽略信号、记录事件、执行自定义操作等。处理程序执行后进程可以继续执行原来的任务。

  7. 信号传递结果在信号处理程序执行后进程可以向操作系统报告信号的处理结果通常以退出状态码的形式。这可以帮助操作系统了解信号处理的成功与否以及进程是否需要进一步处理。

总结来说操作系统向进程发送信号的过程涉及信号的产生、发送、传递、处理以及可能的反馈。这种机制使得操作系统和不同进程之间能够进行通信和协作通常用于处理异常情况、用户交互、进程控制等。

 

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6
标签: linux