Skip Return Title Contents

ВЗАИМОДЕЙСТВИЕ ПРОЦЕССОВ В OS UNIX

Средства взаимодействия процессов

Поскольку OS UNIX является многозадачной ОС, то параллельное выполнение нескольких процессов является обычной практикой ее функционирования. При этом актуальной является проблема управления взаимодействием процессов. Для управления взаимодействием процессов в фазе "пользователь", OS UNIX предоставляет средства внешней синхронизации и коммуникации процессов.

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

Под коммуникацией процессов в OS UNIX понимается реализация обмена данными между параллельно выполняющимися процессами. Наиболее очевидным способом такого взаимодействия процессов является обмен данными через файл, когда два или более процессов записывают данные в некоторый файл, выбранный по договоренности, другие процессы читают данные из этого файла. Для коммуникации процессов через файл характерны следующие проблемы.

Всилу необходимости решения указанных проблем, коммуникация процессов через файл применяется редко. С другой стороны, OS UNIX предоставляет более совершенные средства межпроцессной коммуникации, обычно именуемые IPC (Inter Process Communication). Средства IPC свободны от указанных выше недостатков межпроцессного обмена через файл. Традиционно, к средствам IPC в OS UNIX относятся: именованные и неименованные (обычные) программные каналы, разделяемая память, семафоры, очереди сообщений.


Обработка сигналов

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

Каждый сигнал имеет уникальный номер, однозначно определяющий событие, которым он вызван. Классические версии OS UNIX определяли 16 сигналов с номерами от 1 до 16. В современных версиях OS UNIX список сигналов расширен до 32. Для удобства спецификации сигналов используются символьные мнемоники. Ниже перечислены мнемоники основных сигналов, с кратким описанием событий, инициирующих их появление.

Возможность принудительной посылки конкретному процессу или группе процессов определенного сигнала даже при отсутствии реальной причины его возникновения обеспечивают системный вызов и команда kill. Их аргументы задают номер сигнала и идентификатор процесса-приемника сигнала. Значение идентификатора процесса-приемника должно быть больше 0, когда сигнал посылается процессу индивидуально. При распределенной посылке сигнала аргумент идентификации приемника может быть равен 0, когда сигнал посылается группе процессов, равен (-1), когда сигнал посылается всем процессам владельца процесса-источника сигнала, либо меньше (-1), когда сигнал посылается всем процессам группы владельца процесса, инициатора сигнала.

Для организации обработки сигналов используются следующие сигнальные поля структуры struct proc дескриптора процесса-приемника сигнала: p_sig, p_hold, p_cursig, p_ignore и p_catch.

Поле p_sig используется для регистрации номера принятого сигнала. При этом применяется следующий способ регистрации. Если принят сигнал с номером n, то устанавливается бит (n-1) поля p_sig, который будет сброшен после обработки сигнала. Например, значения поля p_sig, равное 5, означает регистрацию сигналов SIGHUP и SIGQUIT с номерами 1 и 3, соответственно. Следует отметить, что OS UNIX не поддерживает очередь сигналов. Поэтому, если процесс принял последовательно несколько одинаковых сигналов до начала обработки 1-го из них то будет обработан только результат последнего приема. Очевидно, что разнотипные сигналы могут быть одновременно зарегистрированы в поле p_sig и обработаны в порядке их поступления. Поле p_cursig сохраняет номер текущего сигнала, который должен быть обработан.

Поле p_hold фиксирует номера сигналов, обработка которых должна быть задержена на период выполнения какого-либо блока инструкций процедурного сегмента программы процесса. Блок программы процесса, когда появление определенных сигналов нежелательно должен быть ограничен системными вызовами sighold и sigrelse, которые, соответственно, задерживают и освобождают обработку указанных сигналов.

Поля p_ignore и p_catch определяют форму реакции процесса на получение сигнала. После получения сигнала возможны 3 альтернативы реакции процесса - обработка по умолчанию, игнорирование и перехват. Вид реакции устанавливает системный вызов signal, которому в качестве аргументов передается номер сигнала и адрес функции обработки.

