[Linux]一篇文章带你全面理解信号

初识信号

一、什么是信号

在操作系统(OS)中,信号(signal)是一种进程间通信(IPC)的机制,特别是在Unix、类Unix以及其他POSIX兼容的操作系统中。信号是一种异步的通知机制,用来提醒进程一个事件已经发生。

这种说明未免会有些晦涩难懂,现在将从以下几点带你认识信号:

  1. 信号是系统提供的一种让用户(进程)能够给其他进程发送异步信息(可以理解任意时间都可以发送,接受信号的进程无法预料到信号到来的时间)的方式
  2. 信号的发送,是为了让接受信号的进程做一些自己执行流之外的操作(如:你正在打游戏,家里人喊你吃饭,这就是给你的一个信号)
  3. 进程对于信号的到来,可以选择处理、暂时不管或忽略的策略

二、为什么要有信号

  1. 异步通知:信号提供了一种异步通知的方式,允许一个进程或线程在不中断其正常执行的情况下接收通知,以便采取适当的措施。这对于处理外部事件,如用户输入、硬件故障或其他进程的状态变化等非常有用。
  2. 进程间通信:进程可以通过发送信号的方式进行通信。例如,一个进程在完成了某项工作之后,可以向另一个进程发送信号,通知其进行下一步的操作。这对于多任务协作非常有用。
  3. 异常处理:当程序出现异常情况,如除以零或无效内存访问时,操作系统可以通过发送信号来通知进程。进程可以定义信号处理函数来处理这些异常情况,从而防止程序崩溃或数据损坏。
  4. 系统调试:信号还可以用于程序的调试。例如,在程序运行时,可以向进程发送信号以打印程序的状态信息等,帮助开发人员定位问题。
  5. 用户交互:用户可以通过键盘快捷键或类似的输入方式发送信号给正在运行的进程,以请求特定操作。这提供了一种灵活的用户交互方式。

这部分我们将在后面使用代码的形式展现出来。

看见信号

一、先见一下Linux中的信号:

在这里插入图片描述

其中31之后的都是实时信号,这种信号出现了就要立刻处理,常见于实时操作系统

其中介绍几个比较典型的信号

  • 2)SIGINT :‘‘Interrupt from keyboard’’,键盘中断信号,可以通过组合键ctrl+c触发

  • 8)SIGFPE:“Floating-point exception”,除0异常

  • 9)SIGKILL:“Kill signal”,杀死信号

  • 11)SIGSEGV:“Invalid memory reference”,段错误

  • 13)SIGPIPE:“Broken pipe: write to pipe with no readers”,管道信号,如果没有读信号的进程则中断进程

  • 14)SIGALRM:“Timer signal from alarm(2)”,可以在代码中输入alarm(int),来设置闹钟

  • 19)SIGSTOP:“Stop process”,暂停进程

  • 18)SIGCONT:“Continue if stopped”,开始被暂停的进程

二、如何产生信号

  1. kill -num pid

    使用ps axj | grep process_name来查询进程的pid,再向指定进程发送signum信号

  2. 键盘产生

    ctrl+c:触发SIGINT信号

    ctrl+\:触发SIGQUIT信号

  3. 系统调用

    kill(pid_t pid, int sig):对pid进程发送sig信号

    raise(int sig):对自己发送sig信号

    abort():引起进程直接终止,对自己发送SIGABRT信号

  4. 软件条件

    软件条件指:系统在持续时间内会一直对信号产生条件进行判断,如果满足则发送信号,不满足则继续持续查询

    alarm(uint second):设置时钟,在second秒后,系统发送SIGALRM信号。在此期间,系统会不断判断时钟是否满足条件

    SIGPIPE:在创建管道的时候,常常会发现,如果读端全关闭了,则写端就会立刻关闭。这是因为系统会一直检测读写端的情况,如果一旦读端没了,就会发送SIGPIPE信号

  5. 异常

    1)首先,对于除0异常段错误这两个异常是和寄存器相关的,寄存器检测出问题就会分别发送SIGFPE、SIGSEGV信号

    2)对于语言级别的异常,例如throw,他们抛出的异常和信号有关系。不过会对异常进行进一步的封装,以实现各自的目的(这里可以类比后面所要讲signal()函数)

三、自定义信号的处理行为(自定义捕捉)

