1.信号周期

每个信号都有两个标志位分别表示阻塞(block)和未决(pending),还有一个函数指针表示处理动作(handler)。
1.信号 产生时,内核在进程控制块中设置该信号的pending标志为1,直到信号递达才设为0。
简单点说 信号产生pening 为1 信号到达后 pending设置为0等下一个信号产生
pending 在信号发出到递达之间为1 其他时候都为0
SIGHUP信号未阻塞也未产生过,当它递达时执行默认处理动作。
SIGINT信号产生过,但正在被阻塞,所以暂时不能递达。虽然它的处理动作是忽略,但在没有解除阻塞之前 不能忽略这个信号,因为进程仍有机会改变处理动作之后再解除阻塞。
简单点说 当阻塞的时候 信号无法递达 就无法执行handler 哪怕handler是忽略 阻塞的时候信号没有递达 所以pending一直为1
SIGQUIT信号未产生过,一旦产生SIGQUIT信号将被阻塞,它的处理动作是用户自定义函数sighandler。
如果在进程解除对某信号的阻塞之前这种信号产生过多次,将如何处理?
Linux是这样实现的:常规信号在递达之前产生多次只计一次,而实时信号在递达之前产生多次可
以依次放在一个队列里。
2.sigset_t
sigset_t是描述信号的一个类型
sigset_t(信号集)的核心实现就是位图
每一个位的序号对应信号编号(比如第 1 位对应 1 号信号 SIGHUP,第 2 位对应 2 号信号 SIGINT,...,第 31 位对应 31 号信号)。
⚠️ 注意:0 号信号是 Linux 的 "空信号"(用于检测进程是否存在),所以 sigset_t 的第 0 位通常不用,这就是为什么 32 位里只用 31 位表示信号。
sigset_t 是一个通用的 "信号位图数据类型",不是一个具体的集合 ------ 我们可以创建不同的 sigset_t 变量(实例),分别用来表示「阻塞信号集」和「未决信号集」
阻塞集:位 = 1 → 该信号被阻塞(有效 = 阻塞)
未决集:位 = 1 → 该信号未决(有效 = 未决)
3.函数接口
cpp
#include <signal.h>
1.sigemptyset
初始化空信号集
cpp
int sigemptyset(sigset_t *set);
参数:set:指向要初始化的信号集指针。
返回值:成功返回 0,失败返回 - 1(几乎不会失败,除非参数非法)。
注意:所有信号集的初始化第一步,必须先调用此函数或sigfillset。
2. sigfillset
初始化满信号集
cpp
int sigfillset(sigset_t *set);
参数:set:指向要初始化的信号集指针。
返回值:成功返回 0,失败返回 - 1(几乎不会失败,除非参数非法)。
注意:SIGKILL和SIGSTOP即使被加入信号集,也无法被阻塞(系统强制规定)。
- sigaddset
向信号集添加信号
cpp
int sigaddset(sigset_t *set, int signum);
set:已初始化的信号集指针。
signum:要添加的信号(如SIGINT、SIGQUIT,不能是 0)。
返回值:成功返回 0,失败返回 - 1。
4. sigdelset
从信号集删除信号
cpp
int sigdelset(sigset_t *set, int signum);
set:已初始化的信号集指针。
signum:要删除的信号(如SIGINT、SIGQUIT,不能是 0)。
注意:添加前必须先初始化信号集(sigemptyset/sigfillset)。
5. sigismember
判断信号是否在信号集中
cpp
int sigismember(const sigset_t *set, int signum);
set:已初始化的信号集指针。
signum:要判断是否存在的信号(如SIGINT、SIGQUIT,不能是 0)。
返回值:
1:信号存在于信号集中;
0:信号不存在;
-1:参数错误(如信号号非法)
6.sigprocmask
进程信号屏蔽字操作
cpp
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
how:修改屏蔽字的方式(三选一):
SIG_BLOCK:将set中的信号添加到当前屏蔽字(屏蔽字 = 屏蔽字 | set);
SIG_UNBLOCK:从当前屏蔽字移除set中的信号(屏蔽字 = 屏蔽字 & ~set);
SIG_SETMASK:将当前屏蔽字直接设置为set(屏蔽字 = set)。
set:要操作的信号集(NULL 表示不修改屏蔽字,仅获取当前屏蔽字)。
oldset:保存修改前的旧屏蔽字(NULL 表示不保存)。
返回值:成功返回 0,失败返回 - 1(设置errno)。
注意:
仅对单线程进程有效,线程需使用pthread_sigmask;
SIGKILL和SIGSTOP无法被阻塞,即使加入set也无效。
7.sigpending
获取未决信号集
cpp
int sigpending(sigset_t *set);
参数:set:保存未决信号集的指针。
返回值:成功返回 0,失败返回 - 1。
8.sigfillset
cpp
int sigfillset(sigset_t *set);
sigset_t *set:指向要初始化的信号集结构体的指针(sigset_t是系统定义的信号集类型,无法直接操作,必须通过信号集函数操作)。
返回0 POSIX 标准规定,该函数总是成功,不会返回错误
调用sigfillset后,传入的信号集set会被填充系统中所有的信号
此时,用sigismember判断任意信号是否在集合中,结果都是真(1)。
9.sigaction
cpp
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
为进程指定的信号(SIGKILL和SIGSTOP除外)设置新的处理方式(包括处理函数、处理时的阻塞信号集、行为规则),或获取该信号当前的处理配置,同时可保存原有配置。
signum:要处理的信号(除SIGKILL/SIGSTOP外)。
act:指向struct sigaction的指针,包含新的信号处理配置(NULL 表示不修改)。
oldact:保存旧的处理配置(NULL 表示不保存)
成功:返回0。
失败:返回-1,并设置全局变量errno标识错误原因,常见错误码:
EINVAL:signum是无效信号(如0、SIGKILL),或sa_flags包含非法标志。
EFAULT:act或oldact指向的内存地址无效(如空指针、未授权内存)。
EINTR:函数调用被其他信号中断(极少发生)。
cpp
struct sigaction {
// 1. 普通信号处理函数(二选一:与sa_sigaction互斥)
void (*sa_handler)(int);
// 2. 带额外信息的信号处理函数(需配合SA_SIGINFO标志)
void (*sa_sigaction)(int, siginfo_t *, void *);
// 3. 处理信号时的阻塞掩码(信号集)
sigset_t sa_mask;
// 4. 信号处理的行为标志(位图,可组合)
int sa_flags;
// 5. 废弃成员(现代系统无需使用)
void (*sa_restorer)(void);
};
1. sa_handler:普通信号处理函数
作用
指定信号的基础处理逻辑,是一个函数指针,参数为信号编号。

