关于 c :ZeroMQ: 如何对 EINTR 上的不同信号类型做出反应

ZeroMQ: How to react on different signal types on EINTR

注意:这个问题与我的问题相当接近,但我至少可以对提供的解决方案使用一些工作示例,也许 ZeroMQ 会带来一些我不知道的魔力。

目前我对阻塞 ZeroMQ 调用的异常做出反应,如下所示:

1
2
3
4
5
6
7
8
try {
    zmq::poll(&items, number, timeout);
} catch (zmq::error_t &ex) {
    if (ex.num() != EINTR) {
        throw;
    }
}
...

我的意图是:重新抛出所有捕获的异常,但那些由中断的系统调用触发的异常,我通常可以忽略(例如 SIGPROF)并重新启动 zmq::poll

如果是 SIGINT (CTRL-C),我想以不同的方式进行(例如,也重新抛出或终止循环)。

目前我最好的选择是安装一个监听 SIGINT 的信号处理程序,但由于 ZeroMQ 自己捕获信号,所以我更喜欢更复杂的方法。


如果我正确阅读了原始发帖者的问题,@frans 会询问是否有办法重新抛出某些异常,其中 C 异常包含 EINTR 错误代码,但某些信号生成的异常除外。 @frans 目前有一个 SIGINT 信号处理程序,想知道是否有更清洁的方法。

提出了两个不同的问题,关于 POSIX signal() 处理及其与 C 异常的交互:

  • POSIX 信号与 C 异常处理无关。
  • zmq::error_t 是由 zmq::poll() 作为返回 EINTR 的系统调用的结果生成的 C 异常。

TL;DR 回答:不,没有更清洁的方法。

libzmq 似乎没有安装自己的信号处理程序,但如果底层系统调用被中断(即,poll() 返回 -1,errno 复制zmq::error_t 异常。)这可能意味着 POSIX 信号可能已被传递并且特定于进程的处理程序运行,但还有其他原因。

POSIX/Unix/Linux/BSD signal() 是一种操作系统工具,用于指示内核中发生了异常情况。进程可以选择安装自己的处理程序以从这种情况中恢复,例如,SIGINTSIGQUIT 处理程序来关闭文件描述符,进行各种类型的清理等。这些处理程序与 C 异常处理没有任何关系。

其他警告:不要从 POSIX/Unix/Linux/BSD 信号处理程序中抛出 C 异常。这在此 SO 主题中之前已讨论过。


我遇到了一个类似的问题,并且盯着这个问题及其答案看了很长时间,希望如果我足够努力的话,一个解决方案会神奇地出现。

最后,我采用了本文中详述的方法。

底线是我们需要一个基于 ppoll 或更确切地说是 pselect 的 ZMQ 轮询机制(我实现了一个,但我仍然需要将它放在一个单独的库中)。这些 p- 变体将信号掩码作为额外参数。他们设置信号掩码,运行常规 poll/select,然后将信号掩码重置为之前的状态。这意味着在 ppoll/pselect 之外,您可以阻止所有您期望的信号(阻止它们会导致它们被操作系统排队,它们不会丢失),然后仅在 poll/ 期间解除阻止它们select.

这实际上是在轮询器中添加一个"信号outlets"。轮询器将返回(使用 0)当它从实际套接字获得信号时或(使用 -1)当它被您允许使用信号掩码中断的信号中断时。您可以使用信号处理程序来设置一些标志。

现在,在您的 poll 调用之后,当您通常通过现在可读/可写的套接字时,您首先检查您从信号处理程序中设置的标志。信号在 ppoll/pselect 之外的任何地方都被阻塞,因此您可以肯定地知道,除了这里之外,您无需在其他任何地方检查此标志。这允许对 EINTR 进行非常干净、紧凑和稳健的处理。

一些注意事项:为此,您不能使用任何其他阻塞调用,因此您所有的 sendrecv 都应该是非阻塞的(ZMQ_DONTWAIT 标志)。

另一件事是您已经提到的:在库的情况下,用户应该可以自由安装自己的信号处理程序。因此,如果他们想以稳健的方式使用您的库,您可能需要指导用户如何以这种方式正确处理信号。我认为如果您向用户公开标志(或翻转标志的函数)以便他们可以从自己的信号处理程序中调用它应该是可行的。


好的,所以有三种方法,一种简单,两种麻烦

干净

简单的方法是这样运行的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
zmq::socket_t inSock;
zmq::pollitem_t pollOnThese[2];
int quitLoop = 0;

