一文搞懂信号

一.信号

1.1信号特点

信号(Signal)在操作系统中是一种进程间通信机制,用于向进程发送异步通知。以下是信号的几个特点:

  1. 异步通知:信号是异步发送给进程的,即进程在接收到信号时会中断当前的执行流程,转而去处理信号的处理函数。这个特点使得信号非常适合处理一些突发事件或异步事件。

  2. 中断处理:当进程接收到信号时,会立即中断当前的执行,并跳转到预先设置好的信号处理函数(Signal Handler)去执行特定的操作。可以使用系统提供的函数(如 signal()sigaction())来注册信号处理函数。

  3. 非透明性:信号的传达是通过内核来实现的,对进程本身来说是不可见的。进程无法得知信号是由谁发送的,也无法直接向特定进程发送信号,而只能向整个进程组或进程所属的进程组发送信号。

  4. 有限数量:每个操作系统都规定了一定的信号数量,不同的信号具有不同的含义和用途。例如,SIGINT 用于终止进程,SIGSEGV 用于表示段错误等。可以使用 kill 命令或相关函数来发送信号。

  5. 默认处理行为:每个信号都有一个默认的处理行为,例如终止进程、忽略信号或产生核心转储(Core Dump)。然而,可以使用信号处理函数来自定义处理行为,从而实现对信号的控制。

  6. 无法排队:如果同一种信号在进程尚未处理完毕时再次发送,通常情况下,只有一个信号会被接收到。这是因为信号无法排队,进程只会接收到一次同一类型的信号。

  7. 可靠性限制:在信号的传递过程中,可能会存在信号丢失或信号合并等问题。一些信号在多次发送时可能只会被接收到一次。这使得信号在某些场景下的可靠性受到一定的限制。

需要注意,不同的操作系统可能支持不同的信号,并且有不同的信号编号和默认处理行为。因此,在使用信号时应仔细了解操作系统的信号机制和规范。

总的来说,信号提供了一种简单而有效的进程间通信机制,可以用于处理异步事件和相应外部通知。但是,使用信号时需要注意处理函数的可重入性、信号处理函数的执行上下文切换以及信号的可靠性等问题。

1.2信号注意事项

在处理信号时,以下是一些需要注意的事项:

  1. 可重入性:信号处理函数在接收到信号时会被异步调用,因此需要保证信号处理函数的可重入性。避免在信号处理函数中使用不可重入的函数,以及全局变量的不安全访问等操作。推荐使用线程安全的函数和数据结构。

  2. 非阻塞原则:信号处理函数应该尽可能地短小和简单,避免执行耗时的操作,以减少信号堆积和处理延迟。长时间的信号处理函数可能会导致其他信号被阻塞或丢失。

  3. 信号屏蔽:在需要临时关闭某个信号的处理时,可以使用信号屏蔽操作。通过调用 sigprocmask() 函数可临时阻塞或解除阻塞某个信号,确保在特定的代码区域内不会收到特定的信号。

  4. 可靠信号处理:为了保证信号的可靠性,可以使用 sigaction() 函数来注册信号处理函数。sigaction() 允许对信号的处理方式进行更加精确的控制,并提供了一些可靠性增强的选项,可以避免信号的丢失和合并。

  5. 重新注册处理函数:某些信号在接收到后会将默认处理行为重置回去,例如终止进程的信号。如果要持续地处理这些信号,需要在信号处理函数中重新注册处理函数,以确保持续接收和处理该信号。

  6. 对可中断的系统调用的处理:在一些系统调用(如 read()write()sleep() 等)中,当进程接收到信号时,这些系统调用可能会被中断,返回错误(例如 EINTR)。需要在合适的地方对这些可中断的系统调用进行重试。

  7. 与其他异步机制的交互:注意信号处理函数与其他异步事件机制(如线程、定时器、异步IO等)之间的交互。需要合理地进行同步和互斥操作,以避免资源竞争和不确定行为。

  8. 信号的处理次数:注意同一种信号在进程中的处理次数限制。有些信号在多次触发时可能只能被处理一次,这可能导致其中某些信号被丢弃。了解并考虑系统对信号处理次数的限制。

  9. 不可移植性:不同的操作系统可能具有不同的信号机制和限制。因此,在处理信号时要注意确保代码的可移植性,并遵循操作系统的信号规范。