Когда нужно установить стандартную обработку, принятую по умолчанию, в системный вызов signal вместо адреса функции обработки передается 0 или макрос SIG_DFL. Стандартной реакцией процесса на большинство сигналов является принудительное завершение процесса. Реакция по умолчанию на такие сигналы как SIGQUIT, SIGFPE, SIGSEGV, SIGBUS, SIGSYS приводит к принудительному завершению процесса с образованием файла дампа памяти core.

Когда в процессе нужно игнорировать сигнал, в системный вызов signal вместо адреса функции обработки передается 1 или макрос SIG_IGN. Почти все сигналы могут быть принудительно игнорированы. По умолчанию игнорируются сигналы, которые не должны приводить к завершению процесса, например, SIGCLD, SIGUSR1, SIGUSR2.

Под перехватом сигнала понимается передача в системный вызов signal, адреса функции, которая реализует желаемую реакцию процесса на данный сигнал. Следует отметить, что после перехвата сигнала автоматически восстанавливается стандартная реакция на него. Поэтому целесообразно переустанавливать перехват сигнала системным вызовом signal, когда нужно сохранить его нестандартную обработку. Когда необходимость в перехвате сигнала исчерпана следует восстановить обработку по умолчанию, используя системный вызов signal с аргументом 0 или SIG_DFL. Следует отметить, что сигнал безусловного завершения SIGKILL с номером 9 не может быть игнорирован или перехвачен никаким процессом. Следующий фрагмент С-кода демонстрирует перехват сигнала прерывания от терминала.

#include <signal.h>

/* Функция обработки сигнала SIGINT */

void handler() {
  signal(SIGINT, handler);
  puts("Сигнал SIGINT перехвачен");
  return;
} /* handler */

/* Основная функция программы процесса */

main() {
 
  /* Установка перехвата сигнала SIGINT */
 
  signal(SIGINT, handler);
 
  /* Код программы процесса, где перехвачен сигнал SIGINT */
 
  .........................................................
 
  /* Восстановление стандартной обработки сигнала SIGINT */
 
  signal(SIGINTSIG_DFL);
 
} /* main */

Установленная реакция процесса на сигналы отражается контекстом процесса в форме таблице сигналов u_signal. Записи этой таблицы упорядочены по номерам сигналов в виде массива адресов их функций обработки. Формально каждый элемент таблицы сигналов может принимать одно из 3-х значений в соответствии с выбранной альтернативой обработки:

u_signal[i] = 0-принята обработка по умолчанию сигнала i+1;
u_signal[i] = 1-установлено игнорирование сигнала i+1;
u_signal[i] = n-адрес функции обработки в адресном пространстве процесса при перехвате сигнала i+1.

Порожденный процесс наследует весь контекст обработки сигналов от процесса-предка. После смены программы выполнения процесса системным вызовом семейства exec, установленная нестандартная обработка сигналов будет игнорирована. Если сигнал был принят во время реализации системного вызова, то выполнение последнего будет прервано для обработки сигнала. После завершения обработки сигнала прерванный системный вызов перезапускается автоматически. В заключении следует отметить, что механизм обработки сигналов в OS UNIX позволяет реализовать популярную в настоящее время методологию событийного программирования.


Программные каналы

Программный канал есть средство коммуникации процессов, которое можно рассматривать как программно реализованную линию связи для передачи данных между двумя или более процессами. Каналы реализованы в виде снабженных в виде снабженных специальным механизмом управления буферов ввода-вывода, которые трактуются как псевдо-файлы. Формальное сходство с файлами позволяет использовать одинаковый аппарат для доступа к данным в канале и файле. Такой подход соответствует базовой концепции унификации ввода-вывода в OS UNIX. При этом каналы имеют специфические особенности, которые позволяют исключить недостатки организации межпроцессного обмена через файлы. Основные особенности каналов перечислены ниже.

