☰
Linux系统——进程信号之信号的产生
2026/10/24 2:19:17 网站建设 项目流程

💁‍♂️个人主页:进击的荆棘

👇作者其它专栏:

《数据结构与算法》《算法》《C++起始之路》《Linux》


目录

1.信号前导

2.产生信号

3.保存信号

4.捕捉信号

5.可重入函数

6.volatile

7.SIGCHLD信号

8.附录


1.信号前导

1.1生活角度的信号

●在网上买完东西,同时等待不同快递的到来。但即使快递没有到,我们也依然知道快递到时,该如何处理。即我们能“识别快递”

●当快递员到了我们楼下,同时收到了快递到的通知。但此刻我们有事,需5min后才能去取快递。那在这5min之内,我们并没有去取快递,但我们知道有快递到了。即取快递的行为并不是一定要立即执行,可以在“合适的时候去取”

●从收到通知,再到我们拿到快递,在这段时间内,我们并没有拿到快递,但是我们知道有一个快递已经到了。即我们可以“记住有一个快递到了”

●当我们时间合适,顺利拿到快递之后,就要开始处理快递了。而处理快递一般有三种方式:1.执行默认动作(打开快递,使用快递)2.执行自定义动作(可以将快递送给别人)3.忽略快递(放在一边不管)

●快递到的整个过程,对我们来讲是异步的,我们并不能断定快递员什么时候给我们打电话

结论:

●我们为什么可以识别信号?识别信号是内置的,进程识别信号,是内核程序员写的内置特性

●信号产生之后,我们知道如何处理吗?知道。若信号没有产生,我们知道如何处理信号吗?知道。所以,信号的处理方法,在信号产生之前,就已经准备好了

●处理信息,必须立即处理吗?我们可能正在做优先级更高的事情,不立即处理,那什么时候处理?合适的时候

●信号到来 | 信号保存 | 信号处理

●如何进行信息处理?a.默认 b.忽略 c.自定义,后续都叫信号捕捉

1.2技术应用角度的信号

1.2.1样例

// sig.cc #include <iostream> #include <unistd.h> int main() { while(true){ std::cout << "I am a process, I am waiting signal!" << std::endl; sleep(1); } } $ g++ sig.cc -o sig $ ./sig I am a process, I am waiting signal! I am a process, I am waiting signal! ^C

●用户输入命令,在Shell下启动一个前台命令

●用户按下Ctrl+c,这个键盘输入产生一个硬件中断,被OS捕获,解释成信号,发送给目标前台进程

●前台进程因收到信号,所以进程退出

1.2.2signal系统函数

NAME signal - ANSI C signal handling SYNOPSIS #include <signal.h> typedef void (*sighandler_t)(int); sighandler_t signal(int signum, sighandler_t handler); 参数说明: signum:信号编号 handler:函数指针,表⽰更改信号的处理动作,当收到对应的信号,就回调执⾏handler⽅法

其实,Ctrl+c的本质是向前台发送SIGINT即2号信号

证明:

#include <iostream> #include <unistd.h> #include <signal.h> void handler(int signumber) { std::cout << "我是: " << getpid() << ", 我获得了⼀个信号: " << signumber << std::endl; } int main() { std::cout << "我是进程: " << getpid() << std::endl; signal(SIGINT/*2*/, handler); while(true){ std::cout << "I am a process, I am waiting signal!" << std::endl; sleep(1); } } $ g++ sig.cc -o sig $ ./sig 我是进程: 212569 I am a process, I am waiting signal! I am a process, I am waiting signal! ^C我是: 212569, 我获得了⼀个信号: 2 I am a process, I am waiting signal! I am a process, I am waiting signal! ^C我是: 212569, 我获得了⼀个信号: 2 I am a process, I am waiting signal! I am a process, I am waiting signal!

●按下Ctrl+c,进程没有退出,这是因为我们调用的signal函数,改变了2号信号的处理方式,转而使用我们自定义的处理方式

注:

●signal函数只是设置了特定信号的捕捉行为方式,而不是直接调用处理动作。若后续特定信号没有产生,设置的捕捉函数就永远也不会被调用

●Ctrl+c产生的信号只能发给前台进程。一个命令后面加&就可以将进程放到后台运行,这样Shell不必等待进程结束就可以接受新的命令,启动新的进程

●Shell可以同时运行一个前台进程和任意多个后台进程,只有前台进程才能接收到向Ctrl+c这种控制键产生的信号

