目录
一、信号概念:
二、信号的常见状态:
信号递达:
信号未决:
阻塞信号:
忽略信号:
信号在内核中的表示:
三、信号相关函数:
sigset_t (类型):
信号集操作函数:
signal() 函数:
sigpending() 函数:
sigaction() 函数:
sigprocmask() 函数:
四、补充:SIGCHLD信号
一、信号概念:
在Linux系统中,信号(signal)是一种软件中断,它提供了一种机制来通知进程发生了某个事件。当系统检测到某种异常或特殊事件时,会向相关的进程发送一个信号。进程可以指定对每种信号的处理方式,包括忽略信号、捕捉信号、以及执行默认操作。
在Linux终端中我们可以通过 kill -l 来查询所有的信号:
每一个信号都有一个名字和编号,都以SIG开头,其实本质上就是一个宏,例如2号信号其实就是:#define SIGINT 2 这些信号被定义在 signal.h 文件中。
非实时信号:(1 - 31号信号),也叫不可靠信号:
它们的主要特点是不支持排队,即如果发送多次相同的信号,进程可能只能收到一次,信号可能会丢失。
实时信号:(32 - 64号信号),也叫可靠信号:
它们的主要特点是支持排队,即如果发送多次相同的实时信号,进程可以收到多次,信号不会丢失。(一般用来实现更复杂的进程间通信和同步机制)
所以这篇主要围绕非实时信号做文章:
Core Dump(核心转储)
概念:
在OS中,当一个程序异常终止时(例如内存非法访问导致的段错误),OS会将此时进程地址空间的内容以及有关进程状态的其他信息写成一个磁盘文件(文件名一般为core)。这就是核心转储。
在事后可以通过调试器检查croe文件弄清错误原因,但是OS设置默认是不允许产生croe文件的,因为其中可能包含用户的一些隐私信息,不安全,可以通过ulimit命令修改成允许产生croe文件
可以在终端输入 ulimit -c 命令,如果结果为0,就是默认关闭croe dump,就算程序异常终止也不会产生croe文件。
可以通过ulimit -c unlimited开启croe dump功能,不限制croe文件的大小,如果要限制,就将unlimited改成你想要的文件大小(KB)即可,但注意,该命令的生命周期只在当前终端有效。
二、信号的常见状态:
信号递达:
信号递达就是指当信号被发送和接收后,信号的处理过程,当一个信号被发送到一个进程时,需要经过多个步骤才能被接收并处理。
例如给进程A发送系统调用kill():
1:发送信号:OS产生信号,准备发送到进程A。
2:信号递达:进程A收到信号的过程。
2:信号处理:进程A收到信号,并根据信号的类型和处理方式来进行对应的处理
注意:
在信号递达和信号处理的过程中,可能会发生信号丢失或者信号被阻塞的情况,当一个进程处于阻塞状态时,它无法收到任何信号,直到解除阻塞状态,如果有多个信号同时到达进程时会出现信号排队的情况,此时进程会按照一定的规则来处理这些信号。
信号未决:
信号未决(Pending Signal)指的是一个进程接收到但尚未处理的信号。当一个信号被发送给一个进程时,如果该进程当前正在执行某个信号处理函数或者该信号已经处于未决状态,则该信号会被放入进程的信号未决位集(Pending Signal Mask,简称pending表)中,等待进程从信号处理函数返回后进行处理。
在pending表中,每个位代表一个信号默认为0,如果该位为1,则表示该位对应的信号处于未决状态。一个进程可以通过sigpending()系统调用来查询自己的信号未决位集。
如果一个进程接收到多个同类型的信号并且信号处理函数尚未返回,则这些信号将被合并成一个信号,并只记录一次信号未决。
当进程解除信号阻塞状态后,它必须处理所有未决的信号,否则这些信号将继续被保留在信号未决位集中,可能会导致信号丢失或者其他问题。所以要注意及时处理所有未决信号,避免信号积压导致系统异常。
阻塞信号:
阻塞信号是指进程可以选择暂时延迟处理某些特定信号的传递和处理。在Linux中,进程可以通过设置信号阻塞掩码(Signal Mask)来达到这一目的。
当一个信号被发送给一个进程时,内核会首先检查该信号是否在进程的信号阻塞掩码中。如果信号在阻塞掩码中,则该信号将被暂时挂起,直到该信号从阻塞状态解除后才能被处理。可以使用sigprocmask()系统调用来修改信号阻塞掩码。
当进程解除对某个信号的阻塞时,如果有多个该类型的信号在阻塞期间到达,那么这些信号将按照某种规则进行排队,等待进程逐个处理。另外,即使信号被阻塞,但仍然会记录在信号未决位集中,等待进程除阻塞后处理。
忽略信号:
忽略信号是指进程可以选择不对某些特定信号进行处理,即忽略该信号的传递和默认处理行为。在Linux中,进程可以通过设置信号处理函数为SIG IGN来达到这一目的。
当一个信号被发送给一个进程时,内核会首先检查该信号的处理方式。如果进程将该信号的处理函数设置为SIGIGN(忽略信号),则内核将不对该信号进行任何处理,直接丢弃该信号。
但是并不是所有的信号都可以被忽略或捕捉的。如SIGKILL和SIGSTOP,它们具有固定的处理行为。防止用户特意屏蔽所有信号导致进程无法退出
信号在内核中的表示:
示意图:
如图:
SIGHUP信号未阻塞也未产生过,当它递达时执行默认处理动作(SIG_DFL)。
SIGINT信号产生过,但正在被阻塞,所以暂时不能递达。虽然它的处理动作是忽略,但在没有解除阻塞之前不能忽略这个信号,因为进程仍有机会改变处理动作之后再解除阻塞。
SIGQUIT信号未产生过,一旦产生SIGQUIT信号将被阻塞,它的处理动作是用户自定义函数sighandler()。
三、信号相关函数:
sigset_t (类型):
sigset_t是一个数据类型,用于表示一个信号集,他是由一个位图组成的数据结构,用于跟踪和管理多个信号的状态,通常以无符号整数或者数组的形式实现
在 Linux 中,sigset_t 是通过使用位操作来表示信号集的。每个信号对应 sigset_t中的一个位(bit),如果某个位被设置为 1,则表示相应的信号在该信号集中被包含;如果某个位被设置为0,则表示相应的信号在该信号集中不被包含。
信号集操作函数:
上述函数都有相同的返回值:成功返回 0,失败返回 -1,并设置相应的错误码(errno)
signal() 函数:
signal() 函数是一个用于信号处理的POSIX标准C函数,该函数的主要作用是向操作系统注册一个信号处理函数,用于在特定信号发生时执行相应的处理逻辑。
#include <signal.h>
void (*signal(int sig, void (*func)(int)))(int);
- sig:指定要处理的信号编号。
- func:指向一个函数的指针,该函数在信号发生时被调用,其返回类型为void,参数为int类型,表示接收到的信号编号。
- 返回值:返回之前对 sig 信号的处理函数指针,如果没有则返回SIG_ERR,并设置errno。
代码演示:(使用signal函数捕捉SIG_INT,并设置handler函数自定义处理)
#include <iostream>
#include <unistd.h> // getpid
#include <signal.h> // signal函数头文件
void handler(int signum)
{
std::cout << "signum: " << signum << std::endl;
}
int main()
{
// 捕捉2号信号并进行自定义函数处理
if (signal(SIGINT, handler) == SIG_ERR)
{
perror("signal error!");
exit(1);
}
std::cout << "pid: " << getpid() << std::endl;
while (true)
{
sleep(1);
}
return 0;
}
sigpending() 函数:
sigpending() 函数是Linux系统中的一个系统调用,用于查询当前进程的未决信号集合。
未决信号集和:指已经发送给进程但尚未被处理的信号。这些信号可能因为被屏蔽而无法立即传递给进程,但会停留在待处理状态,等待进程解除对它们的阻塞时进行处理。
#include <signal.h>
int sigpending(sigset_t *set);
set:指向一个sigset_t类型的信号集合的指针,用于存储当前进程的未决信号集合。调用成功后,该信号集合将包含所有未决信号的集合。
返回值:成功返回 0,失败返回 -1。
该函数可以在下两个函数中用到,这里就将代码融入到下面。
sigaction() 函数:
sigaction()函数UNIX 和类 UNIX 系统(如 Linux)中用于检查或改变与信号相关联的处理动作的函数。该函数允许程序捕获由系统产生的信号,并定义这些信号的处理方式(如忽略、终止程序或执行用户定义的函数等)。
#include <signal.h>
int sigaction(int sig, const struct sigaction *act, struct sigaction *oact);
- sig:指定要操作的信号的编号。这是一个整型值,对应于不同的信号(如 SIGINT 表示中断信号, SIGTERM 表示终止请求等)。
- act:指向 struct sigaction 结构的指针,该结构定义了新的信号处理动作。如果此参数为 NULL,则不会改变信号的处理方式。
- oact:如果非 NULL,则指向的 struct sigaction 结构会被填充为该信号当前的处理方式。
- 返回值:成功时返回 0;失败时返回 -1 并设置 errno。
struct sigaction 结构:
struct sigaction {
void (*sa_handler)(int);
void (*sa_sigaction)(int, siginfo_t *, void *);
sigset_t sa_mask;
int sa_flags;
void (*sa_restorer)(void); // 历史遗留字段,可直接忽略。
};
- sa_handler:一个函数指针,指向信号处理函数。当信号发生时,会调用这个函数。如果设置为 SIG_IGN,则忽略该信号;如果设置为 SIG_DFL,则使用信号的默认处理方式。
- sa_sigaction:与 sa_handler 类似,但它提供了更多的上下文信息,包括信号的 siginfo_t 结构和上下文指针(ucontext_t)。要使用 sa_sigaction,sa_flags 必须包含 SA_SIGINFO 标志。
- sa_mask:在信号处理函数执行期间,系统会阻塞此信号集中包含的信号。这有助于防止在信号处理函数执行期间发生信号重入问题。
- sa_flags:控制信号处理的各种选项。常见的选项包括 SA_RESTART(如果信号处理函数返回后,被中断的系统调用会自动重启),SA_NODEFER(防止信号被自动阻塞),以及 SA_SIGINFO(使用 sa_sigaction 而不是 sa_handler)。
代码演示:(使用sigaction函数捕捉SIG_INT,并设置handler函数自定义处理,打印pindeng表检测2号信号进入信号未决状态。)
#include <iostream>
#include <signal.h>
#include <unistd.h> // getpid
void Print(sigset_t &pending)
{
for (int sig = 31; sig > 0; sig--)
{
if (sigismember(&pending, sig)) // 检测特定对应的信号是否在pending集中
{
std::cout << 1;
}
else
{
std::cout << 0;
}
}
std::cout << std::endl;
}
int main() {
sigset_t newset, oldset;
// 设置要阻塞的信号集
sigemptyset(&newset);
sigaddset(&newset, SIGINT);
// 阻塞 SIGINT 信号
if (sigprocmask(SIG_BLOCK, &newset, &oldset) == -1) {
perror("sigprocmask");
return 1;
}
printf("SIGINT is blocked. Press Ctrl+C to send the signal.\n");
// 挂起进程,等待信号到达
while(true)
{
sigset_t pending;
sigpending(&pending);
Print(pending);
sleep(2);
}
// 恢复原来的信号屏蔽字
if (sigprocmask(SIG_SETMASK, &oldset, NULL) == -1) {
perror("sigprocmask");
return 1;
}
printf("SIGINT is unblocked. Signal handling resumed.\n");
return 0;
}
sigprocmask() 函数:
sigprocmask() 函数用于修改进程的信号屏蔽字集合,进而控制哪些信号可以被进程接收和处理。
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
- how:指定对信号屏蔽字的操作方式,可以是以下三个值之一:
- SIG_BLOCK:将set指向的信号集合添加到当前进程的信号屏蔽字中。
- SIG_UNBLOCK:从当前进程的信号屏蔽字中移除set指向的信号集合。
- SIG_SETMASK:将当前进程的信号屏蔽字设置为set指向的信号集合。
- set:指向一个信号集合的指针,用于指定需要修改的信号集合。根据how参数的不同,这个集合的意义也会有所不同。
- oldset:是一个可选的输出参数,用于获取调用sigprocmask函数前的旧的信号屏蔽字。如果不需要获取旧的信号屏蔽字,可以将其设置为NULL。
- 返回值:成功返回 0,失败返回 -1,并设置errno。
代码演示:(使用sigprocmask()函数屏蔽SIG_INT信号,并打印pindeng表检测2号信号进入信号未决状态。)
#include <iostream>
#include <signal.h>
#include <unistd.h> // getpid
void Print(sigset_t &pending)
{
for (int sig = 31; sig > 0; sig--)
{
if (sigismember(&pending, sig)) // 检测特定对应的信号是否在pending集中
{
std::cout << 1;
}
else
{
std::cout << 0;
}
}
std::cout << std::endl;
}
int main()
{
sigset_t newset, oldset;
// 清空信号集
sigemptyset(&newset);
// 添加2号信号
sigaddset(&newset, SIGINT);
// 屏蔽信号集
if (sigprocmask(SIG_BLOCK, &newset, &oldset) == -1)
{
perror("sigprocmask");
return 1;
}
std::cout << "Signal No.2 successfully blocked!" << "\n"
<< "pid: " << getpid() << std::endl;
// 挂起进程,等待信号到达
while (true)
{
sleep(2);
sigset_t pending;
sigpending(&pending);
Print(pending);
}
// 恢复原来的信号屏蔽字
if (sigprocmask(SIG_SETMASK, &oldset, NULL) == -1)
{
perror("sigprocmask");
return 1;
}
return 0;
}
四、补充:SIGCHLD信号
这里简单补充一下SIGCHLD信号:
我们都知道在子进程结束前,父进程要等待子进程的结束并进行资源回收,否则会产生僵尸进程,父进程可以阻塞等待子进程结束,也可以非阻塞地查询是否有子进程结束等待清理(也就是轮询的方式)。
采用第一种方式,父进程阻塞了就不能处理自己的工作了;
采用第二种方式,父进程在处理自己的工作的同时还要记得时不时地轮询一 下,程序实现复杂。
其实,子进程在终止时会给父进程发SIGCHLD信号,该信号的默认处理动作是忽略,父进程可以自定义SIGCHLD信号的处理函数,这样父进程只需专心处理自己的工作,不必关心子进程了,子进程终止时会通知父进程,父进程在信号处理函数中调用wait清理子进程即可。
简单实现的话,代码形式如下:
执行图: