Семинар 5. Организация взаимодействия процессов через pipe и FIFO в UNIX.

(Основывается на лекции 4)

Предыдущий семинар | Программа курса | Следующий семинар

Программа семинара

  1. Понятие о потоке ввода-вывода
  2. Понятие о работе с файлами через системные вызовы и стандартную библиотеку ввода-вывода.
  3. Понятие о файловом дескрипторе.
  4. Открытие файла. Системный вызов open().
  5. Системные вызовы close(), read(), write().
  6. Понятие о pipe. Системный вызов pipe().
  7. Организация связи через pipe между процессом-родителем и процессом-потомком. Наследование файловых дескрипторов при вызовах fork() и exec().
  8. Особенности поведения вызовов read() и write() для pip’а.
  9. Понятие о FIFO. Использование системного вызова mknod для создания FIFO. Функция mkfifo.
  10. Особенности поведения вызова open() при открытии FIFO.

Цели занятия

  1. Дать понятие о потоке данных в UNIX.
  2. Научить использовать системные вызовы open(), read(), write(), close() при работе с файлами.
  3. Научить создавать pipe и использовать его для связи между родственными процессами.
  4. Научить создавать FIFO и использовать его для связи между неродственными процессами.
  5. Студент должен четко осознать, что содержимое FIFO хранится в оперативной памяти, а не на диске.

Практические работы

  1. Прогон программы для записи в файл, используя вызовы open(), write(), close().
  2. Написание, компиляция и запуск программы для чтения из файла, используя вызовы open(), read(), close()
  3. Прогон программы для pipe в одном процессе.
  4. Прогон программы для организации однонаправленной связи между родственными процессами через pipe.
  5. Написание, компиляция и запуск программы для организации двунаправленной связи между родственными процессами через pipe.
  6. Прогон программы c FIFO в родственных процессах.
  7. Написание, компиляция и запуск программы с FIFO в неродственных процессах.
  8. Неработающий пример для связи процессов на различных компьютерах.