●前台进程在运行过程种用户随时可能按下Ctrl+c而产生一个信号,即该进程的用户空间代码执行到任何地方都有可能收到SIGINT信号而终止,所以信号相对于进程的控制流程来说是异步(ASynchronous)的

1.3信号概念

信号是进程之间事件异步通知的一种方式,属于软中断

1.3.1查看信号

每个信号都有一个编号和一个宏定义名称,这些宏定义可以在signal.h中找到,例如其中有定义#define SIGINT 2

编号34以上的是实时信号,这里只讨论编号34以下的信号,不讨论实时信号。这些信号各自在什么条件下产生,默认的处理动作是什么,在signal(7)中都有详细说明:man 7 signal

1.3.2信号处理

可选的处理动作有以下三种:

●忽略此信号

#include <iostream> #include <unistd.h> #include <signal.h> void handler(int signumber) { std::cout << "我是: " << getpid() << ", 我获得了⼀个信号: " << signumber<< std::endl; } int main() { std::cout << "我是进程: " << getpid() << std::endl; signal(SIGINT/*2*/, SIG_IGN); // 设置忽略信号的宏 while(true){ std::cout << "I am a process, I am waiting signal!" << std::endl; sleep(1); } } $ g++ sig.cc -o sig $ ./sig 我是进程: 212681 I am a process, I am waiting signal! I am a process, I am waiting signal! ^C^C^C^C^C^CI am a process, I am waiting signal! // 输⼊ctrl+c毫⽆反应 I am a process, I am waiting signal!

●执行该信号的默认处理动作

#include <iostream> #include <unistd.h> #include <signal.h> void handler(int signumber) { std::cout << "我是: " << getpid() << ", 我获得了⼀个信号: " << signumber<< std::endl; } int main() { std::cout << "我是进程: " << getpid() << std::endl; signal(SIGINT/*2*/, SIG_DFL); while(true){ std::cout << "I am a process, I am waiting signal!" << std::endl; sleep(1); } } $ g++ sig.cc -o sig $ ./sig 我是进程: 212791 I am a process, I am waiting signal! I am a process, I am waiting signal! ^C // 输⼊ctrl+c,进程退出,就是默认动作

●提供一个信号处理函数,要求内核在处理该信号时切换到用户态执行这个处理函数,这种方式称为自定义捕捉(catch)一个信号(开头已展示)

源码:

#define SIG_DFL ((__sighandler_t) 0) /* Default action. */ #define SIG_IGN ((__sighandler_t) 1) /* Ignore signal. */ /* Type of a signal handler. */ typedef void (*__sighandler_t) (int); // 其实SIG_DFL和SIG_IGN就是把0,1强转为函数指针类型

按照下图顺序讲解信号

2.产生信号

当前阶段

2.1通过终端按键产生信号

2.1.1基本操作

●Ctrl+c(SIGINT)

●Ctrl+\(SIGQUIT)可以发送终止信号并生成core dump文件,用于事后调试

#include <iostream> #include <unistd.h> #include <signal.h> void handler(int signumber) { std::cout << "我是: " << getpid() << ", 我获得了⼀个信号: " << signumber << std::endl; } int main() { std::cout << "我是进程: " << getpid() << std::endl; signal(SIGQUIT/*3*/, handler); while(true){ std::cout << "I am a process, I am waiting signal!" << std::endl; sleep(1); } } $ g++ sig.cc -o sig $ ./sig 我是进程: 213056 I am a process, I am waiting signal! I am a process, I am waiting signal! I am a process, I am waiting signal! ^\我是: 213056, 我获得了⼀个信号: 3 // 注释掉13⾏代码 $ ./sig 我是进程: 213146 I am a process, I am waiting signal! I am a process, I am waiting signal! ^\Quit

●Ctrl+z(SIGTSTP)可以发送停止信号,将当前前台进程挂起到后台等

#include <iostream> #include <unistd.h> #include <signal.h> void handler(int signumber) { std::cout << "我是: " << getpid() << ", 我获得了⼀个信号: " << signumber << std::endl; } int main() { std::cout << "我是进程: " << getpid() << std::endl; signal(SIGTSTP/*20*/, handler); while(true){ std::cout << "I am a process, I am waiting signal!" << std::endl; sleep(1); } } $ ./sig 我是进程: 213552 I am a process, I am waiting signal! I am a process, I am waiting signal! ^Z我是: 213552, 我获得了⼀个信号: 20 // 将此行代码注释掉后,signal(SIGTSTP/*20*/, handler); $ ./sig 我是进程: 213627 I am a process, I am waiting signal! I am a process, I am waiting signal! I am a process, I am waiting signal! ^Z [1]+ Stopped ./sig sjx@bite:~/code/test$ jobs [1]+ Stopped ./sig

