Обработка исключений посредством передачи сигналов в многопроцессных программах на C

В настоящее время мы пытаемся решить проблему в многопроцессной программе, которая предполагает использование сигналов для безопасного завершения дочерних процессов.

В нашей настройке родительский процесс отправляет сигнал своим дочерним процессам о прекращении и освобождении всех ресурсов, когда он сталкивается с критической ошибкой (например, сбой в malloc(), pipe() или fork()). Эта стратегия предназначена для предотвращения непредсказуемого поведения из-за взаимозависимостей между дочерними процессами.

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

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

Мы уже выяснили, что free() не является функцией, безопасной для асинхронных сигналов, и ее не следует вызывать в функциях обработчика сигналов.

Это подводит нас к нашему центральному запросу:

Какой эвристике следует следовать, чтобы определить время проверки флагов обработки исключений, установленных функциями обработки сигналов?

Каковы безопасные и элегантные стратегии решения этой проблемы?

Спасибо заранее за ваш вклад. Каждый ответ ценен!

Мы рассмотрели два возможных решения:

  1. Ограничение функций обработчика сигнала простой установкой глобального флага, который затем необходимо часто проверять во время обычного выполнения программы, чтобы определить, необходимо ли прерывание. Однако это выглядит не очень элегантно.

    • Когда лучше всего проверить состояние этого флага?
      • Перед каждым запросом дополнительных ресурсов от ОС (malloc(), pipe(), fork())?
      • Прежде чем вернуться из какой-либо функции?
  2. Установка каждого указателя в NULL сразу после его освобождения.

    • Существует ли риск того, что сигнал может прийти сразу после освобождения указателя, но до того, как ему будет присвоено значение NULL?

2. Да, существует (небольшой) риск неудачного выбора времени. Поэтому Закон Подлости предписывает, что это произойдет.

Jonathan Leffler 11.03.2024 13:56

Если ребенок собирается выйти, нужно ли ему освобождать память (или какие-либо другие ресурсы)?

Jonathan Leffler 11.03.2024 13:57

Рассмотрите возможность блокировки сигнала во время выполнения какой-либо работы, например освобождения массива, чтобы сигнал доставлялся только тогда, когда ребенку безопасно его получить.

Jonathan Leffler 11.03.2024 14:02

При завершении программы освобождать выделенную память не требуется. ОС все равно восстановит его. Особенно ненужно освобождать память при аварийном завершении программы. В сценарии аварийного завершения неразумно доверять чему-либо о состоянии программы, поэтому лучшей альтернативой обычно является просто остановка. То есть пусть обработчик сигнала вызывает _exit(1) или abort(), а может быть и exit(1).

John Bollinger 11.03.2024 14:56

Я аксиоматически устанавливаю для каждого указателя значение null после освобождения (поэтому использование после освобождения вызывает ошибку сегмента). В коммерческом критически важном приложении реального времени я применил глобальный подход (1). Поскольку достаточно создать такой глобальный volatile (по сравнению с использованием атомов), его проверка требует небольших затрат. Место установки проверки зависит от того, что делает процесс. Я сделал это на основе времени. Процесс может/не должен продолжаться Х времени без выполнения проверки. Это диктовало, где размещать проверки внутри циклов.

Craig Estey 11.03.2024 15:43

Моя ситуация была немного сложнее. Это был многопоточный, а не многопроцессный процесс. Для «нормальной» связи он использовал сообщения SysV IPC. Программа должна была корректно завершить работу и перезапуститься без каких-либо задержек. Итак, я установил глобальное значение (сигнал не требуется из-за многопоточности) и использовал сообщение «стоп». Каждый поток будет сохранять временную метку «сторожевого таймера» в верхней части своего цикла событий. Основной поток проверит эти временные метки.

Craig Estey 11.03.2024 15:51

Если какой-либо поток «зависает» (ненормальное состояние), основной поток устанавливает глобальное значение и отправляет сигналы всем потокам. Потоки будут сбрасывать информацию о состоянии (для последующей отладки), устанавливать флаг «бездействия/безопасного состояния» и переходить в режим ожидания. Основной поток будет следить за этими флагами. Когда все потоки находились в «безопасном» состоянии (или прошло достаточно времени), основной поток отправлял сигналы для принудительного завершения.

Craig Estey 11.03.2024 15:58
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
7
88
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Стратегией может быть установка освобожденных участников на NULL непосредственно перед свободным звонком:

for(...){
    void *mem = arr[i];
    arr[i] = NULL;
    free(mem);
} 

безопасно вызывать free для значений NULL , когда (половина) массива уже освобождена: ничего не произойдет.

Теперь единственной потенциальной проблемой будет то, что процесс может быть прерван между установкой члена на NULL и вызовом free, так что один участник не будет освобожден.

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

Это отличная идея. В принципе, вы заменили двойное бесплатное на пропущенное бесплатное, но (в целом) пропущенное бесплатное гораздо менее серьезно, чем двойное бесплатное.

Jonathan Leffler 11.03.2024 23:32

Другие вопросы по теме