以上是一些在处理信号时需要注意的事项。合理处理信号可以帮助应用程序与操作系统之间进行有效的通信和响应外部事件。然而,信号处理也是一个复杂的主题,需要仔细考虑各种因素和风险。

1.3信号响应方式

在处理信号时,有三种主要的信号响应方式:

  1. 默认响应(Default Action):每个信号都有一个默认的处理行为。例如,SIGINT 的默认行为是终止进程,SIGTERM 的默认行为是终止进程,SIGKILL 的默认行为是无法被阻塞和处理的直接终止进程等。对于大多数信号,默认行为是终止或终止并生成核心转储。可以使用 man 命令来查看特定信号的默认行为。

  2. 忽略信号(Ignore Signal):可以通过设置信号处理函数为 SIG_IGN 来忽略某个特定的信号。忽略信号意味着对该信号的接收和处理完全忽略,不会触发任何操作。例如,可以忽略 SIGINT 信号以防止进程被键盘中断(Ctrl+C)终止。

  3. 自定义信号处理函数(Signal Handling Function):可以为一个信号指定一个自定义的信号处理函数(Signal Handler)。信号处理函数是一个用户定义的函数,用于处理接收到的信号。可以使用 signal()sigaction() 函数来注册信号处理函数。在接收到信号时,进程会中断当前执行,并跳转到相应的信号处理函数进行操作。在处理函数中可以执行一些自定义的操作,如记录日志、改变标志位、发送信号给其他进程等。

要选择适当的信号响应方式,需要根据实际需求和特定的业务逻辑来决定。例如,对于某个关键的信号,可以选择自定义信号处理函数来执行特定任务,而对于其他一些信号可以选择忽略或采用默认行为。

需要注意的是,使用自定义信号处理函数时应保持函数的简洁和高效,避免耗时和不可重入的操作,以及需要合理处理信号处理函数与其他并发机制(如线程)之间的同步和互斥。

另外,还应注意某些无法被阻塞或处理的信号,如 SIGKILLSIGSTOP,不能被忽略或自定义信号处理函数。这些信号的默认行为是直接终止进程或暂停进程,无法被拦截或处理。

1.4怎么产生信号

在操作系统中,有几种常见的方法可以产生信号:

  1. 来自键盘或终端:键盘上的某些组合键,如Ctrl+C(SIGINT)、Ctrl+Z(SIGTSTP)等,可以通过终端或控制台产生相应的信号。例如,使用Ctrl+C可以发送SIGINT信号,这通常用于终止当前的运行进程。

  2. 使用 kill 命令:可以使用kill命令(或其它类似的命令)向指定的进程发送信号。kill命令的语法是 kill [-<信号>] <进程ID>。其中,信号可以是信号名称(如INT、TERM、HUP等),也可以是信号编号(如2、9、15等),进程ID是要发送信号的目标进程的ID。

  3. 硬件异常:当出现硬件异常(如除零错误,非法指令访问等)时,操作系统会向产生异常的进程发送相应的信号。这些信号通常用于指示进程发生了错误或异常情况,并让进程或操作系统采取适当的措施。

  4. 软件异常:有些特殊的函数调用,如abort()、raise()等,可以在程序中主动产生信号。例如,使用raise()函数可以向当前进程发送指定的信号。

  5. 定时器:操作系统提供了一些定时器功能,可以用来在特定时间间隔内发送指定信号。最常见的是使用timer_create()和timer_settime()函数来创建和设置定时器,并使用SIGALRM信号来触发定时器的信号。

需要注意的是,生成信号的能力通常受到用户的权限和操作系统对信号的限制。具体可生成的信号类型和权限可能因操作系统和用户的不同而有所不同。此外,特定信号的具体处理方式也由接收进程的信号处理机制决定。

1.5常用信号

以下是一些常见的信号及其默认编号及含义:

  1. SIGINT (2):键盘中断信号。通常由用户按下Ctrl+C键产生,用于终止正在运行的进程。

  2. SIGTERM (15):终止信号。用于请求进程正常终止,给进程一个机会进行清理和保存状态。

  3. SIGKILL (9):强制终止信号。无法被阻塞、处理或忽略,用于强制终止进程。

  4. SIGSTOP (19):暂停信号。用于暂停进程的执行,进程将被挂起直到收到继续执行的信号。

  5. SIGCONT (18):继续信号。用于恢复之前暂停的进程的执行。

  6. SIGHUP (1):终端挂起或控制进程断开的信号。用于指示终端会话已经断开,常用于在进程重启时重新加载配置文件。

  7. SIGSEGV (11):段错误信号。通常表示进程访问了无效的内存地址。

  8. SIGILL (4):非法指令信号。表示进程执行了非法、未定义或不协调的指令。

  9. SIGFPE (8):浮点异常信号。表示进程执行了一个浮点运算异常,如除零错误或溢出。

  10. SIGUSR1 (10) 和 SIGUSR2 (12):用户自定义信号。可以由应用程序自定义使用。

  11. SIGPIPE (13):管道破裂信号。当进程写入已关闭的管道时会产生。

  12. SIGALRM (14):定时器信号。通常由操作系统的定时器触发,可用于定时操作或超时处理。

