POSIX是否保证信号不会被发送到部分初始化的线程?

10
在大多数POSIX线程的实现中,在新创建的线程能够运行应用程序代码之前,需要对其进行一些初始化操作,以使其处于一致的状态。这可能涉及在线程结构中解锁锁定、在使用“线程寄存器”的实现中初始化它,初始化线程本地数据(无论是编译器级别的TLS还是POSIX线程特定数据)等。我找不到明确的保证所有这些初始化将在线程接收任何信号之前完成的证据;最接近的是2.4.3中的内容:

以下表格定义了一组应为异步信号安全的函数。因此,应用程序可以在信号捕获函数中不受限制地调用它们:

...

大概率上,其中一些函数(至少有fork函数,必须检查由pthread_atfork函数建立的全局状态)依赖于线程处于一致、已初始化状态。
有一件让我困扰的事情是,我已经阅读了很多glibc/nptl源代码,但无法找到任何显式的同步机制来防止信号在新创建的线程尚未完全初始化时被处理。我期望调用pthread_create的线程在调用clone之前阻止所有信号,并且在初始化完成后,新线程解除阻塞,但我找不到任何相关代码,也在strace输出中没有看到它。

3
哇,如果在设置了 pthread_atfork() 处理程序的情况下从信号处理程序中调用 fork() ,那么你必须非常了解你在做什么(并且信任你的库实现)!特别是如果(通常情况下)预分配处理程序获取一堆锁以确保它们所代表的数据在分叉之前是一致的 - 任何这些锁都可能原则上被线程持有(或更糟糕的是,在正在获取这些锁的过程中)处理信号的线程,这意味着数据将不可避免地不一致(或进程可能会死锁!)。真是开心极了 :-) - psmears
好的,“fork”被列为异步信号安全函数之一,但我同意几乎所有有用的事情都不是“pthread_atfork”注册函数可以做到异步信号安全。尽管如此,仍然有一些有效的用途,例如,如果您的“pthread_atfork”处理程序只是使用固定值重新初始化数据,销毁控制互斥锁并在子进程中初始化新的互斥锁(当然)。 - R.. GitHub STOP HELPING ICE
更令人不安的是,如果一个库(由调用应用程序未知是否为线程化;甚至作为另一个库的依赖项间接动态加载)设置了不是异步信号安全的 pthread_atexit 处理程序,那会发生什么。调用应用程序可能期望 fork 是异步信号安全的(如文件所述),并从信号处理程序中调用它。我猜通过这个思想实验得出结论的意图是揭示了一个透明度 - 线程使用 - 由库创建的模型中的缺陷。pthread_atfork 被引入来解决这个问题。 - R.. GitHub STOP HELPING ICE
完全同意你们两位的评论 - 这只是强化了我的观点,在涉足那些混浊的水域之前,你真的需要知道自己在做什么 :-) - psmears
有一次,我不小心打成了 pthread_atexit 而不是 pthread_atfork。 :-) - R.. GitHub STOP HELPING ICE
3个回答

1

我认为这不是一个真正的答案,但它太大了,不能作为评论。

这是一个非常有趣的问题。我查看了pthread_create的glibc代码,看它如何运行,除非我完全错过了什么,否则似乎没有任何特殊的行为来阻止这种情况(例如,在clone之前阻止所有信号,并在子进程中进行一些设置后解除阻止{记录线程创建时间和设置C++ catch all异常处理程序,即使在C代码中也会发生})。

我原本期望找到一个提到这种情况可能性的注释,甚至可能提到POSIX建议做什么(或者提到它没有说明要做什么)。

也许你应该始终将pthread_create包装在代码块中以阻止和恢复信号,并在所有线程函数中以解除阻止调用开始。

这很可能是pthreads(或glibc或我对代码的理解)的一个疏忽。


我能够收集到的最好信息是,如果在初始化完成之前未在新线程中阻止信号,则实现必须确保这不会导致任何异步信号安全函数出错。由于__thread尚未成为标准,因此不属于POSIX的一部分,因此当然没有要求可以访问TLS变量或具有其正确的值。也许一个可能的实现(我还没有检查glibc是否这样做)是将所有信号处理程序包装在一个处理程序中,在调用应用程序的处理程序之前完成线程初始化(如果需要)。 - R.. GitHub STOP HELPING ICE
我怀疑 glibc 是否会像这样包装信号处理程序。我认为这样做需要它复制内核执行的信号处理并针对每次调用 sigaction 阻塞和恢复信号,以防引入竞态条件。我刚才看了一下,并没有发现它这么做。这也是一个笨拙的解决方案,非常困难,而且可能需要常规线程启动代码能够识别它已被中断,并且不应重新执行已经完成的操作。 - nategoose
我不会感到惊讶,如果旧的LinuxThreads包装了信号处理程序。那是一个丑陋的hack。 :-) - R.. GitHub STOP HELPING ICE
Linux内核的sigaction结构中有一个名为sa_restorer的函数指针,它包装了信号处理程序的底层部分(通过返回到glibc的sa_restore代码来发出_I'm_through_系统调用)。我刚刚在研究这个问题时了解到这一点,并意识到前几天我回答另一个问题时略有错误,因为我认为是内核负责处理这个问题。 - nategoose
看起来答案是,信号处理程序能够在哪个线程中运行的唯一方式是通过 errno。所有其他处理线程或线程本地状态的函数都不是异步信号安全的。我想知道如果一个信号在线程完成初始化其 TLS 区域之前到达,errno 是否真正正常工作在 glibc 上... - R.. GitHub STOP HELPING ICE

0

根据我对POSIX pthread_create规范的理解,这是强制要求的:

新线程的信号状态应初始化如下:

  • 信号掩码将从创建线程继承。
  • 新线程待处理的信号集应为空。

但我没有足够的经验来说不同实现方式是否相同。


1
我认为严格来说,新线程不会从其父线程或创建者那里继承或共享未决信号。 - nategoose
确实。我没有看到任何解释这个语句的方式,说线程在达到一致状态之前不会接收信号。 - R.. GitHub STOP HELPING ICE
@R - “一致状态”是什么意思?在达到这个一致状态之前和微秒级别之后接收到信号的实际影响是什么?可能仍然会被处理或忽略或其他操作。我并不是在争论,而是试图评估实际影响。 - Duck
@Duck:在我查看的glibc线程启动代码中,发生了一些操作。其中之一是初始化当前线程的异常处理默认捕获器(这是在C代码中完成的,并且在C++ {和可能在objective-C中使用pthread}中使用时是必要的)。我不知道在信号处理程序中抛出异常的规则是什么,但如果在此代码之前发生并且没有正确处理,它将无法像应该那样优雅地失败。 - nategoose
@Duck:我指的是用于管理线程的任何数据结构的一致性。 一些想到的事情包括访问errno(早期信号处理程序在访问errno时不应崩溃或覆盖另一个线程的errno),forkpthread_atfork处理程序以及线程本地存储(但这超出了任何当前规范的范围)。 - R.. GitHub STOP HELPING ICE

-1

pthread_create是一个阻塞调用。在调用之前,没有(新的)线程可以发送信号,但在调用之后有一个线程可以发送信号,因此调用返回线程的ID。

因此,我得出结论,该线程必须在那个时候有效并初始化...


我不明白。我的问题不是关于pthread_create向新线程发送信号,而是关于其他信号(由同一进程中的其他线程、其他进程或内核生成)在新线程初始化之前被传递到新线程的问题。 - R.. GitHub STOP HELPING ICE
这不是真的,pthread_create通常不是原子调用。 - nos

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