jobs可以查看后台正在运行的进程

2.1.2理解OS如何得知磁盘有数据

2.1.3初步理解信号起源

注:

●信号其实是从纯软件角度,模式硬件中断的行为

●只不过硬件中断是发给CPU,而信号是发给进程

●两者有相似性,但层级不同

2.2调用系统命令向进程发信号

#include <iostream> #include <unistd.h> #include <signal.h> int main() { while(true){ sleep(1); } }
$ g++ sig.cc -o sig // step 1 $ ./sig & // step 2 $ ps ajx |head -1 && ps ajx | grep sig // step 3 PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND 211805 213784 213784 211805 pts/0 213792 S 1002 0:00 ./sig

首先在后台执行死循环程序,然后用kill命令给它发送SIGSEGV信号

$ kill -SIGSEGV 213784 $ // 多按⼀次回⻋ [1]+ Segmentation fault ./sig

●213784是sig进程的pid。之所以要再次回车才显示Segmentation fault,是因为在213784进程终止掉之前已经回到了Shell提示符等待用户输入下一条命令,Shell不希望Segmentation fault信息和用户的输入交错到一起,所以等用户输入命令之后才会显示

●指定发送某种信号的kill命令可以有多种写法,上面的命令也可以写成kill -11 213784,11是信号SIGSEGV的编号。以前遇到的段错误都是由非法内存访问产生的,而这个程序本身没错,给它发SIGSEGV也能产生段错误

2.3使用函数产生信号

2.3.1 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命令

#include <iostream> #include <unistd.h> #include <sys/types.h> #include <signal.h> // mykill -signumber pid int main(int argc, char *argv[]) { if(argc != 3) { std::cerr << "Usage: " << argv[0] << " -signumber pid" << std::endl; return 1; } int number = std::stoi(argv[1]+1); // +1,是因为要去掉字符串前面的- pid_t pid = std::stoi(argv[2]); int n = kill(pid, number); return n; }

2.3.2 raise

raise函数可以给当前进程发送指定的信号(自己给自己发信号)

NAME raise - send a signal to the caller SYNOPSIS #include <signal.h> int raise(int sig); RETURN VALUE raise() returns 0 on success, and nonzero for failure.

样例:

#include <iostream> #include <unistd.h> #include <signal.h> void handler(int signumber) { // 整个代码就只有这⼀处打印 std::cout << "获取了⼀个信号: " << signumber << std::endl; } // mykill -signumber pid int main() { signal(2, handler); // 先对2号信号进⾏捕捉 // 每隔1S,⾃⼰给⾃⼰发送2号信号 while(true) { sleep(1); raise(2); } }
$ g++ raise.cc -o raise $ ./raise 获取了⼀个信号: 2 获取了⼀个信号: 2 获取了⼀个信号: 2

2.3.3 abort

abort函数使当前进程接收到信号而异常终止

NAME abort - cause abnormal process termination SYNOPSIS #include <stdlib.h> void abort(void); RETURN VALUE The abort() function never returns. // 就像exit函数⼀样,abort函数总是会成功的,所以没有返回值。
#include <iostream #include <unistd.h> #include <stdlib.h> #include <signal.h> void handler(int signumber) { // 整个代码就只有这⼀处打印 std::cout << "获取了⼀个信号: " << signumber << std::endl; } // mykill -signumber pid int main() { signal(SIGABRT, handler); while(true) { sleep(1); abort(); } } $ g++ Abort.cc -o Abort $ ./Abort 获取了⼀个信号: 6 // 实验可以得知,abort给⾃⼰发送的是固定6号信号,虽然捕捉了,但是 还是要退出 Aborted // 注释掉此行代码,signal(SIGABRT, handler); $ ./Abort Aborted

2.4由软件条件产生信号

SIGPIPE是一种由软件产生的信号。alarm函数和SIGALRM信号

NAME alarm - set an alarm clock for delivery of a signal SYNOPSIS #include <unistd.h> unsigned int alarm(unsigned int seconds); 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.

●调用alarm函数可以设定一个闹钟,即告诉内核在seconds秒后给当前进程发SIGALRM信号,该信号的默认处理动作是终止该进程