前面谈到,进程对于信号的处理有3种方式:忽略、暂时不管、处理

这里先去探究处理的方式:

如下是一些系统的默认处理方式

Term   Default action is to terminate the process.

Ign    Default action is to ignore the signal.

Core   Default action is  to terminate the process and dump core.

Stop   Default action is to stop the process.

Cont   Default action is to continue the process if it is currently stopped.

其中对于Ign方式,就是所说的忽略,不去接收和处理信号。

其余的常见方式有:中断(Term、Core,后面会说明两者的差别)、暂停(Stop)、继续(Cont)

但是!上述的信号处理方式不一定满足我们的需求,比如:我在打手游,家里人喊我吃饭,家里的默认处理方式就是放下手机去吃饭,但是我想去倒个垃圾,在路上继续玩游戏。这时候就需要自定义捕捉(自定义信号的处理行为)

方法1:

signal()函数

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

sighandler_t

作为一个参数:用来回调自定义函数,返回类型为void,参数为int(要自定义捕捉的信号)

作为一个返回值:返回一个函数指针,这个函数指针用来记录自定义捕捉之前该信号的捕捉方式。可以拿来和SIG_ERR、SIG_DFL、SIG_IGN比较来判断此前的捕捉方式

int signum
需要自定义捕捉的信号名字或者编号

举例:下述代码更改了2)SIGINT信号的处理方式,并对返回的信息做了判断

#include <stdio.h>  
#include <signal.h>  
#include <unistd.h>  
  
void signal_handler(int signum) {  
    printf("Caught signal %d\n", signum);  
    // 清理并关闭  
    // ...  
    exit(signum);  
}  
  
int main() {  
    // 注册信号处理函数  
    void (*prev_handler)(int);  
    prev_handler = signal(SIGINT, signal_handler);  
    //或者         signal(2, signal_handler);  
  
    if (prev_handler == SIG_ERR) {  
        fprintf(stderr, "Error setting signal handler\n");  
        return 1;  
    }  
  
    // 打印之前为该信号设置的处理函数  
    if (prev_handler == SIG_DFL) {  
        printf("Previous handler was default.\n");  
    } else if (prev_handler == SIG_IGN) {  
        printf("Previous handler was ignore.\n");  
    } else {  
        printf("Previous handler was a function.\n");  
    }  
  
    // 等待信号  
    while(1) {  
        cout<<"pro running..."<<endl;
        sleep(1);  
    }  
  
    return 0;  
}

方法2(了解):

sigaction()函数

#include <signal.h>
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);

该函数比signal()函数难用,但是功能也更全面,比如:可以在设置自定义捕捉的时候同时屏蔽信号

struct sigaction:

struct sigaction {  
    void     (*sa_handler)(int);    // 传统的信号处理函数  
    void     (*sa_sigaction)(int, siginfo_t *, void *); // 用于实时信号的处理函数  
    sigset_t   sa_mask;             // 信号掩码  
    int        sa_flags;            // 标志位  
    void     (*sa_restorer)(void);  // 已被弃用,在大多数系统上应该设置为 NULL  
};

