跳到主要内容
Linux 信号机制:信号的产生、类型与捕获 | 极客日志
C++
Linux 信号机制:信号的产生、类型与捕获 综述由AI生成 Linux 信号是进程间通信的一种机制,用于通知进程发生了某种事件。信号可由键盘输入(如 Ctrl+C)或 kill 命令产生。信号分为普通信号(1-31 号)和实时信号(34-64 号)。进程处理信号的方式包括默认动作、忽略和自定义捕获。使用 signal 系统调用可设置捕获函数,但 SIGKILL(9 号)和 SIGSTOP(19 号)无法被捕获,这是为了保留操作系统强制终止或暂停进程的权限,确保系统稳定性。
技术博主 发布于 2026/3/16 更新于 2026/5/4 17 浏览Linux 信号机制详解
1. 信号的定义
生活中的例子
从生活中的例子理解信号:信号弹、上下课铃声、闹钟等等都可以认为是信号。
那为什么能认为'信号弹、上下课铃声、闹钟'是信号呢?
答:有人提前教过我们,我们记住了这些就是信号。可以推出:即使是现在没有信号产生,我们也知道信号产生之后应该干什么,例如红灯停,绿灯行 → 识别信号,知道信号的处理方法 。
产生信号后,可能不会立即处理
信号产生了,由于某些原因(例如当前处理的事情非常重要,无法暂停),我们可能并不会立即处理这个信号,在合适的时候才会处理。
所以,信号产生后到信号处理这个时间窗口内,进程必须记住信号的到来 。
结论
进程必须识别且能够处理信号:即使信号没有产生,也要具备处理信号的能力,信号的处理能力属于进程内置功能的一部分。
当进程真的收到了一个具体的信号的时候,进程可能并不会立即处理这个信号,合适的时候才会去执行对应的动作,那么进程在这个时间窗口需要具有临时保存哪些信号已经发生了的能力。
注:信号和信号量没有任何关系!
2. 从 Ctrl+C 来看信号的产生
新建以下文件结构:
test_signal/
├── makefile
├── send_signal.sh
└── test_signal.cpp
makefile 写入:
test_signal.out:test_signal.cpp
g++ -o $@ $^ -g -std=c++11
.PHONY :clean
clean:
rm -f test_signal.out
test_signal.cpp 写入无限向终端打印 hello world 的程序:
#include <unistd.h>
#include <iostream>
using namespace std;
int main () {
for (;;) {
std::cout << "Hello World!" << std::endl;
sleep (1 );
}
return 0 ;
}
启动:
./test_signal.out
运行结果:按下 Ctrl+C,进程停止执行。
为什么 Ctrl+C 可以杀死进程?
上方运行的结果可以看到:输入任何 bash 命令都不会起作用,但是 Ctrl+C 杀死了这个进程。
Linux 中,一次登录分配一个终端,一般会配上一个 bash。每一个登录,只允许一个进程是前台进程,可以允许多个进程是后台进程 。
那么前台进程和后台进程的区别是:只有前台进程能获取键盘输入 。
那么无限向终端打印 hello world 的进程为前台进程,bash 为后台进程,这样 bash 无法获取键盘输入。
如果将无限向终端打印 hello world 的进程启动为后台进程:
运行结果:Ctrl+C 无法结束无限向终端打印 hello world 的进程启动的后台进程。
Bash 内部对 Ctrl+C 做了特殊处理 那 Ctrl+C 为什么没有杀死 bash?因为 Bash 内部对 Ctrl+C 做了特殊处理,Ctrl+C 是向前台进程发送了 2 号信号来终止前台进程的(这个后面的文章再解释)。
在 bash-5.3 的根目录下有一个 sig.c 文件,注释说明了原因:
#define NULL_HANDLER (SigHandler *)SIG_DFL
static struct termsig terminating_signals [] = {
#ifdef SIGINT
{ SIGINT, NULL_HANDLER, 0 },
#endif
};
注释里面说的很清楚:这些信号如果不捕获(捕获这个概念在本文 4.形象理解信号的处理方式 提到了)的话,会终止 shell。
结论:Ctrl+C 可以杀死前台进程,Ctrl+C 向前台进程发送 2 号信号 SIGINT 。
3. 信号的种类
细节 1:信号的编号 仔细看的话,从 1 到 64,没有 32 号和 33 号信号(历史原因,这里不讲) ,一共 62 个信号。
普通信号
实时信号 34 号~64 号。
实时信号的特点:实时信号产生了必须立即处理,这里不学。
细节 2:信号的名称 信号的名称的字母都是大写的,其实它们在 Linux 内核中被定义为宏,信号的名称的宏对应的数字就是信号的编号 。
x86 平台下,Linux 内核定义在 /arch/x86/include/uapi/asm/signal.h 中。
注:uapi 这个目录下存储用户空间 API 头文件
#define SIGHUP 1
#define SIGINT 2
#define SIGQUIT 3
#define SIGILL 4
#define SIGTRAP 5
#define SIGABRT 6
#define SIGIOT 6
#define SIGBUS 7
#define SIGFPE 8
#define SIGKILL 9
#define SIGUSR1 10
#define SIGSEGV 11
#define SIGUSR2 12
#define SIGPIPE 13
#define SIGALRM 14
#define SIGTERM 15
#define SIGSTKFLT 16
#define SIGCHLD 17
#define SIGCONT 18
#define SIGSTOP 19
#define SIGTSTP 20
#define SIGTTIN 21
#define SIGTTOU 22
#define SIGURG 23
#define SIGXCPU 24
#define SIGXFSZ 25
#define SIGVTALRM 26
#define SIGPROF 27
#define SIGWINCH 28
#define SIGIO 29
#define SIGPOLL SIGIO
#define SIGPWR 30
#define SIGSYS 31
#define SIGUNUSED 31
#define SIGRTMIN 32
#define SIGRTMAX _NSIG
4. 形象理解信号的处理方式 操作系统为进程发送信号,进程需要处理信号,只有 3 种方式,而且只能 3 选 1 。
默认动作: 手机响了,接听电话(这是大多数人默认的动作)
从上面的实验的运行结果,可以得出:进程收到 2 号信号的默认动作,就是终止自己 。
忽略: 手机响了,选择静音,不接听,忽略这个电话。
自定义动作: 手机响了,但不想接,于是挂断电话并回复短信(自定义动作)。
自定义动作被称为信号的捕获(或捕捉) ,对于 bash 而言,不能执行2 号信号的默认动作 ,需要捕获该信号做进一步处理,否则影响用户体验。
5. 测试信号的捕捉 验证 Ctrl+C 向进程发送了 2 号信号,那么就要使用自定义方法捕获 2 号信号,这里使用 signal 系统调用。
signal 系统调用 signal 是修改特定进程对于信号的处理动作的 。
signum:信号的编号。
handler:函数指针,手册给出:
typedef void (*sighandler_t ) (int ) ;
sighandler_t 为函数指针类型,这个函数执行捕获信号后的自定义动作 ,其返回值为 void,只接受一个 int 类型的信号编号 。
void myhandler (int signo) {
if (signo == 1 ) {
} else if (signo == 2 ) {
} else if (...)
}
int main () {
signal (1 , my_handler);
signal (2 , my_handler);
}
test_signal.cpp:无限循环正常执行,只不过用户键入 Ctrl+C 后需要执行自定义动作。
#include <unistd.h>
#include <iostream>
#include <signal.h>
void myhandler (int signo) {
std::cout << "已执行自定义动作,信号编号为" << signo << "号" << std::endl;
}
int main () {
signal (SIGINT, myhandler);
for (;;) {
std::cout << "Hello World!" << std::endl;
sleep (1 );
}
return 0 ;
}
signal(SIGINT, myhandler); 只需要设置一次,后面都有效。
只有收到 2 号信号时(例如 Ctrl+C 或 kill -2 pid),myhandler 才会调用,调用 signal 不会触发 myhandler 的执行。
运行结果:test_signal.out 进程遇到 2 号信号不会执行默认动作,而是执行打印任务。
反思:能否捕获所有信号? 如果将系统中所有信号全部捕捉,执行自定义动作,例如自定义动作为无限循环,那么是不是 test_signal.out 进程无法被杀死?
#include <unistd.h>
#include <iostream>
#include <signal.h>
#include <sys/types.h>
void myhandler (int signo) {
std::cout << "已执行自定义动作,信号编号为" << signo << "号" << std::endl;
}
int main () {
for (int i = 1 ; i <= 31 ; i++) signal (i, myhandler);
std::cout << "本进程的 pid 为:" << getpid () << std::endl;
for (;;) {
std::cout << "Hello World!" << std::endl;
sleep (1 );
}
return 0 ;
}
#!/usr/bin/bash
if [ "$1 " = "" ] || [ "$1 " = " " ]
then
echo "需要提供进程的 pid,用法:./send_signal.sh pid"
exit -1
fi
pid=$1
for ((i=1 ; i<=31 ; i++))
do
kill -${i} ${pid}
sleep 0.2
done
#!/usr/bin/bash
if [ "$1 " = "" ] || [ "$1 " = " " ]
then
echo "需要提供进程的 pid,用法:./send_signal.sh pid"
exit -1
fi
pid=$1
for ((i=10 ; i<=31 ; i++))
do
kill -${i} ${pid}
sleep 0.2
done
运行结果:19 号信号 SIGSTOP 无法捕获。
#!/usr/bin/bash
if [ "$1 " = "" ] || [ "$1 " = " " ]
then
echo "需要提供进程的 pid,用法:./send_signal.sh pid"
exit -1
fi
pid=$1
for ((i=20 ; i<=31 ; i++))
do
kill -${i} ${pid}
sleep 0.2
done
结论:1~31 号普通信号,只有 9 号信号 SIGKILL、19 号信号 SIGSTOP 无法捕获。
反思:为什么进程无法捕获 9 和 19 号信号? 如果进程捕获了所有信号但是不退出,这样的进程十分危险,它一直占用操作系统的资源,而且可能执行一些危险的动作。
为了避免这样的事情发生,Linux 中设置:9 和 19 号信号无法捕获(可以理解为这两个信号是操作系统留的'后门'),分别用于强制终止进程和强制暂停进程,否则系统将彻底失去对进程的最终控制权 。
结论:内核必须在任何情况下都能无条件终止或停止进程,以保证系统的可控性和稳定性 。
Linux 内核源码验证:为什么进程无法捕获 9 和 19 号信号 linux 6.18.6 在 /kernel/signal.c 中定义:
static bool sig_task_ignored (struct task_struct *t, int sig, bool force) {
void __user *handler;
handler = sig_handler(t, sig);
if (unlikely(is_global_init(t) && sig_kernel_only(sig))) return true ;
if (unlikely(t->signal->flags & SIGNAL_UNKILLABLE) && handler == SIG_DFL && !(force && sig_kernel_only(sig))) return true ;
if (unlikely((t->flags & PF_KTHREAD) && (handler == SIG_KTHREAD_KERNEL) && !force)) return true ;
return sig_handler_ignored(handler, sig);
}
sig_kernel_only 在 include/linux/signal.h 中定义:
#define sigmask(sig) (1UL << ((sig) - 1))
#if SIGRTMIN > BITS_PER_LONG
#define rt_sigmask(sig) (1ULL << ((sig)-1))
#else
#define rt_sigmask(sig) sigmask(sig)
#endif
#define SIG_KERNEL_ONLY_MASK (rt_sigmask(SIGKILL) | rt_sigmask(SIGSTOP))
#define siginmask(sig, mask) \ ((sig) > 0 && (sig) < SIGRTMIN && (rt_sigmask(sig) & (mask)))
#define sig_kernel_only(sig) siginmask(sig, SIG_KERNEL_ONLY_MASK)
x86 下,SIGRTMIN 在 /arch/x86/include/uapi/asm/signal.h 中定义:
BITS_PER_LONG 在 /include/asm-generic/bitsperlong.h 中定义,和具体架构有关:
#ifdef CONFIG_64BIT
#define BITS_PER_LONG 64
#else
#define BITS_PER_LONG 32
#endif
sig_kernel_only(sig) == siginmask(sig, SIG_KERNEL_ONLY_MASK) == siginmask(sig, (rt_sigmask(SIGKILL) | rt_sigmask(SIGSTOP))) == siginmask(sig, ((1ULL << ((SIGKILL)-1 )) | (1ULL << ((SIGSTOP)-1 ))))
相关免费在线工具 Base64 字符串编码/解码 将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
Base64 文件转换器 将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
Markdown转HTML 将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
HTML转Markdown 将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
JSON 压缩 通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online
JSON美化和格式化 将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online