<code to connect inSock to something>

pollOnThese[0].socket = NULL;       // This item is not polling a ZMQ socket
pollOnThese[0].fd = 0;              // Poll on stdin. 0 is the fd for stdin
pollOnThese[0].event = ZMQ_POLLIN;

pollOnThese[1].socket = &inSock;    // This item polls inSock
pollOnThese[1].fd = 0;              // This field is ignored because socket isn't NULL
pollOnThese[1].event = ZMQ_POLLIN;

while (!quitLoop)
{
   zmq::poll(pollOnThese,2);

   if (pollOnThese[0].revents == ZMQ_POLLIN)
   {
      // A key has been pressed, read it
      char c;
      read(0, &c, 1);

      if (c == 'c')
      {
         quitloop = 1;
      }
   }
   if (pollOnThese[1].revent == ZMQ_POLLIN)
   {
      // Handle inSock as required
   }
}

当然这意味着你的"abort"不再是用户按下 CTRL-C,他们只是按下键 \\'c\\',或者实际上任何可以写入这个标准输入的程序都可以发送一个 \\'c \\'。或者,您也可以添加另一个 ZMQ 套接字作为命令通道。这里根本没有对信号的容忍度,但我一直发现将信号的使用与 Actor 模型编程混合使用是一件非常尴尬的事情。见下文。

凌乱

使用信号的混乱方式看起来像这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
zmq::socket_t inSock;
zmq::pollitem_t pollOnThis;
int quitLoop = 0;

<code to connect inSock to something>

<install a signal handler that handles SIGINT by setting quitLoop to 1>

pollOnThis.socket = &inSock;    // This item polls inSock
pollOnThis.fd = 0;              // This field is ignored because socket isn't NULL
pollOnThis.event = ZMQ_POLLIN;

while (!quitLoop)
{
   try
   {
      zmq::poll(&pollOnThis, 1);
   }
   catch (zmq::error_t &ex)
   {
      if (ex.num() != EINTR)
      {
         throw;
      }
   }

   if (pollOnThis.revent == ZMQ_POLLIN && !quitLoop)
   {
      // Handle inSock as required
      bool keepReading = true;
      do
      {
         try
         {
            inSock.recv(&message)
            keepReading = false;
         }
         catch (zmq::error_t &ex)
         {
            if (ex.num() != EINTR)
            {
               throw;
            }
            else
            {
               // We also may want to test quitFlag here because the signal,
               // being asynchronous, may have been delivered mid recv()
               // and the handler would have set quitFlag.
               if (quitFlag)
               {
                  // Abort
                  keepReading = false;
               }
               else
               {
                  // Some other signal interrupted things
                  // What to do? Poll has said a message can be read without blocking.
                  // But does EINTR mean that that meassage has been partially read?
                  // Has one of the myriad of system calls that underpin recv() aborted?
                  // Is the recv() restartable? Documentation doesn't say.
                  // Have a go anyway.
                  keepReading = true;
               }
            } // if
         } // catch
      }
      while (keepReading);

      <repeat this loop for every single zmq:: recv and zmq::send>
   }
}

混合(也很混乱)

在此处的 ZeroMQ 指南文档中,他们通过让信号处理程序写入管道并将管道包含在 zmq_poll 中,就像我在上面使用标准输入所做的那样,它们有点融合了这两种想法。

这是将异步信号转换为同步事件的常用技巧。

但是根本没有提示是否可以重新启动任何 zmq 例程。他们只是将其用作启动干净关闭的一种方式,已放弃任何正在进行的 recv() 或 send()。如果为了彻底中止,您需要完成一系列 recv() 和 send() 例程,那么如果正在使用信号,则无法保证这是可能的。

如果我错了,请原谅我,但感觉就像你正在重新利用 SIGINT 的意义,而不是"立即终止整个程序"。如果是这样,您可能希望稍后恢复您的通信循环。在这种情况下,谁知道在您的信号到达后是否有任何 recv() 或 send() 调用可以恢复。 zmq 将使用一大堆系统调用。它们中的许多在某些情况下是不可重新启动的,并且不知道 ZMQ 是如何使用这些调用的(缺少阅读它们的源代码)。

结论

基本上我会完全避免使用信号,尤其是当您不想安装自己的处理程序时。通过使用标准输入或管道或另一个 ZMQ 套接字(或实际上所有三个)作为包含在 zmq_poll() 中的"中止"通道,您将提供一种简单有效的方法来中止循环,并且不会产生任何并发症从它的使用。