其中定义了有:处理方法(其中第二个sa_sigaction主要用来处理实时信号)、信号掩码(用来屏蔽信号)、信号标志位(其中在使用``sa_sigaction时需要设置为SA_SIGINFO`)

act && oldact

oldact返回旧的处理信息(struct sigaction结构体),act用来传入新的处理信息

举例:下述代码更改了2)SIGINT信号的处理方式

#include <stdio.h>  
#include <signal.h>  
#include <unistd.h>  
  
void handle(int sig)
{
    cout<<"sig "<<sig<<" can't kill me..."<<endl;
}

int main()
{
    struct sigaction newact;
    struct sigaction oldact;
    newact.sa_handler=handle;
    newact.sa_restorer=NULL;
    newact.sa_flags=0;
    sigemptyset(&newact.sa_mask);//将旧的信号掩码清空
    sigaction(2,&newact,&oldact);//处理2号信号
    while(true)
    {
        cout<<"pro running..."<<endl;
        sleep(1);
    }
    return 0;
}

了解信号

一、信号的保存

前面我们已经知道了信号是如何产生的,但是内核大多都不能第一时间检测到信号的产生(对!你没听错,信号产生后,OS第一时间不一定会对其进行处理,因为他“不知道”有信号产生了,至于何时才能检测到,我们留到后续说明),所以我们就需要将产生的信号进行保存,以方便后续内核的信号检测

信号的保存于以下3部分息息相关:

  1. 信号递达(执行信号的处理动作,如:前面的handle函数或默认的信号处理)
  2. 信号未决(在信号产生但是还未递达的时候,该信号就处于未决状态)
  3. 信号阻塞(决定了信号能否被递达处理)tips:注意区分阻塞和忽略,忽略是一种递达的处理方式

对于上述3部分,采用了位图和函数指针数组进行保存

在这里插入图片描述

在进程PCB中维护了这样3张表:bolck(信号阻塞表)、pending(信号未决表)、handler(信号递达表)

前2个位图的类型都是sigset_t,handler的类型是,Linux内核中各部分结构体内容如下:

/* signal handlers */
struct signal_struct *signal;
struct sighand_struct *sighand;//hanler
sigset_t blocked, real_blocked;//block
sigset_t saved_sigmask;
struct sigpending pending;//pending

/*位图结构*/
typedef struct {
	unsigned long sig[_NSIG_WORDS];
} sigset_t;

/*pending*/
struct sigpending {
	struct list_head list;
	sigset_t signal;
};

/*handler*/
struct sighand_struct {
	atomic_t		count;
	struct k_sigaction	action[_NSIG];
	spinlock_t		siglock;
	wait_queue_head_t	signalfd_wqh;
};
struct sigaction {
	__sighandler_t sa_handler;
	unsigned long sa_flags;
#ifdef SA_RESTORER
	__sigrestore_t sa_restorer;
#endif
	sigset_t sa_mask;		/* mask last for extensibility */
};
typedef void __signalfn_t(int);//函数指针
typedef __signalfn_t __user *__sighandler_t;

其中对应比特位/数组下表位置编号对应信号编号,如:1号位置 -> 1号信号SIGHUP

  1. block位图:
    用来阻塞信号(不是忽略),当某一位置置1的时候,就将该位的信号就行屏蔽(即可以接收该信号,但是不处理该信号)

  2. pending位图:

    用来保存接收到的信号,当有信号产生的时候,会将对应位置置1,而内核每次将检测pending位图,对所有置1的位置的信号进行处理,处理完后再将该位置置0

  3. handler数组:

    用来记录对应信号的处理方法,数组中每个元素中都封装包含了一个函数指针,指向你对该信号的自定义捕捉或默认处理方法

二、block、pending表使用代码查看

前面通过内核代码看到了这两个表的定义,现在让我们通过代码查看:在此之前我们先介绍一系列函数、结构体,以方便后续的操作

struct sigset_t位图结构体,具体结构见前面

我们可以创建一个sigset_t结构体,并对其内容进行修改

sigset_t set;
//阻塞signo信号
sigaddset(sigset_t* set, int signo);
//取消阻塞signo信号
sigdelset(sigset_t* set, int signo);
//判断signo是否阻塞
sigismember(sigset_t* set, int signo);
//清空阻塞位图
sigemptyset(sigset_t* set);
//全阻塞
sigfillset(sigset_t* set);

这里可能有些人有疑问:为什么非要通过系统提供的函数来修改位图呢?不能我自己直接去修改set内容吗?反正sigset_t内部就只有一个元素(数组),我轻轻松松就可以按照我的需求进行修改

原因:对于不同的内核环境下,可能实现屏蔽的操作有些许不同,所以必须通过调用函数来修改位图,以便屏蔽不同内核的底层差异


sigprocmask()修改进程的信号屏蔽位(block位图)

 #include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);

set:

我们希望修改的信号屏蔽字

oldset:

保存修改前的信号屏蔽字

how: 指示如何修改,其可以选择如下选项

  1. SIG_BLOCK:set包含了我们希望添加到当前信号屏蔽字的信号,相当于mask=mask|set
  2. SIG_UNBLOCK:set包含了我们希望从信号屏蔽字中解除阻塞的信号,相当于mask=mask&~set
  3. SIG_SETMASK:设置当前信号屏蔽字为set所指向的值,相当于mask=set

sigpending()获取pending位图的内容

#include <signal.h>
int sigpending(sigset_t *set);

通过sigpending函数,可以将当前进程的pending位图放入sigset_t set中

如果想打印pending位图内容,只需用sigismember和set一起使用就行

具体代码演示

  1. block表的查看

    我们先创建一个sigset_t结构体,打印其前31个信号的屏蔽状态,过几秒后,我们手动添加对2、5、7、13、27号信号的屏蔽再次打印查看结构(这里由于还没有调用sigprocmask函数,所以进程的信号屏蔽字不会被修改i)

    int main()
    {
        sigset_t set;
        cout<<"修改屏蔽字之前:"<<endl;
        for(int i=1;i<=31;i++)
        {
            cout<<sigismember(&set,i)<<" ";
        }
        cout<<endl;
    
        sigaddset(&set,2);
        sigaddset(&set,5);
        sigaddset(&set,7);
        sigaddset(&set,13);
        sigaddset(&set,27);
        sleep(2);
        cout<<"修改屏蔽字之后:"<<endl;
        for(int i=1;i<=31;i++)
        {
            cout<<sigismember(&set,i)<<" ";
        }
        cout<<endl;
    
        return 0;
    }
      
    

    运行结果:

    改屏蔽字之前:
    1 0 0 0 1 0 1 0 1 0 0 0 0 1 1 1 0 1 1 0 0 0 1 1 1 1 1 1 1 0 1 
    修改屏蔽字之后:
    1 1 0 0 1 0 1 0 1 0 0 0 1 1 1 1 0 1 1 0 0 0 1 1 1 1 1 1 1 0 1 
    

    有些信号一开始就是默认被屏蔽的,但是其余的信号在我们添加屏蔽后确实有了改变

  2. pending表的查看

    我们先打印进程当前的pending位图,查看有没有未决信号,然后使用sigprocmask将2号信号屏蔽了,再次查看pending位图(这里我们自定义捕捉一下2号信号,方便观察)

    void handler(int sig)
    {
        cout<<"捕捉到了 "<<sig<<" 号信号"<<endl;
        sleep(1);
    }
    void print(sigset_t* set)
    {
        for(int i=1;i<31;i++)
        {
            if(sigismember(set,i))
            {
                cout<<1<<" ";
            }
            else 
            {
                cout<<0<<" ";
            }
        }
        cout<<endl;
    }
    int main()
    {
        signal(SIGINT,handler);//自定义捕捉
        sigset_t set;
    
        sigpending(&set);
        cout<<"未屏蔽之前,发送信号之前,当前进程的pending位图:"<<endl;
        print(&set);
    
        sleep(5);//留时间发送信号,此处是为了方便演示
    
        cout<<"未屏蔽之前,发送信号之后,当前进程的pending位图:"<<endl;
        sigpending(&set);
        print(&set);
    
        sigaddset(&set,2);
        sigprocmask(SIG_BLOCK,&set,nullptr);//这里就不保存原来的屏蔽字了
        cout<<"修改屏蔽字,屏蔽2号信号...:"<<endl;
    
        sigpending(&set);
        cout<<"屏蔽之后,发送信号之前,当前进程的pending位图:"<<endl;
        print(&set);
    
        sleep(5);//留时间发送信号,此处是为了方便演示
        
        sigpending(&set);
        cout<<"屏蔽之后,发送信号之后,当前进程的pending位图:"<<endl;
        print(&set);
    
        return 0;
    }
    

    运行结果:

    未屏蔽之前,发送信号之前,当前进程的pending位图:
    0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 
    ^C捕捉到了 2 号信号
    未屏蔽之前,发送信号之后,当前进程的pending位图:
    0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 
    修改屏蔽字,屏蔽2号信号...:
    屏蔽之后,发送信号之前,当前进程的pending位图:
    0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 
    ^C屏蔽之后,发送信号之后,当前进程的pending位图:
    0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 
    
    1. 在设置屏蔽位之前,pending位图全0,即没有信号发送
    2. 发送2号信号,调用了handler函数(自定义捕捉),同时pending位图也为全0,因为2号信号被处理了,所以变回了0
    3. 在设置屏蔽位之后,pending位图全0,因为此时还没有发送信号
    4. 发送2号信号,没有调用handler函数,通过pending位图2号位置变为1,说明2号信号保存了,但是没有被递达(处理)

    三、一些倔强的,无法被屏蔽的信号

    问题:如果我将进程的所有信号都屏蔽了,那么一旦我运行了这个进程,那完蛋了,杀不掉了!!!

    不用担心,OS不会让你这么干,或许是他早就预料到你会犯下这么尴尬的错误。

    对于一些信号是无法屏蔽的,如:

    1. 9)SIGKILL 杀掉信号
    2. 19)SIGSTOP 暂停信号
    3. 18)SIGCONT这个会做特殊处理,他会额外解除某些信号的屏蔽

    代码演示:

void print(sigset_t* set)
   {
    for(int i=1;i<31;i++)
       {
        if(sigismember(set,i))
           {
            cout<<1<<" ";
           }
        else 
           {
            cout<<0<<" ";
           }
    }
       cout<<endl;
}
   int main()
{
       sigset_t set;
    sigaddset(&set,9);
       sigaddset(&set,19);
       print(&set);
       sigprocmask(SIG_BLOCK,&set,nullptr);
   
       while(true)
       {
           cout<<"proc running..."<<endl;
           sleep(1);
       }
       return 0;
   }

运行结果

   1 0 0 0 1 0 1 0 1 0 0 0 0 1 1 0 0 0 1 1 0 1 0 1 0 1 0 0 1 0
   proc running...
   proc running...
   proc running...
   proc running...
   proc running...
   proc running...
   Killed

可以通过信号屏蔽字看出,我们确实屏蔽了9,19号信号,但是让我们发送kill -9 pid指令后,进程还是被杀掉了。同样的发送kill -19 pid指令后,进程还是会被暂停

四、详谈信号的处理

在前面我们已经了解到了信号处理的代码级表现:位图保存、阻塞,自定义捕捉…现在我们浅谈内核来进一步了解信号的处理:

先说结论:信号的处理发生在由内核态转向用户态的时候发生

在这里插入图片描述

为什么要这样设计,从以下的几点问题来解答:

  1. 为什么要先进入内核态

    进入内核态的原因不单单是需要处理信号,对于异常以及系统调用都会让进程进入内核态。进入内核态就相当于提高权限,如此一来,CPU就有权力去做处理你的一系列需求

  2. 如果没有系统调用和异常的产生,我还能进入内核态吗?

    当然可以,别忘了对于大多数操作系统都支持并发运行,这就涉及到进程调度的操作(时间片到期后就要调度)。而对进程的调度需要相当高的权限,所以每次调度的时候就会陷入内核态,这样一来就成功进入内核态了

  3. 为什么一定要在内核态向用户态转换的时候才会处理信号?

    首先,处于内核态的进程具有极高的权限,他能够直接去修改PCB等一系列核心内容(同时信号的处理也需要修改PCB内容),这样大大提高了处理的效率与能力。其次,从内核态转向用户态这一时间点正好符合的运行的逻辑,下一步要么处理自定义捕捉(需要转向用户态)、要么处理完成返回主执行流(需要转回用户态)

  4. 为什么对于自定义捕捉需要转向用户态再处理,而不是直接在内核态处理?

    为了安全!!!!因为自定义捕捉函数是用户写的,如果在内核态处理用户程序,那么此时用户的代码权限是非常高,OS担心用户恶意访问内核,所以就必须要在用户态执行用户写的自定义捕捉函数

  5. 对于一些可以杀掉进程的信号,为什么不直接杀掉进程,而是要先放进pending中

    为了安全,不出未定义问题!!!!如果进程此时有更重要的事情需要做,直接杀掉进程可能会导致未定义行为,所以让进程先保存信号,把需要回收的回收、释放的释放、没做完的做完,最后在让OS回收进程

运用信号

说了一大堆,如果不会运用信号,那学再多都没用,这里提供几个简单的信号运用例子,以供参考:

一、term与core

有些朋友可能会在平时玩Linux的时候发现,信号的终止类型有两种:term和core。这两种都是可以直接杀掉进程的,很多人可能并不知道他们的区别

term:直接用来杀掉进程,不做任何处理

core:core dump(核心转储),但是需要设置后才能发挥作用,不然就同term一样。

其核心作用是:

  • 通过core定位到进程为什么退出,以及执行到哪行代码退出的

  • 将进程在内存中运行的核心数据(与调试有关)转储到磁盘中形成的core、core.pid(Centos)文件

  • 协助我们进行调试,可以用gdb调试下,输入core-file corename就可以获取详细信息

如何打开core dump?

ulimit命令

-a :查看全部内容

-c num :开启core dump功能,并将core文件大小设定为num

运行结果:

sll@SFT:~/git/testsignal$ ulimit -a
    core file size          (blocks, -c) 0 // 这一行
    data seg size           (kbytes, -d) unlimited
    scheduling priority             (-e) 0
    file size               (blocks, -f) unlimited
    pending signals                 (-i) 7319
    max locked memory       (kbytes, -l) 65536
    max memory size         (kbytes, -m) unlimited
    open files                      (-n) 65535
    pipe size            (512 bytes, -p) 8
    POSIX message queues     (bytes, -q) 819200
    real-time priority              (-r) 0
    stack size              (kbytes, -s) 8192
    cpu time               (seconds, -t) unlimited
    max user processes              (-u) 7319
    virtual memory          (kbytes, -v) unlimited
    file locks                      (-x) unlimited
    
sll@SFT:~/git/testsignal$ ulimit -c 1024// 设置大小
sll@SFT:~/git/testsignal$ ulimit -a
core file size          (blocks, -c) 1024 //打开了核心转储
data seg size           (kbytes, -d) unlimited
scheduling priority             (-e) 0
file size               (blocks, -f) unlimited
pending signals                 (-i) 7319
max locked memory       (kbytes, -l) 65536
max memory size         (kbytes, -m) unlimited
open files                      (-n) 65535
pipe size            (512 bytes, -p) 8
POSIX message queues     (bytes, -q) 819200
real-time priority              (-r) 0
stack size              (kbytes, -s) 8192
cpu time               (seconds, -t) unlimited
max user processes              (-u) 7319
virtual memory          (kbytes, -v) unlimited
file locks                      (-x) unlimited

运用core dump:

C代码:

#include <stdio.h>  
#include <stdlib.h>  

int main() {  
    int *ptr = NULL;  
    printf("Dereferencing a NULL pointer...\n");  
    *ptr = 42;  // 这将导致段错误  
    return 0;  
}

运行代码:

sll@SFT:~/git/testsignal$ ./a.out 
Dereferencing a NULL pointer...
Segmentation fault (core dumped)

目录内容:

sll@SFT:~/git/testsignal$ ll
total 176
drwxrwxr-x  3 sll sll   4096 May 15 11:41 ./
drwxrwxr-x 24 sll sll   4096 May  7 20:43 ../
-rwxrwxr-x  1 sll sll  16696 May 15 11:40 a.out*
-rw-rw-r--  1 sll sll   4870 May 15 11:23 code.cc
-rw-------  1 sll sll 380928 May 15 11:41 core  //新增了core dump文件
-rw-rw-r--  1 sll sll     69 Apr 22 14:20 Makefile
-rwxrwxr-x  1 sll sll  17600 May 15 11:20 test*
-rw-rw-r--  1 sll sll    195 May 15 11:40 test.c

可以发现多了个core文件,使用gdb调试core

sll@SFT:~/git/testsignal$ gdb ./a.out core //方法1启动core
GNU gdb (Ubuntu 9.2-0ubuntu1~20.04.1) 9.2
Copyright (C) 2020 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Type "show copying" and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
    <http://www.gnu.org/software/gdb/documentation/>.

For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from ./a.out...
(No debugging symbols found in ./a.out)
[New LWP 181336]
Core was generated by `./a.out'.
Program terminated with signal SIGSEGV, Segmentation fault.
#0  0x0000555d29e3616d in main ()

(gdb) core-file core //方法2启动core
[New LWP 181336]
Core was generated by `./a.out'.
Program terminated with signal SIGSEGV, Segmentation fault.
#0  0x0000555d29e3616d in main ()

可以看到最后有提示,SIGSEGV提示等一系列信息

二、异常处理-善后处理

当你的进程因为某些原因而退出了,此时你可能还有很多需要释放的资源、需要回收的进程、需要做完的任务,此时如果直接退出就会引发一系列未定义行为,这个时候就需要自定义捕捉去完成善后
同时这也体验的用户的交互性,发送了信号后,进程给予了回应

代码演示:

#include <stdio.h>  
#include <stdlib.h>  
#include <signal.h>  
#include <unistd.h>  
  
// 信号处理函数  
void signal_handler(int signum) {  
    printf("Received signal %d, cleaning up and exiting...\n", signum);  
  
    // 在这里可以进行清理工作,如关闭文件、释放资源等  
  
    // 退出程序  
    exit(signum);  
}  
  
int main() {  
    // 注册SIGINT信号的处理函数  
    if (signal(SIGINT, signal_handler) == SIG_ERR) {  
        perror("signal");  
        return 1;  
    }  
  
    // 模拟一个长时间运行的程序  
    printf("Running, press Ctrl+C to stop...\n");  
    while (1) {  
        sleep(1);  // 休眠一秒  
        printf("Still running...\n");  
    }  
  
    return 0; // 这个return语句实际上永远不会被执行  
}

三、进程间通信(信号的方式不常用)

父进程创建了一个子进程,并在子进程中设置了一个信号处理函数来捕获SIGUSR1信号。然后父进程等待5秒钟,之后向子进程发送SIGUSR1信号。子进程在接收到信号后执行其信号处理函数,并输出一条消息。最后,父进程等待子进程结束并输出子进程的退出状态。

#include <stdio.h>  
#include <stdlib.h>  
#include <signal.h>  
#include <unistd.h>  
#include <sys/wait.h>  
  
// 自定义的信号处理函数  
void signal_handler(int signum) {  
    printf("Child process received signal %d\n", signum);  
    // 在这里执行你想要的操作  
}  
  
int main() {  
    pid_t pid; // 用于存储子进程的ID  
  
    // 创建子进程  
    pid = fork();  
  
    if (pid < 0) { // fork失败  
        fprintf(stderr, "Fork failed\n");  
        return 1;  
    } else if (pid == 0) { // 子进程  
        // 设置信号处理函数  
        if (signal(SIGUSR1, signal_handler) == SIG_ERR) {  
            perror("signal");  
            return 1;  
        }  
  
        // 子进程等待信号  
        printf("Child process is waiting for signal...\n");  
        while (1) {  
            sleep(1);  
        }  
    } else { // 父进程  
        // 父进程等待一段时间然后发送信号给子进程  
        printf("Parent process will send signal to child process after 5 seconds...\n");  
        sleep(5);  
  
        // 发送SIGUSR1信号给子进程  
        if (kill(pid, SIGUSR1) < 0) {  
            perror("kill");  
            return 1;  
        }  
  
        // 等待子进程结束  
        int status;  
        waitpid(pid, &status, 0);  
        printf("Child process exited with status %d\n", status);  
    }  
  
    return 0;  
}

四、系统调试(不常用)

在这个示例中,我们使用了sigaction来注册SIGUSR1信号的处理函数。在信号处理函数中,我们打印了一条包含进程ID和信号编号的消息。然后,程序进入一个无限循环,模拟一个长时间运行的进程。你可以使用kill命令或者其他方式向该程序发送SIGUSR1信号来触发调试输出。

在终端中,你可以使用以下命令来向该程序发送SIGUSR1信号(假设程序的进程ID为12345):

kill -SIGUSR1 12345
#include <stdio.h>  
#include <stdlib.h>  
#include <signal.h>  
#include <unistd.h>  
#include <string.h>  
  
// 自定义的信号处理函数  
void signal_handler(int signum, siginfo_t *info, void *context) {  
    printf("Received signal %d for process %d\n", signum, getpid());  
    // 在这里添加你的调试代码,比如打印堆栈跟踪、记录日志等  
    // ...  
}  
  
int main() {  
    struct sigaction sa;  
    memset(&sa, 0, sizeof(struct sigaction));  
    sa.sa_sigaction = signal_handler;  
    sa.sa_flags = SA_SIGINFO;  
  
    // 注册SIGUSR1信号的处理函数  
    if (sigaction(SIGUSR1, &sa, NULL) == -1) {  
        perror("sigaction");  
        return 1;  
    }  
  
    printf("Process %d is running, send SIGUSR1 to trigger debug output\n", getpid());  
  
    // 模拟一个长时间运行的程序  
    while (1) {  
        sleep(1);  
        // ... 其他代码 ...  
    }  
  
    return 0; // 注意:实际上这行代码永远不会被执行  
}
  • 10
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值