В настоящее время мы пытаемся решить проблему в многопроцессной программе, которая предполагает использование сигналов для безопасного завершения дочерних процессов.
В нашей настройке родительский процесс отправляет сигнал своим дочерним процессам о прекращении и освобождении всех ресурсов, когда он сталкивается с критической ошибкой (например, сбой в malloc(), pipe() или fork()). Эта стратегия предназначена для предотвращения непредсказуемого поведения из-за взаимозависимостей между дочерними процессами.
Проблема возникает, когда мы сталкиваемся со спорадическим двойным освобождением внутри нашей основной структуры, которая доступна для большинства наших функций. Вот ситуация:
Предположим, что дочерний процесс освобождает переменную из основной структуры (скажем, массива). Процесс перебора массива и освобождения каждого элемента, конечно, не мгновенен. Если функция, освобождающая массив, находится на полпути этой операции, и процесс получает сигнал (который запускает обработчик сигнала для освобождения основной структуры и завершения процесса), мы получаем двойное освобождение для первой половины массива, которое уже освобожден.
Мы уже выяснили, что free() не является функцией, безопасной для асинхронных сигналов, и ее не следует вызывать в функциях обработчика сигналов.
Это подводит нас к нашему центральному запросу:
Какой эвристике следует следовать, чтобы определить время проверки флагов обработки исключений, установленных функциями обработки сигналов?
Каковы безопасные и элегантные стратегии решения этой проблемы?
Спасибо заранее за ваш вклад. Каждый ответ ценен!
Мы рассмотрели два возможных решения:
Ограничение функций обработчика сигнала простой установкой глобального флага, который затем необходимо часто проверять во время обычного выполнения программы, чтобы определить, необходимо ли прерывание. Однако это выглядит не очень элегантно.
malloc(), pipe(), fork())?Установка каждого указателя в NULL сразу после его освобождения.
NULL?Если ребенок собирается выйти, нужно ли ему освобождать память (или какие-либо другие ресурсы)?
Рассмотрите возможность блокировки сигнала во время выполнения какой-либо работы, например освобождения массива, чтобы сигнал доставлялся только тогда, когда ребенку безопасно его получить.
При завершении программы освобождать выделенную память не требуется. ОС все равно восстановит его. Особенно ненужно освобождать память при аварийном завершении программы. В сценарии аварийного завершения неразумно доверять чему-либо о состоянии программы, поэтому лучшей альтернативой обычно является просто остановка. То есть пусть обработчик сигнала вызывает _exit(1) или abort(), а может быть и exit(1).
Я аксиоматически устанавливаю для каждого указателя значение null после освобождения (поэтому использование после освобождения вызывает ошибку сегмента). В коммерческом критически важном приложении реального времени я применил глобальный подход (1). Поскольку достаточно создать такой глобальный volatile (по сравнению с использованием атомов), его проверка требует небольших затрат. Место установки проверки зависит от того, что делает процесс. Я сделал это на основе времени. Процесс может/не должен продолжаться Х времени без выполнения проверки. Это диктовало, где размещать проверки внутри циклов.
Моя ситуация была немного сложнее. Это был многопоточный, а не многопроцессный процесс. Для «нормальной» связи он использовал сообщения SysV IPC. Программа должна была корректно завершить работу и перезапуститься без каких-либо задержек. Итак, я установил глобальное значение (сигнал не требуется из-за многопоточности) и использовал сообщение «стоп». Каждый поток будет сохранять временную метку «сторожевого таймера» в верхней части своего цикла событий. Основной поток проверит эти временные метки.
Если какой-либо поток «зависает» (ненормальное состояние), основной поток устанавливает глобальное значение и отправляет сигналы всем потокам. Потоки будут сбрасывать информацию о состоянии (для последующей отладки), устанавливать флаг «бездействия/безопасного состояния» и переходить в режим ожидания. Основной поток будет следить за этими флагами. Когда все потоки находились в «безопасном» состоянии (или прошло достаточно времени), основной поток отправлял сигналы для принудительного завершения.





Стратегией может быть установка освобожденных участников на NULL непосредственно перед свободным звонком:
for(...){
void *mem = arr[i];
arr[i] = NULL;
free(mem);
}
безопасно вызывать free для значений NULL , когда (половина) массива уже освобождена: ничего не произойдет.
Теперь единственной потенциальной проблемой будет то, что процесс может быть прерван между установкой члена на NULL и вызовом free, так что один участник не будет освобожден.
Однако, как отмечалось в комментариях, если программа все равно завершается, звонить бесплатно вообще не обязательно. ОС в любом случае очистит всю память, используемую процессом. На самом деле это может быть даже вредно.
Это отличная идея. В принципе, вы заменили двойное бесплатное на пропущенное бесплатное, но (в целом) пропущенное бесплатное гораздо менее серьезно, чем двойное бесплатное.
2. Да, существует (небольшой) риск неудачного выбора времени. Поэтому Закон Подлости предписывает, что это произойдет.