【ONE·Linux || 信号】

总言

  信号相关认识:简述信号产生、信号保存、信号处理,以及涉及的函数指令。

  


  
  
  
  
  

1、是什么和为什么

  1)、Linux信号说明
  Linux信号本质是一种通知机制,用户/操作系统通过发送一定的信号,通知进程某些事件的发送。进程能够接收到信号,并根据信号通知做出后续的处理工作。
  
  
  2)、概述(结合进程理解)
  1、进程要处理信号,必须具备识别信号的能力(看到信号+处理操作。进程之所以能够“看到”信号,是因为在进程编写时,程序员已经在代码中内置了相关的信号处理操作流程。)
  2、信号处理可以延迟。信号发送的时间是随机的,进程在接收到信号时可能正在处理其他事件或任务。在这种情况下,信号并不会立即被处理,而是被操作系统临时记录(通常是在进程的控制块或某个信号队列中)。这种机制允许进程在完成当前任务后再去处理信号,从而实现信号处理的延迟。
  3、一般而言,信号的产生和处理相对进程而言是异步的。(这意味着信号的发送者和接收者并不需要在时间上保持同步。发送端在发送信号后可能立即去处理其他事件,而接收端在接收到信号时也并不一定会立即响应。这种异步性使得信号成为一种灵活且强大的进程间通信机制,因为发送者和接收者可以在不同的时间点进行操作,而无需相互等待或协调。)
  
  
  PS:信号与信号量是两个不同的概念,没有关系。
  
  
  
  
  

2、如何使用信号(理论→实践)

2.1、信号产生

  所有的信号有其来源,但最终都被OS识别,解释,并发送。

2.1.1、概述(信号处理的方式、查看信号、理解)

  1)、信号处理的常见方式

  1、默认处理:进程自带的处理方式(原先程序设计时就写好的处理逻辑)。
  2、忽略:忽略此信号。
  3、自定义动作:Catch捕捉信号(提供一个信号处理函数,要求内核在处理该信号时切换到用户态执行这个处理函数,这种方式称为捕捉一个信号)。
  
  
  
  2)、查看信号的相关指令介绍

   方式一:kill -l,可查看系统定义的信号列表

在这里插入图片描述

  说明:
  1、每个信号都有一个编号和一个宏定义名称,这些宏定义可以在signal.h中找到。
  2、在大多数现代Linux系统中,一共有62个信号。没有0号信号,31~34中,没有32、33信号。
     前31个[1,31]普通信号。它们包括了一些常见的信号,如SIGHUP(挂起信号)、SIGINT(中断信号)、SIGKILL(强制终止信号)、SIGTERM(请求终止信号)等。
     后31个[34,64]:带RT字段,是实时信号。它们被设计用于需要更高精度和可靠性的应用场景,如实时操作系统中的任务间通信。
  
  
  
  

   方式二:man 7 signal ,可查看特定信号的默认处理方式

NAME
       signal - overview of signals

DESCRIPTION
       Linux supports both POSIX reliable signals (hereinafter "standard signals") and POSIX real-time signals.

   Signal dispositions
       Each signal has a current disposition, which determines how the process behaves when it is delivered the signal.

       The entries in the "Action" column of the tables below specify the default disposition for each signal, as follows://默认处理行为:

       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 (see core(5)).//终止,并生成核心存储

       Stop   Default action is to stop the process.//停止

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

Term:终止进程。这是大多数普通信号的默认处理方式,如SIGKILL、SIGTERM等。
Ign:忽略信号。某些信号可以被系统或用户配置为默认忽略,如SIGCHLD(在某些情况下)和SIGWINCH(窗口大小变化信号,通常用于终端)。
Core:终止进程并生成核心转储文件(core dump)。这通常发生在进程因为某些错误(如段错误)而异常终止时,如SIGSEGV(段错误信号)。
Stop:停止进程的执行。这通常用于调试,如SIGSTOP信号。
Cont:继续执行先前被停止的进程。这通常与SIGSTOP信号配合使用,如SIGCONT信号。
Catch:由用户定义的信号处理函数捕获并处理信号。这通常通过signal()sigaction()系统调用来设置。

  
  
  
  
  3)、问答

  1、如何理解组合键变成信号?
  键盘是通过中断方式与操作系统(OS)进行交互的。当我们按下键盘上的键时,会产生一个硬件中断,这个中断被操作系统的键盘驱动程序捕获。驱动程序能够识别每个键位,并将其转换为相应的字符或键值。

  对于组合键,操作系统也能够识别。当检测到这样的组合键时,操作系统会解析该组合键,并根据系统配置或预定义的行为,将其映射为特定的信号。例如,Ctrl+C通常被映射为SIGINT(中断信号)。

  接下来,操作系统会查找当前前台运行的进程(通常是用户当前正在交互的进程)。一旦找到该进程,操作系统会将相应的信号写入该进程的 进程控制块(PCB) 中的 信号位图结构 中。这个位图结构用于记录进程已经接收到的信号。

组合键信号名称和编号说明
Ctrl+CSIGINT(2)表示终止/中断。这个信号会让程序立即停止运行并退出,执行一系列的清理操作后正常退出。
Ctrl+ZSIGTSTP(20)表示暂停/停止。这个信号会将进程挂起,即将其移动到后台并暂停其执行,同时返回一个作业号和进程号。
Ctrl+\SIGQUIT(3)表示退出并生成core转储文件。这个信号会让进程退出,并生成一个包含进程在退出时内存映像的core文件,这个文件可用于调试。

  
  
  
  2、如何理解信号被进程保存?
  进程需要知道:a.接收到的是什么信号,b.该信号是否已经产生(或尚未处理)。
  由于一个进程能接收到很多信号,这为了管理这些信号,进程内部有一个专门的数据结构,通常称为信号位图信号集。这个数据结构位于进程控制块(PCB)中。
  (下述信号保存中会详细介绍)信号位图是一个位数组,其中每个位都对应一个可能的信号。当进程接收到一个信号时,操作系统会在该进程的信号位图中将对应的位设置为1,表示该信号已经产生。进程可以通过检查这个位图来了解自己接收到了哪些信号。
  
  
  3、信号发送的本质?

  信号发送的本质是操作系统修改目标进程的进程控制块(PCB)中的信号位图集。
  当发送一个信号给进程时,操作系统会执行以下步骤:

查找目标进程的PCB。
在目标进程的信号位图中将对应的位设置为1,表示该信号已经产生。

  这个过程是由操作系统内核完成的,因此它是操作系统级别的操作。用户态的程序通常通过调用如kill()这样的系统调用来发送信号给另一个进程。系统调用会陷入内核,然后由内核执行上述步骤来完成信号的发送。
  
  
  
  
  
  

2.1.2、方式一:通过键盘(按键终端)

2.1.2.1、signal

  1)、基本说明
  相关函数:man signal。在Linux操作系统中,signal函数用于捕获信号并设置自定义处理方式,以便在指定的信号到达时,执行相应的处理代码。

NAME
       signal - ANSI C signal handling

