Linux 信号背后的基本概念
什么是信号?信号是软件中断。
一个健壮的程序需要处理信号。这是因为信号是将异步事件传递给应用程序的一种方式。
用户按 ctrl+c,进程发送信号以杀死另一个进程等都是进程需要进行信号处理的情况。
Linux 信号
在 Linux 中,每个信号都有一个以字符 SIG 开头的名称。例如 :
- 当用户按下 ctrl+c 时生成的 SIGINT 信号。这是从终端终止程序的方法。
- 当闹钟功能设置的定时器关闭时,会产生一个 SIGALRM。
- 当进程调用 abort 函数时,会生成一个 SIGABRT 信号。
- ETC
当信号出现时,进程必须告诉内核如何处理它。可以通过三个选项来处理信号:
- 信号可以忽略。忽略我们的意思是当信号发生时什么都不做。大多数信号可以被忽略,但由硬件异常产生的信号,如除以零,如果忽略可能会产生奇怪的后果。此外,不能忽略一些信号,如 SIGKILL 和 SIGSTOP。
- 可以捕捉到信号。选择此选项后,进程会向内核注册一个函数。当该信号发生时,内核会调用此函数。如果信号对进程来说不是致命的,那么在该函数中,进程可以正确处理信号,否则它可以选择优雅地终止。
- 应用默认操作。每个信号都有一个默认动作。这可能是进程终止、忽略等。
正如我们已经说过的,不能忽略两个信号 SIGKILL 和 SIGSTOP。这是因为这两个信号为 root 用户或内核在任何情况下杀死或停止任何进程提供了一种方式。这些信号的默认操作是终止进程。这些信号既不能被捕捉也不能被忽略。
程序启动时会发生什么?
这完全取决于调用 exec 的进程。当进程启动时,所有信号的状态要么是忽略,要么是默认。除非调用 exec 的进程忽略了信号,否则它更有可能发生后一种选择。
将任何信号上的动作更改为默认动作是 exec 函数的属性。简单来说,如果父进程有一个在信号发生时被调用的信号捕获函数,那么如果父进程执行一个新的子进程,那么这个函数在新进程中没有任何意义,因此相同信号的处置设置为默认值在新的过程中。
另外,由于我们通常有进程在后台运行,所以 shell 只是将退出信号处理设置为忽略,因为我们不希望后台进程被用户按下 ctrl+c 键终止,因为这违背了创建进程的目的在后台运行。
为什么信号捕获函数应该是可重入的?
正如我们已经讨论过的,信号处理的选项之一是捕获信号。在进程代码中,这是通过向内核注册一个函数来完成的,当信号发生时内核调用该函数。要记住的一件事是进程注册的函数应该是可重入的。
在解释原因之前,让我们先了解什么是可重入函数?可重入函数是一个函数,其执行可以由于任何原因(例如由于中断或信号)在两者之间停止,然后可以在其先前的调用完成执行之前再次安全地重新进入。
现在回到问题上来,假设函数 func() 注册用于在信号发生时进行回调。现在假设这个 func() 在信号发生时已经在执行。由于此函数是针对此信号的回调,因此调度程序将停止此信号的当前执行,并且将再次调用此函数(由于信号)。
问题可能是,如果 func() 对某些全局值或数据结构起作用,当此函数的执行在中间停止时,这些全局值或数据结构处于不一致状态,那么对同一函数的第二次调用(由于信号)可能会导致一些不希望的结果。
所以我们说信号捕捉函数应该是可重入的。
线程和信号
我们已经在前面的一节中看到,信号处理有其自身的复杂性(例如使用可重入函数)。为了增加复杂性,我们通常有多线程应用程序,其中信号处理变得非常复杂。
每个线程都有自己的私有信号掩码(一个定义哪些信号可传递的掩码),但信号处理的完成方式由应用程序中的所有线程共享。这意味着线程设置的特定信号的处置很容易被其他线程否决。在这种情况下,所有线程的处置机制都会发生变化。
例如,线程 A 可以选择忽略特定信号,但同一进程中的线程 B 可以选择通过向内核注册回调函数来捕获相同的信号。在这种情况下,线程 A 的请求被线程 B 的请求否决了。
信号仅传递给任何进程中的单个线程。除了硬件异常或定时器到期(传递给导致事件的线程)之外,所有信号都被任意传递给进程。
为了克服这个缺点,可以使用一些 posix API,例如 pthread_sigmask() 等。
- 点赞
- 收藏
- 关注作者
评论(0)