Linux信号

1.信号的介绍

  1. 什么是信号:

(1) 简单来说就是操作系统内核区响应某些条件而产生一个事件,操作系统可以把该事件发给进程,对于这个进程来说,这个事件就是信号。

(2) 进程在接收到信号后会采取相应的动作。

  1. 信号的本质:就是软件层次对中断的模拟,signal机制可以被理解为进程的软中断,它是一种异步通信机制。

例子:

(1) 假设用户启动了一个交互式的前台进程,然后通过ctrl+c结束它,系统通过键盘产生一个硬件中断。

(2) cpu受硬件中断的影响暂停用户空间的代码,

(3) 操作系统将该硬件中断解释为一个SIGINIT信号,并将其记录在*进程的PCB的信号位图*上(信号位图就是一个PCB中表示信号的位图)。

(4) 该进程收到信号后会返回内核区,在进程从内核区返回用户区继续执行之前,会检查PCB中的信号位图,就可以检测到SIGINT信号,SIGINT信号默认处理动作为终止进程,所以就将进程直接终止了。

2.信号的发送

1. 使用硬件发送信号。

​ 比如在终端运行可执行文件时,按ctrl+c。此时会由键盘发送中断信号。

2. 进程异常产生的信号

(1) 进程崩溃的本质:CPU内部有一个状态寄存器。如果CPU的计算过程出现了错误,错误信息就会被存储在这个状态寄存器中,CPU也会立马终止进程的代码。

(2) 操作系统随时在监视CPU的状态,CPU的状态寄存器记录了异常,操作系统立马就会去给造成异常的进程发送信号,不同的信号包含了不同的错误信息,我们就看到了进程的报错信息。

额外的知识点:如何定位程序究竟是在哪一行崩溃的

调试原理:信号之中都有一个core dump标志位。这个标志位的默认值为0,对于大多数进程终止信号,都会将该标志位设置为1,然后将进程的信息转存到core文件中,方便我们后期调试。

  • 写一个执行中会产生段错误的程序
//signal.cpp
#include<iostream>
int main(void)
{
    int* p = nullptr;
    *p = 1;
    return 0;
}

调试步骤:

  1. ulimit -c 查看core文件的大小上限,如果为0,表示不会生成core文件,我们需要给core文件设置一个合适的大小,直接设置为不限制大小也可以。(因为进程终止后需要将进程信息转存到core文件中,如果core文件大小上限为0则无法存入,需要我们手动设置:ulimit -c unlimited)
  2. g++ -g xxx.cpp -o xxx来生成一个可调试的执行文件。
  3. 执行该可执行文件,发生段错误,生成core文件。
  4. 使用gdb进行调试。
