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

简介: [Linux打怪升级之路]-信号的产生

一、信号基础知识

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
1. //查看7号手册
2. 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) {  
    // 处理信号的代码  
}
  1. 设置信号处理函数:使用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、调用系统函数向进程发信号

这里我们可以通过系统函数kill,raise,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. 信号传递结果:在信号处理程序执行后,进程可以向操作系统报告信号的处理结果,通常以退出状态码的形式。这可以帮助操作系统了解信号处理的成功与否,以及进程是否需要进一步处理。

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

 


相关文章
|
2月前
|
Ubuntu 安全 Linux
|
3月前
|
安全 Linux 网络安全
Linux端的ssh如何升级?
Linux端的ssh如何升级?
360 59
|
1月前
|
人工智能 安全 Linux
|
4月前
|
Linux TensorFlow 算法框架/工具
在Linux上安装其他版本的cmake 或 升级cmake
在Linux上安装其他版本的cmake 或 升级cmake
172 2
|
4月前
|
Linux 调度
Linux0.11 信号(十二)(下)
Linux0.11 信号(十二)
41 1
|
4月前
|
Ubuntu Linux 数据库
在Linux中,如何进行软件包升级?
在Linux中,如何进行软件包升级?
|
4月前
|
Linux 数据安全/隐私保护
在Linux中,如何进行文件系统的迁移和升级?
在Linux中,如何进行文件系统的迁移和升级?
|
4月前
|
存储 Unix Linux
Linux0.11 信号(十二)(上)
Linux0.11 信号(十二)
40 0
|
Linux
22、linux信号学习(1)
1、最简单的安装信号 示例1 View Code #include using namespace std;#include void func(int sig){ cout0 将信号传给进程识别码为pid 的进程。
761 0
|
Web App开发 Linux
23、linux信号学习(2)
5)sigaddset #include int sigaddset(sigset_t *set,int signum); ① sigaddset()用来将参数signum 代表的信号加入至参数set 信号集里。
718 0