●这个函数的返回值是0或是以前设定的闹钟时间还余下的秒数。如:某人睡觉,设定了一个30分钟之后的闹钟,20分钟后被人吵醒,还想多睡一会,于是重新设定闹钟为15分钟后响,“以前设定的闹钟时间还余下的时间”是10分钟。若seconds值为0,表示取消以前设定的闹钟,函数的返回值仍然是以前设定的闹钟时间还余下的秒数

2.4.1基本alarm验证(体会IO效率问题)

程序的作用是1秒之内不停地数数,1秒钟到了就被SIGALRM信号终止

结论:

●闹钟会响一次,默认终止进程

●有IO效率低

2.4.2设置重复闹钟

#include <iostream> #include <unistd.h> #include <signal.h> #include <vector> #include <functional> using func_t = std::function<void()>; int gcount = 0; std::vector<func_t> gfuncs; // 把信号 更换 成为 硬件中断 void hanlder(int signo) { for(auto &f : gfuncs) { f(); } std::cout << "gcount : " << gcount << std::endl; int n = alarm(1); // 重设闹钟,会返回上⼀次闹钟的剩余时间 std::cout << "剩余时间 : " << n << std::endl; } int main() { //gfuncs.push_back([](){ std::cout << "我是⼀个内核刷新操作" << std::endl;}); //gfuncs.push_back([](){ std::cout << "我是⼀个检测进程时间⽚的操作,如果时间⽚到了,我会切换进程" << std::endl; }); //gfuncs.push_back([](){ std::cout << "我是⼀个内存管理操作,定期清理操作系统内部的内存碎⽚" << std::endl; }); alarm(1); // ⼀次性的闹钟,超时alarm会⾃动被取消 signal(SIGALRM, hanlder); while (true) { pause(); std::cout << "我醒来了..." << std::endl; gcount++; } } NAME pause - wait for signal SYNOPSIS #include <unistd.h> int pause(void); DESCRIPTION pause() causes the calling process (or thread) to sleep until a signal is delivered that either terminates the process or causes the invocation of a signal-catching function. RETURN VALUE pause() returns only when a signal was caught and the signal-catching function returned. In this case, pause() returns -1, and errno is set to EINTR.
会输出: gcount : 0 剩余时间 : 0 我醒来了... gcount : 1 剩余时间 : 0 我醒来了... gcount : 2 剩余时间 : 0 我醒来了... (以此类推...)

结论:

●闹钟设置一次,起效一次

●重复设置的方法

2.4.3如何理解软件条件

在操作系统中,信号的软件条件指的是有软件内部状态或特定软件操作触发的信号产生机制。这些条件包括但不限定于定时器超时(如alarm函数设定的时间到达)、软件异常(如向已关闭的管道写数据产生的SiGPIPE信号)等。当这些软件条件满足时,操作系统会向相关进程发送相应的信号,以通知进程进行相应的处理。即软件条件是因操作系统内部或外部软件操作而触发的信号产生的

2.4.4如何快速理解系统闹钟

系统闹钟,本质是OS必须自身具有定时功能,并能让用户设置这种定时功能,才可能实现闹钟这样的技术

现代Linux提供了定时功能,定时器也要被管理:先描述,在组织。内核中的定时器数据结构是:

struct timer_list { struct list_head entry; unsigned long expires; void (*function)(unsigned long); unsigned long data; struct tvec_t_base_s *base; };

可以看到:定时器超时时间expires和处理方法function。

操作系统管理定时器,采用的是时间轮的做法,可以把它在组织成为“堆结构”

2.5硬件异常产生信号

硬件异常被硬件以某种方式被硬件检测到并通知内核,然后内核向当前进程发送适当的信号。例如当前进程执行了除0的指令,CPU的运算单元会产生异常,内核将这个异常解释为SIGFPE信号发送个进程。再比如当前进程访问了非法内存地址,MMU会产生异常,内核将这个异常解释为SIGSEGV信号发送给进程。

2.5.1模拟除0

#include <stdio.h> #include <signal.h> void handler(int sig) { printf("catch a sig : %d\n", sig); } // v1 int main() { //signal(SIGFPE, handler); // 8) SIGFPE sleep(1); int a = 10; a/=0; while(1); return 0; }

2.5.2模拟野指针

