Linux 进程信号:产生信号

目录

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

1、signal()函数

2、核心转储

3、ulmit命令 

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

1、kill()函数

2、raise()函数 

3、abort()函数

三、发送信号的过程

读端关闭、写端继续写入的情况 

如何理解软件条件给进程发送信号: 

四、软件条件产生信号

1、alarm()函数

2、模拟日志功能

五、硬件异常产生信号

1、除0异常:

2、野指针或内存越界问题:


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

1、signal()函数

在Linux以及其他类Unix操作系统中,signal()函数是用于处理进程间通信(IPC)机制的一种方法,特别是用于处理异步发生的系统级事件,这些事件被称为“信号”。信号是内核向进程发送的通知,告知进程发生了某种预定义的重要事件。

函数原型:

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

功能

  • signal()函数允许进程注册一个信号处理函数(handler),当进程接收到指定的信号(signum)时,会调用该处理函数。
  • 如果handler是一个函数指针,则当信号signum发生时,系统会调用相应的处理函数。
  • handler可以是以下两种特殊值:
    • SIG_DFL: 将信号恢复为系统默认的行为,通常默认行为可能是终止进程、忽略信号等。
    • SIG_IGN: 忽略指定的信号,即不采取任何动作。

参数

  • signum: 这是待处理的信号编号,可以通过kill -l命令查看系统支持的所有信号名及其对应的数字。
  • handler: 这是指向信号处理函数的指针,该函数需要接受一个整型参数(通常是信号编号),并返回void

函数行为

  • signal()函数被调用后,后续接收到的指定信号会被按照新的handler来处理。
  • 需要注意的是,不同版本的signal()函数有不同的重置行为。在POSIX标准中,如果信号处理函数执行完毕且没有被捕获信号阻止其重置,则可能会自动恢复为默认行为。而有些旧版的实现(如System V)会保持已安装的处理函数,即使已经执行过。

例如,使用signal()注册一个处理函数的基本形式如下:

void sig_handler(int signo) {// 对信号的处理逻辑
}int main() {// 注册SIGINT(Ctrl+C)信号的处理函数signal(SIGINT, sig_handler);// 主程序逻辑...return 0;
}

 示例:

[hbr@VM-16-9-centos signal]$ kill -l1) SIGHUP       2) SIGINT       3) SIGQUIT      4) SIGILL       5) SIGTRAP6) SIGABRT      7) SIGBUS       8) SIGFPE       9) SIGKILL     10) SIGUSR1
11) SIGSEGV     12) SIGUSR2     13) SIGPIPE     14) SIGALRM     15) SIGTERM
16) SIGSTKFLT   17) SIGCHLD     18) SIGCONT     19) SIGSTOP     20) SIGTSTP
21) SIGTTIN     22) SIGTTOU     23) SIGURG      24) SIGXCPU     25) SIGXFSZ
26) SIGVTALRM   27) SIGPROF     28) SIGWINCH    29) SIGIO       30) SIGPWR
31) SIGSYS      34) SIGRTMIN    35) SIGRTMIN+1  36) SIGRTMIN+2  37) SIGRTMIN+3
38) SIGRTMIN+4  39) SIGRTMIN+5  40) SIGRTMIN+6  41) SIGRTMIN+7  42) SIGRTMIN+8
43) SIGRTMIN+9  44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9  56) SIGRTMAX-8  57) SIGRTMAX-7
58) SIGRTMAX-6  59) SIGRTMAX-5  60) SIGRTMAX-4  61) SIGRTMAX-3  62) SIGRTMAX-2
63) SIGRTMAX-1  64) SIGRTMAX

 运行下面这段代码时,程序会进入一个无限循环,在循环中持续输出当前进程的ID。程序默认的行为是在接收到中断信号(SIGINT)时结束运行,而SIGINT信号通常由用户按下Ctrl+C组合键触发。

#include <iostream>
#include <signal.h>
#include <unistd.h>
using namespace std;void catchSig(int signum)
{cout<<"进程捕捉到了一个信号,正在处理中:"<<signum<<"Pid:"<<getpid()<<endl;
}int main()
{signal(SIGINT,catchSig);while(true){cout<<"我是一个进程,我正在运行……, Pid:"<<getpid()<<endl;sleep(1);}return 0;
}
  • signal函数在这里的作用是重新设定程序对于SIGINT信号的处理方式。默认情况下,接收到SIGINT信号会导致进程终止。然而,在代码中通过调用signal(SIGINT, catchSig),您将SIGINT信号的处理函数替换为了自定义的catchSig函数。这样一来,每当程序接收到SIGINT信号时,不会立即退出,而是调用catchSig函数来处理这个信号。
  • catchSig函数会在每次接收到SIGINT信号时执行,并打印出相应的信息。但由于其并未阻止或重置信号,所以在处理完信号后,程序会继续执行main函数中的循环部分,也就是说,即便已经捕获并处理了SIGINT信号,程序还会继续等待下一次信号的到来。

输出kill -SIGINT 16913(进程PID)与ctrl+c的行为一致,可以证明,SIGINT确实是2号信号

如果后续没有任何SIGINT信号产生,catchSig会不会被调用?

答案是不会。只有在接收到相应的信号时,catchSig函数才会被调用。如果没有外部来源(如用户按Ctrl+C)触发SIGINT信号,catchSig函数就不会被执行。

