1. 信号其他相关常见概念
实际执行信号的处理动作称为信号递达(Delivery)
信号从产生到递达之间的状态,称为信号未决(Pending)。
进程可以选择阻塞 (Block )(屏蔽)某个信号。
被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作.
注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的⼀种处理动
作。
2. 在内核中的表示

每个信号都有两个标志位,分别表示阻塞(block)和未决(pending) ,这两个标志位本质上是保存收到的的信号的位图,比特位的位置表示的是第几号信号,比特位的内容表示是否阻塞。 还有一个函数指针表示处理动作。
信号产生时,内核在进程控制块中设置该信号的未决标志,直到信号递达才清除该标志。
在上图的例子中,SIGHUP信号未阻塞也未产生过,当它递达时执行默认处理动作。
-
SIGINT信号产生过,但正在被阻塞 ,所以暂时不能递达。虽然它的处理动作是忽略,但在没有解除阻塞之前不能忽略这个信号,因为进程仍有机会改变处理动作之后再解除阻塞。
-
SIGQUIT信号未产生过,一旦产生SIGQUIT信号将被阻塞 ,它的处理动作是用户自定义函数sighandler。
如果在进程解除对某信号的阻塞之前这种信号产生过多次,将如何处理?Linux是这样实现的:常规信号在递达之前产生多次只计一次,而实时信号在递达之前产生多次可以依次放在一个队列里。
cpp
#include <iostream>
#include <signal.h>
#include <unistd.h>
void handler(int signum)
{
std::cout << "signal: " << signum << std::endl;
signal(2, SIG_DFL);
std::cout << "恢复信号" << std::endl;
}
int main()
{
signal(2, handler); //自定义信号
//signal(2, SIG_IGN); //忽略信号
while (true)
{
sleep(1);
std::cout << "." << std::endl;
}
return 0;
}

3. 信号集操作函数
3.1 sigset

-
从上图来看,每个信号只有⼀个bit的未决标志,非0即1, 不记录该信号产生了多少次,阻塞标志也
是这样表示的。因此, 未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t称为信号
集,这个类型可以表示每个信号的"有效"或"无效"状态。 -
在阻塞信号集中"有效"和"无效"的含义是该信号是否被阻塞,而在未决信号集中"有 效"和"无效"的含义是该信号是否处于未决状态。
-
阻塞信号集也叫做当前进程的信号屏蔽字(Signal Mask),这里的"屏蔽"应该理解为阻塞而不是忽略。
cpp
#include <signal.h>
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset(sigset_t *set, int signo);
int sigdelset(sigset_t *set, int signo);
int sigismember(const sigset_t *set, int signo);
函数sigemptyset初始化set所指向的信号集,使其中所有信号的对应bit清零,表示该信号集不包含任何有效信号。
-
函数sigfillset初始化set所指向的信号集,使其中所有信号的对应bit置1,表示该信号集的有效信号包括系统支持的所有信号。
-
注意,在使用sigset_t类型的变量之前,一定要调用sigemptyset或sigfillset做初始化,使信号集处于确定的状态。初始化sigset_t变量之后就可以在调用sigaddset和sigdelset在该信号集中添加或删除某种有效信号。
这四个函数都是成功返回0,出错返回-1。
sigismember是一个布尔函数,用于判断一个信号集的有效信号中是否包含某种信号,若包含则返回1,不包含则返回0,出错返回-1。
3.2 sigprocmask
在 Linux 中,sigprocmask
是用于 设置或检查进程的信号屏蔽字(信号阻塞集) 的系统调用。它用于 阻塞(屏蔽)或解除阻塞某些信号,以控制进程对特定信号的处理方式。
函数原型:
cpp
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
参数说明
- how
:指定如何修改信号屏蔽字,假设当前的信号屏蔽字为mask。
cpp
SIG_BLOCK:将 set 指定的信号 加入 到当前进程的信号屏蔽字中(即阻塞这些信号)。
mask = mask | set
SIG_UNBLOCK:从当前信号屏蔽字中 移除 set 指定的信号(即解除阻塞)。
mask = mask & ~set
SIG_SETMASK:用 set 指定的信号集 直接替换 进程当前的信号屏蔽字。
mask = set
- set
:指定要修改的信号集。如果 NULL
,则不改变当前信号屏蔽字,仅用于获取当前信号屏蔽字(结合 oldset
使用)。
- oldset
:如果不为 NULL
,则 sigprocmask
在修改信号屏蔽字前,会将进程当前的信号屏蔽字 保存 到 oldset
中。
返回值
成功返回 0
。
失败返回 -1
,并设置 errno
以指示错误原因(如 EINVAL
表示 how
参数无效)。
3.3 sigpending
在 Linux 中,sigpending
用于获取当前 进程 的 未决信号集(pending signals),即那些已经产生但因被阻塞而未能递达的信号。
函数原型
cpp
#include <signal.h>
int sigpending(sigset_t *set);
参数说明
set
:指向 sigset_t
类型的信号集变量,用于存储进程当前未决的信号集合。
返回值
成功返回 0
。
失败返回 -1
,并设置 errno
以指示错误原因(通常不会失败)。