//默认⾏为 [sjx@localhost code_test]$ cat sig.c #include <stdio.h> #include <signal.h> void handler(int sig) { printf("catch a sig : %d\n", sig); } int main() { //signal(SIGSEGV, handler); sleep(1); int *p = NULL; *p = 100; while(1); return 0; } [sjx@localhost code_test]$ ./sig Segmentation fault (core dumped) [sjx@localhost code_test]$ //捕捉⾏为 [sjx@localhost code_test]$ cat sig.c #include <stdio.h> #include <signal.h> void handler(int sig) { printf("catch a sig : %d\n", sig); } int main() { signal(SIGSEGV, handler); sleep(1); int *p = NULL; *p = 100; while(1); return 0; } [sjx@localhost code_test]$ ./sig [sjx@localhost code_test]$ ./sig catch a sig : 11 catch a sig : 11 catch a sig : 11

所以,我们在C/C++当中除零,内存越界等异常,在系统层面上,是被当成信号处理的

注:

通过上面的实验,可以发现:

一直有8号信号被我们捕捉,为什么?上面只提到CPU运算异常后,如何处理后续的流程,实际上OS会检查应用程序的异常情况,其实在CPU中有一些控制和状态寄存器,主要用于控制处理器的操作,通常由操作系统代码使用。状态寄存器可以简单理解为一个位图,对应一些状态标记位、溢出标记位。OS会检测是否存在异常状态,有异常存在就会调用对应的异常处理方法。

除零异常后,我们并没有清理内存,关闭进程打开的文件,切换进程等操作,所以CPU中还保留上下文数据以及寄存器内容除零异常会一直存在,就有了所看到的一直发出异常信号的现象。访问非法内存也是如此。

2.5.3子进程退出core dump

#include <iostream> #include <string> #include <unistd.h> #include <stdlib.h> #include <signal.h> #include <sys/wait.h> int main() { if (fork() == 0) { sleep(1); int a = 10; a /= 0; exit(0); } int status = 0; waitpid(-1, &status, 0); printf("exit signal: %d, core dump: %d\n", status&0x7F, (status>>7)&1); return 0; } SIGABRT P1990 Core Abort signal from abort(3) SIGALRM P1990 Term Timer signal from alarm(2) SIGBUS P2001 Core Bus error (bad memory access) SIGCHLD P2001 Ign Child stopped, terminated, or continued SIGCLD - Ign A synonym for SIGCHLD SIGCONT P1990 Cont Continue if stopped SIGEMT - Term Emulator trap SIGFPE P1990 Core Erroneous arithmetic operation SIGHUP P1990 Term Hangup detected on controlling terminal or death of controlling process SIGILL P1990 Core Illegal Instruction SIGINFO - A synonym for SIGPWR SIGINT P1990 Term Interrupt from keyboard SIGIO - Term I/O now possible (4.2BSD) SIGIOT - Core IOT trap. A synonym for SIGABRT SIGKILL P1990 Term Kill signal SIGLOST - Term File lock lost (unused) SIGPIPE P1990 Term Broken pipe: write to pipe with no readers; see pipe(7) SIGPOLL P2001 Term Pollable event (Sys V); synonym for SIGIO $ 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) 7643 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) 7643 virtual memory (kbytes, -v) unlimited file locks (-x) unlimited

2.5.4Core Dump

●SIGINT的默认动作是终止进程,SiGQUIT的默认处理动作是终止进程并Core Dump

●什么是Core Dump。当一个进程要异常终止是,可以选择把进程的用户空间内存数据全部保存到磁盘上,文件名通常为core,这叫Core Dump

●进程异常终止通常是因为有Bug,如非法内存访问导致段错误,事后可以用调试器检查core文件以查清错误原因,这叫Post-mortem Debug(事后调试)

●一个进程运行产生多大的core文件取决于进程的Resource Limit(这个信息保存在PCB中)。默认是不允许产生core文件的,因为core文件中可能包含用户密码等敏感信息,不安全

●在开发调试阶段可以用ulimit命令改变这个限制,允许产生core文件。首先用ulimit命令改变shell进程的Resource Limit,如允许core文件最大为1024K:$ ulimt -c 1024

$ ulimit -c 1024 $ 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) 7643 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) 7643 virtual memory (kbytes, -v) unlimited file locks (-x) unlimited

写一个死循环程序:

前台运行这个程序,然后再终端键入Ctrl+c或Ctrl+\终止程序

ulimit命令改变了shell进程的Resource Limit,test进程的PCB由Shell进程复制而来,所以也具有和Shell进程相同的Resource Limit值,这样就可以产生Core Dump了,使用core文件:

在终端键入gdb ./test core.4506就可以查看程序在第几行出现错误

也可以先进入调试界面再查看出错行:gdb ./test 然后core-file core

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询