У нас есть общая библиотека С++, которая использует библиотеку 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().
Это работает? Есть ли лучший способ справиться с требованиями нашей общей библиотеки?