Каналы принято использовать следующим образом. Один процесс направляет (записывает) данные в канал. Другой процесс получает (читает) данные из канала. Следующий рисунок иллюстрирует этот простейший канальный вариант взаимодействия процессов.



Рис. Одноканальная схема обмена 2-х процессов
 
 
 Процесс-1 
 
 
     
 
 
 Процесс-2 
 
 
write
 
 Канал 
 
read
=======> =======>
   
     
 

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


Рис. Одноканальная схема обмена клиент-сервер
 
 
 
 
 Общий 
 
 канал 
 
 
 
 
 
 Клиент-1 
 
write  
========>  
 
 
 Сервер 
 
 
  read
 ........ 
 
  .... 
 
========>
 
 Клиент-N 
 
   
========>  
write  
   
 

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

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



Рис. Двухканальный двунаправленный обмен 2-х процессов
 
 
 
 
 Процесс-1 
 
 
 
 
write
 
 Канал-1 
 
read
 
 
 
 
 Процесс-2 
 
 
 
 
=========> =========>
   
 
 
read
 
 Канал-2 
 
write
<========= <=========
   
 

Доступ к данным при любом варианте канального обмена осуществляется через канальные дескрипторы чтения и записи. Канальные дескрипторы концептуально эквивалентны пользовательским дескрипторам открытых файлов в контексте процесса и связаны со входами канала по чтению и записи. Канал открыт для обмена, пока существует связанная с ним пара канальных дескрипторов. Через них канальный механизм осуществляет контроль за смещение указателей чтения-записи, положение которых определяет в какой блок канала можно записать данные и из какого блока они должны быть прочитаны.

Через канал может быть передан неограниченный объем информации, хотя мгновенная емкость канала ограничена 10-ю блоками. Ситуации переполнения канала при записи и чтение пустого канала автоматически блокируются скрытым механизмом синхронизации обмена. Он обеспечивает приостановку процесса записи, когда в канале нет места для размещения новых данных, или процесса чтения, при попытке ввода из пустого канала, который пока открыт по записи.

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

 

Рис. Формальная модель канала
Прочитанные блоки
 
 
 
         
 
 
         
 
 
         
 
 
 
 
 
 
 
 
         
 
 
         
 
 
 
$$$$$$$$$
$$$$$$$$$
 
<= Указатель
   чтения
Указатель =>
записи   
 
$$$$$$$$$
$$$$$$$$$
         
 
 
$$$$$$$$$
$$$$$$$$$
$$$$$$$$$
 
 
 
 
 
 
$$$$$$$$$
$$$$$$$$$
$$$$$$$$$
$$$$$$$$$
$$$$$$$$$
$$$$$$$$$
$$$$$$$$$
$$$$$$$$$
$$$$$$$$$
 
 
 
Непрочитанные блоки
 

OS UNIX поддерживает 2 типа каналов: неименованные (или обычные) и именованные (или FIFO-файлы). Неименованные каналы поддерживают обмен данными только между процессами-родственниками, например, между предком и потомком или между потомками одного предка. Именованные каналы могут быть использованы для коммуникации любых, необязательно родственных, процессов. Каналы указанных типов создаются и открываются по-разному, но используются одинаково.

Неименованный канал создается и открывается системным вызовом pipe, которому в качестве аргументов передается адрес массива 2-х целых чисел. При успешном завершении системный вызов pipe заполняет адресуемый массив канальными дескрипторами чтения и записи. Эту инициализацию обычно выполняет процесс-предок, когда предполагается использовать канальный механизм для обмена с потомками или между потомками.

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

В отличие от обычного канала, каждому именованному каналу должен соответсвовать оригинальный по маршрутному имени канальный файл, который может быть расположен в произвольном каталоге файловой системы OS UNIX. Для создания канального файла в прикладной программе можно использовать системные вызовы mkfifo или mknod. Системный вызов mkfifo ориентирован исключительно на создание FIFO-файлов. Системный вызов mknod может быть использован для создания новых файлов любых типов. Аналогично, в командном режиме именованный канал может быть создан специальной командой /etc/mkfifo или универсальной командой /etc/mknod.

