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

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

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

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

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

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

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

Эта программа связана с этой хорошей нитью .

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

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

#include  #include  #include  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-сигнала из обработчиков pthread_atfork (), и наиболее важным из них является обработчик дочерних элементов , где выполняется только memcpy ().

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

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

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

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

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

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

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