在终端中,可以通过按下组合键Ctrl+\(3号信号)来向当前前台进程发送SIGQUIT信号来终止我们的程序。

2、核心转储

核心转储(Core Dump)是在Linux及类似操作系统中,当一个进程由于异常终止(如 segmentation fault、bus error 或接收到特定信号如SIGABRT、SIGSEGV等)时,操作系统将该进程当时内存中的内容复制到磁盘上的一个文件的过程。这个创建出来的文件通常命名为“core”,或者带有进程ID的附加信息,如“core.pid”。

核心转储文件包含了进程在崩溃瞬间的完整内存映像,包括但不限于以下内容:

  1. 进程的内存布局:包括栈、堆、全局变量、静态存储区的内容,以及其他所有已分配的内存区域。
  2. 程序计数器(PC)和寄存器状态:这些信息能反映程序崩溃时刻的执行点和CPU状态。
  3. 虚拟内存映射表:记录了进程虚拟地址空间到物理内存的映射关系。
  4. 线程上下文:对于多线程程序,还包括所有活动线程的上下文信息。
  5. 共享对象信息:如果程序是动态链接的,还会包含相关共享库的信息。

通过分析核心转储文件,开发人员可以使用调试工具(如GDB)结合程序的可执行文件和相关的符号表,重现程序崩溃时的状态,从而定位和修复导致程序崩溃的错误。这对于排查复杂的软件问题尤其重要,因为它能够捕捉到实际运行时的数据和状态,而不仅仅是源代码层面的信息。为了能够正确生成和利用核心转储文件,系统必须设置适当的权限和资源限制,比如通过ulimit命令调整core文件大小上限,并确保/proc/sys/kernel/core_pattern配置允许生成core文件。

这些信号是计算机程序中用来处理特定事件或异常情况的标准机制。

在这张表格中,"Action" 列描述了每个信号所引发的动作。不同的信号可能会导致不同的行为,具体取决于系统配置以及应用程序如何处理这些信号。以下是表中列出的一些常见动作:

  • Term:终止进程。这意味着接收到此信号后,进程将会被终止。默认情况下,进程不会保存任何数据或清理资源。
  • Core:除了终止进程外,还会创建一个核心转储文件。这个文件包含了进程在崩溃时刻的内存映像,可以帮助调试器分析问题所在。
  • Ign:忽略信号。如果一个进程选择忽略某个信号,则当其接收到该信号时,不会有任何反应。
  • Cont:继续执行。当进程被暂停时,它可以接收到一个信号来恢复运行 

3、ulmit命令 

ulimit -a

当你在终端中执行 ulimit -a 时,它会输出当前 shell 环境下所有可用资源的限制情况。

  • 其中,涉及到核心转储的部分,你会看到一行类似于 core file size (blocks, -c) 的输出项,后面跟着一个数值。这个数值代表了当前进程允许生成的最大核心转储文件大小,单位通常是块(在某些系统中可能是字节)。
  • 如果你设置了限制,这里就会显示具体的限制值;如果没有设置或设置为无限制,则可能会显示 unlimited 或一个较大的数值。
[hbr@VM-16-9-centos signal]$ ulimit -a
core file size          (blocks, -c) 0
//……
[hbr@VM-16-9-centos signal]$

ulimit -c

ulimit -c 专用于核心转储文件大小的控制。你可以用它来查看当前的核心转储文件大小限制,例如:

[hbr@VM-16-9-centos signal]$ ulimit -c 10240
[hbr@VM-16-9-centos signal]$ ulimit -a
core file size          (blocks, -c) 10240
//……
[hbr@VM-16-9-centos signal]$
  • 只执行 ulimit -c 就会显示当前的限制值。
  • 若要设置新的限制,可以执行 ulimit -c <size>,这里的 <size> 是你希望设定的最大核心转储文件大小,单位通常是内存块或者字节(具体依赖于系统)。
  • 若要关闭核心转储功能,也就是不让程序在崩溃时生成核心文件,可以执行 ulimit -c 0
  • 若要允许无限大小的核心转储文件(受限于实际物理存储空间),则执行 ulimit -c unlimited
#include <iostream>
#include <signal.h>
#include <unistd.h>
#include <sys/wait.h>
using namespace std;int main()
{pid_t id = fork();if (id == 0){sleep(1);int a = 100;a /= 0;exit(0);}int status = 0;waitpid(id, &status, 0);cout << "父进程:" << getpid() << " 子进程:" << id << \ " exit sig: "<< (status & 0x7F) << " is core: " << ((status >> 7) & 1) << endl;return 0;
}
  1. fork()函数:程序首先调用fork()创建一个子进程,当id == 0时,说明当前代码运行在新创建的子进程中。子进程睡眠一秒后尝试执行int a = 100; a /= 0;,这会导致除以零异常并终止子进程。

  2. waitpid()函数:主进程中,父进程调用waitpid(id, &status, 0);等待子进程结束,并获取其退出状态。status变量包含了子进程退出时的各种信息,包括退出信号和是否生成了核心转储文件等。

  3. 输出信息:父进程打印出自己的PID以及子进程的PID,然后通过位操作解析status变量,输出子进程的退出信号(这里是SIGFPE,即算术运算错误,通常由除以零引起,对应的十进制数是8)以及是否生成了核心转储文件(is core: 1意味着确实生成了核心文件)。

  4. ulimit命令:开始时,通过ulimit -a可以看到系统的默认配置中,核心转储文件大小被设置为0,这意味着禁用了核心转储功能。接着通过ulimit -c 10240将其更改为10240个内存块(通常每个块512字节或取决于系统定义),这样就允许最多生成约5MB的核心转储文件。

  5. 运行程序后,由于子进程触发了段错误并且系统配置允许生成核心转储,因此产生了名为core.26825的核心转储文件,该文件的大小大约为557056字节。这个文件包含了子进程崩溃时刻的内存映像,可用于后续的故障排查与调试。

    [hbr@VM-16-9-centos signal]$ ./mysignal 
    父进程:26824 子进程:26825 exit sig: 8 is core: 1
    [hbr@VM-16-9-centos signal]$ ll
    total 252
    -rw------- 1 hbr hbr 557056 Mar 25 20:32 core.26825
    -rw-rw-r-- 1 hbr hbr     68 Mar 25 12:38 makefile
    -rwxrwxr-x 1 hbr hbr   9312 Mar 25 20:31 mysignal
    -rw-rw-r-- 1 hbr hbr    810 Mar 25 20:31 signal.cc

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

