如果在取消点时调用信号处理程序会发生什么?

10
假设一个应用程序在取消点,例如read处被阻塞,并接收到一个信号并调用信号处理程序。 Glibc / NPTL通过在系统调用期间启用异步取消来实现取消点,因此据我所知,异步取消将在整个信号处理程序的持续时间内保持有效。当然,这是极其错误的,因为有许多函数不是异步取消安全的,但需要从信号处理程序中安全调用。
这让我有两个问题:
  • 我错了还是glibc / NPTL的行为确实是如此危险?如果是这样,这种危险行为是否符合标准?
  • 根据POSIX,在进程执行取消点函数时如果调用信号处理程序应该发生什么?
  • 编辑:我几乎已经说服自己,任何可能成为目标的线程都必须确保取消点函数永远不会从该线程的上下文中的信号处理程序中调用:

    一方面,任何信号处理程序都可能在可能被取消的线程中调用,并使用任何异步取消不安全函数,则必须在调用任何取消点函数之前禁用取消操作。这是因为从信号中断的代码的角度来看,任何此类取消将等效于异步取消。另一方面,信号处理程序不能禁用取消,除非运行信号处理程序时的代码只使用异步信号安全函数,因为pthread_setcancelstate不是异步信号安全的。

    2
    这一系列的问题确实成功地让我遇到pthread取消时的反应是朝另一个方向快速奔跑。 - caf
    @caf:如果您养成在可能被取消的线程中保持信号阻塞的习惯(或者更好的是,在除主线程以外的所有线程中),并将任何可能成为取消点的资源分配系统调用封装在完全禁用和恢复取消状态的调用中,则即使在相当糟糕的实现上,取消也不会有危险,并且可以成为一个强大的工具。我的一系列问题都是从实现者的角度出发,旨在完全符合规范并可能超越规范,保证符合规范的应用程序没有资源泄漏或状态损坏。 - R.. GitHub STOP HELPING ICE
    3个回答

    4
    回答我自己问题的前半部分: glibc确实表现出了我预测的行为。在取消点被阻塞时运行的信号处理程序将在异步取消下运行。要看到这个效果,只需创建一个线程,调用将永远(或长时间)阻塞的取消点,等待片刻,发送一个信号,再次等待片刻,然后取消并加入它。信号处理程序应该以一种使其明显在被异步终止之前运行了不可预测的时间的方式操纵一些易失性变量。
    至于POSIX是否允许此行为,我仍然不确定100%。 POSIX规定:
    每当启用可取消性并发出取消请求并将该线程作为目标时,该线程调用任何函数(如pthread_testcancel()或read())是取消点的情况下,取消请求必须在函数返回之前得到执行。如果启用可取消性并且使用线程作为目标发出取消请求,同时线程在取消点挂起,则线程将被唤醒并执行取消请求。如果线程在取消点挂起,并且等待它的事件发生,或者指定的超时已过去,那么未指定取消请求是否会被执行或取消请求是否保持挂起状态,线程恢复正常执行。
    假定执行信号处理程序不是“挂起”的情况,因此我倾向于将glibc在这里的行为解释为不符合规范。

    1

    Rich,

    我在进行AC-safe文档审核时遇到了这个问题,这是Alex Oliva为glibc工作的内容。

    我认为GNU C库实现(基于nptl)没有问题。虽然异步取消已在阻塞系统调用周围启用(这些调用需要成为取消点),但此行为仍应符合标准。

    另外,异步取消启用后发出的信号将导致带有异步取消启用的信号处理程序运行。同样真实的是,在处理程序中执行任何未经过异步取消安全的操作都是危险的。

    还真实的是,如果另一个线程将正在运行信号的线程作为目标调用pthread_cancel,则此类取消将立即生效。这仍符合POSIX措辞的“在函数返回之前”(在本例中,read尚未返回,目标线程位于信号处理程序中)。

    信号的问题在于它导致线程处于两种同时状态,既永久处于取消点,又执行指令。如果取消请求到达,则我认为立即采取行动是符合标准的。不过奥斯汀小组可能会澄清这一点。

    glibc实现存在的问题是,它要求所有由将被取消的线程执行的信号处理程序只调用异步取消安全函数。这是一个非明显的要求,不源自标准,但并不使其不符合规范。
    解决信号处理程序脆弱性的潜在解决方案:
    - 不为阻塞系统调用启用async-cancellation,而是在取消实现中启用一个新的IN_SYSCALL位。 - 当调用pthread_cancel并且目标线程已设置IN_SYSCALL时,像异步取消一样向线程发送SIGCANCEL,但SIGCANCEL处理程序什么也不做(除了中断系统调用的副作用)。 - 系统调用的包装器将查找是否已发送取消请求,并在包装器返回之前取消线程。
    虽然在stack overflow上发布这篇文章很有趣,但我不认识其他任何人可以以所需的详细信息回答您的问题。
    我认为任何进一步的讨论都应该在Austin Group邮件列表上作为POSIX标准讨论的一部分进行,或者应该在libc-alpha作为glibc实现讨论的一部分进行。

    你的回答很好地涵盖了glibc中的当前实现,但在我看来,它在符合性讨论的当前状态方面落后了。现在已知,具有临时异步取消的当前glibc实现是不符合标准的(由于在取消发生时违反[无]副作用要求),而且当前实现会产生行为(双重关闭风险和其他危险的竞争条件),这是Austin Group似乎致力于避免的。 - R.. GitHub STOP HELPING ICE
    “close” 问题在 Austin Group 追踪器的第614个问题中有所涉及,我曾经报告过此问题。当时它被认为已经在处理“close”和“EINTR”的问题529中解决了。通过加强当返回“EINTR”时对“close”副作用的要求,问题529得到了解决。由于他们似乎认为这对取消要求close的影响很大,我将其解释为再次确认在我们正在谈论的情况下,与“EINTR”具有相同副作用的取消要求匹配的要求会管理行为。 - R.. GitHub STOP HELPING ICE
    @R.. 很高兴看到标准问题有所进展。我正在https://sourceware.org/bugzilla/show_bug.cgi?id=12683上跟进,提出了glibc的解决方案。 - Carlos O'Donell
    顺便提一下,在我上一条评论中s/they seemed/they deemed/。没有这个,会让人感到困惑... - R.. GitHub STOP HELPING ICE

    0

    我认为你要找的是两件事的组合:

    一些系统调用可能会被信号中断,这会导致返回EINTR错误。这是正常行为,但我从来没有清楚过如果例如在read的中途被中断会发生什么--流中是否有任何内容被读取?也许有人可以评论一下以帮助澄清。

    不应被中断的系统调用,像你担心的那样,应该包装在对sigprocmask(或线程中的pthread_sigmask)的调用中,以防止它们被中断。一旦重新启用信号,任何在阻塞期间接收到的信号都将被传递。与中断一样,如果您阻塞时间过长,可能会因覆盖而错过一些信号(多次接收相同信号计为一个待处理信号)。


    1
    read() 可能会因为信号中断而返回部分读取的数据或 EINTR 错误,也可能会被重新启动。不管怎样,R.. 并不是在询问这个。他在询问 pthreads 取消和信号处理之间的交互。 - ninjalj
    3
    如果read在收到信号并读取了一些数据时被中断,它将在信号处理程序运行后立即返回读取的字节数。如果尚未读取任何数据,则根据信号处理程序的安装方式,它要么返回-1并将errno设置为EINTR(如果没有使用SA_RESTART选项安装处理程序),要么在信号处理程序返回后继续等待数据。 - R.. GitHub STOP HELPING ICE
    是的,正如ninjalj所说,那与我的问题大部分无关...尽管glibc的实现中存在另一个有趣的问题 - 我认为这是一个错误 - 信号中断/重启问题与取消问题相交,可能会导致在取消时泄漏资源。 - R.. GitHub STOP HELPING ICE
    抱歉,我可能误解了你的问题。你尝试过在进行这些调用时向程序发送信号会发生什么吗? - Jonathan
    我可以运行一些测试,但在现实世界中,问题涉及一个很难达到的狭窄时间窗口。也许我可以编写一个信号处理程序,旋转几十亿次,看看它是否可以在进程在取消点被阻塞时运行并异步取消。 - R.. GitHub STOP HELPING ICE
    1
    我编写了一个测试程序,并确认glibc/NPTL表现出了我预测的“不愉快”行为。 - R.. GitHub STOP HELPING ICE

    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接