注意事项
sa_handler与sa_sigaction是互斥关系:若使用sa_handler,则sa_flags不需要设置SA_SIGINFO;若设置了SA_SIGINFO,内核会优先使用sa_sigaction。
2.sa_sigaction:带额外信息的信号处理函数
cpp
void (*sa_sigaction)(int signum, siginfo_t *info, void *ucontext);

核心辅助结构体:siginfo_t
siginfo_t是保存信号附加信息的关键结构体,核心成员如下(POSIX 标准):
cpp
typedef struct siginfo {
int si_signo; // 信号编号(与signum一致)
int si_code; // 信号触发的原因码(如SI_USER表示由用户进程发送,SI_KERNEL表示由内核发送)
pid_t si_pid; // 发送信号的进程PID(仅对进程间信号有效,如kill、sigqueue发送的信号)
uid_t si_uid; // 发送信号的进程的真实UID(同上)
void *si_value; // 信号携带的自定义数据(由sigqueue函数发送,kill函数不支持)
// 其他成员(如si_addr:触发SIGSEGV的无效内存地址,si_status:子进程退出状态等)
} siginfo_t;
作用
定义一个信号集,当当前信号的处理函数正在执行时,这个信号集中的信号会被临时阻塞(无法被处理),直到处理函数执行完毕。
3. sa_mask:处理信号时的阻塞掩码
作用
定义一个信号集,当当前信号的处理函数正在执行时,这个信号集中的信号会被临时阻塞(无法被处理),直到处理函数执行完毕。
4. sa_flags:信号处理的行为标志
作用
设置信号处理的行为规则,是一个位图(可以通过按位或|组合多个标志)。
5. sa_restorer:废弃成员
作用
早期 UNIX 系统中用于恢复进程上下文的函数指针,现代系统中已被内核替代,无需手动设置,也不要使用(该成员在部分系统中甚至被省略)。
cpp
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>
// SIGINT信号的自定义处理函数
// 【打印顺序12.5(条件触发)】:仅当用户按下Ctrl+C且解除SIGINT阻塞时,才会执行此函数的打印
// 该打印会插入在"打印顺序12"和"打印顺序13"之间
void sigint_handler(int signum) {
printf("\nHandler: Received SIGINT(%d)\n", signum);
}
// 辅助函数:打印信号集中SIGINT和SIGQUIT的状态
// 被多次调用,每次调用都会输出对应信号状态,顺序随主流程编号
void print_sigset(const sigset_t *s) {
printf(" SIGINT: %s | SIGQUIT: %s\n",
sigismember(s, SIGINT) ? "YES" : "NO",
sigismember(s, SIGQUIT) ? "YES" : "NO");
}
int main() {
sigset_t set, mask, pending; // 定义信号集变量
struct sigaction sa; // 定义信号动作结构体
// ====================== 打印顺序1:信号集初始化(空集) ======================
sigemptyset(&set); // 初始化信号集为空
printf("After sigemptyset:\n"); // 打印提示
print_sigset(&set); // 打印信号集状态(SIGINT和SIGQUIT均为NO)
// ====================== 打印顺序2:添加SIGINT到信号集 ======================
sigaddset(&set, SIGINT); // 向信号集添加SIGINT
printf("\nAfter sigaddset(SIGINT):\n"); // 打印提示
print_sigset(&set); // 打印状态(SIGINT=YES,SIGQUIT=NO)
// ====================== 打印顺序3:添加SIGQUIT到信号集 ======================
sigaddset(&set, SIGQUIT); // 向信号集添加SIGQUIT
printf("\nAfter sigaddset(SIGQUIT):\n"); // 打印提示
print_sigset(&set); // 打印状态(SIGINT=YES,SIGQUIT=YES)
// ====================== 打印顺序4:从信号集删除SIGQUIT ======================
sigdelset(&set, SIGQUIT); // 从信号集删除SIGQUIT
printf("\nAfter sigdelset(SIGQUIT):\n"); // 打印提示
print_sigset(&set); // 打印状态(SIGINT=YES,SIGQUIT=NO)
// ====================== 打印顺序5:将所有信号加入信号集 ======================
sigfillset(&set); // 把所有信号加入信号集
printf("\nAfter sigfillset:\n"); // 打印提示
print_sigset(&set); // 打印状态(SIGINT=YES,SIGQUIT=YES)
// ====================== 打印顺序6:判断SIGINT是否在信号集中 ======================
if (sigismember(&set, SIGINT)) { // 判断SIGINT是否在集合中(必然成立)
printf(" sigismember: SIGINT in set\n"); // 打印提示
}
// ====================== 打印顺序7:设置SIGINT处理函数成功 ======================
sa.sa_handler = sigint_handler; // 指定自定义处理函数
sigemptyset(&sa.sa_mask); // 处理信号时的阻塞集为空
sa.sa_flags = 0; // 无特殊标志
if (sigaction(SIGINT, &sa, NULL) == -1) { // 设置SIGINT的处理动作
perror("sigaction");
exit(1);
}
printf("\nsigaction: Set SIGINT handler ok\n"); // 打印设置成功提示
// ====================== 打印顺序8:阻塞SIGINT成功 ======================
sigemptyset(&mask); // 初始化掩码集为空
sigaddset(&mask, SIGINT); // 掩码集添加SIGINT
if (sigprocmask(SIG_BLOCK, &mask, NULL) == -1) { // 阻塞SIGINT
perror("sigprocmask");
exit(1);
}
printf("\nsigprocmask: Blocked SIGINT\n"); // 打印阻塞成功提示
// ====================== 打印顺序9:提示用户按下Ctrl+C ======================
printf("Press Ctrl+C within 5s...\n"); // 打印提示语
sleep(5); // 暂停5秒,等待用户操作(按或不按Ctrl+C)
// ====================== 打印顺序10:打印未决信号集状态 ======================
sigpending(&pending); // 获取当前未决信号集
printf("\nsigpending: Pending signals:\n"); // 打印提示
print_sigset(&pending); // 打印未决信号状态(用户按了则SIGINT=YES,否则NO)
// ====================== 打印顺序11:提示SIGINT是否未决 ======================
printf(" SIGINT %s pending\n", sigismember(&pending, SIGINT) ? "is" : "not");
// ====================== 打印顺序12:解除SIGINT阻塞成功 ======================
if (sigprocmask(SIG_UNBLOCK, &mask, NULL) == -1) { // 解除SIGINT阻塞
perror("sigprocmask");
exit(1);
}
printf("\nsigprocmask: Unblocked SIGINT\n"); // 打印解除阻塞提示
// 【关键触发点】若用户之前按下了Ctrl+C,此时内核会立即调用sigint_handler
// 执行"打印顺序12.5":输出Handler的内容,之后再执行打印顺序13
// 若用户未按,则直接执行打印顺序13
// ====================== 打印顺序13:解除阻塞后打印未决信号集状态 ======================
sigpending(&pending); // 再次获取未决信号集
printf("\nsigpending after unblock:\n"); // 打印提示
print_sigset(&pending); // 打印状态(SIGINT必然为NO,因为已处理或未产生)
return 0;
}

