Linux 进程信号:产生信号

news/2024/5/12 14:36:34

目录

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

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.tangninghui.cn.cn/item-12147.htm

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈,一经查实,立即删除!

相关文章

几个常用的控件(2)

目录 一、单选按钮Radiobutton和RadioButtonList 1、Radiobutton控件 &#xff08;1&#xff09;button控制方式 &#xff08;2&#xff09;Radiobutton控制方式 2、RadiobuttonList控件 二、列表框ListBox和下拉列表DropdownList 1、ListBox 2、DropdownList 三、面板…

第八章贪心算法——理论基础,分发饼干题目

目录 概念 什么时候使用 题目举例 分发饼干 力扣题号&#xff1b;455. 分发饼干 - 力扣&#xff08;LeetCode&#xff09; 题目描述 示例 1: 示例 2: 解法一&#xff1a;排序暴力 解法二&#xff1a;贪心 思路 代码实现 总结 概念 贪心算法是一种在每一步选择中都采…

什么算法可以进行小语种的OCR?

对于小语种的OCR识别&#xff0c;可以采用以下算法和技术&#xff1a; 1. 迁移学习&#xff08;Transfer Learning&#xff09;&#xff1a;使用在大语种上预训练好的OCR模型&#xff0c;并通过迁移学习的方式对小语种进行微调。这样可以利用大语种上已有的丰富数据和知识&…

HAL STM32G4 +ADC手动触发采集+各种滤波算法实现

HAL STM32G4 ADC手动触发采集各种滤波算法实现 &#x1f4cd;相关篇《HAL STM32G4 TIM1 3路PWM互补输出VOFA波形演示》 ✨本篇内容也是继欧拉电子相关无刷电机驱动控制学习的相关基础内容。仅作为个人笔记记录使用。 &#x1f4cd;感谢网友提供的相关内容《基于STM32的ADC采样及…

刚刚,百度和苹果宣布联名

百度 Apple 就在刚刚&#xff0c;财联社报道&#xff0c;百度将为苹果今年发布的 iPhone16、Mac 系统和 iOS18 提供 AI 功能。 苹果曾与阿里以及另外一家国产大模型公司进行过洽谈&#xff0c;最后确定由百度提供这项服务&#xff0c;苹果预计采取 API 接口的方式计费。 苹果将…

Facebook防封如何做?附解禁方法

Facebook作为跨境主要业务平台&#xff0c;一直以来封号率都非常高。相信点进来的各位或多或少地遇见了个人号被封&#xff0c;广告账户被禁&#xff0c;FB主页被封等情况。针对此类问题&#xff0c;今天就小编也来分享自己的Facebook防封经验。 一、Facebook被封原因 主要有以…