SYNOPSIS
       #include <signal.h>

       typedef void (*sighandler_t)(int);

       sighandler_t signal(int signum, sighandler_t handler);

  typedef void (*sighandler_t)(int);,可看到sighandler_t是一个函数指针,故而参数handler处需要传入一个函数。根据之前所学,这样子的函数称为回调函数。在这里,signal通过回调的方式,修改对应信号的处理方式。
  signum,该参数可以传递信号名称,也可以传递信号编号。(PS:signum此处还可以传递以下宏)

/* Fake signal functions.  */
#define SIG_ERR	((__sighandler_t) -1)		/* Error return.  */
#define SIG_DFL	((__sighandler_t) 0)		/* Default action.  */
#define SIG_IGN	((__sighandler_t) 1)		/* Ignore signal.  */

#ifdef __USE_UNIX98
# define SIG_HOLD	((__sighandler_t) 2)	/* Add signal to hold mask.  */
#endif

  返回值是原先前为信号signum设置的信号处理程序的指针。如果出现错误,则返回SIG_ERR。

RETURN VALUE
       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.

  
  
  

  2)、使用演示
  相关代码:

#include<iostream>
#include<signal.h>
#include<unistd.h>
#include<sys/stat.h>
using namespace std;

void catchSig(int signum)//typedef void (*sighandler_t)(int);
{
    cout<<"进程捕捉到了一个信号,正在处理该信号:" << signum << ", pid:" << getpid() <<endl;
}

int main()
{
    signal(SIGINT,catchSig);//捕捉SIGINT,2号信号,将其处理方式修改为catchSig中的处理方式

    while(true)//死循环:用于让进行一直运行,方便后续传入信号时,观察现象
    {
        cout << "当前进程正在运行,pid: " << getpid() << endl;
        sleep(1);
    }

    return 0;
}

  演示结果:

在这里插入图片描述
  
  对1:signal函数,仅仅是用于修改对特定信号的后续处理动作,并不是直接调用对应的处理动作。(这就意味着,只有当前进程在后续运行时发送了signum信号,该信号才会真正执行修改后的相关动作[handler方法中的操作]。若后续当前进程没有接收到信号,则handler方法中实现的信号操作函数不会被调用)
  
  对2:特定信号的处理动作一般只有一个,当选择自定义捕捉后,无法执行原先设置的默认动作/忽略(三选一)。此时即使用kill指令(kill -信号 进程id),获取到的信号操作也是修改后的结果。

在这里插入图片描述
  
  
  以下为多个信号(2、3)执行相同的处理动作的演示: SIGINTSIGQUIT都是终止进程的,修改后我们无法使用相关键盘快捷键或kill - 2kill -3 杀死进程。此时可以使用kill -9 来处理(kill -9不能被捕捉,后续将介绍到)。
在这里插入图片描述

  
  man 7 signal查看信号默认操作。对于2、3信号,其都是终止进程,这里Term、Core的区别是什么?这就需要谈到核心转储。

       Signal     Value     Action   Comment
       ──────────────────────────────────────────────────────────────────────
       SIGHUP        1       Term    Hangup detected on controlling terminal
                                     or death of controlling process
       SIGINT        2       Term    Interrupt from keyboard

       SIGQUIT       3       Core    Quit from keyboard

  
  
  
  

2.1.2.2、core dump(重要!)

  1)、基础说明

在这里插入图片描述 引入与回顾

  在进程等待章节,我们曾谈过status的低16个比特位含义。其中,进程被信号所杀时,最低7为表示进程接收到的信号,第8位为core dump,在进程等待中,这里表示该进程终止时,是否发生核心转储。
在这里插入图片描述
  
  

在这里插入图片描述 什么是核心转储?

  核心转储(Core Dump): 当一个进程异常终止时,操作系统可以选择将该进程的用户空间内存数据(包括代码、数据、堆栈等)全部或部分地保存到磁盘上,通常这个文件的名称是“core”或者带有进程ID的后缀(如“core.12345”),这就是所谓的核心转储。
  核心转储的目的是为了调试和分析进程崩溃的原因。通过检查核心转储文件,开发者可以看到进程在崩溃时的内存状态,包括变量值、堆栈追踪等信息,这对于诊断和解决程序中的错误非常有帮助。
  

  上述信号2SIGINT和信号3SIGQUIT的区别就在于,2号信号的默认处理动作是终止进程,而3号信号终止进程的同时会生成核心存储文件。
  但根据上述演示,我们发现并没有所谓的core文件,这是因为一般对于云服务器而言,其核心存储功能是被关闭的
  
  
  
  

在这里插入图片描述 如何打开云服务器中的核心存储?

  因core文件中可能包含用户密码等敏感信息并不安全,在开发调试阶段,可以用ulimit命令改变这个限制:

  ulimit -a :用于显示当前shell进程及其子进程所能使用的所有资源限制的当前值。这包括文件大小、内存使用、CPU时间等多个方面的限制。

  ulimit -c 10240 :可打开当前云服务器的core dump,通常而言只在当前终端生效。
  这里10240是指允许生成的核心文件大小的上限为10240个数据块(block)在大多数Linux系统中,一个数据块通常是512字节,所以这里的10240实际上表示的是约5MB的核心文件大小限制。这个设置通常只在当前shell会话中有效,也就是说,它只会影响当前终端中启动的进程。如果希望在系统级别上更改这个限制,可能需要修改系统配置文件或使用其他方法。
在这里插入图片描述
  PS:一个进程允许产生多大的core文件取决于进程的Resource Limit(这个信息保存在PCB中)
  
  
  
  

  2)、相关演示一:core文件的应用场景举例
  核心转储所生成的core文件主要用于调试。进程异常终止通常是因为有Bug,比如非法内存访问导致段错误,事后可以用调试器检查core文件以查清错误原因,这叫做Post-mortem Debug(事后调试)

在这里插入图片描述
  
  相比于逐行调试,使用core文件,可以相对方便的帮助我们找出错误原因。

  step1:gdb XXX 打开生成的可执行程序
  step2:core-file core.XXX 打开生成的核心数据文件,gdb可查看到相关错误原因

[wj@VM-4-3-centos signal]$ gdb #打开gdb
GNU gdb (GDB) Red Hat Enterprise Linux 7.6.1-120.el7
Copyright (C) 2013 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-redhat-linux-gnu".
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.