信号集操作阶段:依次打印空集、添加 SIGINT、添加 SIGQUIT、删除 SIGQUIT、填充集的状态,验证信号集操作的正确性。
设置信号处理函数:提示 SIGINT 处理函数设置成功。
阻塞 SIGINT 并等待:提示用户 5 秒内按 Ctrl+C。
如果用户按了 Ctrl+C:SIGINT 被阻塞,进入未决状态,sigpending会显示 SIGINT 是未决的。
如果用户没按 Ctrl+C:sigpending会显示 SIGINT 未决。
解除 SIGINT 阻塞:此时未决的 SIGINT 会被立即处理(执行sigint_handler,打印信号信息),随后再次检查未决信号集,SIGINT 已不存在。
4.信号处理
信号一般是在进程从内核态返回用户态的时候处理


进程通过sigaction()/signal()函数,向内核 "登记":当收到SIGQUIT信号(对应键盘的Ctrl+\,默认动作是终止进程并生成核心转储文件)时,不执行内核的默认动作,而是执行进程自己写的sighandler函数(用户空间代码)。
内核会记录这个 "信号 - 处理函数" 的映射关系,后续收到该信号时,会按这个映射处理。
进程原本在用户态执行main函数的业务逻辑,触发以下场景时会切换到内核态(这是进入内核的唯一合法途径):
中断:比如用户按Ctrl+\触发键盘硬件中断、硬盘读写完成的中断;
异常:比如进程访问非法内存、除零错误;
系统调用:比如进程调用read()/sleep()等接口。
内核先处理完上述中断 / 异常(比如处理键盘中断、修复异常);
内核准备从内核态返回用户态,恢复main函数执行前,会执行一个关键操作:检查当前进程的未决信号队列(是否有已发送、未阻塞、待处理的信号,即 "信号递达");
此时内核发现SIGQUIT信号需要处理(递达),且进程已注册了自定义处理函数,于是内核决定改变返回路径。
内核决定返回用户态后不是恢复 main 函数的上下文继续执行,而是执行 sighandler 函数,sighandler 和 main 函数使用不同的堆栈空间,它们之间不存在调用和被调用的关系,是两个独立的控制流程。
sighandler执行完毕后,并不会直接回到main函数,而是触发一个内核预定义的特殊系统调用sigreturn:
这个系统调用是 "自动执行" 的(无需程序员手动写),作用是告诉内核:信号处理函数执行完了,请恢复原流程;
执行sigreturn的过程,就是进程从用户态再次切换到内核态的过程,内核会处理这个系统调用的逻辑。
内核处理完sigreturn系统调用后,会再次检查进程的未决信号队列:
若没有新的信号需要处理,内核会恢复之前保存的main函数的执行上下文;
进程从内核态切换回用户态,继续执行main函数中被中断的那一行代码。
当某个信号的处理函数被调用时,内核自动将当前信号加入进程的信号屏蔽字,当信号处理函数返回时自动恢复原来 的信号屏蔽字,
这样就保证了在处理某个信号时,如果这种信号再次产生,那么 它会被阻塞到当前处理结束为止。
如果 在调用信号处理函数时,除了当前信号被自动屏蔽之外,还希望自动屏蔽另外一些信号,则用sa_mask字段说明这些需(sigaction函数) 要额外屏蔽的信号,
当信号处理函数返回时自动恢复原来的信号屏蔽字。
这段话是解释普通信号重复产生时通常只会被处理一次(接收一个) 的核心依据
普通信号
信号屏蔽导致阻塞:当第一个普通信号的处理函数被调用时,内核自动将该信号加入进程的信号屏蔽字。此时你发送的后 2 个相同普通信号,会被内核标记为 **"未决状态"**(无法递达,即阻塞)。
普通信号的未决队列是 "单值型":内核对于普通信号的未决状态,只记录 "是否存在该信号未决",不会记录产生的次数。也就是说,不管阻塞期间来了 1 个、2 个还是 100 个相同的普通信号,内核都只记 "有 1 个该信号未决"。
阻塞结束后的处理:当信号处理函数返回,屏蔽字恢复后,内核会处理这个 "未决的普通信号"(仅 1 次),之后未决状态被清除。原本的 2 个被阻塞的信号,其实已经被合并成了 1 个,因此只会处理 1 次,剩下的相当于 "消失了"。
实时信号
信号屏蔽导致阻塞:和普通信号一样,第一个实时信号的处理函数调用时,该信号被自动屏蔽,后 2 个相同实时信号会进入未决状态(阻塞)。
实时信号的未决队列是 "队列型":内核对于实时信号的未决状态,会按产生顺序排队记录,保留每一个信号的实例(只要队列没满)。也就是说,阻塞期间来了 2 个实时信号,内核会在未决队列里存 2 个该信号的节点。
阻塞结束后的处理:当屏蔽字恢复后,内核会按照排队顺序,依次处理这 2 个未决的实时信号,最终 3 个信号都会被处理(第一个处理函数执行时处理,后两个阻塞后依次处理)。
5.再谈volatile
volatile是编程语言(C/C++/CUDA 等)中的类型修饰符,核心作用是禁止编译器对变量进行激进的优化,强制每次读写该变量时都直接操作内存(物理内存 / 共享内存 / 寄存器),而非缓存到 CPU/GPU 的寄存器中。
编译器优化就会直接从cpu寄存器读取 而不是从内存中读取
不需要访问内存 所以效率更高
写变量 改变的是内存的 不会改变cpu的
比如下面这个例子
flag被优化了
ctrl c的时候
handler函数会改变内存中flag的值
但是对于cpu寄存来说flag还是0
循环就无法停止
我们发送信号就无法达到我们想要的效果
要解决这个问题也很简单 给flag加上关键字volatile
cpp
#include<iostream>
#include <unistd.h>
#include <signal.h>
using namespace std;
volatile int flag = 0;
void handler(int signo)
{
cout << "catch a signal: " << signo << endl;
flag = 1;
}
int main()
{
signal(2, handler);
// 在优化条件下, flag变量可能被直接优化到CPU内的寄存器中
while(!flag); // flag 0,!flag 真
cout << "process quit normal" << endl;
return 0;
}