[root@localhost project]# gdb signal
(gdb) core-file core.42876 
[New LWP 42876]
Core was generated by `./signal'.
Program terminated with signal 11, Segmentation fault.
#0  0x00000000004006ed in main () at signal.cpp:5
5           *p = 1;
Missing separate debuginfos, use: debuginfo-install glibc-2.17-157.el7.x86_64 libgcc-4.8.5-44.el7.x86_64 libstdc++-4.8.5-44.el7.x86_64

有时会出现没有core文件的情况,core文件是由core文件的配置文件控制生成的。配置文件为/proc/sys/kernel/core_pattern中,可以设置pid,uid等属性,有兴趣可以研究一下,一般来说,这些属性用不到的。

3. 使用系统调用发送信号

(1) kill函数。

例子:

  1. 执行demo.cpp,每秒输出一行linux
//demo.cpp
#include<iostream>
#include<unistd.h>
int main(void)
{
    while(1)
    {
        sleep(1);
        std::cout<<"linux"<<std::endl;
    }
    return 0;
}
  1. 执行ps -ef | grep demo,获取demo的pid
[root@localhost project]# ps -ef | grep demo
root      43465   3573  0 01:14 pts/0    00:00:00 ./demo
root      43585  43485  0 01:14 pts/1    00:00:00 grep --color=auto demo
  1. 执行kill.cpp,kill掉demo
//kill.cpp
#include<iostream>
#include<sys/types.h>
#include<signal.h>
int main(int argc,char* argv[])
{
    if(argc != 2)
    {
        std::cout<<"process needs a pid parament"<<std::endl;
        return -1;
    }
    if(kill(atoi(argv[1]),SIGINT) == -1)
    {
        perror("kill func error");
        return -1;
    }   
    return 0;
}

(2) 也可以直接使用kill命令发生信号,其实kill命令就是对kill函数的封装。

  1. 执行demo.cpp,每秒输出一行linux
  2. 执行ps -ef | grep demo,获取demo的pid
  3. 执行kill pid(kill 10596),杀掉进程

3. 信号的接收处理

signal函数:

signal函数作用非常简单,就是对信号进行处理。

对信号的处理方式一共有三种:

  1. 默认处理:每一种信号都有默认的处理方式。
//SIG_DFL约等于这段话没写
if(signal(SIGINT,SIG_DFL) == SIG_ERR)
    {
        perror("signal func error");
        return -1;
    }
  1. 忽略处理:可以让进程忽略某些信号,注意SIGKILL和SIGSTOP信号不能被忽略,也就是9号和19号信号。
#include<iostream>
#include<signal.h>
#include<unistd.h>
//接收信号
int main(void)
{
    //第一个参数表示处理哪种信号,第二个参数表示对信号的处理方式
    // signal() returns the previous value of the signal handler, or SIG_ERR on error.  
    //In the event of an error, errno is set to indicate the cause.
    if(signal(SIGINT,SIG_IGN) == SIG_ERR)
    {
        perror("signal func error");
        return -1;
    }
    while (1)
    {
        //此时ctrl+c无法停下来,因为ctrl+c发送的是SIGINT信号,SIG_IGN忽略了SIGINT信号
        //kill命令能停下,因为kill命令发送的是9号信号
        std::cout<<"Linux is great"<<std::endl;
        sleep(1);
    }
    
    return 0;
}
  1. 对信号进行捕捉:注意,SIGKILL和SIGSTOP信号不能被捕捉。
#include<iostream>
#include<signal.h>
#include<unistd.h>
//捕捉信号
void signal_handler_func(int signalNum)
{
   //捕捉到SIGINT信号后打印
   //运行结果就是ctrl+c后会执行这条语句,并且程序不会停下来
   std::cout<<"catch SIGINT signal"<<std::endl;
}
int main(void)
{
   //第一个参数表示处理哪种信号,第二个参数表示对信号的处理方式
   // signal() returns the previous value of the signal handler, or SIG_ERR on error.  
   //In the event of an error, errno is set to indicate the cause.
   if(signal(SIGINT,signal_handler_func) == SIG_ERR)
   {
       perror("signal func error");
       return -1;
   }
   while (1)
   {
       //此时ctrl+c也无法停下来,因为SIGINT信号被捕捉,只会打印而不会中断
       //要用kill停下来
       std::cout<<"Linux is great"<<std::endl;
       sleep(1);
   }
   
   return 0;
}

4. 用信号处理僵尸进程

  1. 两种处理僵尸进程的方法:

(1) 直接将父进程kill掉,这在绝大多数项目中当然是不行的,我们还指望父进程完成任务呢。

(2) 父进程调用wait函数,但这样父进程就阻塞了,严重浪费资源,waitpid函数也比较麻烦,如果设置为阻塞状态会存在和wait函数同样的问题,不阻塞又涉及到进程间通信了,比较麻烦。

  1. 用信号+wait函数处理僵尸进程
#include<unistd.h>
#include<iostream>
#include<cstring>
#include<signal.h>
#include<sys/types.h>
#include<sys/wait.h>
void SIGCHILD_handler_func(int sigChild)
{
    if(wait(nullptr) == -1)
    {
        perror("wait func error");
        exit(1); //退进程
    }
}
int main(void)
{
    pid_t pid = fork();
    if(pid == -1)
    {
        perror("fork func error");
        return -1;
    }
    else if(pid > 0)
    {
        //使用信号的好处就在于,wait是同步的,信号是异步的
        //信号本来就是用于异步通信
        signal(SIGCHLD,SIGCHILD_handler_func);  
        while (1)
        {
            sleep(1);
            std::cout<<"Hello World"<<std::endl;
            /* code */
        }
          
    }
    else
    {
        
    }
    return 0;
}
posted @ 2023-05-03 00:34  曹剑雨  阅读(23)  评论(0编辑  收藏  举报