在Unix/Linux系统编程中,kill()raise()函数都是用来向进程发送信号的重要接口。

1、kill()函数

 kill()函数允许一个进程向另一个进程发送信号。其基本原型为:

#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);
  • pid:表示目标进程的ID,如果为-1,则向同一进程组的所有进程发送信号。
  • sig:表示要发送的信号,可以是SIGTERM(终止)、SIGKILL(强制终止)等各种预定义信号或其他用户自定义信号。

例如,如果我们想结束进程ID为1234的进程,可以调用kill(1234, SIGTERM);来发送一个终止信号。

#include <iostream>
#include <cstdlib>
#include <cstring>
#include <sys/types.h>
#include <signal.h>
using namespace std;void Usage(const char* proc)
{cout << "Usage:\r\n\t" << proc << " signumber processid" << endl;
}int main(int argc, char* argv[])
{if (argc != 3){Usage(argv[0]);exit(EXIT_FAILURE);}int signumber = atoi(argv[1]);int procid = atoi(argv[2]);kill(procid, signumber);return 0;
}
//窗口二执行sleep:
[hbr@VM-16-9-centos signal]$ sleep 10000//窗口一:
[hbr@VM-16-9-centos signal]$ ps axj | head -1 && ps axj | grep sleepPPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
21405  4199 21405 21405 ?           -1 S     1003   0:00 sleep 180
20975  4926  4926 20975 pts/5     4926 S+    1003   0:00 sleep 10000
20212  5087  5086 20212 pts/4     5086 S+    1003   0:00 grep --color=auto sleep
25552 25681 25552  1522 ?           -1 Sl       0   3:30 /bin/sh -c sleep 100[hbr@VM-16-9-centos signal]$ ./mysignal 9 4926//窗口二
[hbr@VM-16-9-centos signal]$ sleep 10000
Killed
  1. 首先,在窗口二中执行了sleep 10000命令,这是一个持续休眠10000秒的进程,PID为4926。
  2. 接着,在窗口一中进行了以下操作:
    1. 使用ps axj命令查看所有进程的相关信息,并使用head -1显示表头,可以看到各列分别代表什么含义。
    2. 使用grep sleep查找所有名为sleeep的进程,找到了PID为4926的进程正在执行sleep 10000命令。
  3. 然后,窗口一中执行了编译好的mysignal程序,并传入了信号9(SIGKILL,用于强制终止进程)和目标进程PID(即4926)。
  4. 最后,在窗口二中可以看到,由于接收到SIGKILL信号,原本正在休眠的sleep 10000进程被立即强制终止,并打印出“Killed”信息。

2、raise()函数 

raise()函数则是让进程发送信号给自己,其基本原型为:

#include <signal.h>
int raise(int sig);
  • sig:同样是要发送的信号,与kill()函数中的含义相同。

例如,当一个进程需要响应某种情况而自我终止时,可以调用raise(SIGTERM);或者raise(SIGABRT);(触发异常终止)等。

#include <iostream>
#include <string>
#include <unistd.h>
#include <signal.h>
using namespace std;
using namespace std;int main(int argc, char *argv[])
{cout << "begin" << endl;sleep(1);raise(8);return 0;
}
[hbr@VM-16-9-centos signal]$ ./mysignal 
begin
Floating point exception (core dumped)
  1. 输出"begin",表示程序已开始运行。

  2. sleep(1)函数让程序暂停1秒,模拟一些延迟或等待。

  3. raise(8)调用发送信号给当前进程。数字8代表SIGFPE信号,即“浮点异常”信号,通常会在程序执行非法浮点运算(如除以零)时产生。

3、abort()函数

abort() 函数在编程中,尤其是在C/C++等语言中,是一个标准库函数,它用于强制终止(异常结束)当前进程。当调用该函数时,会产生一个SIGABRT信号发送给当前进程。这个信号通常会导致进程立即停止执行,并返回一个非零值给操作系统,指示程序异常终止。

