20

我们有一个 C++ 共享库,它使用ZeroC 的 Ice库进行 RPC,除非我们关闭 Ice 的运行时,否则我们会观察到子进程挂在随机互斥体上。Ice 运行时启动线程,有许多内部互斥体,并为服务器保留打开的文件描述符。

此外,我们有一些自己的互斥锁来保护我们的内部状态。

我们的共享库被数百个内部应用程序使用,因此我们无法控制进程何时调用 fork(),因此我们需要一种方法来安全地关闭 Ice 并在进程 fork 时锁定我们的互斥锁。

在pthread_atfork()上阅读关于处理互斥锁和内部状态的 POSIX 标准:

或者,一些库可能只提供一个子例程,将库中的互斥锁和所有相关状态重新初始化为某个已知值(例如,最初执行图像时的状态)。但是,这种方法是不可能的,因为如果互斥锁或锁仍处于锁定状态,则允许实现对互斥锁和锁的 *_init() 和 *_destroy() 调用失败。在这种情况下,子例程无法重新初始化互斥锁和锁。

在 Linux 上,此测试 C 程序从子 pthread_atfork() 处理程序中的 pthread_mutex_unlock() 返回 EPERM。Linux 需要将 _NP 添加到 PTHREAD_MUTEX_ERRORCHECK 宏中才能编译。

这个程序是从这个好线程链接的。

鉴于在孩子中解锁或销毁互斥锁​​在技术上是不安全或不合法的,我认为最好有指向互斥锁的指针,然后让孩子在堆上创建新的 pthread_mutex_t 并单独留下父的互斥锁,从而拥有一个小内存泄漏。

唯一的问题是如何重新初始化库的状态,我正在考虑重置 pthread_once_t。可能是因为 POSIX 有一个 pthread_once_t 的初始化程序,它可以重置为其初始状态。

#include <pthread.h>
#include <stdlib.h>
#include <string.h>

static pthread_once_t once_control = PTHREAD_ONCE_INIT;

static pthread_mutex_t *mutex_ptr = 0;

static void
setup_new_mutex()
{
    mutex_ptr = malloc(sizeof(*mutex_ptr));
    pthread_mutex_init(mutex_ptr, 0);
}

static void
prepare()
{
    pthread_mutex_lock(mutex_ptr);
}

static void
parent()
{
    pthread_mutex_unlock(mutex_ptr);
}

static void
child()
{
    // Reset the once control.
    pthread_once_t once = PTHREAD_ONCE_INIT;
    memcpy(&once_control, &once, sizeof(once_control));
}

static void
init()
{
    setup_new_mutex();
    pthread_atfork(&prepare, &parent, &child);
}

int
my_library_call(int arg)
{
    pthread_once(&once_control, &init);

    pthread_mutex_lock(mutex_ptr);
    // Do something here that requires the lock.
    int result = 2*arg;
    pthread_mutex_unlock(mutex_ptr);

    return result;
}

在上面的 child() 示例中,我只通过复制使用 PTHREAD_ONCE_INIT 初始化的新 pthread_once_t 来重置 pthread_once_t。只有在子进程中调用库函数时才会创建新的 pthread_mutex_t。

这是 hacky 但也许是处理这个绕过标准的最好方法。如果 pthread_once_t 包含一个互斥体,那么系统必须有一种方法可以从其 PTHREAD_ONCE_INIT 状态对其进行初始化。如果它包含指向在堆上分配的互斥锁的指针,那么它将被迫分配一个新的并在 pthread_once_t 中设置地址。我希望它不会将 pthread_once_t 的地址用于任何可以解决此问题的特殊情况。

在 comp.programming.threads 组中搜索pthread_atfork()显示了很多很好的讨论,以及 POSIX 标准真正为解决这个问题提供了多少。

还有一个问题是应该只从 pthread_atfork() 处理程序调用异步信号安全函数,而且似乎最重要的是子处理程序,其中只完成了一个 memcpy()。

这行得通吗?有没有更好的方法来处理我们共享库的要求?

4

2 回答 2

24

恭喜,您发现标准存在缺陷。pthread_atfork从根本上无法解决它为解决互斥锁而创建的问题,因为子进程中的处理程序不允许对它们执行任何操作:

  • 它无法解锁它们,因为调用者将是新创建的子进程中的新主线程,并且与获得锁的线程(在父线程中)不同。
  • 它不能摧毁它们,因为它们被锁定了。
  • 它不能重新初始化它们,因为它们还没有被销毁。

一种可能的解决方法是在此处使用 POSIX 信号量代替互斥锁。信号量没有所有者,因此如果父进程锁定它(sem_wait),父进程和子进程都可以解锁(sem_post)它们各自的副本,而不会调用任何未定义的行为。

顺便说一句,sem_post它是异步信号安全的,因此孩子使用绝对合法。

于 2011-07-07T03:30:20.913 回答
9

我认为这是调用 fork() 的程序中的错误。在多线程进程中,子进程应该只调用异步信号安全函数。如果一个程序想要在没有 exec 的情况下进行 fork,它应该在创建线程之前这样做。

线程化 fork()/pthread_atfork() 并没有真正好的解决方案。它的某些部分似乎可以工作,但这不是可移植的,并且可能会跨操作系统版本中断。

于 2010-09-10T23:02:27.280 回答