这些信号仅是一部分常用的信号,不同的操作系统和环境中可能还有其他系统特定的信号。可以使用命令 kill -l 查看系统支持的所有信号及其编号。另外,还可以通过自定义信号处理函数来处理这些信号,以满足特定应用程序的需求。

1.6函数接口

在 C 语言中,有一些常用的函数接口可用于处理信号。以下是一些常见的信号相关函数接口:

  1. signal() 函数:
    void (*signal(int signum, void (*handler)(int)))(int)
    该函数用于注册信号处理函数。它接受两个参数:signum 表示要处理的信号编号,handler 为信号处理函数的指针。signal() 函数会返回先前的信号处理函数指针。通过传递 SIG_IGN(忽略信号)或 SIG_DFL(默认处理)作为 handler,可以忽略或使用默认行为处理信号。需要注意的是,signal() 函数在不同的操作系统中可能会有不同的行为和使用限制。

  2. sigaction() 函数:
    int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact)
    该函数提供了更为高级和可靠的信号处理方式,相较于signal()函数,具有更多的选项和功能。它接受三个参数:signum 表示要处理的信号编号,act 为新的信号处理动作结构体指针,oldact 可用于保存旧的信号处理动作设置。通过设置 act 结构体的成员可以注册自定义的信号处理函数,以及指定一些信号处理的标志和选项。

  3. kill() 函数:
    int kill(pid_t pid, int signum)
    该函数用于向指定进程发送信号。它接受两个参数:pid 为目标进程的 ID,signum 表示要发送的信号编号。通过向进程发送不同的信号可以触发相应的处理动作。如果 pid 为正值且具有合法进程 ID,则向该进程发送信号;如果 pid 为0,则信号将发送到与调用进程属于同一进程组的所有进程;如果 pid 为-1,则信号将发送给与调用进程具有相同用户 ID 的所有进程。

  4. sigprocmask() 函数:
    int sigprocmask(int how, const sigset_t *set, sigset_t *oldset)
    该函数用于设置进程的信号屏蔽字,即阻塞或解除阻塞指定的信号集合。how 参数表示对信号屏蔽字的修改方式,可以是 SIG_BLOCK、SIG_UNBLOCK 或 SIG_SETMASK。通过指定 set 参数来设置要更改的信号集合。oldset 参数用于保存旧的信号屏蔽字。

  5. sigpending() 函数:
    int sigpending(sigset_t *set)
    该函数用于获取进程当前挂起(未决)的信号。如果有挂起的信号,则将其保存到 set 指向的信号集中。可以通过该函数了解当前未被处理的信号。

除了以上提到的函数,还有一些其他的信号相关函数接口,在不同的操作系统和平台上可能存在差异。因此,在编写处理信号的代码时,应该查阅相关的文档和手册,确保在目标平台上正确使用合适的函数接口。

1.7信号屏蔽函数

信号集屏蔽函数用于设置和操作信号集的屏蔽状态,以控制哪些信号会被阻塞。在 C 语言中,常用的信号集屏蔽函数有以下几个:

  1. sigemptyset():
    int sigemptyset(sigset_t *set)
    该函数用于清空信号集,将所有信号都从集合中移除。传递一个信号集指针给 set 参数,函数将设置该指针所指向的信号集为空集。

  2. sigfillset():
    int sigfillset(sigset_t *set)
    该函数用于将所有信号都添加到信号集中。传递一个信号集指针给 set 参数,函数将设置该指针所指向的信号集为包含所有信号的集合。

  3. sigaddset():
    int sigaddset(sigset_t *set, int signum)
    该函数用于将特定信号添加到信号集中。传递一个信号集指针给 set 参数,signum 参数表示要添加的信号编号。函数将该信号添加到信号集中。

  4. sigdelset():
    int sigdelset(sigset_t *set, int signum)
    该函数用于从信号集中删除指定的信号。传递一个信号集指针给 set 参数,signum 参数表示要删除的信号编号。函数将从信号集中删除指定的信号。

  5. sigismember():
    int sigismember(const sigset_t *set, int signum)
    该函数用于检查给定信号是否在信号集中。传递一个信号集指针给 set 参数,signum 参数表示要检查的信号编号。如果指定的信号在信号集中,则返回非零值;否则返回 0。

  6. sigprocmask():
    sigprocmask 是用于设置或修改进程的信号屏蔽字(signal mask)的函数。它可以阻塞或解除阻塞指定的信号集。