Для начала работы с именованным каналом он должен быть открыт во всех процессах, которые будут использовать его как средство обмена. Чтобы открыть именованный канал, каждый заинтересованный в обмене процесс должен использовать системный вызов open, которому передается имя канального файла и желаемый режим обмена - чтение или запись. Системный вызов open является универсальным средством открытия любых файлов, но для FIFO-файлов его реализация имеет свои особенности. В частности, когда именованный канал открывается по чтению (записи), системный вызов open переводит реализующий его процесс в состояние ожидания, пока канал не будет открыт по записи (чтению) каким-либо другим процессом. При успешном завершении системный вызов open возвращает канальный дескриптор чтения или записи, который может быть использован для работы с каналом в контексте данного процесса и его потенциальных потомков.

Обмен данными через обычный и именованный каналы реализован одинаково через системные вызовы read и write. Для работы с каналом им передаются соответствующий канальный дескриптор чтения или записи, адрес и размер объекта обмена. Также как системный вызов open, системные вызовы read и write являются универсальным средством ввода-вывода данных через заданный дескриптор в контексте процесса, которое с равным успехом применимо для работы с каналом, каталогом, обычным и специальным файлом. Однако, канальная реализация этих универсальных системных вызовов ввода-вывода имеет специфические особенности.

При канальном обмене системный вызов read применяется для ввода данных из канала. Он возвращает реальное число прочитанных данных или 0, когда канал пуст и закрыт по записи всеми пишущими процессами. Попытка чтения из пустого канала, не закрытого по записи вызывает блокировку читающего процесса. Эта ситуация возникает, когда процесс чтения данных из канала опережает процесс записи данных в него. Чтение данных из канала является деструктивным, т.е. прочитанные данные не могут быть перепрочитаны вновь.

При канальном обмене системный вызов write применяется для вывода данных в канал. Он возвращает реальное число записанных данных. Попытка записи в полный канал вызывает блокировку пишущего процесса. Такая ситуация возникает, когда процесс записи данных в канал опережает процесс чтения данных из него. Если канал закрыт всеми читающими процессами, то пишущий процесс получает сигнал SIGPIPE при попытке записи данных в канал.

Блокировка чтения пустого и записи полного канала может быть исключена переводом канала в режим неблокированного ввода-вывода путем установки режимных флагов O_NDELAY или O_NONBLOCK с помощью системного вызова fcntl. Установка режимных флагов обеспечивает немедленный возврат кода 0 или (-1) системными вызовами ввода-вывода read и write при попытке чтения пустого или записи в полный канал. Для именованного канала режим неблокированного ввода-вывода может быть изначально заказан при открытии канала системным вызовом open.

Для корректного завершения работы с каналом любого типа нужно закрыть канал, освободив все канальные дескрипторы во всех процессах, которые использовали канал для обмена. Как указано выше, следует применять системный вызов close, для освобождения канальных дескрипторов в контексте всех процессов, использовавших канал для обмена. Время жизни обычного канала определяет период существования его канальных дескрипторов в контекстах взаимодействующих процессов. Очевидно, что обычный канал не может существовать после завершения процессов, которые использовали его для обмена. В отличие от обычных каналов, именованный канальный файл сохраняется независимо от существования использующих его процессов, также как файл любого другого типа. Однако, в отличие от обычных файлов данные в именованном канале не будут сохранены после завершения всех процессов, которые использовали его для обмена. Длина канального файла будет равна нулю. Канальный FIFO-файл нулевой длины будет присутствовать в файловой системе, пока он не удален командой rm или системным вызовом unlink.

Следующий фрагмент C-кода демонстрирует передачу содержимого текстовой строки между двумя процессами через существующий именованный канал chanel. Первый процесс выполняет программу writer, обеспечивая запись строки в канал.