План занятия

  1. Понятие о потоке ввода-вывода.

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

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

  2. Понятие о работе с файлами через системные вызовы и стандартную библиотеку ввода-вывода для языка C.

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

    Из курса программирования на языке C вам должны быть известны, как мы надеемся, функции работы с файлами из стандартной библиотеки ввода-вывода такие, как fopen(), fread(), fwrite(), fprintf(), fscanf(), fgets() и т.д. Эти функции входят как неотъемлемая часть в стандарт ANSI на язык C и позволяют программисту получать информацию из файла или записывать ее в файл при условии, что программист обладает определенными знаниями о содержимом передаваемых данных. Так, например, функция fgets() используется для ввода из файла последовательности символов, заканчивающейся символом '\n' - перевод каретки; функция fscanf() производит ввод информации, соответствующей заданному формату, и т. д. С точки зрения потоковой модели операции, определяемые функциями стандартной библиотеки ввода-вывода, не являются потоковыми операциями, так как каждая из них требует наличия некоторой структуры передаваемых данных.

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

  3. Файловый дескриптор.

    PCB и контекст процесса в лекции На лекции мы говорили, что информация о файлах, используемых процессом, входит в соcтав его системного контекста и хранится в его блоке управления - PCB. В операционной системе UNIX можно упрощенно полагать, что информация о файлах, с которыми процесс осуществляет операции потокового обмена, наряду с информацией о потоковых линиях связи, соединяющих процесс с другими процессами и устройствами ввода-вывода, хранится в некотором массиве, получившем название таблицы открытых файлов или таблицы файловых дескрипторов. Индекс элемента этого массива, соответствующий определенному потоку ввода-вывода, получил название файлового дескриптора для этого потока. Таким образом, файловый дескриптор представляет собой небольшое целое неотрицательное число, которое для текущего процесса в текущий момент времени однозначно определяет некоторый действующий канал ввода-вывода. Некоторые файловые дескрипторы на этапе старта любой программы ассоциируются со стандартными потоками ввода-вывода. Так, например, файловый дескриптор 0 соответсвует стандартному потоку ввода, файловый дескриптор 1 - стандартному потоку вывода, файловый дескриптор 2 - стандартному потоку для вывода ошибок. В нормальном интерактивном режиме работы стандартный поток ввода связывает процесс с клавиатурой, а стандартные потоки вывода и вывода ошибок - с текущим терминалом.

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

  4. Открытие файла. Системный вызов open().

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

    Системный вызов open() использует набор флагов для того, чтобы специфицировать операции, которые предполагается применять к файлу в дальнейшем или которые должны быть выполнены непосредственно в момент открытия файла. Из всего возможного набора флагов на текущем уровне знаний нас будут интересовать только флаги O_RDONLY, O_WRONLY, O_RDWR, O_CREAT и O_EXCL. Первые три флага являются взаимоисключающими: хотя бы один из них должен быть употреблен и наличие одного из них не допускает наличия двух других. Эти флаги описывают набор операций, которые, при успешном открытии файла, будут разрешены над файлом в последующем: только чтение, только запись, чтение и запись. Как вам известно из материалов первого семинара, у каждого файла существуют атрибуты прав доступа для различных категорий пользователей. Если файл с заданным именем существует на диске, и права доступа к нему для пользователя, от имени которого работает текущий процесс, не противоречат запрошенному набору операций, то операционная система сканирует таблицу открытых файлов от ее начала к концу в поисках первого свободного элемента, заполняет его и возвращает индекс этого элемента в качестве файлового дескриптора открытого файла. Если файла на диске нет, не хватает прав или отсутствует свободное место в таблице открытых файлов, то констатируется возникновение ошибки.

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

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

    Детальнее об операции открытия файла и ее месте среди набора всех файловых операций будет рассказываться на лекции "Файловая система с точки зрения пользователя". Работу системного вызова open() с флагами O_APPEND и O_TRUNC мы разберем на семинаре, посвященном организации файловых систем в UNIX.

     

  5. Системные вызовы read(), write(), close().

    Для совершения потоковых операций чтения информации из файла и ее записи в файл применяются системные вызовы read() и write(). Мы сейчас не акцентируем внимание на понятии указателя текущей позиции в файле и взаимном влиянии значения этого указателя и поведения системных вызовов. Этот вопрос будет обсуждаться в дальнейшем.

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

  6. Прогон программы для записи информации в файл.

    Для иллюстрации вышесказанного давайте рассмотрим программу, находящуюся в файле /ftp/pub/sem5/stud/05-1.c, откомпилируем ее и запустим на исполнение. Обратите внимание на использование системного вызова umask() с параметром 0 для того, чтобы права доступа к созданному файлу точно соответствовали указанным в системном вызове open().
     

  7. Написание, компиляция и запуск программы для чтения информации из файла.

    Измените предыдущую программу так, чтобы она читала записанную ранее в файл информацию и печатала ее на экране. Желательно удалить все лишние операторы.
     

  8. Понятие о pipe. Системный вызов pipe().

    Понятие pipe в лекцииНаиболее простым способом для передачи информации с помощью потоковой модели между различными процессами или даже внутри одного процесса в операционной сиcтеме UNIX является pipe (канал, труба, конвейер). Nota bene Важным отличием pip'а от файла является то, что прочитанная информация немедленно удаляется из него и не может быть прочитана повторно. О pip'е можно думать, как о трубе ограниченной емкости, расположенной внутри адресного пространства операционной системы, доступ к входному и выходному отверстию которой осуществляется с помощью системных вызовов. В действительности pipe представляет собой область памяти, недоступную пользовательским процессам напрямую, зачастую организованную в виде кольцевого буфера (хотя существуют и многочисленные другие виды организации). По буферу при операциях чтения и записи перемещаются два указателя, соответствующие входному и выходному потокам. При этом выходной указатель никогда не может перегнать входной и наоборот. Для создания нового экземпляра такого кольцевого буфера внутри операционной системы используется системный вызов pipe(). При своей работе он организует выделение области памяти под буфер и указатели и заносит информацию, соответствующую входному и выходному потокам данных в два элемента таблицы открытых файлов, связывая тем самым с каждым pipe'ом два файловых дескриптора. Для одного из них разрешена только операция чтения из pip'a, а для другого - только операция записи в pipe. Для выполнения этих операций мы можем использовать те же самые системные вызовы read() и write(), что и при работе с файлами. Естественно, что по окончании необходимости использования входного или/и выходного потока данных, нам необходимо закрыть соответствующий поток с помощью системного вызова close() для освобождения системных ресурсов. Необходимо отметить, что, когда все процессы, использующие pip'e, закрывают все ассоциированные с ним файловые дескрипторы, операционная система ликвидирует pipe. Таким образом, время существования pip'а в системе не может превышать время жизни процессов, работающих с ним.
     

  9. Прогон программы для pipe в одном процессе.

    Достаточно понятной иллюстрацией действий по созданию pip'a, записи в него данных, чтению из него и освобождению выделенных ресурсов может служить программа, организующая работу с pip'ом в рамках одного процесса. Ее исходный текст расположен в файле /ftp/pub/sem5/stud/05-2.c. Откомпилируйте ее и запустите на исполнение.
     

  10. Организация связи через pipe между процессом-родителем и процессом-потомком. Наследование файловых дескрипторов при вызовах fork() и exec().

    Понятно, что если бы все достойнство pip'ов сводилось к замене функции копирования из памяти в память внутри одного процесса на пересылку информации через операционную систему, то овчинка не стоила бы выделки. Однако таблица открытых файлов наследуется процессом-ребенком при порождении нового процесса системным вызовом fork() и входит в состав неизменяемой части системного контекста процесса при системном вызове exec() (за исключением тех потоков данных, для файловых дескрипторов которых был специальными средствами выставлен признак, побуждающий операционную систему закрыть их при выполнении exec(); однако их рассмотрение выходит за рамки нашего курса). Это обстоятельство позволяет организовать передачу информации через pipe между родственными процессами, имеющеми общего прародителя, создавшего pipe.
     

  11. Прогон программы для организации однонаправленной связи между родственными процессами через pipe.

    Давайте рассмотрим программу, осуществляющую однонаправленную связь между процессом-родителем и процессом-ребенком, находящуюся в файле /ftp/pub/sem5/stud/05-3.c, откомпилируем ее и запустим на исполнение.

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

  12. Написание, компиляция и запуск программы для организации двунаправленной связи между родственными процессами через pipe..

    Направленность связи в лекцииPipe принципиально служит для организации однонаправленной или симплексной связи. Если бы в предыдущем примере мы попытались организовать через pipe двустороннюю связь, когда процесс-родитель пишет информацию в pipe, предполагая, что ее получит процесс-ребенок, а затем читает информацию из pip'а, предполагая, что ее записал порожденный процесс, то могла бы возникнуть ситуация, в которой процесс-родитель прочитал бы собственную информацию, а процесс-ребенок не получил бы ничего. Для использования одного pip'a в двух направлениях, необходимы специальные средства синхронизации процессов, о которых речь пойдет на лекциях "Алгоритмы синхронизации" и "Механизмы синхронизации". Более простой способ организации двунаправленной связи между родственными процессами заключается в использовании двух pip'ов. Модифицируйте программу из предыдущего примера для организации такой двусторонней связи, откомпилируйте ее и запустите на исполнение.

    Необходимо отметить, что в некоторых UNIX-подобных системах (например, в Solaris2) реализованы полностью дуплексные pip'ы [11]. В таких системах для обоих файловых дескрипторов, ассоциированных с pip'ом, разрешены и операция чтения, и операция записи. Однако, такое поведение не характерно для pip'ов и не является переносимым.

  13. Особенности поведения вызовов read() и write() для pip’а.

    Системные вызовы read() и write() имеют определенные особенности поведения при работе с pipe, связанные с его ограниченным размером, задержками в передаче данных и возможностью блокирования обменивающихся информацией процессов. Организация запрета блокирования этих вызовов для pipe выходит за рамки нашего курса.Nota bene Будьте внимательны при написании программ, обменивающихся большими объемами информации через pipe. Помните, что за один раз  из pip'а может прочитаться меньше информации, чем вы запрашивали, и за один раз в pipe может записаться меньше информации, чем вам хотелось бы. Проверяйте значения, возвращаемые вызовами!

    Nota beneОдна из особенностей поведения блокирующегося системного вызова read() связана с попыткой чтения из пустого pip'а. Если есть процессы, у которых этот pipe открыт для записи, то системный вызов блокируется и ждет появления информации. Если таких процессов нет, он вернет значение 0 без блокировки процесса. Эта особенность приводит к необходимости закрытия файлового дескриптора, ассоциированного с входным концом pip'a, в процессе, который будет использовать pipe для чтения (close(fd[1]) в процессе-ребенке в программе /ftp/pub/sem5/stud/05-2.c). Аналогичной особенностью поведения при отсутствии процессов, у которых pipe открыт для чтения, обладает и системный вызов write(), с чем связана необходимость закрытия файлового дескриптора, ассоциированного с выходным концом pip'a, в процессе, который будет использовать pipe для записи (close(fd[0]) в процессе-родителе в той же программе).

    Задача повышенной сложности: определите размер pipe для вашей операционной системы.
     

  14. Понятие о FIFO. Использование системного вызова mknod для создания FIFO. Функция mkfifo.

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

    Понятие FIFO в лекцииДля организации потокового взаимодействия любых процессов в операционной системе UNIX применяется средство связи, получившее название FIFO (от First Input First Output) или именованный pipe. FIFO во всем подобен pip'у, за одним исключением: данные о  расположении FIFO  в адресном пространстве ядра и его состоянии процессы могут получать не через родственные связи, а через файловую систему. Для этого при создании именованного pip'a на диске заводится файл специального типа, обращаясь к которому процессы могут узнать интересующую их информацию. Для создания FIFO применяется системный вызов mknod() или существующая в некоторых версиях UNIX функция mkfifo(). Nota beneСледует отметить, что при их работе не происходит действительного выделения области адресного пространства операционной системы под именованный pipe, а только заводится файл-метка, существование которого позволяет осуществить реальную организацию FIFO в памяти при его открытии  с помощью уже известного нам ситемного вызова open(). После открытия именованный pipe ведет себя точно так же, как и неименованный. Для дальнейшей работы с ним применяются системные вызовы read(), write() и close().  Время существования FIFO в адресном пространстве ядра операционной системы, как и в случае pip'а, не может превышать времени жизни последнего из использующих его процессов. Когда все процессы, работающие с FIFO, закрывают все файловые дескрипторы, ассоциированные с ним, система освобождает ресурсы, выделенные под FIFO. Вся непрочитанная информация теряется. В то же время файл-метка остается жить на диске, и он может быть использован для новой реальной организации FIFO в дальнейшем.

    Важно понимать, что файл типа FIFO не служит для размещения на диске информации, которая записывается в именованный pipe. Nota beneЭта информация располагается внутри адресного пространства операционной системы, а файл является только меткой, создающей предпосылки для ее размещения. Не пытайтесь просмотреть содержимое этого файла с помощью Midnight Commander (mc) !!! Это приведет к его глубокому зависанию!

  15. Особенности поведения вызова open() при открытии FIFO.

    Системные вызовы read() и write() при работе с FIFO имеют такие же особенности поведения, как и при работе с pip'ом. Системный вызов open() при открытии FIFO также ведет себя несколько иначе, чем при открытии других типов файлов, что связано с возможностью блокирования выполняющих его процессов. Если FIFO открывается только для чтения, и не задан флаг O_NDELAY, то процесс, осуществивший системный вызов, блокируется до тех пор, пока какой-либо другой процесс не откроет FIFO на запись. Если флаг O_NDELAY задан, то возвращается значение файлового дескриптора, ассоциированного с FIFO. Если FIFO открывается только для записи, и не задан флаг O_NDELAY, то процесс, осуществивший системный вызов, блокируется до тех пор, пока какой-либо другой процесс не откроет FIFO на чтение. Если флаг O_NDELAY задан, то констатируется возникновение ошибки и возвращается значение -1. Задание флага O_NDELAY в параметрах системного вызова open() приводит и к тому, что процессу, открывшему FIFO,  запрещается блокировка при выполнении последующих операций чтения из этого потока данных и записи в него.
     

  16. Прогон программы c FIFO в родственных процессах.

    Для иллюстрации взаимодействия процессов через FIFO давайте рассмотрим программу, находящуюся в файле /ftp/pub/sem5/stud/05-4.c, откомпилируем ее и запустим на исполнение. В этой программе информацией между собой обмениваются процесс-родитель и процесс-ребенок. Обратим внимание, что повторный запуск этой программы приведет к ошибке при попытке создания FIFO, так как файл с заданным именем уже существует. Здесь нужно либо удалять его перед каждым прогоном программы с диска вручную , либо после первого запуска модифицировать исходный текст, исключив из него все, связанное с ситемным вызовом mknod(). С системным вызовом, предназначенным для удаления файла при работе процесса, мы познакомимся позже при изучении файловых систем.
     

  17. Написание, компиляция и запуск программы с FIFO в неродственных процессах.

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

  18. Неработающий пример для связи процессов на различных компьютерах.

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

Предыдущий семинар | Программа курса | Следующий семинар