(gdb) core-file core.11146   #调试:打开core文件,可查看原因。此处主要是我们使用ctrl+\终止了进程。
[New LWP 11146]
Missing separate debuginfo for the main executable file
Try: yum --enablerepo='*debug*' install /usr/lib/debug/.build-id/db/c2fcd6a93ca927c05259d60af199084d385166
Core was generated by `./signal.out'.
Program terminated with signal 3, Quit.
#0  0x00007f313d7549e0 in ?? ()
(gdb) 

  
  
  
  
  3)、相关演示二:验证进程等待函数waitpid的参数,status状态信息中,代表core dump的标记位
  相关代码:写一个父子进程,使用waitpid,演示其参数status,从中获取core信息参数。

int main()
{
    pid_t id = fork();
    if(id == 0)
    {   //子进程
        int a = 100;
        a /= 0;//除数不能为0,此处会报错,信号8:【SIGFPE | Core | Floating point exception】
        exit(0);
    }

    //父进程:进程等待
    int status = 0;
    waitpid(id, &status, 0);//阻塞式等待
    cout <<"父进程:" << getpid() << " ,子进程:" << id << " ,退出信号:" << (status & 0X7F) << ", is core dump:" << ((status >> 7) & 1) << endl;

    return 0;
}

  
  
  演示结果如下:可以看到((status >> 7) & 1) 显示的结果为1,说明此时核心转储功能是打开的。

在这里插入图片描述

  PS:假如使用ulimit命令关闭了核心转储的功能,则此处除触发了默认执行core相关行为的信号(如信号8:SIGFPE、信号3:SIGQUIT等),is core dump获取到的值仍旧为0,表示没有发生核心转储,因为我们关闭了它。

pid_t waitpid(pid_t pid, int *status, int options);

  换句话说,waitpid的第二参数status的第8位core dump表示在进程终止时,是否发生核心转储(这是根据实际真实情况决定的,而非根据信号原先的默认设置来判断)。
  
  
  扩展学习: C/C++程序Crash/崩溃 - Coredump分析基础linux下gdb调试方法和技巧详解
  
  
  
  
  

2.1.3、方式二:调用系统函数向进程发信号

  总结理解: ①用户调用系统接口,执行OS提供的系统调用代码。②OS提取参数或设置特定数值,向目标进程中写入信号,修改进程中的信号标记位。③进程接收到信号,后续会处理该信号,即执行信号对应的动作。
  
  

2.1.3.1、kill

  man 2 kill :可查看相关使用。实际上 kill命令是调用kill()函数实现的,kill()函数可以给一个指定的进程发送指定的信号。

NAME
       kill - send signal to a process

SYNOPSIS
       #include <sys/types.h>
       #include <signal.h>

       int kill(pid_t pid, int sig);
      
RETURN VALUE
       On success (at least one signal was sent), zero is returned.  On error, -1 is returned, and errno is set appropriately.

  
  
  演示代码如下:实现一个我们自己的kill指令(简单版)

//int kill(pid_t pid, int sig);
//演示内容:在命令行中,./mykill 9 pid,终止进程
static void Usage(string proc)
{
    cout << "Usage:\r\n\t" << proc << " signumber processid" << endl;
}

int main(int argc, char* argv[])
{
    if(argc != 3)//检测命令行指令
    {
        Usage(argv[0]);
        exit(1);
    }
	// int atoi (const char * str); Convert string to integer
    int signumber = atoi(argv[1]);// 信号
    int procid = atoi(argv[2]);// 进程id
    kill(procid, signumber);

    return 0;
}

  演示结果如下:
在这里插入图片描述

  
  
  
  
  
  

2.1.3.2、raise

   man 3 raise:raise函数可以给当前进程发送指定的信号,即自己给自己发信号。

NAME
       raise - send a signal to the caller

SYNOPSIS
       #include <signal.h>

       int raise(int sig);

DESCRIPTION
       The  raise()  function sends a signal to the calling process or thread.  In a sin‐
       gle-threaded program it is equivalent to

           kill(getpid(), sig);

  
  
  使用演示:

int main( )
{
    cout << "当前进程正在运行,pid: " << getpid() << endl;
    sleep(2);
    raise(8);
    return 0;
}

在这里插入图片描述

  
  
  
  

2.1.3.3、abort

  man 3 abort:abort函数使当前进程接收到信号而异常终止。就像exit函数一样,abort函数总是会成功的,所以没有返回值。

NAME
       abort - cause abnormal process termination

SYNOPSIS
       #include <stdlib.h>

       void abort(void);

DESCRIPTION
       The abort() first unblocks the SIGABRT signal, and then raises that signal for the
       calling process.  This results in the abnormal termination of the  process  unless
       the  SIGABRT  signal  is  caught  and  the  signal  handler  does  not return (see
       longjmp(3)).

       If the abort() function causes process termination, all open  streams  are  closed
       and flushed.

       If the SIGABRT signal is ignored, or caught by a handler that returns, the abort()
       function will still terminate the process.  It does this by restoring the  default
       disposition for SIGABRT and then raising the signal for a second time.

RETURN VALUE
       The abort() function never returns.

  
  
  使用演示:实际上abort()有时也和exit()一样用于终止进程。

int main( )
{
    cout << "当前进程正在运行,pid: " << getpid() << endl;
    sleep(2);
    abort();//等同于rasie(6)、kill(getpid(),6)
    return 0;
}

在这里插入图片描述

  
  
  
  
  
  

2.1.4、方式三:由软件条件产生信号

  总结理解: ①OS识别到某种软件条件被触发或不满足,②OS构建信号,将其发送给指定的进程,③进程接收到信号,后续会对信号做出处理。
  

2.1.4.1、SIGPIPE

  SIGPIPE是一种由软件条件产生的信号,在“管道”中曾介绍过。
  
  1)、是什么
  该信号主要用于处理管道(pipe)或套接字(socket)的写入操作: 当一个进程尝试向一个已关闭的管道或套接字写入数据时,内核会向该进程发送SIGPIPE信号。

   ①管道的一端被关闭: 在Linux中,管道是进程间通信的一种方式。如果一个进程关闭了管道的一个端点(例如读端),而另一个进程还在尝试写入该管道,那么写入进程会收到一个SIGPIPE信号。

   ②socket连接被关闭: 在网络编程中,socket连接是常见的通信方式。如果客户端或服务器中的一方关闭了socket连接,而另一方还在尝试写入数据,那么写入方会收到一个SIGPIPE信号。
  
  
  2)、常见处理方式
  进程在接收到SIGPIPE信号后,通常会选择终止或采取适当的处理措施。 这些处理措施可能包括关闭相关的文件描述符、释放已分配的内存、终止子进程等,以确保在遇到管道或套接字写入错误时能够正确处理,并保持系统的稳定性和可靠性。(可做出的选择和2.1.1中介绍的信号处理的常见方式一致。)
  ①终止: 这是SIGPIPE信号的默认行为。如果进程没有指定任何特殊的信号处理函数,那么当SIGPIPE信号被发送时,进程会被终止。
  ②忽略信号: 进程可以选择忽略SIGPIPE信号,使用signal(SIGPIPE, SIG_IGN);。这样,当进程尝试写入已关闭的管道或socket时,它不会收到SIGPIPE信号,但写操作会失败并设置errno为EPIPE。
  ③自定义处理: 进程可以为SIGPIPE信号指定一个自定义的信号处理函数,这样当信号被触发时,进程可以执行特定的操作,如关闭文件描述符、释放内存、通知其他进程等。

  
  
  
  

2.1.4.2、alarm

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

NAME
       alarm - set an alarm clock for delivery of a signal

SYNOPSIS
       #include <unistd.h>

       unsigned int alarm(unsigned int seconds);

DESCRIPTION
       alarm()  arranges  for  a SIGALRM signal to be delivered to the calling process in
       seconds seconds.

       If seconds is zero, any pending alarm is canceled.

       In any event any previously set alarm() is canceled.

RETURN VALUE
       alarm() returns the number of seconds remaining  until  any  previously  scheduled
       alarm was due to be delivered, or zero if there was no previously scheduled alarm.

  SIGALRM信号为14,其默认action是Term,即终止进程。

       Signal     Value     Action   Comment
       ──────────────────────────────────────────────────────────────────────
       SIGALRM      14       Term    Timer signal from alarm(2)

  
  
  
  2)、相关演示一:验证1s内程序能进行多少次累加
  
  代码如下:

int main()
{
    alarm(1);//设定闹钟:一秒后发送信号14,终止进程

    int count = 0;
    while(true)
    {
        cout << " count:" << count++ << endl;
    }

    return 0;
}

在这里插入图片描述

  
  问题:观察上述多次运行,我们发现count值的位数都在万级别,以CPU运行速度,不可能只到万位,这是为什么呢?
  回答:上述代码执行过程涉及IO,如cout数据输出、网络发送(本地程序和远端云服务器),这些都严重拖垮累算速度。
  
  
  
  3)、相关演示二:基于上述演示,若想要单纯计算CPU算力(无IO干扰),如何操作?

  代码如下:

uint64_t count = 0;//为了防止int大小不够

void catchSig(int signum)//typedef void (*sighandler_t)(int);
{
    cout<<"进程捕捉到信号:" << signum << " ,pid:" << getpid() << ", count:" << count <<endl;
}

int main()
{
    signal(SIGALRM, catchSig);//将SIGALRM的处理动作自定义捕捉。

    alarm(1);//设定闹钟:一秒后发送信号14,SIGALRM

    while(true) count++; 

    return 0;
}

  演示结果:
在这里插入图片描述

  
  
  需要注意的是,alarm设定的闹钟一旦触发,就会自动移除 若想周期性执行,只需要在catchSig中重复设置alarm即可。【定时器功能】。

uint64_t count = 0;//为了防止int大小不够

void catchSig(int signum)//typedef void (*sighandler_t)(int);
{
    cout<<"进程捕捉到信号:" << signum << " ,pid:" << getpid() << ", count:" << count <<endl;
    alarm(1);
}

int main()
{
    signal(SIGALRM, catchSig);//将SIGALRM的处理动作自定义捕捉。

    alarm(1);//设定闹钟:一秒后发送信号14,SIGALRM

    while(true) count++; 

    return 0;
}

  解释:main函数中alarm设置,1秒后触发闹钟并被catchSig捕获,catchSig内设置有alram闹钟,再1s后闹钟发送SIGALRM信号又被catchSig捕获,如此反复循环。

[wj@VM-4-3-centos signal]$ ls
makefile  signal.cc  signal.out
[wj@VM-4-3-centos signal]$ ./signal.out //上述设置后结果如下,每隔1s,catchSig就会自动捕获一次SIGALRM信号。
进程捕捉到信号:14 ,pid:7693, count:505841653
进程捕捉到信号:14 ,pid:7693, count:1011844295
进程捕捉到信号:14 ,pid:7693, count:1517480886
进程捕捉到信号:14 ,pid:7693, count:2024547213
进程捕捉到信号:14 ,pid:7693, count:2530452059
进程捕捉到信号:14 ,pid:7693, count:3036268502
进程捕捉到信号:14 ,pid:7693, count:3542241957
进程捕捉到信号:14 ,pid:7693, count:4045296230
^C //CTRL+C
[wj@VM-4-3-centos signal]$ 

  
  
  
  
  
  
  
  

2.1.5、方式四:硬件异常产生信号

   概述: 在Linux中,硬件异常产生信号是操作系统与硬件之间协作处理错误或异常情况的一种机制。当硬件检测到某种异常或错误条件时,它会通知操作系统(OS),然后操作系统会向相关的进程发送一个或多个信号。这些信号用于通知进程发生了硬件异常,以便进程可以采取适当的处理措施。
  
  硬件异常类型: 硬件异常可能包括多种类型,如无效的内存访问(如野指针、访问未分配的内存等)、除法错误(如除零错误)、硬件故障(如CPU、内存或I/O设备的错误)等。
  
  

2.1.5.1、演示一:除0

  1)、基础演示代码如下

void catchSig(int signum)
{
    sleep(1);
    cout << "捕获到一个信号:" << signum << " ,当前进程为:" << getpid() << endl;
}

int main()
{
    signal(SIGFPE, catchSig);
    
    int a = 5;
    a /= 0; //error

    while(true) sleep(1);

    return 0;
}

  演示结果如下:
在这里插入图片描述
  
  


  2)、问题一: 上述演示中,除数为0时会生成8号信号SIGFPE如何理解这里的/0错误?为什么说/0是硬件异常导致的信号发送?

  回答: 在计算机中进行计算的是CPU,属于硬件设备。CPU内部具有寄存器,除了供给我们使用的通用寄存器(EA、EC、EB、EX等)外,还有其它寄存器,如状态寄存器。

  状态寄存器又名条件码寄存器,它是计算机系统的核心部件——运算器的一部分。
  状态寄存器用来存放两类信息:
    一类是体现当前指令执行结果的各种状态信息(条件码),如有无进位(CF位)、有无溢出(OV位)、结果正负(SF位)、结果是否为零(ZF位)、奇偶标志位(P位)等;
    另一类是存放控制信息(PSW:程序状态字寄存器),如允许中断(IF位)、跟踪标志(TF位)等。有些机器中将PSW称为标志寄存器FR(Flag Register)。

  /0会导致溢出,OS在计算完毕后进行检测,当发现状态寄存器中溢出标记位为1时,能够识别到有溢出问题。 此时OS会找到当前运行的进程,提取其pid,并将信号发送给进程。进程在合适时候会根据接受到的信号做出动作处理。(也正因此,/0错误看似是我们的代码问题,实则是硬件异常。

  
  
  


  3)、问题二: 为什么上述程序运行后会出现死循环?
  回答: 根据上述代码,我们的while循环一直存在,进程一直在运行,寄存器中的异常一直没有被解决。(即使CPU调度,进程上下文被切换,该异常状态也会随之保存,并在下一次再被调度时随进程上下文回来,那么该异常状态又被OS识别到。)
  PS: 实际上,出现硬件异常时,一般默认行为是退出进程,即使不退出(比如,更改其默认行为,被自定义捕获),我们也做不了什么(无法将其状态标志位有1改为0)。 进程终止会释放PCB结构,其上下文也被释放,所以该异常状态能被清除。

void catchSig(int signum)
{
    sleep(1);
    cout << "捕获到一个信号:" << signum << " ,当前进程为:" << getpid() << endl;
    exit(8);//测试:捕捉信号后让进程退出
}

int main()
{
    signal(SIGFPE, catchSig);
    
    int a = 5;
    a /= 0; //error

    while(true) sleep(1);

    return 0;
}

  echo $? :获取进程退出码。
在这里插入图片描述  
  
  
  
  

2.1.5.2、演示二:野指针、越界

   野指针、越界这类属于段错误,对应信号为SIGSEGV(11).

       Signal     Value     Action   Comment
       ──────────────────────────────────────────────────────────────────────
       SIGSEGV      11       Core    Invalid memory reference

  
  演示代码:

void catchSig(int signum)
{
    sleep(1);
    cout << "捕获到一个信号:" << signum << " ,当前进程为:" << getpid() << endl;
    exit(11);//测试:进程退出
}

int main()
{
    signal(SIGSEGV, catchSig);
    
    int *p = nullptr;
    *p = 2; //error

    while(true) sleep(1);

    return 0;
}

  演示结果:可以看到这里野指针问题发送的信号正是SIGSEGV,因此才会被自定义捕获。

在这里插入图片描述
  
  
  问题: 如何理解野指针和越界问题?
  回答: 无论是指针或者下标访问,它们都需要通过地址,找到对应的目标位置。而我们语言层面上的地址属于虚拟地址,实际中要将虚拟地址转换为物理地址,此时就需要涉及页表和MMU(Memory Manager Unit 内存管理单元)。其中,MMU属于硬件结构(硬件设备里也会存在寄存器),当程序试图访问一个非法地址(如野指针指向的地址或越界的地址)时,MMU会发现这个地址在页表中没有对应的条目,于是会触发一个页错误(page fault)。操作系统会捕获这个页错误,并检查导致页错误的原因。如果是因为访问了一个非法的地址,操作系统会将这个错误转换为一个信号(如SIGSEGV),并发送给对应的进程。进程可以选择捕获这个信号并执行一些清理操作,或者直接由操作系统终止。
  
  
  
  
  
  
  
  
  
  
  
  

2.2、信号保存

2.2.1、一些概念

  1)、相关说明
  信号递达(Delivery):实际执行信号的处理动作称为信号递达。(之前描述的三种常见信号处理方式(默认、捕获、忽略),在执行时都称为信号递达。)
  
  信号未决(Pending) :信号从产生到递达之间的状态,称为信号未决。(根据之前所学,进程接受到信号时并不总是立即处理,因此会产生一段空白期,这段收到信号,将信号保存下来但并未处理的时期,就称之为信号未决。)
  
  阻塞 (Block):进程可以选择阻塞某个信号,被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作。
  阻塞和忽略的区别: 阻塞发生在递达之前,信号尚未被处理;忽略发生在阻塞之后,是进程对信号的一种处理方式。
  
  
  
  
  2)、信号在内核中的示意图表
在这里插入图片描述

  block:阻塞信号集。结构和pending一样是位图,区别在于位图的内容。表示对应信号是否被阻塞。
  pending:未决信号集。信号产生时,内核在进程控制块中设置该信号的未决标志,直到信号递达才清除该标志。
  handler:handler处理方法表。其中填入的都是函数指针,指向一个个信号的具体处理方法。

#define SIG_ERR	((__sighandler_t) -1)		/* Error return.  */
#define SIG_DFL	((__sighandler_t) 0)		/* Default action.  */
#define SIG_IGN	((__sighandler_t) 1)		/* Ignore signal.  */

  根据上图,对于handler表,当获取一个信号signal是,handler会进行匹配:
  ①(int)handler[signal] == 0,则执行SIG_DFL(Default action)默认动作
  ②(int)handler[signal] == 1,则执行SIG_IGN(Ignore signal)忽略操作。若都不符则执行自定义操作,handler[signal]()
  
  
  

  问题: 综合上述,信号被处理是怎样一个过程?
  回答: pending→block→handler。
在这里插入图片描述

  
  
  
  3)、sigset_t类型
  sigset_t,信号集: 位图结构,属于操作系统提供的一个数据类型,对于每种信号用一个bit表示每个信号的“有效”或“无效”状态。和使用内置类型、自定义类型无区别,对于sigset_t,用户直接使用即可。

  例如, 未决和阻塞标志可以用该数据类型来存储,在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞,而在未决信号集中“有效”和“无效”的含义是该信号是否处于未决状态。

  阻塞信号集也叫做当前进程的信号屏蔽字(Signal Mask),这里的“屏蔽”应该理解为阻塞而不是忽略。
  
  
  注意事项:
  ①sigset类型变量,不允许用户自己进行位操作,OS会给我们提供对应的操作位图的方法。【对sigset本身做操作】
  ②OS提供的一些用于完成某些功能的系统接口,其参数有可能包含sigset_t定义的变量或对象【使用sigset来完成一些特定功能】
  
  
  
  
  
  

2.2.2、信号集操作相关函数及其使用

2.2.2.1、相关函数介绍

  1)、对sigset本身进行操作的函数

在这里插入图片描述

  1、 在使用sigset_ t类型的变量之前,一定要调用sigemptysetsigfillset做初始化,使信号集处于确定的状态。

int sigemptyset(sigset_t *set);
//初始化set所指向的信号集,使其中所有信号的对应bit清零,表示该信号集不包含任何有效信号。

int sigfillset(sigset_t *set);
//初始化set所指向的信号集,使其中所有信号的对应bit置位,表示 该信号集的有效信号包括系统支持的所有信号。

  2、在对信号集初始化后,就可以在该信号集中加入或删除某个信号。这四个函数都是成功返回0,出错返回-1。

int sigaddset (sigset_t *set, int signum);//在set指向的信号集中添加某种有效信signum

int sigdelset(sigset_t *set, int signum);//在set指向的信号集中删除某种有效信signum

  3、sigismember是一个布尔函数,用于判断一个信号集(set)的有效信号中是否包含某种信号(signum)。若包含则返回1,不包含则返回0,出错返回-1。

int sigismember(const sigset_t *set, int signum); 

  
  
  
  2)、使用sigset来完成一些特定功能的函数
  1、int sigpending(sigset_t *set):获取当前调用进程的pending信号集,通过set参数传出(可以看到此处set类型即sigset_t)。调用成功则返回0,出错则返回-1。

在这里插入图片描述

  
  
  
  2、int sigprocmask(int how, const sigset_t *set, sigset_t *oldset):调用sigprocmask函数可以读取或更改进程的信号屏蔽字(阻塞信号集)。若成功则为0,若出错则为-1 。

在这里插入图片描述

  如果oldset是非空指针,则读取进程的当前信号屏蔽字通过oldset参数传出。
  如果set是非空指针,则更改进程的信号屏蔽字,参数how指示如何更改。
  如果oldsetset都是非空指针,则先将原来的信号屏蔽字备份到oldset里,然后根据sethow参数更改信号屏蔽字。
  
  假设当前的信号屏蔽字为maskhow参数的可选值如下:

       SIG_BLOCK
              The set of blocked signals is the union of the current set and the set argument.
              //mask = mask | set. 参数set中包含有希望添加到信号屏蔽字的信号

       SIG_UNBLOCK
              The signals in set are removed from the current set of blocked signals.  
              It is permissible to attempt to unblock a signal which is not blocked.
              //mask = mask & ~set.参数set中包含有希望解除阻塞的信号

       SIG_SETMASK
              The set of blocked signals is set to the argument set.
              //mask = set.直接设置信号屏蔽字为set所指向的值
              

  
  
  
  

2.2.2.2、编码样例

  1)、使用演示一:捕获所有信号,将其处理动作都自定义后,是否能够生成一个不会被异常或杀掉的进程?

  相关操作如下:

void catchSig(int signum)
{
    cout << "当前捕捉到的信号是:" << signum << endl;
}

int main()
{
    for(int i = 1; i <= 31; ++i )
    {
        signal(i, catchSig);//捕获1~31信号,将其处理动作都设置为自定义行为
    }

    while(true) sleep(1);//死循环:不让进程结束,方便演示观察现象

    return 0;
}

  
  演示结果如下:可以看出大部分信号都可以通过signal函数捕获。9号信号SIGKILL属于管理员信号,无法被捕捉。(实际上信号设计者本身就考虑到该问题,故有此设置)

在这里插入图片描述

  
  
  
  2)、使用演示二:将2号信号阻塞,获此时发送2号信号,pending信号集中2号信号对应的比特位是否0→1?
  首先,要知道信号阻塞后无法递达,那么未决信号集pending只的状态应该一直保持为1,直到阻塞取消,该信号被递达处理。(这里,我们就是为了验证观察这一过程)

  演示代码如下:

//对信号进程捕捉:为了方便观察,不让信号执行默认处理动作。
void handler(int signum)
{
    cout << "捕获信号:" << signum << " , pid:" << getpid() << endl;
}

//显示信号集
void Showpending(sigset_t &pending)
{
    //int sigismember(const sigset_t *set, int signum);
    for(int i = 1; i <= 31; ++i)
    {
        if(sigismember(&pending, i))
            cout << "1" ;
        else
            cout << "0" ;
    }
    cout << endl;
}

int main()
{
    //1、定义信号集对象
    sigset_t bset, obset;

    //2、初始化信号集
    //int sigemptyset(sigset_t *set);
    sigemptyset(&bset);
    sigemptyset(&obset);

    //3、添加要进行屏蔽(阻塞)的信号
    //int sigaddset(sigset_t *set, int signum);
    sigaddset(&bset,2);//此处也可以传递SIGINT

    //4、设置set到内核中对应的进程内部:上述3步骤只是在用户栈上进行处理,并未设置到OS内部,因此需要借助相关函数达成该功能。
    //int  sigprocmask(int  how,  const  sigset_t*set, sigset_t *oldset);
    int ret = sigprocmask(SIG_BLOCK, &bset, &obset);
    assert(ret == 0);
    (void)ret;
    cout << "成功阻塞信号,pid:"  << getpid() << endl;

    //5、循环打印当前进程的pending信号集:用以后续发送2号信号时观察变化
    sigset_t pending;//用于下述获取进程中的pending信号集
    sigemptyset(&pending);
    signal(2,handler);//用于捕获2号信号:下述一旦解除了2号信号的阻塞状态,2号信号递达后,执行默认操作则进程终止,此处为了方便观察将其捕获
	int count = 0;//用于后续信号恢复
    
    while(true)
    {
        //5.1、获取当前进程的pending信号集
        //int sigpending(sigset_t *set);
        sigpending(&pending);
        
        //5.2、显示pending信号集
        Showpending(pending);
        sleep(1);
        
        //5.3、对2号信号展开恢复:演示0→1→0的过程
        if(count++ == 10)
        {
            cout << "解除对于2号信号的阻塞" << endl;
            int ret = sigprocmask(SIG_SETMASK, &obset, nullptr);
            assert(ret == 0 );
            (void)ret;
        }
    }

    return 0;
}

  
  演示结果如下:

[wj@VM-4-3-centos signal]$ ./signal.out 
成功阻塞信号,pid:5994
0000000000000000000000000000000
0000000000000000000000000000000
0000000000000000000000000000000
0000000000000000000000000000000
0000000000000000000000000000000
0000000000000000000000000000000
^C0100000000000000000000000000000  //使用CTRL+C:发送2号信号
0100000000000000000000000000000
0100000000000000000000000000000
0100000000000000000000000000000
0100000000000000000000000000000
解除对于2号信号的阻塞 //此处文字顺序与编写代码时cout打印顺序有关,不影响验证
捕获信号:2 , pid:5994
0000000000000000000000000000000
0000000000000000000000000000000
0000000000000000000000000000000
0000000000000000000000000000000
0000000000000000000000000000000
0000000000000000000000000000000
^\Quit

  
   细节理解:
  1、需要注意,虽然我们阻塞了2号信号,但若不对进程发送2号信号,则pending信号集中2号信号的标志位不会由0变为1。
  2、上述有对block阻塞信号集修改的函数(sigprocmask),也有对handler处理方法表修改的函数(signal),但似乎没有对pending未决信号集修改的相关函数?实际上,发送信号给进程的过程,就是对pending做修改,因此并不需要设置相关接口来直接设置pending。
  
  
  
  
  3)、使用演示三:阻塞所有信号,是否能够生成一个不会被异常或者用户杀掉的进程?(基于演示二的延伸)

   演示代码:在演示二中,我们只是阻塞了2号信号,这里我们尝试对1~31号信号均进行阻塞。

void Showpending(sigset_t pending)
{
    for(int sig = 1; sig <= 31; ++sig)
    {
        if(sigismember(&pending, sig))
            cout << "1" ;
        else cout << "0";
    }
    cout << endl;
}

void Blocksig(int signum)
{
    sigset_t obset;
    sigemptyset(&obset);
    sigaddset(&obset,signum);

    int n = sigprocmask(SIG_BLOCK, &obset, nullptr);
    assert(n == 0);
    (void)n;
}

int main()
{
    //依次屏蔽所有信号
    for(int signum = 1; signum <= 31; ++signum)
    {
        Blocksig(signum);
    }
    //重复打印当前进程的pending信号集
    sigset_t pending;
    cout << "当前进程正在运行,pid:" << getpid() << endl;
    while(true)
    {
        sigpending(&pending);
        Showpending(pending);
        sleep(1);
    }

    return 0;
}

  观察脚本:bash sentsignal.sh

#!/bin/bash

i=1
id=$(pidof signal.out)
while [ $i -le 31 ]
do
    if [ $i -eq 9 ];then
        let i++
        continue
    fi
    if [ $i -eq 19 ];then
        let i++
        continue
    fi
    kill -$i $id
    echo "kill -$i $id"
    let i++
    sleep 1
done

  
  
  演示结果如下:同演示一,总有信号能无法被阻塞,9号信号、19、20。
在这里插入图片描述
  
  
  
  
  
  
  
  

2.3、信号处理

2.3.1、原理说明

  问题引入:在上述内容介绍中,我们一直强调信号被接受后,进程会在合适的时候处理信号,那么,什么时候合适?如何处理(具体流程)?以下将进行简要说明。
  

2.3.1.1、什么时候处理信号?

  1)、简述
  信号相关的数据字段保存在进程的进程控制块(PCB)中,其属于内核范畴。只有处于内核状态,才能处理内核范畴内的数据。
  通常,进程在正常运行时处于用户态,它们无法直接访问内核态的数据结构,包括进程控制块中的信号信息。只有当进程因系统调用、缺陷、异常等缘故,才会从用户态进入内核态进行相关操作。而 当进程从内核状态返回用户态时,就会对当前进程接受到的信号进行检测并处理
  
  
  
  
  2)、用户态VS内核态
  用户态:一个受诸多管控的状态。
  内核态:操作系统执行自己的代码的一个状态,拥有非常高的优先级。
  

  用户态(User Mode)
  权限受限: 用户态是应用程序运行时的状态。在这种状态下,应用程序的代码只能访问其自己的内存空间,并且不能执行特权指令(如修改硬件设置、访问其他进程的内存等)。
  安全性: 由于权限受限,用户态运行的应用程序即使发生错误,也不会对整个系统造成灾难性的影响。这有助于保护系统的稳定性和安全性。
  系统调用: 当用户态的程序需要执行特权操作时,它必须通过系统调用(System Call)接口请求操作系统内核的帮助。系统调用是一种软件中断,它会将控制权从用户态转移到内核态。
  
  
  内核态(Kernel Mode)
  高优先级: 内核态是操作系统内核代码运行时的状态。在这种状态下,代码可以执行特权指令,访问所有内存地址,以及管理硬件设备等。
  系统资源管理: 内核态负责管理系统资源,包括CPU调度、内存管理、文件系统、网络堆栈等。它是操作系统与硬件之间的桥梁。
  稳定性与安全性: 虽然内核态拥有更高的权限,但这也意味着它必须非常小心地处理错误,因为任何错误都可能导致系统崩溃或安全问题。因此,内核代码通常经过严格的测试和审查。

  
  用户态与内核态之间的切换
  系统调用: 如前所述,当用户态的程序需要执行特权操作时,它会通过系统调用请求内核的帮助。这会导致从用户态切换到内核态。
  中断和异常: 硬件中断(如I/O操作完成、定时器中断等)和软件异常(如除零错误、无效的内存访问等)也会导致从用户态切换到内核态。操作系统内核会处理这些中断和异常,并在必要时返回用户态。

在这里插入图片描述
  根据上图可知,内核也是在所有进程的地址空间上下文中跑的。根据处于用户态还是内核态来判断是否有权限执行OS相关代码。
  CPU中存在两套寄存器,一套供给用户使用,另一套CPU自用。其中,CR3寄存器有相应比特位可表示当前CPU的执行权限(内核/用户),而在调用系统接口时,这些接口内置了int 80指令,能够切换状态。
  
  
  
  
  
  

2.3.1.2、如何处理信号?

在这里插入图片描述

  
  
  
  
  

2.3.2、信号捕捉操作

  如果信号的处理动作是用户自定义函数,在信号递达时就调用这个函数,这称为捕捉信号。

2.3.2.1、signal

  在信号产生的小节中,我们已经学习了解过该函数,此处不做过多演示。
  
  
  

2.3.2.2、sigaction

  1)、基本介绍

  man sigaction:可以读取和修改与指定信号相关联的处理动作。

NAME
       sigaction - examine and change a signal action

SYNOPSIS
       #include <signal.h>

       int sigaction(int signum, const struct sigaction *act,
                     struct sigaction *oldact);

DESCRIPTION
       The  sigaction()  system call is used to change the action taken by a process
       on receipt of a specific signal.  (See signal(7) for an overview of signals.)

       signum specifies the signal and can be any valid signal  except  SIGKILL  and
       SIGSTOP. //SIGKILL(9)、SIGSTOP(19),此二者不被捕获

       If  act  is non-NULL, the new action for signal signum is installed from act.
       If oldact is non-NULL, the previous action is saved in oldact.//这里的参数用法和sigprocmask中set\oldset同

       The sigaction structure is defined as something like://struct sigaction 实际是OS提供的类型,该结构体类型如下述:

           struct sigaction {
               void     (*sa_handler)(int); //处理方法:1、SIG_IGN忽略信号;2、SIG_DFL默认动作;3、自定义捕获
               void     (*sa_sigaction)(int, siginfo_t *, void *); //用于实时信号,这里不涉及到
               sigset_t   sa_mask; //可添加需要屏蔽的信号
               int        sa_flags; //段包含一些选项,若不需要则设为0
               void     (*sa_restorer)(void); //暂时不考虑
           };

RETURN VALUE //调用成功则返回0,出错则返回- 1。
       sigaction() returns 0 on success; on error, -1 is returned, and errno  is  set  to
       indicate the error.

  
   sigset_t sa_mask
  ①当某个信号正在被处理时,内核自动将当前信号加入进程的信号屏蔽字,直到处理完成,自动恢复原来的信号屏蔽字。这样就保证了在处理某个信号时,相同信号再次产生,那么它会被阻塞到当前处理结束为止。
  ②如果在调用信号处理函数时,除了当前信号被自动屏蔽之外,还希望自动屏蔽另外一些信号,则用sa_mask字段说明这些需要额外屏蔽的信号,当信号处理函数返回时,自动恢复原来的信号屏蔽字。

       sa_mask  specifies  a  mask of signals which should be blocked (i.e., added to the
       signal mask of the thread in which the signal handler is invoked) during execution
       of  the  signal handler.  In addition, the signal which triggered the handler will
       be blocked, unless the SA_NODEFER flag is used.

  
  
  
  
  2)、使用演示一
  基础演示:举例sigaction的用法。

void handler(int signum)
{
    cout << "捕获到一个信号:" << signum << endl;
}

int main()
{   
    cout << "getpid: " << getpid() << endl;
    //1、用户在栈上定义一个内核数据类型的对象
    struct sigaction act, oact;
    sigemptyset(&(act.sa_mask));
    act.sa_flags = 0;
    act.sa_handler = handler;//函数指针

    //2、将用户定义的对象设置进当前调用的进程PCB中
    sigaction(2, &act, &oact);

    //3、为方便观察,设置死循环
    while(true) sleep(1);

    return 0;
}

  演示结果如下:

[wj@VM-4-3-centos signal]$ ls
makefile  sentsignal.sh  signal.cc  signal.out
[wj@VM-4-3-centos signal]$ ./signal.out 
getpid: 8501
^C捕获到一个信号:2
^C捕获到一个信号:2
^C捕获到一个信号:2
^C捕获到一个信号:2
^C捕获到一个信号:2
^C捕获到一个信号:2
^C捕获到一个信号:2
^\Quit
[wj@VM-4-3-centos signal]$ 

  
  
  
  
  
  3)、使用演示二:解释为什么要有block
  (即上述介绍sigset_t sa_mask时所说的内容)
  问题:处理信号的时候,执行自定义动作,若在处理信号期间,又收到相同信号,OS如何处理?
  回答:OS在处理信号时,只能处理一个信号,再发相同信号或者其它被载入sa_mask的信号,都会被屏蔽(block)。
  
  对上述代码做一些处理:

void Showpending(sigset_t & pending)
{
    for(int i = 1; i <= 31; ++i)
    {
        if(sigismember(&pending, i)) 
            cout << 1;
        else cout << 0;
    }
    cout << endl;
}


void handler(int signum)
{
    cout << "捕获到一个信号:" << signum << endl;
    sigset_t pending;
    int count = 30;
    while(count--)
    {
        sigpending(&pending);
        Showpending(pending);
        sleep(1);
    }
}

int main()
{   
    cout << "getpid: " << getpid() << endl;
    //1、用户在栈上定义一个内核数据类型的对象
    struct sigaction act, oact;
    sigemptyset(&(act.sa_mask));
    act.sa_flags = 0;
    act.sa_handler = handler;//函数指针

    sigaddset(&(act.sa_mask), 1);//把1、3信号也同样屏蔽掉
    sigaddset(&(act.sa_mask), 3);


    //2、将用户定义的对象设置进当前调用的进程PCB中
    sigaction(2, &act, &oact);

    //3、为方便观察,设置死循环
    while(true) sleep(1);

    return 0;
}

在这里插入图片描述

  
  
  
  
  
  

3、其它

3.1、可重入函数

  说明:当一个函数被不同的控制流程调用,有可能在第一次调用还没返回时就再次进入该函数,这种现象称为重入。若重入引起错乱,则称这样的函数为不可重入函数,反之称为可重入(Reentrant) 函数
  
  
  以下是判断函数是否可重入的条件之一:
  调用了malloc或free:因为malloc也是用全局链表来管理堆的。
  调用了标准I/O库函数:标准I/O库的很多实现都以不可重入的方式使用全局数据结构。
  
  扩展:可重入函数
  
  
  
  

3.2、volatile关键字

  volatile 作用:保持内存的可见性。volatile 是 C 和 C++ 语言中的一个关键字,它告诉编译器,被该关键字修饰的变量不允许被优化访问(特别是当它涉及到内存或寄存器访问时),对该变量的任何操作都必须在真实的内存中进行操作。

  演示如下:
在这里插入图片描述
  原因分析: 出现上述情况,是因为没有 volatile 关键字,编译器可能会优化 while (!flag) 循环:由于while (!flag) 循环中并未使用修改flag,那么优化处理会在编译阶段就把flag存放入CPU寄存器当中。这导致了之后每次 while 循环检查的flag并不是物理内存中最新的flag,于是就产生了数据二异性的问题。
  
  解决方法: volatile int flag = 0; 通过将 flag 声明为 volatile,我们确保了每次循环都会从内存中读取 flag 的当前值。
  
  
  演示使用到的相关代码:

int flag = 0;//全局变量

void handler(int sig)
{
    (void)sig;
    cout << "flag: " << flag << " →" ;
    flag = 1;
    cout << flag << endl;
}

int main()
{
    signal(2,handler);
    cout << "进程运行,捕获信号成功。pid: " << getpid() << endl;
    while(!flag);
    cout << "进程正常退出,pid:" << getpid() << " , flag: " << flag << endl;
    return 0;
}
signal.out:signal.cc
	g++ -o $@ $^ -std=c++11  -O3

.PHONY:clean
clean:
	rm -rf *.out

  
  
  
  
  
  
  

3.3、SIGCHLD信号

3.3.1、介绍

在多数Unix和类Unix操作系统(例如Linux)中,当一个进程终止(无论是正常结束还是异常终止)时,它会向其父进程发送一个SIGCHLD信号。这个信号是子进程状态改变通知的一种机制,让父进程得知其子进程已经结束或已停止(通过其他信号如SIGSTOP、SIGTSTP等被停止)。
  

  SIGCHLD信号通常用于以下目的:
  清理子进程: 父进程可以捕获SIGCHLD信号,并在信号处理函数中调用wait()或waitpid()等函数来清理(即回收)已终止的子进程的进程控制块(PCB)和资源。如果不这么做,子进程将变成僵尸进程(zombie process),占用系统资源。
  获取子进程状态: 通过wait()或waitpid()函数,父进程可以获得子进程的终止状态码和退出信号(如果有的话),这对于调试和错误处理非常有用。
  处理子进程停止: 虽然SIGCHLD信号主要用于通知子进程终止,但如果子进程是通过停止信号被停止的,父进程同样可以通过捕获SIGCHLD信号并使用waitpid()函数与WUNTRACED选项来获取子进程的停止状态。

  需要注意的是,SIGCHLD信号的默认行为是忽略的,这意味着如果父进程没有显式地捕获或阻塞该信号,它将不会收到任何通知。
  
  
  
  

3.3.2、相关演示

  1)、验证子进程退出,父进程能接收到信号SIGCHLD

在这里插入图片描述

void handler(int signum)
{
    cout << "捕获到一个信号:" << signum << " ,father:"<< getpid() <<endl;
}

int main()
{
    signal(SIGCHLD ,handler);
    if( fork() == 0)//子进程
    {
        cout << "child pid: " << getpid() << endl;
        sleep(1);//休眠1s就退出
        exit(1);
    }
    while(true)//父进程
    {
        sleep(1);
    }
    return 0;
}

  
  
  
  
  2)、演示:父进程自定义SIGCHLD信号的处理函数,在其中调用wait获得子进程的退出状态并打印
  

void handler(int sig)
{
    pid_t id;
    while ((id = waitpid(-1, NULL, WNOHANG)) > 0)//等待任意一个退出的子进程
    {
        printf("wait child success: %d\n", id);
    }
    printf("child is quit! %d\n", getpid());
}

int main()
{
    signal(SIGCHLD, handler);
    pid_t cid;
    if ((cid = fork()) == 0)
    { // child
        printf("child : %d\n", getpid());
        sleep(3);
        exit(1);
    }
    while (1)
    {
        printf("father proc is doing some thing!\n");
        sleep(1);
    }
    return 0;
}

  
  
  
  
  3)、演示用户层对SIGCHLD处理动作设置为忽略
  说明:父进程捕获SIGCHLD信号,将其处理动作置为SIG_IGN。这样fork出来的子进程在终止时会自动清理掉,不会产生僵尸进程,也不会通知父进程。

  具体来说,当父进程忽略SIGCHLD信号时,系统不会向父进程发送该信号来通知子进程的终止。同时,由于父进程没有显式地调用wait()或waitpid()等函数来等待子进程结束,系统会在子进程终止时自动执行一种类似于“隐式等待”的操作,即自动回收子进程的资源。

在这里插入图片描述

int main()
{
    signal(SIGCHLD, SIG_IGN);

    pid_t cid;
    if ((cid = fork()) == 0)//子进程
    { 
        printf("child : %d\n", getpid());
        sleep(3);
        exit(1);
    }

    while (true)
    {
        printf("father proc:%d,  doing another thing!\n", getpid());
        sleep(1);
    }
    return 0;
}

  
  这种行为是操作系统为了处理那些不关心其子进程终止状态的父进程而设计的。通过自动回收子进程,系统可以避免僵尸进程的产生,从而节省系统资源。
  
  PS:系统默认的忽略动作和用户捕获自定义的忽略,通常是没有区别的,但这是一个特例。此方法对于Linux可用,但不保证在其它UNIX系统上都可用。
  
  
  
  
  
  
  
  
  
  
  
  

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值