Подтвердить что ты не робот

Как использовать pthread_atfork() и pthread_once() для повторной инициализации мьютексов в дочерних процессах

У нас есть общая библиотека С++, которая использует библиотеку ZeroC Ice для RPC, и, если мы не выключим среду выполнения Ice, мы наблюдаем, как дочерние процессы висят на случайных мьютексах. Ice runtime запускает потоки, имеет множество внутренних мьютексов и сохраняет дескрипторы открытых файлов на серверах.

Кроме того, у нас есть несколько собственных мьютексов, чтобы защитить наше внутреннее состояние.

Наша разделяемая библиотека используется сотнями внутренних приложений, поэтому мы не контролируем процесс, когда процесс вызывает fork(), поэтому нам нужен способ безопасного отключения Ice и блокировки наших мьютексов во время вилки процесса.

Чтение стандарта POSIX на pthread_atfork() при обработке мьютексов и внутреннего состояния:

В качестве альтернативы, некоторые библиотеки могли бы предоставить только дочернюю подпрограмму, которая повторно инициализирует мьютексы в библиотеке и все связанные состояния с некоторым известным значением (например, что было, когда изображение было первоначально выполнено). Однако этот подход невозможен, поскольку реализациям разрешено сбой * _init() и * _destroy() вызывает мьютексы и блокировки, если мьютекс или блокировка по-прежнему заблокированы. В этом случае дочерняя процедура не может повторно инициализировать мьютексы и блокировки.

В Linux эта программа test C возвращает EPERM из pthread_mutex_unlock() в обработчике child pthread_atfork(). Linux требует добавления _NP к макросу PTHREAD_MUTEX_ERRORCHECK для его компиляции.

Эта программа связана с этим хорошим потоком.

Учитывая, что технически небезопасно или легально разблокировать или уничтожить мьютекс в ребенке, я думаю, что лучше иметь указатели на мьютексы, а затем дать ребенку новый pthread_mutex_t в куче и оставить только родительские мьютексы, тем самым имея небольшую утечку памяти.

Единственная проблема заключается в том, как повторно инициализировать состояние библиотеки, и я думаю о перезапуске pthread_once_t. Возможно, потому что у POSIX есть инициализатор для pthread_once_t, который может быть reset в его исходное состояние.

#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() я только reset pthread_once_t, создав копию нового pthread_once_t, инициализированного PTHREAD_ONCE_INIT. Новый pthread_mutex_t создается только тогда, когда функция библиотеки вызывается в дочернем процессе.

Это взломанный, но, возможно, лучший способ борьбы с этим обходом стандартов. Если pthread_once_t содержит мьютекс, то система должна иметь способ инициализировать его из состояния PTHREAD_ONCE_INIT. Если он содержит указатель на мьютекс, выделенный в куче, он будет вынужден назначить новый и установить адрес в pthread_once_t. Я надеюсь, что он не использует адрес pthread_once_t для чего-то особенного, который мог бы победить это.

Поиск Группа comp.programming.threads для pthread_atfork() показывает много хороших обсуждений и насколько мало стандартов POSIX действительно обеспечивает решение этой проблемы.

Также существует проблема, что нужно только вызывать функции с поддержкой асинхронного сигнала из обработчиков pthread_atfork(), и оказывается, что наиболее важным является обработчик дочерних элементов, где выполняется только memcpy().

Это работает? Есть ли лучший способ справиться с требованиями нашей общей библиотеки?

4b9b3361

Ответ 1

Поздравляем, вы обнаружили дефект в стандарте. pthread_atfork принципиально неспособен решить проблему, которую он создал для решения с помощью мьютексов, потому что обработчику в дочернем устройстве не разрешено выполнять какие-либо операции с ними:

  • Он не может их разблокировать, потому что вызывающий объект будет новым основным потоком во вновь создаваемом дочернем процессе и не тем же потоком, что и поток (в родительском), который получил блокировку.
  • Он не может уничтожить их, потому что они заблокированы.
  • Он не может повторно инициализировать их, потому что они не были уничтожены.

Одним из возможных способов решения проблемы является использование семафоров POSIX вместо мьютексов. Семафор не имеет владельца, поэтому, если родительский процесс блокирует его (sem_wait), как родительский, так и дочерний процессы могут разблокировать (sem_post) свои соответствующие копии, не вызывая поведения undefined.

Как хорошо, sem_post безопасен для асинхронного сигнала и, следовательно, определенно легален для использования ребенком.

Ответ 2

Я считаю это ошибкой в ​​программах, вызывающих fork(). В многопоточном процессе дочерний процесс должен вызывать только асинхронные сигнальные функции. Если программа хочет использовать fork без exec, она должна сделать это до создания потоков.

На самом деле не очень хорошее решение для потоковой fork()/pthread_atfork(). Некоторые куски, похоже, работают, но это не переносимо и не может переломить версии ОС.