Книга: Linux программирование в примерах

10.7. Сигналы для межпроцессного взаимодействия

10.7. Сигналы для межпроцессного взаимодействия

«ЭТО УЖАСНАЯ МЫСЛЬ! СИГНАЛЫ НЕ ПРЕДНАЗНАЧЕНЫ ДЛЯ ЭТОГО! Просто скажите НЕТ».

- Джефф Колье (Geoff Collyer) -

Одним из главных механизмов межпроцессного взаимодействия (IPC) являются каналы, которые описаны в разделе 9.3 «Базовая межпроцессная коммуникация каналы и FIFO». Сигналы также можно использовать для очень простого IPC[111]. Это довольно грубо; получатель может лишь сказать, что поступил определенный сигнал. Хотя функция sigaction() позволяет получателю узнать PID и владельца процесса, пославшего сигнал, эти сведения обычно не очень помогают.

ЗАМЕЧАНИЕ. Как указывает цитата в начале, использование сигналов для IPC почти всегда является плохой мыслью. Мы рекомендуем по возможности избегать этого. Но нашей целью является научить вас, как использовать возможности Linux/Unix, включая их отрицательные моменты, оставляя за вами принятие информированного решения, что именно использовать.

Сигналы в качестве IPC для многих программ могут быть иногда единственным выбором. В частности, каналы не являются альтернативой, если две взаимодействующие программы не запущены общим родителем, а файлы FIFO могут не быть вариантом, если одна из взаимодействующих программ работает лишь со стандартными вводом и выводом. (Примером обычного использования сигналов являются определенные системные программы демонов, таких, как xinetd, которые принимают несколько сигналов, уведомляющих, что нужно повторно прочесть файл настроек, осуществить проверку непротиворечивости и т.д. См. xinetd(8) в системе GNU/Linux и inetd(8) в системе Unix.)

Типичная высокоуровневая структура основанного на сигналах приложения выглядит таким образом:

for(;;){
 /* Ожидание сигнала */
 /* Обработка сигнала */
}

Оригинальным интерфейсом V7 для ожидания сигнала является pause():

#include <unistd.h> /* POSIX */
int pause(void);
pause()
приостанавливает процесс; она возвращается лишь после того, как сигнал будет доставлен и его обработчик вернется из вызова. По определению, pause() полезна лишь с перехваченными сигналами — игнорируемые сигналы при их появлении игнорируются, а сигналы с действием по умолчанию, завершающим процесс (с созданием файла образа или без него), продолжают действовать так же.

Проблема в только что описанной высокоуровневой структуре приложения кроется в части «Обработка сигнала». Когда этот код запускается, вы не захотите обрабатывать другой сигнал; вы хотите завершить обработку текущего сигнала до перехода к следующему. Одним из возможных решений является структурирование обработчика сигнала таким образом, что он устанавливает флаг и проверяет его в главном цикле: volatile sig_atomic_t signal_waiting = 0; /* true, если не обрабатываются сигналы */
void handler(int sig) {
 signal_waiting = 1;
 /* Установка других данных, указывающих вид сигнала */

В основном коде флаг проверяется:

for (;;) {
 if (!signal_waiting) { /* Если возник другой сигнал, */
  pause(); /* этот код пропускается */
  signal_waiting = 1;
 }
 /* Определение поступившего сигнала */
 signal_waiting = 0;
 /* Обработка сигнала */
}

К сожалению, этот код изобилует условиями гонки:

for (;;) {
 if (!signal_waiting) {
  /* <--- Сигнал может появиться здесь, после проверки условия! */
  pause(); /* pause() будет вызвана в любом случае */
  signal_waiting = 1;
 }
 /* Определение поступившего сигнала
    <--- Сигнал может переписать здесь глобальные данные */
 signal_waiting = 0;
 /* Обработка сигнала
    <--- То же и здесь, особенно для нескольких сигналов */
}

Решением является блокирование интересующего сигнала в любое время, кроме ожидания его появления. Например, предположим, что интересующим нас сигналом является SIGINT:

void handler(int sig) {
 /* sig автоматически блокируется функцией sigaction() */
 /* Установить глобальные данные, касающиеся этого сигнала */
}
int main(int argc, char **argv) {
 sigset_t set;
 struct sigaction act;
 /* ...обычная настройка, опции процесса и т.д. ... */
 sigemptyset(&set); /* Создать пустой набор */
 sigaddset(&set, SIGINT); /* Добавить в набор SIGINT */
 sigprocmask(SIG_BLOCK, &set, NULL); /* Заблокировать его */
 act.sa_mask = set; /* Настроить обработчик */
 act.sa_handler = handler;
 act.sa_flags = 0;
 sigaction(sig, &act, NULL); /* Установить его */
 ... /* Возможно, установить отдельные обработчики */
 ... /* для других сигналов */
 sigemptyset(&set); /* Восстановить пустой, допускает SIGINT */
 for (;;) {
  sigsuspend(&set); /* Ждать появления SIGINT */
  /* Обработка сигнала. SIGINT здесь снова блокируется */
 }
 /* ...любой другой код... */
 return 0;
}

Ключом к использованию этого является то, что sigsuspend() временно заменяет маску сигналов процесса маской, переданной в аргументе. Это дает SIGINT возможность появиться. При появлении он обрабатывается; обработчик сигнала возвращается, а вслед за ним возвращается также sigsuspend(). Ко времени возвращения sigsuspend() первоначальная маска процесса снова на месте.

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

При наличии всего этого не следует в новом коде использовать pause(). pause() был стандартизован POSIX главным образом для поддержки старого кода. То же самое верно и для функции sigpause() System V Release 3. Вместо этого, если нужно структурировать свое приложение с использованием сигналов для IPC, используйте исключительно функции API sigsuspend() и sigaction().

ЗАМЕЧАНИЕ. Приведенный выше код предполагает, что маска сигналов процесса начинается пустой. Код изделия должен вместо этого работать с любой маской сигналов, имеющейся на момент запуска программы.

Оглавление книги


Генерация: 0.475. Запросов К БД/Cache: 3 / 0
поделиться
Вверх Вниз