/* Программа процесса записи строки в канал chanel */  

main() { 
static char *str = "Hello"; /* передаваемая строка */
int fd;/* дескриптор канала */
  
fd = open("chanel",1);/* открыть канал по записи */
write(fd,str,strlen(str));/* записать данные в канал */
close(fd);/* освободить дескриптор канала */
exit(0);/* завершить процесс writer */
} /* main writer */ 

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

/* Программа процесса чтения данных из канала chanel */  

main() { 
char c;/* принимаемый символ */
int fd;/* канальный дескриптор */
  
fd = open("chanel",0);/* открыть канал по чтению */
while(read(fd,&c,1) > 0) /* чтение данных из канала */
write(1,&c,1);/* запись данных в канал */
close(fd);/* освободить дескриптор канала */
exit(0);/* завершить процесс reader */
} /* main reader */ 

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

В практике работы с OS UNIX обычные каналы часто используются для организации конвейера команд. Конвейер образует цепочка параллельных процессов, реализующая последовательную обработку данных. Процессы выполнения соседних команд конвейера взаимодействуют через обычный канал. Для обозначения каналов в конвейере используется символ '|', который понимает командный процессор при разборе командной строки. Стандартный вывод каждого конвейерного процесса, кроме последнего в цепочке команд, перенаправляется на вывод в канал. Стандартный ввод каждого конвейерного процесса, кроме первого в цепочке команд, перенаправляется на ввод из канала. Например, следующий конвейер обеспечивает постраничный просмотр содержания текущего каталога в длинном формате:

$ ls -l | more

Для программной реализации перенаправления ввода-вывода при конвейерной обработке могут быть использованы системные вызовы dup, dup2 и fcntl (режим F_DUPFD) в сочетании с системным вызовом close. Эти средства позволяют ассоциировать заданный дескриптор канала с минимальным по номеру свободным элементом таблицы открытых файлов контекста процесса. Например, следующий фрагмент C-кода освобождает файловый дескриптор стандартного вывода (по умолчанию равный 1) и делает его синонимом дескриптор fd, который соответствует ранее открытому файлу или каналу.

close(1);/* освобождает дескриптор стандартного вывода */
dup(fd);/* дублирует дескриптор fd */
close(fd);/* освобождает дубликат дескриптора fd */

Аналогичная последовательность действий выполняется для перенаправления стандартного ввода в канал или файл. Следующий, более представительный, фрагмент C-кода моделирует интерпретацию приведенного выше конвейера командным процессором Shell с помощью обычного канала и рассмотренных средств перенаправления ввода-вывода.

/* Модель конвейера команд: ls  -l  |  more */

main() { 
int fd[2]; /*массив канальных дескрипторов*/

/* инициализация канальных дескрипторов */
if(pipe(fd)<0)
 exit(1); 

/* Создание 1-го процесса потомка */

if(fork() == 0) { 
 close(1);/* Стандартный вывод*/
 dup(fd[1]);/* перенаправляется*/
 close(fd[0]);/* на вывод в канал*/
 close(fd[1]);/* по дескриптору fd[1]*/
 execl("/bin/ls","ls","-l",0);/* Замена программы*/
 exit(1);/* 1-го потомка*/
}/* if */ 

/* Создание 2-го процесса потомка */

if(fork() == 0) { 
 close(0);/* Стандартный ввод*/
 dup(fd[0]);/* перенаправляется*/
 close(fd[0]);/* на ввод из канала*/
 close(fd[1]);/* по дескриптору fd[0]*/
 execl("/bin/more","more",0);/* Замена программы*/
 exit(1);/* 2-го потомка*/
}/* if */ 

/* Закрытие канала в процессе-предке */

close(fd[0]);   
close(fd[1]); 

/* Ожидание завершения потомков */

while(wait(0)!=(-1)); 
exit(0); 
 
} /* main */ 

More Less Title Contents

Hosted by uCoz