IPC сигналы

В *nix-подобных системах существует достаточно способов для общения между процессами. Каждый из способов оптимален для определённых целей.  Если систематизировать все способы, то их можно представить как на рисунке. В данной статье мы рассмотрим  IPC с помощью сигналов(signal на рисунке).

 



Сигналы предназначены для общения между процессами. Но не для передачи больших объёмов данных. Хотя и возможно их использование для этих целей. Сигналы активно используются ядром системы для сообщения процессу о событии требующем внимания. Для многих сигналов уже прописано стандартное поведение. Независимо от наших действий при запуске каждой программы уже установлено поведение обработчиков сигналов. Например для SIGSTOP (ctrl+z) – перевод процесса в sleep состояние, для SIGINT(ctrl+c) – завершение процесса, SIGQUIT (ctrl+/) – выход процесса с core dump .

Все сигналы в системе можно подразделить на стандартные(от 0 до SIGRTMIN) и realtime(от SIGRTMIN до SIGRTMAX) сигналы:

  •  
    • стандартные сигналы одного типа при доставке не ставятся в очередь. Т.е., если в очереди есть стандартный сигнал и придёт ещё один сигнал или больше такого же типа(например два сигнала SIGHUP и приходит ещё сигнал SIGHUP), то процесс получит сигнал один раз. Т.е. возможна “потеря” сигналов(из-за использования типа сигналов для которых не предполагается постановка в очередь). Также для стандартных сигналов не гарантируется порядок доставки как при отправке.

    • Для realtime сигналов гарантируется порядок доставки и при поступления одновременно процессу более одного сигнала одного и того же типа (например SIGRTMAX-5) выполняется их постановление в очередь.

       Процесс может устанавливать какие сигналы и когда он хочет обрабатывать. Нельзя только переопределить SIGKILL и SIGSTOP. Интересным является сигнал SIGTERM – по соглашению сигнал предшествует сигналу SIGKILL. Это форма «вежливого» SIGKILL. Процесс получивший сигнал сигнал SIGTERM знает, что ему даётся несколько секунд на завершение работы(закрытие лог файлов, очистка следов работы за собой) и добровольный выход, иначе, если он останется в памяти, будет послан SIGKILL, который завершит безоговорочно процесс(так действуют например процесс init и команды killall, kill (man 1) ).

     Устанавливать обработчики для обработки исключений полезно. Например, если при работе программы произошла ошибка segment violation (исключение выработалось процессором → сведения получила система → система отправила процессу сигнал об этом), то уже на это установлен стандартный обработчик. Поведением для которого является завершение процесса. Если же установить обработчик, то возможно самостоятельно завершить программу (закрыв при этом файлы и завершив корректно работу) или даже продолжить выполнение программы, разобрав причину ошибки. Аналогично можно обрабатывать ошибки связанные с доступом к памяти SIGBUS, ошибки связанные с проблемами работы аппаратной части SIGEMT. Обычно деление на 0 или арифметическая ошибка во время выполнения вызывает завершение программы, конечно, такие ошибки можно предусмотреть заранее, но если они всё же случатся, то возможно узнать об этом, корректно установив обработчик для SIGFPE и не допустив краха программы(подобное возможно сделать для ряда сигналов как SIGXCPU, SIGXFDz и др. )

          Сигналы работают быстрее, чем другие способы IPC и обходятся системе меньшим потреблением ресурсов. Но при передаче больших объёмов данных это преимущество теряется. Поэтому наиболее оптимально использовать, чтобы дать знать какому-либо процессу о происшествии события. Например, при выполнении программы, которая производит интенсивные вычисления принято давать отчёт пользователю о ходе процесса. Более рациональным будет установить в программе глобальную переменную(допустим % выполнения) и перед началом вычислений взводить таймер. По истечении которого будет генерироваться ALRM(raise(),), и по этому сигналу обновлять значение переменной. Без сигналов пришлось бы выполнять эти действия в цикле выполнения программы, что постоянно бы отвлекало часть ресурсов.

               Сигналы присутствуют в *nix с самого начала развития и были первой формой IPC. Для установки обработчика сигнала изначально использовалась функция signal. Её поведение различно от *nix к *nix и её использование может повлечь проблемы с переносимостью. Она доступна только из соображений обратной совместимости(снизу вверх, backward compatibility). Рекомендуется более стандартизированная и переносимая функция sigaction, которая даёт больше возможностей, хотя и установка обработчика с её помощью сложнее. Sigaction позволяет не только установить обработчик для определённого типа сигнала(а signal только это и может делать), но и узнать о том, кто послал сигнал и передавать дополнительные данные. Передача дополнительных данных через sigaction не реализована на всех платформах, в целях переносимости лучше использовать sigqueue. Sigqueue может передавать либо int либо void *.

            Также поведение обработчика сигнала при доставке сигнала зависит устанавливался ли обработчик с помощью signal(обработка сигнала прервется, перейдем на обработку поступившего сигнала и после вернемся назад) или sigaction(один и тот же сигнал не может прерывать сам себя).

           Сигналы можно принимать не только посредством установки обработчиков, но и через виртуальный файл. С помощью функции signalfd() создаётся файловый дескриптор, через который можно принимать сигналы. Преимущество — программа не прерывается сигналами, а проверить был ли сигнал или нет можно когда удобно.

         При использовании сигналов для IPC не следует забывать, что всё же их главное назначение не для IPC. Есть определённый ограничения, как например опасность использования небезопасных(non-reentrant) функций. Reentrant считается такая функция, которая не использует глобальных переменных. В следствии чего её можно использовать в неклассическом *nix процессе(более одного потока). Есть функции, которые вообще non-reentrant(malloc(), pritnf(), scanf()), а есть функции для которых существуют reentrant аналоги (например для exit() есть _exit()). Non-reeentrant функции опасно использовать в обработчике. Но в примере(сервере) такие функции используются). Попробуем пояснить:

         При запуске программы(где не используется распараллеливание) создаётся процесс. В процессе есть раздел инициализированных данных, неинициали- зированных, куча, стек. Изначально в процессе один поток. При обработке сигнала обработчиком, выполнение основного потока приостанавливается и обработчик запускается отдельным потоком. Потоки разделяют общее адресное пространство. Причём основной поток может быть прерван на выполнении non-reentrant функции, а если обработчик сигнала выполнит non-reentrant функцию, то произойдёт перезатирание данных(глобальных переменных). В примере-сервере использование non-reentrant функций безопасно, потому что server установив обработчики находится в режиме ожидания (функция pause()). То есть обработчикам сигналов нечего прерывать в основном потоке. При входе в обработчик сигнала, в целях безопасности, вручную блокируются другие сигналы с помощью sigsetmask().

         В качестве примера написаны две программы — сервер и клиент. Сервер выполняет предопределённое действие(отсчёт количества обращений к нему) по запросу со стороны клиента. В системе может находиться только один сервер. И сколь угодно клиентов(ограничено количеством pid).

