💐专栏导读
🌸作者简介:花想云 ,在读本科生一枚,C/C++领域新星创作者,新星计划导师,阿里云专家博主,CSDN内容合伙人…致力于 C/C++、Linux 学习。
🌸专栏简介:本文收录于 Linux从入门到精通,本专栏主要内容为本专栏主要内容为Linux的系统性学习,专为小白打造的文章专栏。
🌸相关专栏推荐:C语言初阶系列、C语言进阶系列 、C++系列、数据结构与算法。
💐文章导读
本章我们将学习Linux中信号的概念。包括信号的概念、信号的发送以及信号的4中产生方式~
一、🐧什么是信号
1.🐦生活中的信号
在生活中存在各种各样的信号,例如:红绿灯、闹钟、手势……每当我们接收到一个信号,我们就会执行对应的操作,例如红灯停、绿灯行……
为什么我们会对不同的信号有对应的执行动作呢?原因是:
-
我们能够识别一个信号,知道其中的含义
; -
我们从小接受的教育告诉我们应当如何去做
;
但是我们收到一个信号之后必须去执行相应的动作吗?那也不一定。
-
假设你在网上买了很多件商品,再等待不同商品快递的到来。但即便快递没有到来,你也知道快递来临时,你该怎么处理快递。也就是你能
“识别快递”
。 -
当快递员到了你楼下,你也收到快递到来的通知,但是你正在打游戏,需5min之后才能去取快递。那么在在这5min之内,你并没有下去去取快递,但是你是知道有快递到来了。也就是取快递的行为并不是一定要立即执行,可以理解成“
在合适的时候去取
”。 -
在收到通知,再到你拿到快递期间,是有一个时间窗口的,在这段时间,你并没有拿到快递,但是你知道有一个快递已经来了。本质上是你“
记住了有一个快递要去取
”。 -
当你时间合适,顺利拿到快递之后,就要开始处理快递了。而处理快递一般方式有三种:
1. 执行默认动作
(幸福的打开快递,使用商品)2. 执行自定义动作
(快递是零食,你要送给你你的女朋友)3. 忽略快递
(快递拿上来之后,扔掉床头,继续开一把游戏)。 -
快递到来的整个过程,对你来讲是
异步
的,你不能准确断定快递员什么时候给你打电话。
2.🐦技术应用中的信号
在Linux中,我们常常通过键盘按下 Ctrl + c
来终止一个前台进程。
用户按下Ctrl + c
,这时键盘会产生一个硬件中断
,被OS获取,解释为信号,发送给目标进程,前台进程因为收到这个信号,进而引起进程退出。
$ ./myprocess
myprocess running..., pid: 27851
myprocess running..., pid: 27851
myprocess running..., pid: 27851
myprocess running..., pid: 27851
myprocess running..., pid: 27851
myprocess running..., pid: 27851
^C
$
前台进程与后台进程
-
./filename
启动一个进程,该进程为前台进程
,在这条命令后加一个&
可以将该进程放到后台运行;
$ ./filename # 启动后,进程在前台运行
$ ./filename & # 启动后,进程在后台运行
-
Shell
可以同时运行一个前台进程和任意多个后台进程; - 若有进程在前台运行时,
Shell
必须等待进程结束才能接受新的指令或启动心得进程; -
Ctrl + c 只能发送给前台进程
;
在许久之前,我们也曾用过 kill -9
的指令来终止一个进程,它的本质也是给进程发送 9 号信号来让进程终止。
3.🐦查看信号列表
在Linux中我们可以使用kill -l
来查看系统定义的信号列表。
$ kill -l
1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP
6) 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
我们一眼看去貌似总共有64个信号,但是再仔细一看我们发现这里面是没有 32,33 的信号的,且信号是从1开始编号的。
在Linux系统中,信号的编号范围通常是1到31。这些信号的含义是由POSIX标准定义的,是标准的UNIX信号。然而,32到63之间的信号编号通常被用于扩展,称为实时信号
(Real-time signals)。本章我们不讨论实时信号。
每个信号都有一个编号
和一个宏定义名称
,这些宏定义可以在signal.h
中找到。
4.🐦信号的发送
因为信号可能随时产生,所以在信号产生前,进程可能在做优先级更高的事情,当信号来临时,进程可能不能立即处理该信号,而是在往后合适的实际处理。
那么当进程收到信号时,如果它暂时来不及处理这个信号,那么它就必须将这个信号暂时保存起来。也就是一个信号总会经过这三个过程:
- 信号的产生;
- 信号的保存;
- 信号的处理;
那么进程是如何记录接受到的信号的呢?答案是先描述再组织
。在进程的task_struct
结构体中存在一个位图
结构(uint32_t signals)用来管理信号。
比特位的位置对应信号的编号,比特位的内容(0或1)对应是否收到该信号。所以
- 所谓发送信号,本质是
写入信号
。直接将指定进程中的信号位图中指定位置的比特位由0置1;
由于task_struct
是内核数据结构的,只能由OS进行修改,所以可以推出
- 无论后面有多少种产生信号的方式,最终都必须由
OS
来完成最后的发送过程。
二、🐧信号的捕捉
在介绍信号产生之前,我们先来谈谈信号的捕捉,以便于后面知识的理解。
前面我们提到,当我们收到一个取快递的信号时,我们可能根据场景的不同而选择执行不同的处理动作。同样的,我们因为可以通过 signal
函数来捕捉信号,让进程在接收到该信号后,不去执行默认动作(例如2号信号的默认动作的终止进程),转而执行我们的自定义动作。
signal 函数的原型如下:
#include <signal.h>
void (*signal(int signum, void (*handler)(int)))(int);
-
参数:
-
signo
:要设置处理函数的信号编号,可以是标准信号(如SIGINT
)或用户自定义的信号。 -
handler
:是一个指向函数的指针,该函数接受一个整数参数,表示接收到的信号编号。
-
-
返回值:
- 返回一个指向之前信号处理函数的指针,如果之前没有设置过处理函数,则返回
SIG_DFL
(默认处理)。
- 返回一个指向之前信号处理函数的指针,如果之前没有设置过处理函数,则返回
-
功能:
-
signal
函数用于为指定的信号设置一个处理函数。当进程接收到指定的信号时,将调用相应的处理函数。
-
示例
#include <iostream>
#include <unistd.h>
#include <signal.h>
using namespace std;
// 处理函数
void handler(int signo)
{
cout << "Received SIGINT." << endl;
_exit(1); // 直接退出
}
int main()
{
// 注册2号信号(SIGINT)的处理函数
signal(SIGINT, handler);
while(true)
{
cout << "Waiting for SIGINT..." << endl;
sleep(1);
}
return 0;
}
$ g++ mysignal.cc -o mysignal
$ ./mysignal
Waiting for SIGINT...
Waiting for SIGINT...
Waiting for SIGINT...
Waiting for SIGINT...
Waiting for SIGINT...
Waiting for SIGINT...
Waiting for SIGINT...
Waiting for SIGINT...
^CReceived SIGINT.
在这个例子中,程序注册了一个用于处理 SIGINT
信号的处理函数 handler
。当用户按下Ctrl+C
发送SIGINT
信号时,程序不会终止而是会调用这个处理函数来执行相应的操作。
注意
- 9号信号是不能被捕捉、阻塞和忽略的。
三、🐧信号的产生
前面我们已经提到了两种信号产生的方式:键盘、kill 指令。
1.🐦键盘输入
当你在键盘上按下Ctrl+C时
,这个动作实际上会产生一个中断信号,这个信号被称为SIGINT
(Interrupt Signal)。操作系统内核(kernel)通过键盘驱动程序来监测键盘的输入事件。当Ctrl+C组合键被按下,键盘驱动程序会通知内核,内核然后生成一个SIGINT
信号并将其发送给与当前前台进程相关联的终端。
以下是简要的工作流程:
-
键盘输入: 当你按下键盘上的键时,键盘控制器检测到这个事件,并将相应的扫描码发送到计算机。
-
中断请求(IRQ): 键盘控制器通过硬件中断请求(IRQ)通知CPU有一个新的中断事件发生。
-
中断服务程序: 操作系统内核中有一个与键盘输入相关的中断服务程序,它被调用以处理键盘中断。
-
生成信号: 中断服务程序检测到Ctrl+C组合键后,它会生成一个
SIGINT
信号。 -
信号传递: 生成的
SIGINT
信号被发送给当前前台进程的进程组。前台进程是与终端相关联的活跃进程。 -
信号处理: 如果前台进程注册了
SIGINT
的信号处理函数,该函数将被调用以执行相应的操作。如果没有注册处理函数,则默认操作是终止进程。
总体来说,键盘输入被硬件中断机制捕获,通过中断服务程序和信号机制,通知操作系统内核产生了一个SIGINT
信号,最终传递给前台进程。
2.🐦硬件中断
当我们在程序中发生了除0、访问空指针等非法的操作时,就会引起异常,出发硬件中断被内核捕获,内核会向该进程发送信号终止该进程。
2.1 🐱除0
#include <iostream>
using namespace std;
int main()
{
int n = 10;
cout << n / 0 << endl;
return 0;
}
$ g++ test.cc
$ ./a.out
Floating point exception
当在代码中进行除零操作时,会导致产生浮点异常,这将触发硬件中断。通常情况下,这个硬件中断是浮点异常中断,它会被操作系统内核捕获。内核会检查正在运行的进程是否设置了适当的信号处理函数,如果设置了,执行相应的处理操作。如果进程没有设置处理函数,通常会导致进程被终止。
以下是简要的工作流程:
-
除零操作: 在代码中进行除零操作时,例如浮点数除以零,将会导致浮点异常。
-
硬件中断: 产生的浮点异常触发了硬件中断,通常是浮点异常中断。
-
中断服务程序: 操作系统内核中有一个与浮点异常相关的中断服务程序,它被调用以处理浮点异常。
-
信号生成: 中断服务程序生成一个与浮点异常相关的信号,例如
SIGFPE
(浮点异常信号)。 -
信号传递: 生成的信号被发送给当前运行进程。
-
信号处理: 如果进程设置了
SIGFPE
的信号处理函数,该函数将被调用以执行相应的操作。如果没有设置处理函数,通常会执行默认方法导致进程被终止。
在默认情况下,如果进程没有显式设置信号处理函数来处理浮点异常,操作系统通常会终止该进程,并生成一条错误消息。这是为了防止程序执行处于未定义状态的操作,并确保系统的稳定性。
2.2 🐱访问空指针
#include <iostream>
using namespace std;
int main()
{
int* p = NULL;
cout << *p << endl;
return 0;
}
$ g++ test.cc
$ ./a.out
Segmentation fault
当代码中访问了空指针,这将导致内存访问异常,通常是由硬件中断机制引发的。这种异常会被操作系统内核捕获,并根据系统的处理策略来终止进程或采取其他适当的措施。以下是简要的工作流程:
-
空指针访问: 在代码中发生对空指针的访问,例如尝试读取或写入空指针指向的内存位置。
-
硬件中断: 产生的内存访问异常触发硬件中断,通常是由内存管理单元(MMU)检测到的。
-
中断服务程序: 操作系统内核中有一个与内存访问异常相关的中断服务程序,它被调用以处理异常。
-
信号生成: 中断服务程序可能生成一个与内存访问异常相关的信号,例如
SIGSEGV
(段错误信号)。 -
信号传递: 生成的信号被发送给当前运行进程。
-
信号处理: 如果进程设置了对
SIGSEGV
的信号处理函数,该函数将被调用以执行相应的操作。如果没有设置处理函数,通常会导致进程被终止。
在默认情况下,如果进程没有显式设置信号处理函数来处理段错误,操作系统通常会终止该进程,并在控制台或日志中记录相应的错误信息。这是为了防止程序执行处于未定义状态的操作,确保系统的稳定性,并协助开发者调试潜在的问题。
3.🐦kill 指令
我们可以用kill 想指定进程发送指令。我们除了可以直接使用kill指令向进程发送信号外,还可以在程序中调用kill函数来进行指令发送。
kill
函数是一个用于向指定进程发送信号的系统调用。它可以用于向指定进程发送任何一个有效的信号,例如终止进程、挂起进程、继续执行进程等。以下是 kill
函数的基本信息:
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);
-
参数:
-
pid
:要发送信号的目标进程的进程ID。 -
sig
:要发送的信号的编号,可以是标准信号(如SIGKILL
)或用户自定义的信号。
-
-
返回值:
- 如果成功,返回0;如果失败,返回-1,并设置相应的错误码(errno)。
-
功能:
-
kill
函数用于向指定进程发送信号。信号可以是预定义的标准信号(如SIGKILL
、SIGTERM
)或用户自定义的信号。通常用于进程间通信、控制进程的行为,或者强制终止进程等。
-
-
注意事项:
- 如果将
pid
参数设置为0,则信号会发送给调用进程的进程组中的所有成员。 - 如果将
pid
参数设置为-1,则信号会发送给调用进程有权发送信号的任意进程(权限通常由effective user ID
决定)。 - 如果发送
SIGKILL
信号(编号为9),则表示强制终止目标进程,目标进程将无法捕获或忽略该信号。
- 如果将
所以我们可以用kill函数来实现一个我们自己的kill指令mykill
。
示例
#include <iostream>
#include <string>
#include <cerrno>
#include <cstring>
#include <signal.h>
#include <sys/types.h>
#include <unistd.h>
void Usage(string proc)
{
cout << "Usage: \n";
cout << proc << "信号编号 目标进程\n"
<< endl;
}
int main(int argc, char *argv[])
{
// kill -9 xxxx
if (argc != 3)
{
Usage(argv[0]);
exit(-1);
}
int signo = atoi(argv[1]);
int target_id = atoi(argv[2]);
int n = kill(target_id, signo);
if (n != 0)
{
cerr << errno << " : " << strerror(errno) << endl;
exit(2);
}
return 0;
}
4.🐦软件条件
SIGALRM
是一种由软件条件产生的信号,它可以由alarm
函数产生。alarm
函数是一个用于设置定时器的系统调用,它的主要功能是在指定的时间间隔后向进程发送 SIGALRM
信号。以下是 alarm
函数的基本信息:
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
-
参数:
-
seconds
:设置定时器的时间间隔,单位是秒。当定时器计时到达指定的秒数后,将发送SIGALRM
信号给进程。
-
-
返回值:
- 返回之前设置的剩余秒数,如果之前没有设置定时器,则返回 0。
-
功能:
-
alarm
函数用于设置一个定时器,当定时器计时到达指定的秒数后,进程将收到SIGALRM
信号。该信号默认会终止进程,但可以通过注册信号处理函数来改变其行为。
-
-
注意事项:
- 如果之前已经设置了定时器,调用
alarm
函数将取消之前的定时器,并用新的时间间隔重新设置。 - 如果将
seconds
参数设置为 0,表示取消之前的定时器,即不再发送SIGALRM
信号。
- 如果之前已经设置了定时器,调用
下面是一个简单的示例,演示如何使用 alarm
函数:
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
void handler(int signo) {
printf("Received SIGALRM\n");
}
int main() {
// 注册信号处理函数
signal(SIGALRM, alarm_handler);
// 设置定时器,5秒后发送SIGALRM信号
unsigned int remaining_time = alarm(5);
printf("Timer set. Remaining time: %u seconds\n", remaining_time);
// 进程执行其他任务
sleep(10);
printf("Program completed.\n");
return 0;
}
在这个例子中,程序注册了一个用于处理 SIGALRM
信号的处理函数 alarm_handler
。然后使用 alarm(5)
设置了一个5秒的定时器,5秒后将会触发 SIGALRM
信号。在实际的应用中,可以利用定时器来执行一些定时任务或超时处理。
四、🐧核心转储
我们向进程发送不同的信号,有时我们会发现有不少信号的默认执行动作是终止进程。它们之间看似效果雷同,但其实是有差别的。
使用 man 7 signal
查看signal的详细文档时,我们会看到这样的信息:
我们观察到有些信号的 Action 是 Term 有些是 Core,它们二者都是终止进程但是却有差别。
-
Term:单纯的终止进程,没有多余的动作
; -
Core:先进性核心转储,再终止进程
;
🐦核心转储及其作用
核心转储(core dump)是指在程序发生严重错误导致异常终止时,操作系统将程序的内存内容以及相关的调试信息保存到一个特殊的文件中,以供后续分析和调试使用。这个文件通常被称为核心转储文件或核心文件。
核心转储文件包含了程序崩溃时内存的快照,以及与进程相关的其他信息,如寄存器状态、调用栈、变量值等。这对于开发人员来说是非常有价值的,因为它提供了关于程序崩溃原因的详细信息,有助于识别和调试问题。
核心转储文件通常以 “core” 或者在某些系统中以进程ID为文件名的形式保存在程序当前工作目录或系统指定的核心转储文件目录中。它们对于排查由于程序错误、内存损坏或其他异常情况引起的问题非常有用。
有一些关键的概念和注意事项与核心转储相关:
-
ulimit 设置: 操作系统可能会设置
ulimit
(用户资源限制)来限制核心转储文件的大小,以避免占用过多磁盘空间。 -
调试符号: 为了更好地解析核心转储文件,通常需要保留程序的调试符号。调试符号是编译时信息,包含了程序源代码的映射关系,有助于将内存地址映射回源代码。
-
调试工具: 使用调试工具(如
gdb
)可以加载核心转储文件,并允许开发人员分析崩溃时的状态、查看堆栈跟踪,以及检查变量值等。 -
产生核心转储: 在Unix-like系统中,可以通过在程序中调用
ulimit
设置允许生成核心转储文件,或者在终端运行程序时使用ulimit -c unlimited
临时修改。
示例
在云服务器中,核心转储功能一般是关闭
的,需要我们手动打开。
- 打开核心转储功能;
$ 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) 7266
max locked memory (kbytes, -l) 64
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) 4096
virtual memory (kbytes, -v) unlimited
file locks (-x) unlimited
- 编写程序;
#include <iostream>
using namespace std;
int main()
{
int *p = NULL;
cout << *p << endl;
return 0;
}
$ g++ test.cc -o test
$ ./test
Segmentation fault (core dumped)
$ ll
total 248
-rw------- 1 hxy hxy 557056 Mar 6 16:54 core.6554
-rwxrwxr-x 1 hxy hxy 8920 Mar 6 16:54 test
-rw-rw-r-- 1 hxy hxy 109 Mar 6 16:54 test.cc
$
- 当我们想调试该代码时;
$ g++ test.cc -o test -g
$ ./test
$ gdb test
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/>...
Reading symbols from /home/hxy/code/test_6_23/core/test...done.
(gdb) core-file core.6753
[New LWP 6753]
Core was generated by `./test'.
Program terminated with signal 11, Segmentation fault.
#0 0x00000000004007c1 in main () at test.cc:8
8 cout << *p << endl;
Missing separate debuginfos, use: debuginfo-install glibc-2.17-326.el7_9.x86_64 libgcc-4.8.5-44.el7.x86_64
如上所示,当我们使用gdb加载核心转储文件时,可以直接将错误信息以及出错位置告诉我们。
早在获取子进程status时,我们曾看过一张图片:
当子进程被信号杀死之后,我们可以查看终止信号以及core dump标记。若core标记为0,则表示没有启用核心转储功能,若为1,则表示启用。
我们来简单验证一下:文章来源:https://www.toymoban.com/news/detail-840316.html
- 编写代码;
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>
using namespace std;
int main()
{
pid_t id = fork();
if(id == 0)
{
int *p = NULL;
cout << *p << endl;
}
int status = 0;
waitpid(id, &status, 0);
cout << "exit code: " << ((status >> 8) & 0xFF) << endl;
cout << "exit signal: " << (status & 0x7F) << endl;
cout << "core dump flag: " << ((status >> 7) & 0x1) << endl;
return 0;
}
- 在打开核心转储功能时,运行程序:
$ ./test
exit code: 0
exit signal: 11
core dump flag: 1
$ ll
total 484
-rw------- 1 hxy hxy 557056 Mar 6 17:11 core.7770
-rw-rw-r-- 1 hxy hxy 78 Mar 6 17:11 makefile
-rwxrwxr-x 1 hxy hxy 9176 Mar 6 17:11 test
-rw-rw-r-- 1 hxy hxy 465 Mar 6 17:10 test.cc
- 在关闭核心转储功能时,运行程序:
$ ulimit -c 0 # 关闭核心转储
$ ./test
exit code: 0
exit signal: 11
core dump flag: 0
文章来源地址https://www.toymoban.com/news/detail-840316.html
到了这里,关于『Linux从入门到精通』第 ㉖ 期 - 信号概念 & 信号的产生的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!