Как использовать pthread_atfork () и pthread_once () для повторной инициализации мьютексов в дочерних процессах - PullRequest
19 голосов
/ 12 апреля 2010

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

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

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

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

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

В Linux эта тестовая программа C возвращает EPERM из pthread_mutex_unlock () в дочернем обработчике pthread_atfork (). 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_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 действительно предоставляет для решения этой проблемы.

Существует также проблема, заключающаяся в том, что нужно вызывать функции async-signal-safe только из обработчиков pthread_atfork (), и, по-видимому, наиболее важным является дочерний обработчик , где выполняется только memcpy () .

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

Ответы [ 2 ]

22 голосов
/ 07 июля 2011

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

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

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

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

7 голосов
/ 11 сентября 2010

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

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...