6.SIGCHLD****信号
触发条件:当子进程发生以下状态变化时,内核会自动向其父进程发送 SIGCHLD 信号:
子进程终止(如调用exit、被信号杀死);
子进程被暂停(如收到SIGSTOP信号);
子进程被恢复运行(如收到SIGCONT信号)
SIGCHLD 的默认处理动作是 "忽略(SIG_IGN)"------ 即父进程若不主动处理该信号,内核不会强制父进程做任何操作。
但这会带来问题:若子进程终止后,父进程既不处理 SIGCHLD,也未调用wait/waitpid回收子进程资源,子进程会变成僵尸进程(Z 状态),残留 PCB(进程控制块)占用系统资源。
SIGCHLD 的主要价值是让父进程 "异步感知子进程的状态变化",从而在不阻塞自身工作的前提下,回收子进程资源:
父进程可以捕获 SIGCHLD 信号,在信号处理函数中调用waitpid(推荐)或wait,主动回收已终止的子进程,避免僵尸进程产生。
前面讲过用wait和waitpid函数清理僵尸进程,父进程可以阻塞等待子进程结束,也可以非阻 塞地查询是否有子进 程结束等待清理(也就是轮询的方式)。
采用第一种方式,父进程阻塞了就不 能处理自己的工作了;
采用第二种方式,父 进程在处理自己的工作的同时还要记得时不时地轮询一 下,程序实现复杂。
其实,子进程在终止时会给父进程发SIGCHLD信号,该信号的默认处理动作是忽略,父进程可以自 定义SIGCHLD信号 的处理函数,
这样父进程只需专心处理自己的工作,不必关心子进程了,子进程 终止时会通知父进程,父进程在信号处理
函数中调用wait清理子进程即可。
事实上,由于UNIX 的历史原因,要想不产生僵尸进程还有另外一种办法:父进程调 用sigaction将SIGCHLD的处理动作 置为SIG_IGN,这样fork出来的子进程在终止时会自动清理掉,不 会产生僵尸进程,也不会通知父进程。
系统默认的忽 略动作和用户用sigaction函数自定义的忽略 通常是没有区别的,但这是一个特例。
此方法对于Linux可用,但不保证 在其它UNIX系统上都可 用。
cpp
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
#include <sys/wait.h>
void handler(int sig)
{
pid_t id;
while( (id = waitpid(-1, NULL, WNOHANG)) > 0 ){
printf("wait child success: %d\n", id);
}
printf("child is quit! %d\n", getpid());
}
int main()
{
signal(SIGCHLD, handler);
pid_t cid;
if( (cid = fork()) == 0 ){
printf("child : %d\n", getpid());
sleep(3);
exit(1);
}
while(1){
printf("father proc is doing some thing!\n");
sleep(1);
}
return 0;
}
像这个代码子进程结束就没有zombie状态

