目录
记得点赞!!!
信号简介
什么是信号? 在计算机科学领域,信号是一种用于通知进程或线程发生了某些事件的一种机制。它是一种异步通信方式,用于在不同的执行上下文之间传递信息。信号通常用于处理外部事件,如用户输入、硬件故障或其他进程的状态变化。
为何需要信号? 信号在操作系统中的应用广泛,主要有以下几个用途:
- 异步通知:允许一个进程或线程在不中断其正常执行的情况下接收通知,以便采取适当的措施。
- 进程间通信:进程可以使用信号来通知其他进程发生的事件,这对于多任务协作非常有用。
- 异常处理:操作系统可以使用信号来通知进程发生了异常情况,如除以零或无效内存访问,以帮助进程处理这些异常。
- 用户交互:用户可以通过键盘快捷键或类似的输入方式发送信号给正在运行的进程,以请求特定操作。
信号的生活类比
想象一下你正在开车,而你的手机接收到一条短信。这条短信就类似于信号。它是一个异步的通知,告诉你有一条新消息,但你可以决定何时查看消息,而不必立刻停下来查看。同样,计算机中的信号也是一种异步通知方式,它可以告知进程发生了某些事件,而进程可以选择在合适的时候处理这些事件,而不必立即中断其正常执行。
Linux中的信号
Linux中的信号概念
在Linux中,信号是一种轻量级的进程间通信机制,用于通知进程发生了某些事件或异常。每个信号都有一个唯一的编号,以及一个关联的名称和含义。信号可以用于与进程通信,中断正在执行的进程,以及处理各种事件。
常见信号列表:使用kill -l命令查看系统定义的信号
你可以使用kill -l
命令来查看系统定义的常见信号列表,这些信号通常包括:
信号的分类
标准信号与实时信号 信号分为两类:标准信号和实时信号。
- 标准信号:标准信号是Linux中最常见的信号类型,用于通知进程发生的一般事件或异常,如中断、退出、终止等。标准信号的编号从1到31。
- 实时信号:实时信号引入了更多的灵活性,用于更复杂的通信需求。实时信号的编号从32到63。它们以
SIGRTMIN
和SIGRTMAX
为标志的宏定义表示。
信号编号及其含义 在Linux系统中,每个信号都有一个唯一的编号,以及一个关联的名称和含义。以下是一些常见的信号及其含义:
- SIGHUP (1):挂起信号,通常在终端连接断开时发送给进程。
- SIGINT (2):中断信号,通常由用户按下Ctrl+C发送给进程以中断其执行。
- SIGQUIT (3):退出信号,通常由用户按下Ctrl+\发送给进程,导致进程退出并生成核心转储文件。
- SIGKILL (9):强制终止信号,无法被捕获、忽略或阻塞,用于立即终止进程。
- SIGTERM (15):终止信号,通常用于请求进程正常退出。
- SIGUSR1 (30):用户自定义信号1,通常由用户自定义用途。
- SIGUSR2 (31):用户自定义信号2,通常由用户自定义用途。
- 以及更多信号...
信号的处理机制
进程的信号处理机制是一个非常重要的概念,涉及进程如何识别和处理接收到的信号。以下是关于进程信号处理机制的详细信息:
1. 进程的信号处理能力:
- 进程在创建时具备了信号处理能力,这是由操作系统为每个进程提供的基本功能。无论是否有信号发生,进程都能够处理它们。
2. 识别对应的信号:
- 操作系统为每个信号分配一个唯一的编号(称为信号号码),并为每个信号提供了一个名称和含义。进程可以使用这些编号、名称和含义来识别不同类型的信号。
3. 处理对应信号:
- 进程可以通过注册信号处理程序(Signal Handler)来定义对特定信号的处理方式。信号处理程序是用户定义的函数,当特定信号到达时,操作系统会自动调用该函数。
- 进程可以采取以下三种处理方式来处理信号:
- 执行默认操作:对于每个信号,操作系统定义了默认的处理方式,进程可以选择接受操作系统的默认处理。
- 忽略信号:进程可以选择忽略特定信号,使其对该信号不做任何响应。
- 自定义信号处理程序:进程可以为特定信号注册一个自定义的信号处理程序,以在信号到达时执行特定的操作。
信号的产生方式
-
kill信号
- 通过
kill
命令或kill()
系统调用,一个进程可以向另一个进程发送信号。这种方式通常用于进程间通信和进程管理。 - 例如,
kill -SIGTERM <pid>
命令可以用于请求指定进程以正常方式终止。这里,<pid>
是目标进程的进程ID。 kill()
系统调用允许进程在代码中发送信号,例如:#include <signal.h> int kill(pid_t pid, int sig);
- 这允许进程发送特定信号(
sig
)给指定进程(由pid
表示)。
- 通过
-
系统调用
- 系统调用是用于与操作系统交互的接口。有多个系统调用和库函数可以用于生成信号。
raise()
系统调用用于在自己的代码中生成信号,例如:#include <signal.h> int raise(int sig);
signal()
库函数用于注册信号处理程序,例如:
它允许进程定义对特定信号(#include <signal.h> void (*signal(int signum, void (*handler)(int)))(int);
signum
)的自定义信号处理程序。abort()
库函数用于生成SIGABRT
信号,它通常用于异常终止程序。
-
软件条件
- 一些系统调用允许进程设置条件来触发信号产生。例如,
alarm()
系统调用用于设置定时器,它在指定的时间后生成SIGALRM
信号,可以用于执行定时操作或周期性任务。
- 一些系统调用允许进程设置条件来触发信号产生。例如,
-
硬件崩溃导致异常
- 一些信号是由硬件引发的,通常是由硬件异常引发的,如访问无效内存地址。
- 例如,访问无效内存地址或进行非法操作时,硬件(通常由内存管理单元或MMU处理)可以引发
SIGSEGV
(段错误)信号,通知操作系统和进程发生了错误。 - 操作系统捕获这些硬件异常并生成相应的信号,以允许进程采取适当的措施。
信号集
了解阻塞信号集(Blocked Signal Set)、等待信号集(Pending Signal Set)以及信号处理程序(Signal Handler)之间的关系对于理解Linux中的信号处理机制至关重要。以下是它们之间的关系:
1. 阻塞信号集(Blocked Signal Set)
- 阻塞信号集是进程设置的信号集,其中包含了一组信号。这些信号在阻塞集中的信号不会被立即传递给进程。
- 阻塞信号集用于控制哪些信号会被阻止(阻塞),以便在进程执行关键操作或需要禁用某些信号的情况下使用。
- 进程可以使用系统调用
sigprocmask
或sigblock
来设置或修改阻塞信号集,从而控制阻塞的信号。 - 阻塞信号集允许进程在某些情况下推迟处理特定信号,直到它们从阻塞中解除。
2. 等待信号集(Pending Signal Set)
- 等待信号集包含了已经被发送到进程但尚未被处理的信号集合。
- 当进程收到一个信号时,该信号会首先进入等待信号集,表示它已经发送到进程,但尚未被处理。
- 进程可以使用系统调用
sigpending
来查询等待信号集中的信号,以了解有哪些信号已经到达,但尚未被处理。 - 进程可以处理等待信号集中的信号,通常按照它们到达的顺序。
3. 信号处理程序(Signal Handler)
- 信号处理程序是用户定义的函数,它定义了信号到达时进程应该采取的操作。每个信号都可以关联一个信号处理程序。
- 信号处理程序可以执行各种操作,如记录日志、清理资源、中断进程等,具体取决于信号的类型和处理程序的实现。
- 进程可以使用系统调用
signal
或更高级的方式(如sigaction
)来注册信号处理程序,以定义对特定信号的处理方式。 - 信号处理程序通常会在信号到达后异步地执行,它们可以访问信号相关的信息,如信号编号、发送者进程等。
它们的关系
- 当一个信号被发送到进程,它首先进入等待信号集。
- 如果该信号未在阻塞信号集中,进程的信号处理程序将被调用,用于处理该信号。
- 处理程序可以执行任何适当的操作,然后从等待信号集中删除该信号。
- 进程可以使用阻塞信号集来控制是否阻止某些信号传递,以推迟其处理。
一些思考?
阻塞和忽略一样吗?
忽略是处理信号,把pending位置零。而阻塞是拦截信号抵达。
信号集的操作
sigprocmask
是一个用于设置和修改进程信号屏蔽掩码(blocked signal mask)的系统调用,它允许进程控制哪些信号会被阻止(阻塞)。通过设置信号屏蔽掩码,进程可以在关键操作期间或需要禁用特定信号时,临时地阻止信号的传递和处理。
以下是关于sigprocmask
的详细解释:
函数签名:
#include <signal.h>
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
:一个指向sigset_t
类型的指针,用于指定要添加、移除或替换的信号集。oldset
:一个可选的指向sigset_t
类型的指针,用于接收旧的信号屏蔽掩码。如果不需要旧的掩码,则可以将其设置为NULL
。
返回值:
- 如果函数调用成功,将返回0。
- 如果出现错误,将返回-1,并设置
errno
以指示错误的原因。
使用实例:
#include <stdio.h>
#include <signal.h>
int main() {
sigset_t block_set, old_set;
// 初始化要阻塞的信号集,例如阻塞SIGINT和SIGQUIT
sigemptyset(&block_set);
sigaddset(&block_set, SIGINT);
sigaddset(&block_set, SIGQUIT);
// 阻塞指定信号
sigprocmask(SIG_BLOCK, &block_set, &old_set);
// 执行关键操作,这时SIGINT和SIGQUIT信号会被阻止
// 恢复之前的信号屏蔽掩码
sigprocmask(SIG_SETMASK, &old_set, NULL);
return 0;
}
在上面的示例中,sigprocmask
函数用于阻塞了SIGINT和SIGQUIT信号,然后执行关键操作。最后,通过恢复旧的信号屏蔽掩码,重新开启了对这两个信号的处理。这种机制允许程序在需要的时候控制信号的传递,以确保程序的稳定性和可靠性。
sigpending
是一个系统调用,用于查询进程的等待信号集(pending signal set),它返回一个包含在等待信号集中的信号的集合。等待信号集包含了已经发送到进程但尚未被处理的信号。
以下是关于sigpending
的详细信息:
函数签名:
#include <signal.h>
int sigpending(sigset_t *set);
参数:
set
:一个指向sigset_t
类型的指针,用于接收查询结果,即等待信号集中的信号。
返回值:
- 如果函数调用成功,将返回0。
- 如果出现错误,将返回-1,并设置
errno
以指示错误的原因。
使用示例:
#include <stdio.h>
#include <signal.h>
int main() {
sigset_t pending_set;
// 查询等待信号集中的信号
if (sigpending(&pending_set) == -1) {
perror("sigpending");
return 1;
}
// 检查特定信号是否在等待信号集中
if (sigismember(&pending_set, SIGINT)) {
printf("SIGINT is in the pending signal set.\n");
} else {
printf("SIGINT is not in the pending signal set.\n");
}
return 0;
}
在上面的示例中,sigpending
函数用于查询等待信号集中的信号。然后,使用sigismember
函数检查特定信号(此处为SIGINT)是否包含在等待信号集中。这允许进程了解已经发送到进程但尚未被处理的信号,以便进一步处理它们。
sigaction
是一个系统调用,用于设置和修改信号的处理行为,包括注册信号处理程序。这是比较灵活和强大的信号处理方式,与较旧的signal
函数相比,提供了更多的控制和可定制性。
以下是有关sigaction
的详细信息:
函数签名:
#include <signal.h>
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
参数:
signum
:要设置或修改的信号的编号,例如SIGINT
。act
:一个指向struct sigaction
类型的指针,用于指定新的信号处理行为。oldact
:一个可选的指向struct sigaction
类型的指针,用于接收以前的信号处理行为。如果不需要以前的行为,可以将其设置为NULL
。
struct sigaction
结构: struct sigaction
结构用于指定信号的处理行为,包括信号处理程序和其他属性。它的定义如下:
struct sigaction
{
void (*sa_handler)(int); // 信号处理程序的函数指针
void (*sa_sigaction)(int, siginfo_t *, void *); // 信号处理程序的函数指针(替代
sa_handler) sigset_t sa_mask; // 阻塞的信号集
int sa_flags; // 用于指定各种标志,如 SA_RESTART、SA_SIGINFO 等
void (*sa_restorer)(void); // 用于某些平台的废弃字段
};
返回值:
- 如果函数调用成功,将返回0。
- 如果出现错误,将返回-1,并设置
errno
以指示错误的原因。
使用实例:
#include <stdio.h>
#include <signal.h>
void signal_handler(int signum) {
printf("Received signal: %d\n", signum);
}
int main() {
struct sigaction new_action, old_action;
// 设置信号处理程序
new_action.sa_handler = signal_handler;
sigemptyset(&new_action.sa_mask);
new_action.sa_flags = 0;
// 注册新的信号处理程序
if (sigaction(SIGINT, &new_action, &old_action) == -1) {
perror("sigaction");
return 1;
}
// 在此期间,进程将使用新的信号处理程序来处理 SIGINT 信号
return 0;
}
在上面的示例中,sigaction
函数用于注册了一个新的信号处理程序,当进程接收到SIGINT信号时,它将调用signal_handler
函数。struct sigaction
结构中的其他属性也可以设置,以更精细地控制信号的处理行为。
signal
是一个函数,用于在Unix/Linux系统中注册信号处理程序(Signal Handler)。它的目的是为特定信号指定处理方式,当进程接收到该信号时,将执行指定的处理函数。以下是 signal()
函数的详细解释:
函数签名:
#include <signal.h>
void (*signal(int signum, void (*handler)(int)))(int);
参数:
signum
:要注册信号处理程序的信号编号,如SIGINT
、SIGTERM
等。handler
:是一个函数指针,指定了处理信号的函数。通常,它是一个用户定义的函数,其原型为void handler(int signum)
,其中signum
表示接收到的信号。
返回值:
- 函数返回一个函数指针,该指针指向以前注册的信号处理程序。如果以前未注册处理程序,则返回
SIG_ERR
。在错误发生时,还可以通过检查全局变量errno
获取错误代码。
使用示例:
#include <stdio.h>
#include <signal.h>
void signal_handler(int signum) {
printf("Received signal: %d\n", signum);
}
int main() {
// 注册SIGINT信号处理程序
if (signal(SIGINT, signal_handler) == SIG_ERR) {
perror("signal");
return 1;
}
// 进程将执行signal_handler函数来处理SIGINT信号
// 阻止进程提前退出
while (1) {
// 无限循环
}
return 0;
}
在上面的示例中,signal()
函数用于注册了一个信号处理程序 signal_handler
,当进程接收到 SIGINT
信号时,将执行 signal_handler
函数。该示例中的进程在无限循环中等待,以便触发 SIGINT
信号并执行信号处理程序。
需要注意的是,signal()
函数是比较旧的信号处理方式,它在某些情况下可能不提供足够的灵活性。对于更多的控制和可定制性,建议使用 sigaction
函数来注册信号处理程序,因为它提供了更多的选项和标志。
还有一些调用
这些函数是与信号集(Signal Set)相关的函数,它们用于创建、修改和查询信号集,以便进程可以管理哪些信号会被阻塞、等待等。以下是这些函数的详细解释:
sigemptyset(sigset_t *set)
这个函数用于初始化一个空的信号集。它将清除传递给它的 set
中的所有信号,使 set
不包含任何信号。
sigfillset(sigset_t *set)
这个函数用于将所有已定义的信号添加到信号集 set
中,使 set
包含所有可能的信号。
sigaddset(sigset_t *set, int signo)
这个函数用于将指定的信号 signo
添加到信号集 set
中。这表示 set
中将包含 signo
信号。
sigdelset(sigset_t *set, int signo)
这个函数用于从信号集 set
中移除指定的信号 signo
。这表示 set
中将不再包含 signo
信号。
sigismember(const sigset_t *set, int signo)
这个函数用于检查信号集 set
是否包含指定的信号 signo
。如果 set
包含 signo
信号,函数将返回非零值,否则返回零。
signal对比sigaction
signal
和 sigaction
都用于注册信号处理程序,但它们在功能和灵活性上有一些重要的区别。以下是它们之间的对比:
1. signal
:
signal
是较旧的信号处理函数,最初在早期的Unix系统中引入。- 它的原型为:
void (*signal(int signum, void (*handler)(int)))(int)
。 signal
函数有一些限制,其中一些取决于系统和实现。它的行为因系统而异。- 在某些系统上,
signal
使用简单的void (*handler)(int)
类型的信号处理程序。 - 不能在信号处理程序中使用
sigaction
等其他功能,因为它们可能引发不可预测的行为。
2. sigaction
:
sigaction
是更灵活、可预测和标准化的信号处理函数,通常是更现代的选择。- 它的原型为:
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact)
。 sigaction
函数允许更多的控制,包括:- 通过
sa_flags
标志来指定各种选项,如SA_RESTART
、SA_SIGINFO
等。 - 可以使用
sa_sigaction
代替sa_handler
来传递更多的参数。 - 提供更多的信号处理选项,允许更好地管理信号。
- 更一致的行为,不受实现限制的影响。
- 通过
- 具有更明确的错误处理方式,可以检查
errno
来获取错误信息。
3. 总结:
signal
可能会因系统不同而有不同的行为,它更适用于简单的信号处理需求。sigaction
提供了更多的控制和更一致的行为,通常更适用于复杂的信号处理需求,尤其是需要处理多个信号或需要额外信息的情况。
通常建议使用 sigaction
,特别是在需要更多灵活性和可维护性的情况下,因为它可以更好地满足各种信号处理需求,并提供更可预测的行为。但在某些情况下,使用 signal
也可以足够。
volatile关键字
volatile
是一个C/C++关键字,通常用于修饰变量,表示该变量可能会在程序的控制流之外被修改。在信号处理的上下文中,volatile
关键字可以用来告诉编译器不要优化对该变量的读写操作,因为信号处理程序可能会在不同的时间点修改这些变量。
在信号处理中,有一些变量可能会被信号处理程序和主程序同时访问。如果不使用 volatile
关键字,编译器可能会对这些变量进行优化,认为它们的值在信号处理程序中不会被修改,从而导致不正确的行为。使用 volatile
可以告诉编译器,这些变量的值可能在程序控制流之外被修改,因此它不应该对这些变量进行优化。
例如,考虑以下示例:
#include <stdio.h>
#include <signal.h>
volatile int flag = 0;
void signal_handler(int signum) {
flag = 1;
}
int main() {
signal(SIGINT, signal_handler);
while (!flag) {
// 在循环中等待信号处理程序设置 flag
}
printf("Received signal!\n");
return 0;
}
在这个示例中,flag
是一个 volatile
变量,它可能会在信号处理程序中被修改。如果不使用 volatile
修饰,编译器可能会认为 flag
的值在 while
循环中不会被修改,因此可能会进行优化,导致循环永远不会结束。使用 volatile
关键字可以告诉编译器不要对 flag
进行优化,确保循环正确地等待信号处理程序设置 flag
。
总之,volatile
关键字在信号处理中的作用是确保编译器不会对被信号处理程序修改的变量进行不必要的优化,以维护程序的正确性。
总结
信号在Linux中具有重要的作用,它们是一种进程间通信和进程控制的机制,对于操作系统和应用程序都至关重要。以下是关于信号在Linux中的重要性的总结:
-
进程间通信: 信号是一种轻量级的进程间通信机制,允许一个进程向另一个进程发送异步通知或请求。这对于协调多个进程的活动非常有用,例如父子进程之间的通信、进程组间的通信等。
-
进程控制: 信号可以用于控制进程的行为,如终止进程、暂停进程、恢复进程、重新加载配置等。管理员和操作系统可以使用信号来管理系统中运行的进程。
-
异常处理: 信号用于处理进程发生的异常情况,如除零操作、访问违规内存等。程序员可以为这些异常情况注册信号处理程序,以便处理错误并采取适当的措施。
-
定时器: 信号可以用于实现定时器功能,允许进程在特定时间间隔内执行某些操作。例如,
SIGALRM
信号可用于定时执行操作,例如定时轮询文件或网络状态。 -
异步通知: 信号是异步的,它们可以随时被发送给进程,而不需要等待。这使得信号非常适用于事件驱动编程,如处理用户输入或外部事件。
-
系统监控和调试: 信号对于系统监控和调试工具非常重要。管理员和开发人员可以使用信号来观察和干预进程的行为,例如获取进程的状态、生成核心转储(core dump)等。
总之,信号在Linux中扮演着多种重要角色,使进程之间的通信和控制更加灵活和强大。了解信号的概念和使用方法对于开发、调试和系统管理都非常重要。