1. 信号的概念
Linux下的信号机制是一种进程间通信(IPC)的方式,用于在不同进程之间传递信息。
信号是一种异步的信息传递方式,这意味着发送信号的进程只发送由信号作为载体的命令,而并不关心接收信号的进程如何处置这个命令(做不做、何时做、怎么做、结果如何等)。
这样,在发送完命令之后,发送命令的进程就可以着手自己的其他任务,而无需等待对方反馈的结果(或者说无需与对方进行同步)。同时也就意味着接收信号的进程对于命令的处置是相对自由的,甚至可以直接忽略。
例如,菜鸟驿站给你发消息告诉你包裹到了(这就是一种信号),你可以去取,也可以不取(富哥不想要了,又懒得退款,或者单纯忘了);你可以立即去取,也可以等几个小时/几天再取;你可以自己去取,也可以叫好友帮取,甚至叫个跑腿去取。
但有时候我们又希望自己发送的信号是绝对有效的,不可被轻视的。于是,我们可以将信号分为可靠信号和不可靠信号,一共62种(1~64,除开32和33):
不可靠信号:也称为非实时信号,不支持排队,信号可能会丢失。比如发送多次相同的信号,进程只能收到一次,信号值取值区间为1~31。
可靠信号:也称为实时信号,支持排队,信号不会丢失,发多少次,就可以收到多少次,信号值取值区间为34~64。
使用 [ kill -l ] 指令可以进行查看:
2. 信号的处理
2.1 默认处理
每个信号都有默认的处理方式,如SIGINT
信号的默认处理是终止进程,SIGTERM
信号的默认处理也是终止进程,SIGSEGV
信号的默认处理是产生核心转储并终止进程。
man 7 signal
找到标题:Standard signals
- Core(核心转储并终止进程):如SIGABRT、SIGFPE信号,当这些信号发生时,系统会进行核心转储并且终止进程。
- Term(终止进程):像SIGALRM、SIGHUP信号,这些信号的默认动作是终止进程。
- Ign(忽略信号):对于SIGCHLD、SIGCLD信号,系统默认会忽略这些信号。
- Cont(继续执行):对于SIGCONT信号,其默认动作是如果进程处于停止状态则继续执行。
- Stop(停止进程):对于SIGSTOP信号,它会使进程停止执行,且不能被捕获、忽略或阻塞。
可以看到,大多数信号的默认处理方式都是终止进程。
2.2 捕获信号
进程可以通过设置信号处理函数来捕获信号并执行自定义的处理逻辑。例如,可以在信号处理函数中记录日志、进行资源清理等操作。
#include <signal.h>typedef void (*sighandler_t)(int);sighandler_t signal(int signum, sighandler_t handler);
功能
设置信号处理程序:
signal
函数用于设置一个函数来处理特定的信号。当指定的信号发生时,系统会调用相应的信号处理函数。
参数
sig:表示要处理的信号码。常见的信号常量包括
SIGABRT
(程序异常终止)、SIGFPE
(算术运算出错)、SIGILL
(非法指令)、SIGINT
(中断信号,如Ctrl+C)、SIGSEGV
(非法访问存储器)、SIGTERM
(终止请求)等。func:是一个指向函数的指针,用于指定信号处理程序。可以是自定义的函数,也可以是以下预定义函数之一:
SIG_DFL:表示使用该信号的默认处理程序。
SIG_IGN:表示忽略该信号。
返回值
成功时:返回信号处理程序之前的值。
出错时:返回
SIG_ERR
。
例如,我们编写一个循环打印 hello world 的程序, 并让他捕获2号信号,即ctrl + c发出的信号:
#include <iostream>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>void SigHandler(int sigid)
{std::cout << "获得信号: " << sigid << std::endl;exit(sigid);
}int main()
{signal(SIGINT, SigHandler);for(int i = 0; true; i++){std::cout << "Hello World[" << i << "]" << ", pid:[" << getpid() << "]" << std::endl;sleep(1);}return 0;
}
当我们在程序运行的过程中按下 ctrl + c 时,程序会显示获得信号:
2.3 忽略信号
进程可以通过设置信号处理函数为SIG_IGN
来忽略某个信号。但有些信号是不能被忽略的,如SIGKILL
和SIGSTOP
信号。
3. 信号的产生方式
用户输入:用户在终端输入特定的快捷键组合,如
Ctrl+C
会产生SIGINT
信号,Ctrl+\
会产生SIGQUIT
信号,Ctrl+Z
会产生SIGTSTP
信号。程序执行异常:当程序执行过程中出现错误或异常情况时,内核会发送相应的信号。例如,对一个数除0会产生
SIGFPE
信号,非法访问一段内存会产生SIGBUS
信号,访问未分配的虚拟内存会产生SIGSEGV
信号。进程间发送信号:一个进程可以通过系统调用向另一个进程发送信号。例如,使用
kill
函数可以向指定进程发送信号,raise
函数可以向本进程发送信号,sigqueue
函数可以向一个进程发送信号并传递额外数据。
用户输入方式我们在前面已经见识过,就不再多说。
3.1 程序执行异常
我们的程序在遇到运行时错误时会报错并退出,这就是因为程序在发生执行异常时会引发硬件异常中断,操作系统检测到之后,就会向引发异常的进程发送一个信号,进而终止进程。
下面的代码中,我们尝试捕捉由除零引发的异常和使用野指针引发的异常:
#include <iostream>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>void SigHandler(int sigid)
{std::cout << "获得信号: " << sigid << std::endl;exit(sigid);
}int main()
{for(int i = 1; i < 32; i++){signal(i, SigHandler);}// 8) SIGFPEint a = 10;a /= 0;// 11) SIGSEGV// int *p = nullptr;// *p = 100;return 0;
}
除零异常导致程序退出:
使用野指针导致程序退出:
3.2 进程间发送信号
3.2.1 kill命令
kill -[信号编号] [指定进程pid]
在命令行使用kill指令可像指定进程发送指定信号。
3.2.2 kill函数
#include <sys/types.h>
#include <signal.h>int kill(pid_t pid, int sig);
功能
kill
函数用于向指定的进程或进程组发送信号,以实现进程间的通知或控制。
参数
pid:表示要发送信号的目标进程或进程组的标识符。
pid > 0:将信号发送给进程标识为
pid
的进程。pid = 0:将信号发送给与调用
kill
函数的进程属于同一进程组的所有进程。pid = -1:将信号发送给除了进程1(
init
进程)和调用者自身以外的所有进程。pid < -1:将信号发送给进程组ID等于pid绝对值的所有进程。
sig:表示要发送的信号。可以是以下常见信号常量之一
返回值
成功时:返回0。
出错时:返回-1,并设置
errno
以指示错误原因。
3.2.3 raise函数
#include <signal.h>int raise(int sig);
功能:向调用该函数的进程发送一个信号(即向自己发送信号)。
参数:
sig
表示要发送的信号编号。返回值:成功时返回0;失败时返回非0值。
#include <iostream>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>void SigHandler(int sigid)
{std::cout << "获得信号: " << sigid << std::endl;exit(sigid);
}int main()
{for(int i = 1; i < 32; i++){signal(i, SigHandler);}for(int i = 1; i < 32; i++){sleep(1);// 给自己发信号if(i == 9 || i == 19) // 9-SIGKILL 19-SIGSTOP不能被自定义捕获 continue;raise(i);}return 0;
}
9号信号SIGKILL和19号信号SIGSTOP无法被自定义捕获,这是为了防止出现所有信号都被捕获而无法通过信号使目标进程被强制停止的情况。
3.2.4 abort函数
#include <stdlib.h>void abort(void);
功能:abort
函数用于立即终止当前程序的执行,通常在检测到不可恢复的错误时使用。
特点:
不执行清理工作:
abort
函数不会执行任何atexit
注册的函数或对象析构函数,也不会刷新流缓冲区或关闭打开的文件等常规清理操作。产生核心转储文件:在某些系统上,如果系统配置允许,
abort
函数会产生一个核心转储文件,用于调试程序异常终止的原因。发送信号:
abort
函数会向调用进程发送SIGABRT
信号,进程不应忽略此信号。
3.2.5 sigqueue函数
功能:向指定的进程发送特定的信号,并可以传递一个额外的数据值,提供了比kill函数更丰富的功能,可用于进程间的高级通信。
暂时不做介绍。
4. 补充:前后台进程
在大多数用户交互式操作系统当中,都会把进程分为前台进程和后台进程,在Linux中二者的概念如下:
前台进程:是与用户直接交互的进程,占有控制终端,可以从终端接收输入并向终端发送输出。在任何时刻,只有一个进程组可以在前台运行。
后台进程:是不与用户直接交互的进程,在后台默默运行,不占用控制终端。
用户在命令行启动可执行程序时,该可执行程序默认以前台方式运行,由于前台进程只有一个,所以操作系统会将bash切换到后台运行,待可执行程序运行结束或者被切换到后台时,再将bash切换回前台。
如果用户希望进程被启动后在后台运行,可在其后跟上 & :
可以看到,我们让SigTest运行起来之后,bash和SigTest都在往标准输出打印,当我们输入ls指令之后,发现处于前台的bash获得了我们的输入,并运行了ls。
此时,如果我们想要通过[ctrl + c]的方式来结束SigTest是行不通的,因为只有前台进程能接收到我们的输入。此时我们有两种做法:
- 使用kill -9指定终止SigTest。
- 将SigTest放到前台,再使用[ctrl + c]。
前者不再进行说明了,接下来我们讲解一下如何在命令行控制进程的前后台切换。
在Linux中,与前后台相关的指令主要用于管理进程的运行状态,以下是一些常用的指令:
后台运行指令
- &:在命令末尾添加“&”符号,可以使程序在后台运行,例如“./matmul &”将运行一个名为matmul的程序并使其在后台运行,这样用户就可以在前台继续执行其他命令。
- nohup:nohup命令用于在后台运行命令,即使终端关闭或用户退出,命令也会继续执行。例如,“nohup ping 101.lug.ustc.edu.cn &”将在后台运行ping命令,并将输出重定向到nohup.out文件中。
前后台切换指令
- Ctrl+Z:将当前正在前台执行的命令或进程暂停,并放入后台。例如,当一个命令在前台运行时,按下Ctrl+Z,该命令将被暂停并放入后台,屏幕上会显示相关的状态信息。
- jobs:用于查看当前在后台运行的进程或任务的列表,以及它们的状态和作业号。例如,“jobs -l”将显示详细的后台进程信息,包括进程ID、状态等。
- fg:将后台中的进程调至前台继续运行。例如,“fg %1”将把后台中作业号为1的进程调至前台运行。
- bg:将后台中暂停的进程继续在后台运行。例如,“bg %2”将使后台中作业号为2的进程在后台继续运行。
其他相关指令
- ps:用于查看当前系统中正在运行的进程的状态和信息。例如,“ps aux”将显示所有用户的所有进程的详细信息。
- kill:用于向进程发送信号,通常用于终止进程。例如,“kill -9 PID”将强制终止指定PID的进程。
- top:用于实时查看系统中各个进程的资源占用情况,包括CPU、内存等。
- htop:是top命令的增强版,提供了更直观和交互性更强的界面,用于查看和管理进程。