具体来说,abort()函数执行以下操作:

  1. 异常终止程序:调用abort函数后,进程会立刻停止执行,不会进行任何清理工作,如释放内存、关闭文件等。

  2. 生成core dump(如果系统配置允许):在一些操作系统中,进程在接收到SIGABRT信号并终止时,可能会生成一个core dump文件,这个文件包含了进程在崩溃时刻的内存映像,对于后续调试非常有用。

  3. 返回状态码:abort函数使得进程以非正常方式退出,其退出状态码通常为1,表示程序异常终止。

#include <iostream>
#include <string>
#include <unistd.h>
#include <signal.h>
using namespace std;using namespace std;static void Usage(string proc)
{cout << "Usage:\r\n\t" << proc << "signumber processid" << endl;
}int main(int argc, char *argv[])
{cout << "begin" << endl;abort();return 0;
}
[hbr@VM-16-9-centos signal]$ ./mysignal 
begin
Aborted (core dumped)

当你运行这个程序时,控制台输出了"begin",然后由于abort()函数的调用,程序被异常终止,所以紧接着显示"Aborted (core dumped)",表明程序已经因为接收到abort信号而终止,并且可能产生了core dump文件(取决于系统的core dump设置)。

三、发送信号的过程

系统调用接口是操作系统为用户态进程提供的一种机制,使得用户程序能够请求操作系统内核服务。当用户程序需要执行一些特权操作(如读写文件、创建进程、发送信号等),而这些操作在用户态下无法直接完成时,就需要通过系统调用来请求内核的帮助。

  1. 用户调用系统接口: 用户程序通过编程语言(如C/C++)提供的库函数(如kill()函数)发起一个系统调用。在底层,这个函数会生成一个特定的中断或异常,使CPU从用户态切换到内核态。

  2. 执行OS对应的系统调用代码: 当CPU进入内核态后,开始执行操作系统内核中与该系统调用对应的处理代码。对于发送信号的操作,内核会识别出这是一个发送信号的系统调用,并继续进行处理。

  3. OS提取参数/设置特定数值: 内核从寄存器或栈上获取用户程序传递过来的参数,比如要发送的信号编号(signumber)和目标进程ID(procid)。

  4. OS向目标进程写信号: 操作系统根据获取的进程ID找到目标进程,并在其内部结构体(如进程控制块PCB)中设置相应的信号信息,将指定的信号挂起或立即发送给目标进程。

  5. 修改对应进程的信号标记位: 对于待处理的信号,操作系统会在进程的信号集里置位,表示有新的信号到达。如果进程正在执行,但设置了阻塞该信号,则信号会被保存起来稍后处理;否则,进程会立刻响应这个信号。

  6. 进程后续处理信号: 进程在适当的时候(例如从系统调用返回到用户态,或执行到sigreturn指令时)检查并处理信号。处理方式取决于进程对信号的设置,可能是忽略信号、捕获并执行自定义处理函数,或者是默认行为(如终止进程)。 

读端关闭、写端继续写入的情况 

在Unix/Linux系统中,当进程间通过管道(pipe)进行通信时,管道的一端负责写入数据,另一端负责读取数据。如果发生以下情况:

  • 写端持续尝试向管道中写入数据;
  • 读端不仅没有读取管道中的数据,反而关闭了其对管道的读取端口;

这时会出现特定的问题:

  1. 当读端关闭后,管道中的缓冲区如果已满并且写端还在继续写入数据,内核将会阻止写端进一步写入数据,因为没有进程在读取这些数据。

  2. 如果写端仍然尝试写入数据到已关闭的管道,操作系统会检测到这一情况,并且会向试图写入管道的进程发送一个SIGPIPE信号。

  3. SIGPIPE信号默认的行为是终止接收信号的进程(即写端进程)。这意味着进程会因收到SIGPIPE信号而异常结束,返回值通常指示发生了Broken pipe错误。

在这种情况下,操作系统通过发送SIGPIPE信号确保了资源的有效管理,防止了写端进程无意义地往无法读取的管道中写入数据,同时也避免了系统资源的浪费。如果程序需要处理这种情况,可以通过信号处理函数捕获并处理SIGPIPE信号,而不是默认地让进程退出。

如何验证?步骤如下:

  1. 创建匿名管道。
  2. 父进程fork出子进程。
  3. 子进程负责写入管道,父进程负责读取管道。
  4. 父进程关闭读端管道文件描述符,并调用waitpid等待子进程。
  5. 子进程继续尝试写入管道直至完成。
  6. 子进程因SIGPIPE信号退出,父进程通过waitpid获取子进程的退出状态,并检查其原因是否为SIGPIPE。

