Swift 中的信号处理

此包为底层的 C sigaction 函数提供了 Swift 语法,以及延迟或取消 sigaction 处理程序的方法。

它兼容 macOS 和 Linux。在使用 sigaction 处理程序延迟功能之前,请仔细阅读文档。

使用示例

我相信好的例子胜过千言万语。

使用 Swift 语法的简单传统 sigaction 处理程序示例

let action = Sigaction(handler: .ansiC({ sigID in
   logger.debug("Handling signal \(Signal(rawValue: sigID)) from sigaction")
}))
try action.install(on: .terminated)

延迟的 sigaction 示例

let delayedSigaction = try SigactionDelayer_Unsig.registerDelayedSigaction(Signal.terminated, handler: { signal, doneHandler in
   logger.debug("Received signal \(signal); delaying it to clean-up")
   myAsyncCleanUp{
      logger.debug("Cleanup is done, allowing signal to go through")
      doneHandler(true)
   })
})

在同一代码中包含这两个示例时,当程序收到终止信号(macOS 上为 15)时,将记录以下内容

Received signal SIGTERM; delaying it to clean-up
Cleanup is done, allowing signal to go through
Handling signal SIGTERM from sigaction

延迟 Sigaction 如何工作?

延迟信号是一个不寻常的过程,在使用此方法之前,您应该了解其内部工作原理。

我们有两种延迟策略。一种是将信号设置为忽略,直到信号被允许通过(SigactionDelayer_Unsig);另一种是阻止传入信号,直到信号被允许通过(SigactionDelayer_Block)。

两种策略都有优点和缺点。

当信号被阻止或忽略时,也会使用 libdispatch (又名 GCD) 对其进行监控,libdispatch 本身使用 BSD 上的 kqueue 和 Linux 上的 signalfd 监控信号。这允许在需要时解除信号阻止或设置 sigaction 处理程序。

两种方法的重要注意事项libdispatch 只能检测发送给整个进程的信号,而不能检测发送给线程的信号。因此,发送给线程的延迟信号将被永久阻止或忽略。

Unsigaction 策略的详细信息(SigactionDelayer_Unsig

这是推荐的方法。

此方法不需要任何特定的引导。您可以随时延迟信号。

但是,一旦信号被注册为延迟,就不应手动更改 sigaction。(此项目的 Sigaction 结构提供的 install 方法除外,该方法知道信号上可能存在的取消信号操作,并可以相应地更新它们。)

当信号首次注册为延迟时,会发生以下几件事

  • sigaction 处理程序保存在内部结构中;

  • 信号被设置为忽略;

  • 生成一个专用线程(如果尚未生成),该线程解除阻止所有信号;

  • 创建一个 dispatch source 以使用 GCD 监控传入信号。

然后,当收到信号时,会发生以下情况

  • libdispatch 通知 sigaction 延迟器,延迟器将反过来

  • 临时重新安装信号的已保存 sigaction(在客户端表示可以之后),以及

  • 将信号发送到专用线程。这会触发 sigaction,但不会通知 libdispatch

  • 最后,延迟器将信号设置回被忽略状态。

此方法的注意事项:

  • 在信号发送和设置回被忽略状态之间存在不可避免的竞争条件 (AFAICT);

  • 发送回去的信号丢失了原始信号的 siginfo;

  • 在 Linux 上,信号延迟很脆弱。有关更多信息,请参阅阻止策略的 Linux 注意事项。

阻止策略的详细信息(SigactionDelayer_Block

您必须在使用此方法之前对其进行引导,在创建任何线程之前,将您要延迟的所有信号提供给引导方法。引导将首先阻止当前(主)线程上的所有给定信号,然后生成一个线程,在该线程上将解除阻止所有这些信号。

这允许我们的专用线程成为唯一允许接收要监控的信号的线程。

当信号注册为延迟时,延迟器也会阻止专用线程上的信号!

当收到信号时,libdispatch 将通知延迟器,延迟器将解除阻止信号,从而允许信号被传递。

此方法的注意事项:

  • 在 macOS 上,当收到在所有线程上被阻止的信号时,它似乎被分配给任意线程。在另一个线程上解除阻止信号根本不会解除阻止它。为了解决这个问题,我们在解除阻止信号之前检查信号是否在专用线程上挂起。如果不是,我们将信号发送到我们的线程,从而再次丢失 sigaction,就像使用 unsigaction 策略时一样。此外,原始信号将永远在受影响的线程上保持挂起状态。

  • 在 Linux 上,存在一个问题,与手册页所说的相反,当首次安装此信号的 dispatch source 时,libdispatch 确实会修改信号的 sigaction。因此,从理论上讲,这种策略不应该有效(老实说,另一种策略也不应该有效)。但是,有人注意到,安装信号源*之后*更改 sigaction 足以避免此问题。因此,我们在安装信号源之前保存 sigaction,然后在安装源之后恢复它,我们就没问题了。但是,此解决方案似乎很脆弱,并且将来可能会失效,甚至现在都可能无法可靠地工作。