函数原型如下:

int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);

how 参数指定了对信号屏蔽字的修改方式,可以取以下三个值:

  • SIG_BLOCK: 将 set 中的信号添加到当前信号屏蔽字中。
  • SIG_UNBLOCK: 将 set 中的信号从当前信号屏蔽字中解除阻塞。
  • SIG_SETMASK: 将当前信号屏蔽字替换为 set

set 参数是一个指向信号集的指针,表示要添加或解除阻塞的信号集。

oldset 参数(可选)是一个用来保存之前信号屏蔽字的信号集指针。如果不为 NULL,则 oldset 指向的信号集会被填充为函数调用之前的信号屏蔽字。

调用 sigprocmask 函数后,进程的信号屏蔽字会根据 how 参数对 set 中指定的信号进行修改。

示例用法:

#include <stdio.h>
#include <signal.h>

int main() {
    
    
    sigset_t newmask, oldmask;
    
    // 设置屏蔽信号集包含 SIGINT 和 SIGQUIT
    sigemptyset(&newmask);
    sigaddset(&newmask, SIGINT);
    sigaddset(&newmask, SIGQUIT);
    
    // 阻塞 newmask 中的信号
    sigprocmask(SIG_BLOCK, &newmask, &oldmask);
    
    // 此处可以进行一些需要屏蔽信号的操作
    
    // 恢复原来的信号屏蔽字
    sigprocmask(SIG_SETMASK, &oldmask, NULL);
    
    return 0;
}

以上示例中,通过 sigprocmask 函数将 SIGINTSIGQUIT 信号添加到当前进程的信号屏蔽字中,实现对这两个信号的阻塞。在执行需要阻塞信号的操作后,通过再次调用 sigprocmask 恢复原来的信号屏蔽字,解除对这两个信号的阻塞。

需要注意的是,sigprocmask 函数仅对当前进程的信号屏蔽字进行修改,并不影响其他进程的信号处理。

这些函数接口都是针对 <signal.h> 头文件中定义的 sigset_t 类型的信号集进行操作。它们一般用于配合其他信号处理函数或系统调用(如 sigprocmask())来设置或查询进程的信号屏蔽状态。需要注意的是,这些函数在不同的操作系统和平台上可能会有细微的差异,因此在使用时应查阅相关的文档和官方手册来了解具体的使用方式和兼容性。

1.8未处理信号集

实时信号和非实时信号在被屏蔽时的行为有所不同。

  1. 非实时信号(标准信号):
    当一个非实时信号(编号为1~31)被屏蔽时,如果有多个该信号同时到达,只会为其保留一个未决(待处理)的信号。也就是说,非实时信号在被屏蔽时不会排队,只会保留一个待处理的信号。此时,如果该信号被解除屏蔽,只会处理一次。

  2. 实时信号(实时时钟信号):
    实时信号(编号为34~64)在被屏蔽时会排队保留,它们不会被合并或丢弃。如果多个相同的实时信号同时到达,并且被屏蔽,这些信号将按照到达的顺序排队,依次保留在未决(待处理)的信号集中。当该信号被解除屏蔽时,在信号处理函数中会按照队列顺序处理这些信号,一个接一个地处理每个实时信号。

需要注意的是,如果实时信号的队列溢出,而处理函数尚未处理完所有排队的实时信号时,新到达的实时信号将被丢弃。因此,在实时信号的处理函数中,应当尽早处理队列中的实时信号,以避免队列溢出。

总结起来,如果非实时信号被屏蔽,只会保留一个未处理的实例。而实时信号在被屏蔽时会排队保留,并在解除屏蔽后按照到达的顺序进行处理。

猜你喜欢

转载自blog.csdn.net/m0_73731708/article/details/133101989