Разберём работу сервера:

      Задача сервера сводится к выполнению по приходу сигнала SIGALRM записи в файл числа обращений. Число обращений хранится в глобальной переменной intglobal_count. Но, отправлять сигналы на работу могут только процессы прошедшие синхронизацию. Если это попытается сделать посторонний процесс(включая процессы root), то они будут тихо проигнорированы.

Информация о том свой это или постороний процесс хранится в контейнере, в списке пар ключа и переменной. При этом в одном контейнере все ключи уникальны(single map). Данные хранятся в виде pid – bool var, где bool var изначально устанавливается в false.

      Для того, чтобы быть отправлять сигналы на работу SIGALRM серверу и не быть проигнорированными клиенты должны пройти согласование:

  • отправить серверу один раз сигнал SIGWINCH. При этом сервер создаст новое поле в структуре map. Добавив туда в качестве ключа pid процесса-отправителя(клиент) и установив второе поле (переменная ) в false.

  • Отправить серверу 5 сигналов c сопроводительными данными(1, 2 , 3, 4, 5).

         Сервер ждёт именно 5 сигналов и именно с такими данными. Только получив их от определённого процесса, он будет обрабатывать сигналы-запросы на выполнение от клиента. Получилось нечто вроде пароля. В случае недостаточности его, можно генерировать sha или md5 ключ из случайных чисел из /dev/random. При этом, если передавать исходные данные для генерации ключа через сигналы всем кто пожелает, то это теряет смысл. Если же сделать более безопасно, то уже выйдем за рамки общения только с помощью сигналов (например данные для генерации ключа можно хранить в файле в произвольной директории, при этом шифруя его алгоритмом AES + перекрывая сверху своим ключом. Сам ключ к файлу нигде не хранить, а принимать его с командной строки). Но такие меры безопасности здесь излишни.

Особенности:

 Сервер и клиент представляют собой две отдельные программы. Клиенту, для того чтобы сделать запрос на выполнение работы, необходимо связаться с сервером. Pid выдаются системой, нельзя определить для запускаемой программы какой pid она получит.

Клиент при запуске отправляет сигнал всем процессам своей группы сигнал SIGWINCH, а сервер имеет обработчик для этого сигнала. Установка обработчика производится с помощью sigaction(), которая содержит в структуре, передаваемой вторым параметром const struct sigaction *act pid источника сигнала. Сервер получив сигнал, знает pid источника и отвечает отправителю, клиент запоминает в ответе pid сервера. Связь установлена.

Клиент отсчитывает временные интервалы 935 ms(milliseconds) и отправляет серверу сигнал на выполнение работы SIGALRM. При этом, при большом количестве клиентов или интенсивных вычислениях выполняемых в обработчике сигнала или если уменьшить интервал отправки сигналов, то возможна ситуация, когда сервер не успеет выполнить действия за интервал. Не спасают ни стандартные ни сигналы реального времени. Если использовать первые, то при поступлении более одного сигнала в очередь, остальные будут проигнорированы. Из этого следует, что часть сигналов будет утеряна и сервер не сделает действий, хотя он должен был. Если использовать сигналы реального времени, то при хроническом накоплении сигналов в очереди, возникнет переполнение стека.

Решением является следующая схема:

Клиент отправляет серверу сигнал SIGALRM. И не отправляет больше сигналов ему до тех пор, пока не получит ответ от сервера о выполненной работе, для этого устанавливается обработчик void signal_work_is_done(………..). Сервер выполнив работу запрошенную клиентом, отправляет ему ответ.

 

Анализ сервера: server_analysis.pdf
Анализ клиента: client_analysis.pdf
Код: server.tar   client.tar

Copyright (c) 2011 me@gekannt.net

Сделать бесплатный сайт с uCoz