为了验证当读端关闭时,写端进程会接收到SIGPIPE信号并退出的情况,可以按照以下步骤编写并执行一个程序:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <signal.h>
#include <string.h>// 使用signal代替sigaction设置信号处理函数
static void handle_SIGPIPE(int sig) {printf("Child received SIGPIPE signal.\n");_exit(EXIT_FAILURE);
}int main() {// 创建匿名管道int pipefd[2];if (pipe(pipefd) == -1) {perror("pipe");exit(EXIT_FAILURE);}// 父进程fork出子进程pid_t child_pid = fork();if (child_pid == -1) {perror("fork");exit(EXIT_FAILURE);} else if (child_pid == 0) { // 子进程close(pipefd[0]); // 关闭读端// 设置SIGPIPE信号处理函数if (signal(SIGPIPE, handle_SIGPIPE) == SIG_ERR) {perror("signal");exit(EXIT_FAILURE);}// 尝试不断写入数据for (;;) {write(pipefd[1], "data", 4);}} else { // 父进程close(pipefd[1]); // 关闭写端// 关闭读端管道文件描述符close(pipefd[0]);// 等待子进程结束int status;while (waitpid(child_pid, &status, 0) != child_pid) {}// 检查子进程退出状态if (WIFEXITED(status)) {printf("Child exited normally with status %d.\n", WEXITSTATUS(status));} else if (WIFSIGNALED(status)) {if (WTERMSIG(status) == SIGPIPE) {printf("Child terminated by SIGPIPE signal.\n");} else {printf("Child terminated by signal %d: %s\n", WTERMSIG(status), strsignal(WTERMSIG(status)));}} else {printf("Child terminated abnormally.\n");}}return 0;
}
[hbr@VM-16-9-centos signal]$ ./mysignal 
Child received SIGPIPE signal.
Child exited normally with status 1.

如何理解软件条件给进程发送信号: 

如何理解软件条件给进程发送信号:a,OS先识别到某种软件条件触发或者不满足 b,Os 构建信号,发送给指定的进程

  • 在操作系统中,当特定的软件条件触发或不满足时,操作系统首先会检测并识别到这一情况。
  • 一旦这种条件发生,操作系统就会依据预定的规则和机制,构建相应的信号对象。这个信号代表着一种软件级别的中断或事件通知,它携带着关于特定条件的信息。
  • 接下来,操作系统会立即将构建好的信号精准地发送给相关联的进程。这个过程就好比是系统给进程发送了一个内部消息,告诉进程有某种重要的事情发生,需要进程对此作出响应。
  • 例如,当管道读端关闭而写端仍在尝试写入数据时,操作系统就会向写端进程发送SIGPIPE信号,促使进程采取相应的行动,通常是终止进程,以此避免无效的系统资源消耗和潜在的错误状况。

四、软件条件产生信号

1、alarm()函数

alarm() 是 POSIX 标准中定义的一个系统调用函数,位于 <unistd.h> 头文件中。这个函数允许用户在程序中设定一个定时器,指定在未来的某个时间点(以秒为单位)向当前进程发送一个 SIGALRM 信号。

调用格式如下:

#include <unistd.h>
unsigned int alarm(unsigned int seconds);
  • 参数 seconds 表示等待的秒数。当你调用 alarm(seconds) 时,内核会启动一个定时器,这个定时器将在指定的 seconds 秒后到期。
  • 到期时会发生的事情是,内核会给当前进程发送一个 SIGALRM 信号。这是个异步事件,意味着即使进程正在执行其他任务,内核也会将其打断,插入这个信号事件。
  • 默认情况下,进程对 SIGALRM 信号的处理动作是终止进程(类似于接收到 SIGTERM 信号的效果)。然而,进程可以通过调用 signal() 或者 sigaction() 函数来重新设置对 SIGALRM 信号的处理方式,比如忽略信号、捕获信号并执行自定义处理函数等。

总结一下,alarm() 函数的主要作用是在指定的时间间隔后向进程发送一个信号,从而实现定时操作或者超时检测等功能。如果不对 SIGALRM 信号进行特殊处理,进程将在信号到达时结束运行。

2、模拟日志功能

