Системное программное обеспечение - Учебное пособие (Терехин А.Н.)

4.7      именованные каналы (fifo)

Рассмотренные выше программные каналы имеют важное ограничение: так как доступ к ним возможен только посредством дескрипторов, возвращаемых при порождении канала, необходимым условием взаимодействия процессов через канал является передача этих дескрипторов по наследству при порождении процесса. Именованные каналы (FIFO-файлы) расширяют свою область применения за счет того, что подключиться к ним может любой процесс в любое время, в том числе и после создания канала. Это возможно благодаря наличию у них имен.

FIFO-файл представляет собой отдельный тип файла в файловой системе UNIX, который обладает всеми атрибутами файла, такими как имя владельца, права доступа и размер. Для его создания в UNIX System V.3 и ранее используется системный вызов mknod(), а в BSD UNIX и System V.4 – вызов mkfifo() (этот вызов поддерживается и стандартом POSIX):

 

#include <sys/types.h>

#include <sys/stat.h>

#include <fcntl.h>

#include <unistd.h>

int mknod (char *pathname, mode_t mode, dev);

 

#include <sys/types.h>

#include <sys/stat.h>

int mkfifo (char *pathname, mode_t mode);

В обоих вызовах первый аргумент представляет собой имя создаваемого канала, во втором указываются права доступа к нему для владельца, группы и прочих пользователей, и кроме того, устанавливается флаг, указывающий на то, что создаваемый объект является именно FIFO-файлом (в разных версиях ОС он может иметь разное символьное обозначение – S_IFIFO или I_FIFO). Третий аргумент вызова mknod() игнорируется.

После создания именованного канала любой процесс может установит с ним связь посредством системного вызова open(). При этом действуют следующие правила:

если процесс открывает FIFO-файл для чтения, он блокируется до тех пор, пока какой-либо процесс  не откроет тот же канал на запись

если процесс открывает FIFO-файл на запись, он будет заблокирован до тех пор, пока какой-либо процесс  не откроет тот же канал на чтение

процесс может избежать такого блокирования, указав в вызове open() специальный флаг (в разных версиях ОС он может иметь разное символьное обозначение – O_NONBLOCK или O_NDELAY). В этом случае в ситуациях, описанных выше, вызов open() сразу же вернет управление процессу

Правила работы с именованными каналами, в частности, особенности операций чтения-записи, полностью аналогичны неименованным каналам.

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

 Модель «клиент-сервер».

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

/* процесс-сервер*/      

#include <stdio.h>

#include <sys/types.h>

#include <sys/stat.h>

#include <fcntl.h>

#include <unistd.h>

 

int main(int argc, char **argv)

{

int fd;

int pid;

mkfifo("fifo", S_IFIFO | 0666);

/*создали специальный файл FIFO с открытыми для всех правами доступа на чтение и запись*/           

fd = open("fifo", O_RDONLY | O_NONBLOCK);      

/* открыли канал на чтение*/                  

while  (read (fd, &pid, sizeof(int)) == -1) ;

printf("Server \%d  got message from \%d ! ", getpid(), pid);

close(fd);

unlink("fifo");/*уничтожили именованный канал*/

return 0;

}

 

/* процесс-клиент*/      

#include <sys/types.h>

#include <sys/stat.h>

#include <unistd.h>

#include <fcntl.h>

int main(int argc, char **argv)

{

int fd;

int pid = getpid( );

fd = open("fifo", O_RDWR);

write(fd, &pid, sizeof(int));

close(fd);

return 0;

}

Нелокальные переходы.

Рассмотрим некоторые дополнительные возможности по организации управления ходом процесса в UNIX, а именно возможность передачи управления в точку, расположенную вне данной функции.

Как известно, оператор goto позволяет осуществлять безусловный переход только внутри одной функции. Это ограничение связано с необходимостью сохранения целостности стека: в момент входа в функцию в стеке отводится место, называемое стековым кадром, где записываются адрес возврата, фактические параметры, отводится место под автоматические переменные. Стековый кадр освобождается при выходе из функции. Соответственно, если при выполнении безусловного перехода процесс минует тот фрагмент кода, где происходит освобождение стекового кадра, и управление непосредственно перейдет в другую часть программы (например, в объемлющую функцию), то фактическое состояние стека не будет соответствовать текущему участку кода, и тем самым стек подвергнется разрушению.

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

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

#include <setjmp.h>

int setjmp(jmp_buf env);

void longjmp(jmp_buf env, int val);

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

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

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

Отметим, что если программист желает определить в программе несколько точек назначения для нелокальных переходов, каждая из них должна быть зарегистрирована в своей структуре типа jmp_buf. С другой стороны, разумеется, на одну и ту же точку назначения можно переходить из разных мест программы, при этом, чтобы различить, из какой точки был произведен  нелокальный переход, следует указывать при переходах разные значения параметра val. В любом случае, при вызове longjmp() значение параметра val не должно быть нулевым (даже если оно есть 0, то возвращаемое значение setjmp() будет установлено в 1). Кроме того, переход должен производиться только на такие точки, которые находятся в коде одной из вызывающих функций для той функции, откуда осуществляется переход (в том числе, переход может быть произведен из функции-обработчика сигнала). При этом в момент перехода все содержимое стека, используемое текущей функцией и всеми вызывающими, вплоть до необходимой, освобождается.

 Использование нелокальных переходов.

#include <signal.h>

#include <setjmp.h>

jmp_buf env;

void abc(int s)

{

     …

longjmp(env,1);    /*переход  - в точку *** */

}

 

int main(int argc, char **argv)

{

if (setjmp(env) == 0)      

/* запоминается данная точка процесса - *** */

{      

signal(SIGINT,abc);   /* установка реакции на сигнал */

…                                        

/* цикл обработки данных после вызова функции setjmp() */

}

else

{

/* цикл обработки данных после возврата из обработчика сигнала */

}

...

}