下面这个代码也不会有zombie状态
cpp
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
int main()
{
signal(17, SIG_IGN);
pid_t cid;
if ((cid = fork()) == 0) {
printf("child : %d\n", getpid());
sleep(3);
exit(1);
}
while (1) {
printf("father proc is doing some thing!\n");
sleep(1);
}
return 0;
}
父进程显式将SIGCHLD信号(17 号信号)的处理动作设为SIG_IGN(忽略),在 Linux 系统下,内核会自动回收该父进程的子进程资源。
虽然SIGCHLD信号默认处理方式也是忽略 但是和我们signal忽略本质上是不一样的
这是两者最关键的区别:
默认忽略SIGCHLD:
父进程未主动设置SIGCHLD的处理方式,使用系统默认的 "忽略" 动作。此时子进程终止后,若父进程未调用wait/waitpid回收资源,子进程会残留为僵尸进程(Z 状态)------ 其 PCB(进程控制块)仍占用系统资源,直到父进程退出后由 init 进程回收。
(本质:默认忽略只是 "父进程不处理 SIGCHLD 信号",但内核不会自动回收子进程资源)
显式设置SIG_IGN(Linux 系统):
父进程通过signal(SIGCHLD, SIG_IGN)或sigaction主动将SIGCHLD设为 "忽略"。
此时子进程终止后,Linux 内核会自动回收其子进程的资源,不会产生僵尸进程。
(本质:这是 Linux 的特例,显式忽略SIGCHLD会触发内核的自动回收逻辑)
信号通知的感知
两者的父进程都不会收到SIGCHLD信号(因为处理动作是 "忽略"),但资源处理的逻辑完全不同:
默认忽略:信号被忽略,但子进程资源未被回收;
显式SIG_IGN:信号被忽略,且子进程资源被内核自动回收。