我有一个多线程应用程序,其中主线程产生多个(3+)线程,每个线程执行不同的任务.其中一个线程应该运行一个简单的TCP服务器,该服务器一次只接受一个连接并从该连接接收数据.

应用程序捕获并处理SIGTERM,以便协调线程之间的正确清理.在接收到这个信号时,它只是将一个全局共享kill标志(类型std::atomicstd::bool)设置为true.

现在,在最初的设计中,主线程执行服务器职责.当它接收到SIGTERM时,它所在的Accept()或recv()调用将返回EINTR,应用程序可以检测到这一点并知道是时候加入其他线程了.

我试图重新工作,这有一个衍生的线程作为服务器.虽然服务器功能确实可以正常工作,但信号处理却不能.当应用程序接收到SIGTERM时,kill标志被设置,但服务器线程继续阻塞accept()或recv(),这取决于它是否已经连接了客户端.

在调查这个问题后,我了解到:

  • 可以将信号发送到进程中的任何可用线程
  • 每个线程可以有自己的信号掩码来阻止某些信号的接收

从我遇到的问题中也可以明显看出,除非在accept()/recv()上阻塞的特定线程捕获到信号,否则该线程将继续被阻塞,因为阻塞函数不会返回EINTR.

问题:

  1. 为什么每次我测试原始设计(其中主线程是服务器)都能正常工作?我已经测试过几百次了.我可以想象,在某个时刻,其他线程中的一个会收到该信号,而主线程会继续阻塞.为什么这种情况从来没有发生过?

  2. 你有什么建议来纠正这个问题?我希望继续让服务器在派生的线程中运行,而不是在主线程中运行.以下是我读到的一些解决方案:

A.生成一个单独的信号捕获器线程,并阻止所有其他线程中的信号.我不确定这对我的情况有什么帮助,因为如果服务器线程在syscall上被阻塞,并屏蔽了所有信号,就没有办法向它发出信号来唤醒并开始清理.我读过有关条件变量的文章,但我同样不明白这将如何解锁被阻止的系统调用.

B.切换到使用非阻塞套接字,并使用SELECT()/Poll()/EPOLL()编写服务器.我可以看到这是可行的,尽管对于一次只能处理一个客户端的服务器来说,这似乎有点过头.然而,如果这是最好的解决方案,我愿意这样做.但这是否意味着所有派生的线程都被有效地禁止使用阻塞syscall?另一个尚未写入的线程应该执行一些串行I/O.是否也需要使用这些多路复用函数写入这些I/O?

有没有办法让2a在我的情况下起作用,或者2b是我唯一的解决方案?

限制:这个项目使用的是C++17,我的团队不允许使用我们(相当标准的)Linux系统和C++标准库之外的任何库.Boost和其他第三方代码不是我们的 Select .我们还直接使用了p线程,而不是通过C++STL,但我认为这不应该影响这种情况.

我还没有try 实施任何解决方案,因为我正在研究哪一个方案最适合我的情况.

推荐答案

背景

在调查这个问题后,我了解到:

  • 可以将信号发送到进程中的任何可用线程

是.此外,发送到整个进程的信号最多只能传递到它的一个线程.这是两个不同的概念,尽管密切相关.

  • 每个线程可以有自己的信号掩码来阻止某些信号的接收

是的,尽管更准确的说法是每个线程does都有自己的信号掩码.即使线程的信号掩码没有阻止任何信号,它仍然在那里.

这对我来说也变得很明显,因为我有这样的问题,除非 在Accept()/recv()上被阻塞的特定线程捕获 信号时,线程将继续被阻塞为阻塞 函数不会返回EINTR.

是.函数调用以EINTR失败意味着该调用的执行被中断,以允许调用线程运行信号处理程序,并且该处理程序是在没有SA_RESTART选项的情况下安装的,或者有问题的函数不在支持中断时重新启动的函数之列.除处理信号的线程外,信号处理程序正在处理的信号对其他线程没有直接影响.

最初的设计

  1. 为什么每次我测试原始设计(其中主线程是服务器)都能正常工作?我已经测试过了 上百次了.我可以想象,在某种程度上,其中一个 线程将接收到该信号,并且主线程将 继续封锁.为什么这种情况从来没有发生过?

对于如何从没有阻塞给定信号的线程中 Select 处理该信号的线程,没有任何规范.没有要求 Select 是不确定的,这样您测试的次数就会有很大的相关性.话虽如此,您的原始代码中可能至少存在一个竞争,如果SIGTERM到达的时间完全错误,则应用程序不会干脆关闭,但这很容易成为百万分之一级别的事件.

除此之外,为了 Select 一个线程来处理传入的信号,系统会以固定的顺序判断符合条件的线程,这可能与它们的创建顺序有关.当当前在I/O操作上被阻塞的线程可用时,优先 Select 它们也是合理的.这些是系统实现细节的示例,可以解释您的旧设计运行良好的原因.

新的设计选项

  1. 你有什么建议来纠正这个问题?我希望继续让服务器在派生的线程中运行,而不是在 主要的一个.

这有几个不同的方面,你似乎没有清楚地划分其中的各个方面.应用程序必须

  1. 接收SIGTERM或其他终止信号.
  2. 确保指示所有线程终止,包括
  3. 确保所有线程notice的指令及时关闭.

若要在接收到信号时自定义程序行为,需要安装自定义信号处理程序.你已经做了这么多.

您目前正在使用一个原子变量作为标志来通知您的线程已请求关闭. 这是合理的.

您正在努力解决的主要问题是(3)如何确保所有线程及时注意到关闭请求.正如您所发现的,至多一个线程的阻塞syscall将被中断,因此无限期阻塞的系统调用是一个需要解决的问题.

以下是我读到的一些解决方案:

指定的信号捕获线程

A.派生一个单独的信号捕获器线程,并阻止所有 其他的线索.

指定一个特定的线程来接收任何入站SIGTERM非常有用,因为这为您提供了一种在普通代码中执行大部分响应的方法,而不是受到信号处理程序行为的限制.这是一个比您可能意识到的更大的优势,但它是一个与您所要求的不同的问题的解决方案.

通过信号通知其他线程

我不确定这对我的处境有什么帮助 因为如果服务器线程在系统调用上被阻塞并被屏蔽 所有信号,将无法向它发出唤醒和启动的信号 清理.

除了SIGTERM处理程序外,其他线程不需要阻塞all个信号.可能还有其他一些他们也应该阻止的,但他们可能会允许一些.SIGTERM处理程序可以通过向每个人发送这些信号中的一个来提醒他们.一些合理的 Select 可能是SIGABRTSIGALRMSIGUSR1SIGUSR2.

无论是否由指定的线程负责,通过信号通知其他线程的优点是它可以中断(一些)阻塞的系统调用,但这有一个问题. 假设一个应用程序这样做...

    something_nonblocking();
    block_indefinitely();
    if (I_should_terminate()) {
        clean_up();
        return;
    }

如果该线程在其执行something_nonblocking()时接收到通知信号,则该信号将不会中断block_indefinitely().即使something_nonblocking()中的最后一件事是判断是否I_should_terminate(),信号也有可能在确定判断结果之后(如false)但在输入block_indefinitely()之前到达,从而使信号对于实现迅速、干净的关机无效.

您可以通过发送两次信号来减少此问题出现的可能性,但您不能完全消除它.

通过I/O通知其他线程

提醒其他线程终止的另一种 Select 是...

B.切换到使用非阻塞套接字,并使用 Select ()/Poll()/EPOLL().我可以看到这是可行的,尽管看起来 对于一次只能处理一个客户端的服务器来说是过度杀伤力.

我不明白你为什么认为这是过度杀戮. 关键是避免I/O阻塞,防止您立即执行准备好的工作(即关闭). 这正是这些功能的目的.

我想您可能只是想使用它们来设置阻塞I/O的超时时间,这是可行的,但是如果您使用这种通用方法,那么您应该考虑设置一个I/O通道--例如管道--通过它可以将终止通知直接传送到I/O线程(S).如果该线程在其监视的FD中包括这样的通道的读取端,则不仅将存在真正的多路复用,而且该线程将更好地响应关闭通知,因为它不必等待超时到期.

然而,如果这是最好的解决方案,我愿意这样做.但确实是这样 这意味着所有生成的线程实际上都被禁止了 阻止系统调用?

说大也大吧.如果您确信(或至少愿意假设)syscall不会阻塞很长时间,那么阻止syscall是可以的.例如,本地文件系统上常规文件的I/O通常通过阻塞操作进行,但只有在特殊情况下,这样的操作才会阻塞足够长的时间来干扰您的关机.

另一条线索还没有 写入应该执行一些串行I/O.这是否也需要 用这些多路传输函数写的吗?

也许吧.如果这些串行I/O操作阻塞的时间可能比您愿意等待线程关闭开始的时间更长,则实现终止行为目标需要对其进行控制.用其中一个多路传输函数对它们进行选通是实现这一点的一种方法.依靠一两个信号来打断他们可能是另一回事.我会建议 Select 一种方法,并始终如一地使用它,但这是一个设计 Select .

总体建议

  • Do use a dedicated signal-handling thread. In particular, I would suggest that
    • 注册一个什么都不做的信号处理程序,用于SIGTERM和任何其他你想由这个线程处理的信号
    • 这些信号通过它们的信号掩码为所有其他线程阻塞
    • 处理程序线程的所有信号but都将被阻塞
    • 处理机通过pause()sigsuspend()同步等待信号
  • 请务必使用select/poll/或epoll来控制I/O操作的执行,否则可能会阻塞比您愿意等待关闭开始的时间更长的I/O操作
  • 一定要设置一个管道或类似的I/O通道,可以将其添加到多路复用器的监视集中,并让信号处理线程使用它来确保线程在需要帮助时及时注意到终止信号.
    • 当信号处理线程检测到信号时,在设置全局原子标志以记录该信号后,它将写入此通道
    • 其他线程只需要看到该通道是可读的. 他们不需要实际使用其中的任何数据,也不应该这样做,这样它就保持了可读性.

Linux相关问答推荐

IntelliJ(PyCharm)不再识别Linux中的AltGr快捷键

如何更改文件的上次访问/修改/更改日期?

当 skylake 有 fsgsbase 时,为什么使用 __builtin_ia32_wrfsbase64 会收到非法指令?

nohup 是否可以跨管道工作?

如何指定链接时使用的库版本?

每次来宾重新启动后 Vagrant 执行脚本或命令(vagrant up)

使用 awk 或 sed 删除特定字符

从 Linux 到 Windows 交叉编译 C++ 应用程序的手册?

diff 命令仅获取不同行的数量

如何像 Nautilus 那样从命令行挂载?

使用 linux 命令行 (bash) 从网络摄像头拍照

Monit 守护程序 - 连接到 monit 守护程序时出错

如何将路径名中的..转换为 bash 脚本中的绝对名称?

我可以打开一个套接字并将其传递给 Linux 中的另一个进程吗

在 Linux / Mono 上运行 ServiceStack 的最佳方式是什么?

/dev/random 非常慢?

后缀 - status=bounced(未知用户myuser)

如何将初始输入通过管道传输到随后将是交互式的进程中?

在 Docker 容器中运行的 JVM 的驻留集大小 (RSS) 和 Java 总提交内存 (NMT) 之间的差异

如何制作和应用SVN补丁?