产生1到9号命令

9号进程不可被捕捉,不可被屏蔽(SIGSTIOP(19)号进程也是)
cpp
#include <iostream>
#include <signal.h>
#include <vector>
#include <functional>
#include <unistd.h>
void PrintPend(sigset_t &pending)
{
printf("我是一个进程,pid:%d, pending: ", getpid());
for (int signo = 31; signo >= 1; signo--)
{
if (sigismember(&pending, signo))
{
std::cout << "1";
}
else
{
std::cout << "0";
}
}
std::cout << std::endl;
}
void handler(int signum)
{
std::cout << "递达" << signum << "号信号" << std::endl;
std::cout << "######################" << std::endl;
sigset_t pending;
int n = sigpending(&pending);
PrintPend(pending);
std::cout << "######################" << std::endl;
}
int main()
{
signal(2, handler);
// 1. 屏蔽所有信号
sigset_t block, oblock;
sigemptyset(&block);
sigemptyset(&oblock);
sigaddset(&block, 2);
int n = sigprocmask(SIG_SETMASK, &block, &oblock);
int cnt = 0;
while (true)
{
sleep(1);
// 2. 获取pending信号集
sigset_t pending;
int m = sigpending(&pending);
// 3. 打印
PrintPend(pending);
cnt++;
if(cnt == 10)
{
std::cout << "解除对二号的屏蔽" << std::endl;
sigprocmask(SIG_SETMASK, &oblock, NULL);
}
}
return 0;
}

由上面的结果可知,当我们准备递达的时候,要首先清空pending信号集中对应的位图。
4. 终止信号core vs term
在 Linux 中,终止信号(Termination Signals) 主要用于终止进程,而不同的终止信号可以导致进程以不同的方式退出。其中,core 和 term 是两类不同的终止方式。
core
终止信号会导致进程异常退出,并在当前路径或指定目录下生成一个 core dump 文件。进程在退出前,其内存中的核心数据会被拷贝到磁盘,形成 core dump,这一过程称为核心转储(Core Dump)。
而 term
终止信号(如 SIGTERM
、SIGINT
)则仅会使进程退出,不会生成 core dump 文件。

不过在云服务器上,core dump的功能是默认关闭的,我们可以通过ulimit命令查看、修改 。
cpp
ulimit -a:查看当前用户的所有资源限制。
ulimit -c:查看 core dump 文件的大小限制(单位:blocks,0 表示不生成 core dump)。

设置core dump大小为40960

cpp
int main()
{
printf("hello, henu\n");
printf("hello, henu\n");
printf("hello, henu\n");
printf("hello, henu\n");
printf("hello, henu\n");
printf("hello, henu\n");
int a = 10;
a /= 0;
printf("hello, henu\n");
printf("hello, henu\n");
printf("hello, henu\n");
return 0;
}
在 gdb
中使用 core-file core
命令加载 core dump 文件,可以直接查看导致程序崩溃的代码行。


正常终止
当进程正常终止时,会设置一个"退出状态"。在Linux中,进程通过 exit
系列函数(如 exit()
、_exit()
)来设置退出状态。该状态通常是一个整数值,父进程可以通过 wait()
或 waitpid()
函数获取子进程的退出状态,从而判断子进程的执行结果。
退出状态 :在正常终止时,进程的退出状态通常是 0
,表示成功执行;如果退出状态是非零值,则表示程序执行过程中出现了错误。图中的"正常终止"右侧的8位设置为 0
,这表明这些位不用于存储信号信息。
被信号所杀
当进程由于接收到某个信号而终止时,退出状态包含有关信号的信息。
终止信号编号:图中的右侧8位存储了导致进程终止的信号编号。例如:
cpp
SIGINT(信号编号2):通常是由用户按下 Ctrl+C 触发的终止信号。
SIGTERM(信号编号15):用于请求进程正常终止的信号。
SIGKILL(信号编号9):强制终止进程,不能被捕获或忽略。
core dump标志 :如果进程因为信号终止,并且系统设置了生成 core dump 文件的标志(通常为 1
),系统将保存进程终止时的内存镜像和寄存器状态。这些信息对开发者调试程序异常终止问题非常有帮助。图中提到的"core dump标志"表示这一点。
未使用区域:图中提到的左侧"未用"区域表明,在信号终止的情况下,该区域未用于存储与进程终止相关的其他信息。因为信号终止本身会填充信号编号和相关信息,而这些"未用"区域不需要额外的数据。
cpp
#include <iostream>
#include <cstdio>
#include <vector>
#include <functional>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
pid_t id = fork();
if (id == 0)
{
sleep(2);
printf("hello bit\n");
printf("hello bit\n");
printf("hello bit\n");
printf("hello bit\n");
printf("hello bit\n");
int a = 10;
a /= 0;
printf("hello bit\n");
exit(1);
}
int status = 0;
waitpid(id, &status, 0);
printf("signal: %d, exit code: %d, core dump: %d\n",
(status & 0x7F), (status >> 8) & 0xFF, (status >> 7) & 0x1);
}