#include <iostream>
#include <string>
#include <vector>
#include <functional>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <signal.h>
#include <stdlib.h>using namespace std;// 定义一个函数类型,用于回调
typedef function<void()> CallbackFunc;
// 存储回调函数的向量
vector<CallbackFunc> callbacks;// 全局计数器
uint64_t count = 0;//该函数暂时禁用cout的同步,以提高输出速度,最后恢复同步。void showCount()
{ios_base::sync_with_stdio(false);cout << "final count : " << count << endl;ios_base::sync_with_stdio(true);
}void showLog()
{cout << "【日志功能】:触发了一次定时器事件" << endl;
}//通过fork一个子进程,并在子进程中执行who命令,来记录当前登录的用户。void logUser()
{pid_t childPid = fork();if (childPid == 0){execl("/usr/bin/who", "who", nullptr);_exit(1); // 使用_exit确保不会执行exit相关的清理操作,避免影响性能}else if (childPid > 0){wait(nullptr); // 父进程等待子进程执行完成}else{cerr << "Failed to fork a new process." << endl;}
}// 当接收到SIGALRM信号时,执行回调函数队列中的所有函数,并重新设置定时器。
void catchSig(int signum)
{for (auto& f : callbacks){f(); // 执行每个回调函数}alarm(1); // 重新设置1秒后的定时器
}//初始化定时器和回调函数队列,然后进行无限循环以累加count。
int main(int argc, char* argv[])
{// 设置SIGALRM信号的处理函数signal(SIGALRM, catchSig);// 启动定时器,1秒后触发SIGALRM信号alarm(1);// 向回调函数队列中添加函数callbacks.push_back(showCount);callbacks.push_back(showLog);callbacks.push_back(logUser);// 无限循环,持续累加countwhile (true){++count;}return 0;
}

这段C++程序利用Unix/Linux系统中的信号机制实现了定时执行一系列函数的功能。下面是程序执行过程的详细解释:

  1. 编译和运行: 用户在终端上编译并运行该程序,得到可执行文件mysignal,然后执行它。

  2. 初始化: 在main函数中,首先注册SIGALRM信号的处理函数catchSig,这意味着当接收到SIGALRM信号时,将调用catchSig函数。

  3. 设置定时器: 程序调用alarm(1),设置一个1秒的定时器。当定时器到期时,系统将向当前进程发送SIGALRM信号。

  4. 回调函数: 将三个函数showCountshowLoglogUser添加到callbacks向量中。这些函数将在接收到SIGALRM信号时按照顺序执行。

    • showCount函数输出当前的全局变量count值。
    • showLog函数输出提示信息,表示执行了日志功能。
    • logUser函数创建一个子进程,执行/usr/bin/who命令(显示当前登录用户列表),然后等待子进程结束。
  5. 主循环: 程序进入一个无限循环,不断地递增全局变量count的值。

  6. 信号处理: 当定时器到期时,系统发送SIGALRM信号给当前进程,从而调用catchSig函数。在catchSig函数中,依次执行callbacks向量中的所有函数,然后再重新设置一个1秒的定时器。

  7. 输出: 因此,您在终端看到的是每隔一秒左右,showCount输出count的值,然后是showLog输出的消息。logUser执行的who命令结果没有在输出中直接显示,但该命令确实在后台执行了。

    [hbr@VM-16-9-centos signal]$ ./mysignal 
    final count : 560421312
    这个是日志功能
    final count : 1124579773
    这个是日志功能
    final count : 1687847541
    这个是日志功能
    final count : 2251294324
    这个是日志功能
    final count : 2809612263
    这个是日志功能
    ^C
  8. 手动中断: 用户通过按下Ctrl+C向程序发送SIGINT信号,程序因此被中断,并打印出最后一个final count的值和“这个是日志功能”的提示,然后退出。

综合起来,这个程序展示了如何使用Unix/Linux系统信号实现定时任务调度,并通过回调函数执行不同的操作。然而,由于count的递增与定时器触发的回调函数执行是异步进行的,所以showCount输出的并不是定时器触发时刻的count增量,而是每次回调执行时的累计值。

五、硬件异常产生信号

硬件异常被硬件以某种方式被硬件检测到并通知内核,然后内核向当前进程发送适当的信号。

  • 例如当前进程执行了除以0的指令,CPU的运算单元会产生异常,内核将这个异常解释为SIGFPE信号发送给进程。
  • 再比如当前进程访问了非法内存地址,,MMU会产生异常,内核将这个异常解释为SIGSEGV信号发送给进程。

1、除0异常

#include "signal.h"
#include <iostream>
#include <signal.h>
#include <unistd.h>void catchSig(int sig)
{std::cout << "catch signal " << sig << std::endl;
}
int main()
{for (int i = 1; i <= 31; i++)signal(i, catchSig);while (1)sleep(1);
}[hbr@VM-16-9-centos signal]$ ./mysignal 
catch signal 1                           [hbr@VM-16-9-centos signal]$ kill -1 29748                        
catch signal 2                           [hbr@VM-16-9-centos signal]$ kill -2 29748
Killed                                   [hbr@VM-16-9-centos signal]$ kill -9 29748
[hbr@VM-16-9-centos signal]$             [hbr@VM-16-9-centos signal]$ 
  1. 硬件层面:在进行除法运算时,是由CPU这一硬件组件负责执行计算任务。当CPU检测到除数为零的异常情况时,其内部的硬件机制会立即响应并标识这一错误状态。

  2. 状态寄存器与操作系统:CPU拥有状态寄存器,其中包含了一系列状态标志位,用于反映最近一次算术或逻辑运算的结果状态。对于除以零这样的异常,CPU会设置相应的标志位(例如溢出标志或其他错误标志)。操作系统(OS)通过周期性的检查或中断机制,能够识别到这些异常状态,并据此采取行动。

  3. 信号处理与进程响应:当操作系统检测到除零异常时,它通常会生成一个特定的信号(例如SIGFPE),并将该信号发送给引发异常的进程。默认情况下,进程接收到SIGFPE信号后会终止执行,但开发人员可以通过信号处理函数自定义进程对该信号的响应。尽管如此,由于除零错误属于严重的计算错误,通常情况下,进程难以从这种错误中恢复并继续有意义的执行。

  4. 死循环的可能性:如果不正确地处理除零异常,或者系统未能有效处理这一信号,异常状态可能保留在寄存器中而得不到清除。在这种情况下,如果程序逻辑不当,异常可能导致进程陷入死循环,反复尝试进行无效或错误的计算,无法恢复正常执行流程。

综上所述,除以零是计算机系统中的一种硬件异常,操作系统通过监控硬件状态并发送适当的信号来应对这种异常,而进程通常会因该异常而终止,但也有可能在特殊情况下因错误处理而导致持续的死循环问题。正确的异常处理和信号响应对于确保程序稳定性和健壮性至关重要。

2、野指针或内存越界问题

野指针示例:

void catchSig(int sig) {if (sig == SIGSEGV) {// 当捕获到SIGSEGV信号时,表示程序发生了段错误。printf("Caught segmentation fault (SIGSEGV)!\n");_exit(EXIT_FAILURE);} else {// 如果捕获到的信号不是SIGSEGV,打印错误信息到标准错误输出。fprintf(stderr, "Caught unexpected signal %d\n", sig);_exit(EXIT_FAILURE);}
}int main() {signal(SIGSEGV, catchSig);  // 注册SIGSEGV信号处理器int *ptr;// 指针未初始化,此时ptr的值是不确定的printf("%d\n", *ptr);  // 这一行可能触发段错误(SIGSEGV)return 0;
}[hbr@VM-16-9-centos signal]$ ./mysignal 
Caught segmentation fault (SIGSEGV)!

内存越界示例:

#include <stdio.h>int main() {int arr[5] = {0, 1, 2, 3, 4}; // 定义一个大小为5的整型数组int *ptr = arr;// 访问数组的有效索引是从0到4for (int i = 0; i <= 5; ++i) { // 注意这里是<=5,而不是<5printf("%d\n", ptr[i]); // 当i=5时,访问arr[5]即越界访问}return 0;
}

在这两个例子中,当试图访问通过野指针或超过数组边界的内存地址时,操作系统会通过MMU检测到这一错误,并且通常会发送一个信号(例如SIGSEGV)给进程,如果不捕获并处理这个信号,程序通常会立即终止。 

  1. 在编程过程中,访问数组或动态分配的内存区域时,如果引用了无效的内存地址(野指针)或超出合法范围(内存越界),那么同样会引发硬件级别的异常。

  2. 访问任何内存地址都需要经过地址转换,从应用程序使用的逻辑地址(即虚拟地址)转换为实际的物理内存地址。

  3. 这种地址转换是由操作系统和硬件(MMU,即内存管理单元)共同协作完成的。MMU负责维护页表,将虚拟地址映射到物理地址空间。

  4. 当尝试访问的虚拟地址无法正确映射到物理地址,比如因为地址未分配、已经释放、或者超出了分配区域的边界,这时MMU在转换过程中会检测到错误并通知操作系统。

  5. 因此,无论是由于野指针导致的非法地址引用,还是内存越界访问,都会在MMU转换过程中触发异常,进而可能导致进程接收到信号(如SIGSEGV,段错误信号),并根据预设的处理方式来决定进程是否退出、暂停或其他操作。在未妥善处理的情况下,进程也有可能因此陷入死循环或不稳定状态。

所有的信号,无论源于何种软件或硬件条件,本质上都会经过操作系统的监测和识别。一旦触发了某个信号源,操作系统便会迅速介入,对信号进行恰当的解析,并将其转发给目标进程。简而言之,无论是何种类型的信号,最终都是由操作系统统一识别并妥善送达给相应进程进行处理的。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/777372.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

伦敦金与纸黄金有什么区别?怎么选?

伦敦金与纸黄金都是与黄金相关的投资品种&#xff0c;近期黄金市场的上涨吸引了投资者的关注&#xff0c;那投资者想开户入场成为黄金投资者应该选择纸黄金还是伦敦金呢&#xff1f;两者有何区别呢&#xff1f;下面我们就来讨论一下。 伦敦金是一种起源于伦敦的标准化黄金交易合…

HarmonyOS实战开发-实现带有卡片的电影应用

介绍 本篇Codelab基于元服务卡片的能力&#xff0c;实现带有卡片的电影应用&#xff0c;介绍卡片的开发过程和生命周期实现。需要完成以下功能&#xff1a; 元服务卡片&#xff0c;用于在桌面上添加2x2或2x4规格元服务卡片。关系型数据库&#xff0c;用于创建、查询、添加、删…

FMEA的本质——FMEA软件

免费试用FMEA软件-免费版-SunFMEA FMEA&#xff0c;即故障模式与影响分析&#xff08;Failure Modes and Effects Analysis&#xff09;&#xff0c;是一种预防性的质量工具&#xff0c;广泛应用于各种行业和领域&#xff0c;特别是在制造业、航空航天、医疗设备、汽车工业等领…

AGV全电动无人堆高车选购时要注意的4点

AGV 随着机器人技术在中国的快速发展&#xff0c;国内企业开始推出区别于传统叉车的无人叉车&#xff0c;旨在为企业降本增效&#xff0c;降低人工成本与对人的依赖。同时&#xff0c;也将人工从危险恶劣的环境中解放出来。随着技术的持续提升&#xff0c;如今&#xff0c;无人…

DVB-S系统仿真学习

DVB-S系统用于卫星电视信号传输&#xff0c;发送端框图如下所示 扰码 实际数字通信中&#xff0c;载荷数据的码元会出现长连0或长连1的情况&#xff0c;不利于接收端提取时钟信号&#xff0c;同时会使得数据流中含有大量的低频分量&#xff0c;使得QPSK调制器的相位长时间不变…

【Frida】【Android】05_Objection实战

&#x1f6eb; 系列文章导航 【Frida】【Android】01_手把手教你环境搭建 https://blog.csdn.net/kinghzking/article/details/136986950【Frida】【Android】02_JAVA层HOOK https://blog.csdn.net/kinghzking/article/details/137008446【Frida】【Android】03_RPC https://bl…

线程创建方式、构造方法和线程属性

欢迎各位&#xff01;&#xff01;&#xff01;推荐PC端观看 文章重点&#xff1a;学会五种线程的创造方式 目录 1.开启线程的五种方式 2.线程的构造方法 3.线程的属性及获取方法 1.开启线程的五种方式 创造线程的基本两步&#xff1a;&#xff08;1&#xff09;使用run方法…

2024最新华为OD机试试题库全 -【二叉树计算】- C卷

1. 🌈题目详情 1.1 ⚠️题目 给出一个二叉树如下图所示: 请由该二叉树生成一个新的二叉树,它满足其树中的每个节点将包含原始树中的左子树和右子树的和。 左子树表示该节点左侧叶子节点为根节点的一颗新树;右子树表示该节点右侧叶子节点为根节点的一颗新树。 1.2 �…

智能车主控板原理图原理讲解

智能车主控板原理图原理讲解 综述&#xff1a;本篇文章对智能车主控板的一部分电路进行原理分析&#xff0c;文末附加整体原理图。 1. 电源电路 &#xff08;1&#xff09;通过外接电池供电并通过电源模块电路&#xff0c;运用稳压芯片lm2940&#xff0c;将电源电压转化为5V…

分布式处理

前言 大家好&#xff0c;我是jiantaoyab&#xff0c;这是我作为学习笔记原理篇的最后一章&#xff0c;一台计算机在数据中心里是不够的。因为如果只有一台计算机&#xff0c;我们会遇到三个核心问题。第一个核心问题&#xff0c;叫作垂直扩展和水平扩展的选择问题&#xff0c;…

【leetcode】双“指针”

标题&#xff1a;【leetcode】双指针 水墨不写bug 我认为 讲清楚为什么要用双指针 比讲怎么用双指针更重要&#xff01; &#xff08;一&#xff09;快乐数 编写一个算法来判断一个数 n 是不是快乐数。 「快乐数」 定义为&#xff1a; 对于一个正整数&#xff0c;每一次将该数…

排序C++

题目 法1 sort升序排序&#xff0c;再逆序输出 #include<iostream> #include<algorithm> using namespace std;const int N 5e53;//注意const&#xff0c;全局 int a[N]; int main() {//错误int N5e53;//错误const int a[N];int n;cin >> n;for (int i 1;…

HBase Shell基本操作

一、进入Hbase Shell客户端 先在Linux Shell命令行终端执行start-dfs.sh脚本启动HDFS&#xff0c;再执行start-hbase.sh脚本启动HBase。如果Linux系统已配置HBase环境变量&#xff0c;可直接在任意目录下执行hbase shell脚本命令&#xff0c;就可进入HBase Shell的命令行终端环…

【容器源码篇】Set容器(HashSet,LinkedHashSet,TreeSet的特点)

文章目录 ⭐容器继承关系&#x1f339;Set容器&#x1f5d2;️HashSet源码解析构造方法public HashSet()public HashSet(Collection<? extends E> c)public HashSet(int initialCapacity, float loadFactor)HashSet(int initialCapacity, float loadFactor, boolean dum…

VLAN实验记录---对抗遗忘

sw1的接口6应该调成混杂模式&#xff0c;因为pc2,4,5,6的pvid各不相同而网段相同&#xff0c;所以往上去路由时应该剥离标记&#xff08;VLAN里面是标记而不是标签&#xff09;出去&#xff0c;这样 路由器上的物理接口用来管理不带标记的流量&#xff0c;而vlan2流量的往上打上…

记录 AI绘图 Stable Diffusion的本地安装使用,可搭建画图服务端

开头 最近刷短视频看到了很多关于AI绘图&#xff0c;Midjourney&#xff0c;gittimg.ai&#xff0c;Stable Diffusion等一些绘图AI工具&#xff0c;感受到了AI绘画的魅力。通过chatGPT生成关键词再加上绘图工具&#xff0c;真是完美&#xff0c;文末教大家如何用gpt提词 Midj…

每日算法之接雨水

题目描述 给定 n 个非负整数表示每个宽度为 1 的柱子的高度图&#xff0c;计算按此排列的柱子&#xff0c;下雨之后能接多少雨水。 示例 1&#xff1a; 输入&#xff1a;height [0,1,0,2,1,0,1,3,2,1,2,1] 输出&#xff1a;6 解释&#xff1a;上面是由数组 [0,1,0,2,1,0,1…

打造核心竞争力:高效Web系统数据中台的设计与实践_光点科技

在数字化的浪潮中&#xff0c;数据已经成为企业赖以生存和发展的核心资源。一个高效的Web系统数据中台&#xff0c;能够赋予企业在激烈的市场竞争中立于不败之地的能力。本文将深入探讨如何设计和实施一个能够提升企业数据管理水平和支持业务决策的高效数据中台架构。 数据中台…

【YOLOv8训练结果评估】YOLOv8如何使用训练好的模型对验证集进行评估及评估参数详解

《博主简介》 小伙伴们好&#xff0c;我是阿旭。专注于人工智能、AIGC、python、计算机视觉相关分享研究。 ✌更多学习资源&#xff0c;可关注公-仲-hao:【阿旭算法与机器学习】&#xff0c;共同学习交流~ &#x1f44d;感谢小伙伴们点赞、关注&#xff01; 《------往期经典推…

P8623 [蓝桥杯 2015 省 B] 移动距离 Python

[蓝桥杯 2015 省 B] 移动距离 题目描述 X 星球居民小区的楼房全是一样的&#xff0c;并且按矩阵样式排列。其楼房的编号为 $1,2,3, \cdots $ 。 当排满一行时&#xff0c;从下一行相邻的楼往反方向排号。 比如&#xff1a;当小区排号宽度为 6 6 6 时&#xff0c;开始情形如…