/FAQServer от 2:5025/38.12@fidonet/Подборка по компьютерной тематике/Юникс/Морис Дж. Бах АРХИТЕКТУРА ОПЕРАЦИОННОЙ СИСТЕМЫ UNIX. Перевод/Глава 11. ВЗАИМОДЕЙСТВИЕ ПРОЦЕССОВ



    ГЛАВА 11

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




    Наличие  механизмов  взаимодействия  дает произвольным процессам возмож-
ность осуществлять обмен данными и синхронизировать свое выполнение с други-
ми процессами. Мы уже рассмотрели несколько форм  взаимодействия  процессов,
такие  как  канальная  связь,  использование поименованных каналов и посылка
сигналов. Каналы (непоименованные) имеют недостаток, связанный  с  тем,  что
они известны только потомкам процесса, вызвавшего системную функцию pipe: не
имеющие родственных связей процессы не могут взаимодействовать между собой с
помощью  непоименованных  каналов.  Несмотря на то, что поименованные каналы
позволяют взаимодействовать между собой процессам,  не  имеющим  родственных
связей, они не могут использоваться ни в сети (см. главу 13), ни в организа-
ции множественных связей между различными группами взаимодействующих процес-
сов:  поименованный канал не поддается такому мультиплексированию, при кото-
ром у каждой пары взаимодействующих процессов имелся бы свой выделенный  ка-
нал. Произвольные процессы могут также связываться между собой благодаря по-
сылке  сигналов  с  помощью системной функции kill, однако такое 'сообщение'
состоит из одного только номера сигнала.
    В данной главе описываются другие формы взаимодействия процессов. В  на-
чале  речь  идет  о трассировке процессов, о том, каким образом один процесс
следит за ходом выполнения другого  процесса,  затем  рассматривается  пакет
IPC:  сообщения,  разделяемая память и семафоры. Делается обзор традиционных
методов сетевого взаимодействия процессов, выполняющихся на разных  машинах,
и,  наконец,  дается представление о 'гнездах', применяющихся в системе BSD.
Вопросы сетевого взаимодействия, имеющие  специальный  характер,  такие  как
протоколы,  адресация  и  др.,  не рассматриваются, поскольку они выходят за
рамки настоящей работы.


      11.1 ТРАССИРОВКА ПРОЦЕССОВ  

    В системе UNIX имеется простейшая форма  взаимодействия  процессов,  ис-
пользуемая  в целях отладки, - трассировка процессов. Процесс-отладчик, нап-

     ---------------------------------------------------------
     | if ((pid = fork()) == 0)                              |
     | {                                                     |
     |     /* потомок - трассируемый процесс */              |
     |     ptrace(0,0,0,0);                                  |
     |     exec('имя трассируемого процесса');               |
     | }                                                     |
     | /* продолжение выполнения процесса-отладчика */       |
     | for (;;)                                              |
     | {                                                     |
     |     wait((int *) 0);                                  |
     |     read(входная информация для трассировки команд)   |
     |     ptrace(cmd,pid,...);                              |
     |     if (условие завершения трассировки)               |
     |          break;                                       |
     | }                                                     |
     ---------------------------------------------------------

             Рисунок 11.1. Структура процесса отладки

                                    330



ример sdb, порождает трассируемый процесс и управляет его выполнением с  по-
мощью  системной  функции  ptrace, расставляя и сбрасывая контрольные точки,
считывая и записывая данные в его виртуальное адресное пространство. Трасси-
ровка процессов, таким образом, включает  в  себя  синхронизацию  выполнения
процесса-отладчика и трассируемого процесса и управление выполнением послед-
него.
    Псевдопрограмма, представленная на Рисунке 11.1, имеет типичную структу-
ру  отладочной программы. Отладчик порождает новый процесс, запускающий сис-
темную функцию ptrace, в результате чего в соответствующей  процессу-потомку
записи таблицы процессов ядро устанавливает бит трассировки. Процесс-потомок
предназначен для запуска (exec) трассируемой программы. Например, если поль-
зователь ведет отладку программы a.out, процесс-потомок запускает файл с тем
же  именем. Ядро отрабатывает функцию exec обычным порядком, но в финале за-
мечает, что бит трассировки установлен, и посылает  процессу-потомку  сигнал
прерывания. На выходе из функции exec, как и на выходе из любой другой функ-
ции, ядро проверяет наличие сигналов, обнаруживает только что посланный сиг-
нал  прерывания и исполняет программу трассировки процесса как особый случай
обработки сигналов. Заметив установку бита трассировки, процесс-потомок  вы-
водит своего родителя из состояния приостанова, в котором последний находит-
ся  вследствие исполнения функции wait, сам переходит в состояние трассиров-
ки, подобное состоянию приостанова (но не показанное на диаграмме  состояний
процесса, см. Рисунок 6.1), и выполняет переключение контекста.
    Тем временем в обычной ситуации процесс-родитель (отладчик) переходит на
пользовательский уровень, ожидая получения известия от трассируемого процес-
са.  Когда  соответствующее  известие процессом-родителем будет получено, он
выйдет из состояния ожидания (wait), прочитает (read) введенные  пользовате-
лем  команды  и превратит их в серию обращений к функции ptrace, управляющих
трассировкой процесса-потомка. Синтаксис вызова системной функции ptrace:

    ptrace(cmd,pid,addr,data);

где в качестве cmd указываются различные команды, например,  чтения  данных,
записи  данных, возобновления выполнения и т.п., pid - идентификатор трасси-
руемого процесса, addr - виртуальный адрес ячейки в  трассируемом  процессе,
где будет производиться чтение или запись, data - целое значение, предназна-
ченное  для записи. Во время исполнения системной функции ptrace ядро прове-
ряет, имеется ли у отладчика потомок с идентификатором pid  и  находится  ли
этот потомок в состоянии трассировки, после чего заводит глобальную структу-
ру данных, предназначенную для передачи данных между двумя процессами. Чтобы
другие  процессы, выполняющие трассировку, не могли затереть содержимое этой
структуры, она блокируется ядром, ядро записывает в нее параметры cmd,  addr
и  data, возобновляет процесс-потомок, переводит его в состояние 'готовности
к выполнению' и приостанавливается до получения от него ответа.  Когда  про-
цесс-потомок продолжит свое выполнение (в режиме ядра), он исполнит соответ-
ствующую  (трассируемую) команду, запишет результат в глобальную структуру и
'разбудит' отладчика. В зависимости от типа команды потомок может вновь  пе-
рейти  в  состояние  трассировки  и ожидать поступления новой команды или же
выйти из цикла обработки сигналов и продолжить свое выполнение.  При  возоб-
новлении  работы отладчика ядро запоминает значение, возвращенное трассируе-
мым процессом, снимает с глобальной структуры блокировку и возвращает управ-
ление пользователю.
    Если в момент перехода процесса-потомка в состояние трассировки отладчик
не находится в состоянии приостанова (wait), он не обнаружит  потомка,  пока
не  обратится к функции wait, после чего немедленно выйдет из функции и про-
должит работу по вышеописанному плану.



                                    331
    --------------------------------------------------------
    | int data[32];                                        |
    | main()                                               |
    | {                                                    |
    |     int i;                                           |
    |     for (i = 0; i < 32; i++)                         |
    |         printf('data[%d] = %d\n@,i,data[i]);         |
    |     printf('ptrace data addr Ox%x\n',data);          |
    | }                                                    |
    --------------------------------------------------------

      Рисунок 11.2. Программа trace (трассируемый процесс)


    Рассмотрим две программы, приведенные на Рисунках 11.2 и 11.3 и  именуе-
мые  trace  и debug, соответственно. При запуске программы trace с терминала
массив data будет содержать нулевые значения; процесс выводит адрес  массива
и  завершает  работу.  При запуске программы debug с передачей ей в качестве
параметра значения,  выведенного  программой  trace,  происходит  следующее:
программа  запоминает  значение  параметра  в переменной addr, создает новый
процесс, с помощью функции ptrace подготавливающий себя к трассировке, и за-
пускает программу trace. На выходе из функции  exec  ядро  посылает  процес-
су-потомку (назовем его тоже trace) сигнал SIGTRAP (сигнал прерывания), про-

    --------------------------------------------------------------
    | #define TR_SETUP 0                                         |
    | #define TR_WRITE 5                                         |
    | #define TR_RESUME 7                                        |
    | int addr;                                                  |
    |                                                            |
    | main(argc,argv)                                            |
    |      int argc;                                             |
    |      char *argv[];                                         |
    | {                                                          |
    |      int i,pid;                                            |
    |                                                            |
    |      sscanf(argv[1],'%x',&addr);                           |
    |                                                            |
    |      if ((pid = fork() == 0)                               |
    |      {                                                     |
    |          ptrace(TR_SETUP,0,0,0);                           |
    |          execl('trace','trace',0);                         |
    |          exit();                                           |
    |      }                                                     |
    |      for (i = 0; i < 32, i++)                              |
    |      {                                                     |
    |          wait((int *) 0);                                  |
    |          /* записать значение i в пространство процесса с  |
    |           * идентификатором pid по адресу, содержащемуся в |
    |           * переменной addr */                             |
    |          if (ptrace(TR_WRITE,pid,addr,i) == -1)            |
    |               exit();                                      |
    |          addr += sizeof(int);                              |
    |      }                                                     |
    |      /* трассируемый процесс возобновляет выполнение */    |
    |      ptrace(TR_RESUME,pid,1,0);                            |
    | }                                                          |
    --------------------------------------------------------------

        Рисунок 11.3. Программа debug (трассирующий процесс)

                                    332



цесс  trace переходит в состояние трассировки, ожидая поступления команды от
программы debug. Если процесс, реализующий программу debug, находился в сос-
тоянии приостанова, связанного с выполнением функции wait,  он  'пробуждает-
ся',  обнаруживает  наличие порожденного трассируемого процесса и выходит из
функции wait. Затем процесс debug вызывает функцию ptrace, записывает значе-
ние переменной цикла i в пространство данных процесса trace по  адресу,  со-
держащемуся  в  переменной  addr,  и увеличивает значение переменной addr; в
программе trace переменная addr хранит адрес точки входа в массив data. Пос-
леднее обращение процесса debug к функции ptrace вызывает  запуск  программы
trace, и в этот момент массив data содержит значения от 0 до 31. Отлад-
чики,  подобные  sdb,  имеют  доступ к таблице идентификаторов трассируемого
процесса, из которой они получают информацию об адресах данных, используемых
в качестве параметров функции ptrace.
    Использование функции ptrace для трассировки процессов является  обычным
делом, но оно имеет ряд недостатков.
  *  Для  того, чтобы произвести передачу порции данных длиною в слово между
    процессом-отладчиком и трассируемым процессом, ядро должно выполнить че-
    тыре переключения контекста: оно переключает контекст  во  время  вызова
    отладчиком  функции ptrace, загружает и выгружает контекст трассируемого
    процесса и переключает контекст вновь на процесс-отладчик  по  получении
    ответа от трассируемого процесса. Все вышеуказанное необходимо, посколь-
    ку у отладчика нет иного способа получить доступ к виртуальному адресно-
    му  пространству трассируемого процесса, отсюда замедленность протекания
    процедуры трассировки.
  * Процесс-отладчик может вести одновременную трассировку  нескольких  про-
    цессов-потомков,  хотя  на  практике эта возможность используется редко.
    Если быть более критичным, следует отметить, что отладчик может  трасси-
    ровать  только своих ближайших потомков: если трассируемый процесс-пото-
    мок вызовет функцию fork, отладчик не будет иметь контроля над порождае-
    мым, внучатым для него, процессом, что является серьезным препятствием в
    отладке многоуровневых программ. Если трассируемый процесс вызывает фун-
    кцию exec, запускаемые образы задач тоже  подвергаются  трассировке  под
    управлением  ранее  вызванной  функции  ptrace, однако отладчик может не
    знать имени исполняемого образа, что  затрудняет  проведение  символьной
    отладки.
  *  Отладчик  не  может вести трассировку уже выполняющегося процесса, если
    отлаживаемый процесс не вызвал предварительно функцию  ptrace,  дав  тем
    самым ядру свое согласие на трассировку. Это неудобно, так как в указан-
    ном  случае  выполняющийся процесс придется удалить из системы и переза-
    пустить в режиме трассировки.
  * Не разрешается трассировать setuid-программы, поскольку это  может  при-
    вести  к  нарушению  защиты  данных (ибо в результате выполнения функции
    ptrace в их адресное пространство производилась бы запись  данных)  и  к
    выполнению    недопустимых    действий.   Предположим,   например,   что
    setuid-программа запускает файл с именем 'privatefile'. Умелый пользова-
    тель с помощью функции ptrace мог бы заменить имя  файла  на  '/bin/sh',
    запустив  на  выполнение командный процессор shell (и все программы, ис-
    полняемые shell'ом), не имея на то соответствующих  полномочий.  Функция
    exec  игнорирует  бит setuid, если процесс подвергается трассировке, тем
    самым адресное пространство setuid-программ защищается от  пользователь-
    ской записи.

    Киллиан [Killian 84] описывает другую схему трассировки процессов, осно-
ванную на переключении файловых систем (см. главу 5). Администратор монтиру-
ет файловую систему под именем '/proc'; пользователи идентифицируют процессы
с помощью кодов идентификации и трактуют их как файлы, принадлежащие катало-
гу  '/proc'. Ядро дает разрешение на открытие файлов, исходя из кода иденти-

                                    333

фикации пользователя процесса и кода идентификации группы. Пользователи  мо-
гут обращаться к адресному пространству процесса путем чтения (read) файла и
устанавливать точки прерываний путем записи (write) в файл. Функция stat со-
общает  различную  статистическую  информацию, касающуюся процесса. В данном
подходе устранены три недостатка, присущие функции  ptrace.  Во-первых,  эта
схема  работает быстрее, поскольку процесс-отладчик за одно обращение к ука-
занным системным функциям может передавать больше информации, чем при работе
с ptrace. Во-вторых, отладчик здесь может вести трассировку совершенно  про-
извольных  процессов, а не только своих потомков. Наконец, трассируемый про-
цесс не должен предпринимать предварительно никаких действий по подготовке к
трассировке; отладчик может трассировать и  существующие  процессы.  Возмож-
ность вести отладку setuid-программ, предоставляемая только суперпользовате-
лю, реализуется как составная часть традиционного механизма защиты файлов.


      11.2 ВЗАИМОДЕЙСТВИЕ ПРОЦЕССОВ В ВЕРСИИ V СИСТЕМЫ  

    Пакет  IPC (interprocess communication) в версии V системы UNIX включает
в себя три механизма. Механизм сообщений дает процессам возможность посылать
другим процессам потоки сформатированных данных, механизм разделения  памяти
позволяет  процессам совместно использовать отдельные части виртуального ад-
ресного пространства, а семафоры - синхронизировать свое выполнение с выпол-
нением параллельных процессов. Несмотря на то, что они  реализуются  в  виде
отдельных блоков, им присущи общие свойства.
  *  С  каждым механизмом связана таблица, в записях которой описываются все
    его детали.
  * В каждой записи содержится числовой ключ (key), который представляет со-
    бой идентификатор записи, выбранный пользователем.
  * В каждом механизме имеется системная функция  типа  'get',  используемая
    для  создания  новой или поиска существующей записи; параметрами функции
    являются идентификатор записи и различные флаги (flag). Ядро ведет поиск
    записи по ее идентификатору в соответствующей таблице. Процессы могут  с
    помощью флага IPC_PRIVATE гарантировать получение еще неиспользуемой за-
    писи. С помощью флага IPC_CREAT они могут создать новую запись, если за-
    писи  с  указанным  идентификатором нет, а если еще к тому же установить
    флаг IPC_EXCL, можно получить уведомление об ошибке в том  случае,  если
    запись с таким идентификатором существует. Функция возвращает некий выб-
    ранный  ядром дескриптор, предназначенный для последующего использования
    в других системных функциях, таким образом, она работает аналогично сис-
    темным функциям creat и open.
  * В каждом механизме ядро использует следующую формулу для поиска по деск-
    риптору указателя на запись в таблице структур данных:

    указатель = значение дескриптора по модулю от числа записей в таблице

    Если, например, таблица структур сообщений состоит из 100 записей, деск-
    рипторы, связанные с записью номер 1, имеют значения, равные 1, 101, 201
    и т.д. Когда процесс удаляет запись, ядро увеличивает значение связанно-
    го с ней дескриптора на число записей в таблице:  полученный  дескриптор
    станет  новым дескриптором этой записи, когда к ней вновь будет произве-
    дено обращение при помощи функции типа 'get'.  Процессы,  которые  будут
    пытаться  обратиться к записи по ее старому дескриптору, потерпят неуда-
    чу. Обратимся вновь к предыдущему примеру. Если с записью 1 связан деск-
    риптор, имеющий значение 201, при его удалении ядро назначит записи  но-
    вый  дескриптор, имеющий значение 301. Процессы, пытающиеся обратиться к
    дескриптору 201, получат ошибку, поскольку этого дескриптора больше нет.
    В конечном итоге ядро произведет перенумерацию дескрипторов, но пока это
    произойдет, может пройти значительный промежуток времени.
  * Каждая запись имеет некую структуру данных, описывающую права доступа  к

                                    334

    ней и включающую в себя пользовательский и групповой коды идентификации,
    которые  имеет  процесс,  создавший  запись,  а также пользовательский и
    групповой коды  идентификации,  установленные  системной  функцией  типа
    'control'  (об  этом ниже), и двоичные коды разрешений чтения-записи-ис-
    полнения для владельца, группы и прочих пользователей, по аналогии с ус-
    тановкой прав доступа к файлам.
  * В каждой записи имеется другая информация, описывающая состояние записи,
    в частности, идентификатор последнего из процессов, внесших изменения  в
    запись  (посылка сообщения, прием сообщения, подключение разделяемой па-
    мяти и т.д.), и время последнего обращения или корректировки.
  * В каждом механизме имеется системная функция типа 'control', запрашиваю-
    щая информацию о состоянии записи, изменяющая эту информацию или удаляю-
    щая запись из системы. Когда процесс запрашивает информацию о  состоянии
    записи,  ядро  проверяет,  имеет ли процесс разрешение на чтение записи,
    после чего копирует данные из записи таблицы по адресу, указанному поль-
    зователем. При установке значений принадлежащих записи  параметров  ядро
    проверяет,  совпадают  ли между собой пользовательский код идентификации
    процесса и идентификатор пользователя (или создателя), указанный в запи-
    си, не запущен ли процесс под управлением суперпользователя; одного раз-
    решения на запись недостаточно для установки параметров.  Ядро  копирует
    сообщенную  пользователем информацию в запись таблицы, устанавливая зна-
    чения пользовательского и группового кодов идентификации, режимы доступа
    и другие параметры (в зависимости от типа механизма). Ядро  не  изменяет
    значения  полей, описывающих пользовательский и групповой коды идентифи-
    кации создателя записи, поэтому пользователь, создавший запись, сохраня-
    ет управляющие права на нее. Пользователь может удалить запись, либо ес-
    ли он является суперпользователем, либо если идентификатор процесса сов-
    падает с любым из идентификаторов, указанных в  структуре  записи.  Ядро
    увеличивает  номер дескриптора, чтобы при следующем назначении записи ей
    был присвоен новый дескриптор. Следовательно, как уже ранее  говорилось,
    если процесс попытается обратиться к записи по старому дескриптору, выз-
    ванная им функция получит отказ.


      11.2.1 Сообщения  

    С сообщениями работают четыре системных функции: msgget, которая возвра-
щает (и в некоторых случаях создает) дескриптор сообщения, определяющий оче-
редь  сообщений и используемый другими системными функциями, msgctl, которая
устанавливает и возвращает связанные с дескриптором сообщений параметры  или
удаляет  дескрипторы,  msgsnd, которая посылает сообщение, и msgrcv, которая
получает сообщение.
    Синтаксис вызова системной функции msgget:

    msgqid = msgget(key,flag);

где msgqid - возвращаемый функцией дескриптор, а key и flag имеют ту же  се-
мантику, что и в системной функции типа 'get'. Ядро хранит сообщения в связ-
ном  списке (очереди), определяемом значением дескриптора, и использует зна-
чение msgqid в качестве указателя на массив заголовков очередей. Кроме выше-
указанных полей, описывающих общие для всего механизма права доступа,  заго-
ловок очереди содержит следующие поля:
  * Указатели на первое и последнее сообщение в списке;
  * Количество сообщений и общий объем информации в списке в байтах;
  * Максимальная емкость списка в байтах;
  * Идентификаторы процессов, пославших и принявших сообщения последними;
  *  Поля,  указывающие время последнего выполнения функций msgsnd, msgrcv и
    msgctl.
    Когда пользователь вызывает функцию msgget для того, чтобы создать новый

                                    335

дескриптор, ядро просматривает массив очередей сообщений в поисках существу-
ющей очереди с указанным идентификатором. Если такой очереди нет, ядро выде-
ляет новую очередь, инициализирует ее и возвращает идентификатор пользовате-
лю. В противном случае ядро проверяет наличие необходимых прав доступа и за-
вершает выполнение функции.
    Для посылки сообщения процесс использует системную функцию msgsnd:

    msgsnd(msgqid,msg,count,flag);

где msgqid - дескриптор  очереди  сообщений,  обычно  возвращаемый  функцией
msgget,  msg - указатель на структуру, состоящую из типа в виде назначаемого
пользователем целого числа и массива символов, count - размер информационно-
го массива, flag - действие, предпринимаемое  ядром  в  случае  переполнения
внутреннего буферного пространства.
    Ядро  проверяет  (Рисунок 11.4), имеется ли у посылающего сообщение про-
цесса разрешения на запись по указанному дескриптору, не выходит  ли  размер
сообщения  за  установленную  системой  границу,  не содержится ли в очереди
слишком большой объем информации, а также является ли тип сообщения  положи-
тельным  целым  числом.  Если все условия соблюдены, ядро выделяет сообщению
место, используя карту сообщений (см. раздел 9.1), и копирует  в  это  место
данные  из  пространства пользователя. К сообщению присоединяется заголовок,
после чего оно помещается в конец связного списка  заголовков  сообщений.  В
заголовке сообщения записывается тип и размер сообще-

    --------------------------------------------------------------
    | алгоритм msgsnd       /* послать сообщение */              |
    | входная информация:  (1) дескриптор очереди сообщений      |
    |                      (2) адрес структуры сообщения         |
    |                      (3) размер сообщения                  |
    |                      (4) флаги                             |
    | выходная информация: количество посланных байт             |
    | {                                                          |
    |    проверить правильность указания дескриптора и наличие   |
    |     соответствующих прав доступа;                          |
    |    выполнить пока (для хранения сообщения не будет выделено|
    |     место)                                                 |
    |    {                                                       |
    |        если (флаги не разрешают ждать)                     |
    |              вернуться;                                    |
    |        приостановиться (до тех пор, пока место не освобо-  |
    |         дится);                                            |
    |    }                                                       |
    |    получить заголовок сообщения;                           |
    |    считать текст сообщения из пространства задачи в прост- |
    |     ранство ядра;                                          |
    |    настроить структуры данных: выстроить очередь заголовков|
    |     сообщений, установить в заголовке указатель на текст   |
    |     сообщения, заполнить поля, содержащие счетчики, время  |
    |     последнего выполнения операций и идентификатор процес- |
    |     са;                                                    |
    |    вывести из состояния приостанова все процессы, ожидающие|
    |     разрешения считать сообщение из очереди;               |
    | }                                                          |
    --------------------------------------------------------------

              Рисунок 11.4. Алгоритм посылки сообщения


ния,  устанавливается  указатель на текст сообщения и производится корректи-

                                    336

ровка содержимого различных полей заголовка очереди, содержащих статистичес-
кую информацию (количество сообщений в очереди и их суммарный объем  в  бай-
тах,  время последнего выполнения операций и идентификатор процесса, послав-
шего сообщение). Затем ядро выводит из состояния приостанова  все  процессы,
ожидающие  пополнения очереди сообщений. Если размер очереди в байтах превы-
шает границу допустимости, процесс приостанавливается до тех пор, пока  дру-
гие  сообщения не уйдут из очереди. Однако, если процессу было дано указание
не ждать (флаг IPC_NOWAIT), он немедленно возвращает управление с  уведомле-
нием об ошибке. На Рисунке 11.5 показана очередь сообщений, состоящая из за-
головков  сообщений,  организованных  в связные списки, с указателями на об-
ласть текста.
    Рассмотрим программу, представленную на Рисунке 11.6.  Процесс  вызывает
функцию  msgget для того, чтобы получить дескриптор для записи с идентифика-
тором MSGKEY. Длина сообщения принимается равной 256 байт, хотя используется
только первое поле целого типа, в область текста сообщения копируется  иден-
тификатор  процесса, типу сообщения присваивается значение 1, после чего вы-
зывается функция msgsnd для посылки сообщения. Мы вернемся к  этому  примеру
позже.
    Процесс  получает сообщения, вызывая функцию msgrcv по следующему форма-
ту:
    count = msgrcv(id,msg,maxcount,type,flag);

где id - дескриптор сообщения, msg - адрес пользовательской структуры, кото-
рая будет содержать полученное сообщение, maxcount - размер  структуры  msg,
type  - тип считываемого сообщения, flag - действие, предпринимаемое ядром в
том случае, если в очереди со-

    Заголовки                                         Область
    очередей                                          текста
    --------           Заголовки сообщений        -->--------
    |      |     --------    --------    -------- |  |      |
    |    --+---->|      |--->|      |--->|      | |  |      |
    |      |     ----+---    ----+---    ----+--- |  |      |
    |------|         |           |           ------  |      |
    |      |         ------------|------------------>|------|
    |      |                     |                   |      |
    |      |                     |                   |      |
    |------|                     |                   |      |
    |      |     --------        |                   |      |
    |    --+---->|      |        |                   |      |
    |      |     ----+---        |                   |      |
    |------|         |           |                   |      |
    |   щ  |         |           |                   |------|
    |   щ  |         ------------|------------------>|------|
    |   щ  |                     |                   |      |
    |   щ  |                     |                   |      |
    |   щ  |                     ------------------->|------|
    |   щ  |                                         |      |
    |   щ  |                                         |------|
    |   щ  |                                         |   щ  |
    |   щ  |                                         |   щ  |
    |   щ  |                                         |   щ  |
    --------                                         --------

    Рисунок 11.5. Структуры данных, используемые в организации сообщений


общений нет. В переменной count пользователю возвращается число  прочитанных
байт сообщения.

                                    337

    Ядро  проверяет  (Рисунок 11.7), имеет ли пользователь необходимые права
доступа к очереди сообщений. Если тип считываемого сообщения  имеет  нулевое
значение,  ядро  ищет  первое  по счету сообщение в связном списке. Если его
размер меньше или равен размеру,  указанному  пользователем,  ядро  копирует
текст сообщения в пользовательскую структуру и соответствующим образом наст-
раивает  свои  внутренние структуры: уменьшает счетчик сообщений в очереди и
суммарный объем информации в байтах, запоминает время получения сообщения  и
идентификатор процесса-получателя, перестраивает связный список и освобожда-
ет  место  в  системном пространстве, где хранился текст сообщения. Если ка-
кие-либо процессы, ожидавшие получения  сообщения,  находились  в  состоянии
приостанова  из-за  отсутствия свободного места в списке, ядро выводит их из
этого состояния. Если размер сообщения превышает значение maxcount,  указан-
ное  пользователем,  ядро посылает системной функции уведомление об ошибке и
оставляет сообщение в очереди. Если, тем не менее, процесс игнорирует  огра-
ничения  на  размер  (в поле flag установлен бит MSG_NOERROR), ядро обрезает
сообщение, возвращает запрошенное количество байт  и  удаляет  сообщение  из
списка целиком.

    --------------------------------------------------------------
    | #include                                      |
    | #include                                        |
    | #include                                        |
    |                                                            |
    | #define MSGKEY     75                                      |
    |                                                            |
    | struct msgform {                                           |
    |        long     mtype;                                     |
    |        char     mtext[256];                                |
    | };                                                         |
    |                                                            |
    | main()                                                     |
    | {                                                          |
    |        struct msgform msg;                                 |
    |        int msgid,pid,*pint;                                |
    |                                                            |
    |        msgid = msgget(MSGKEY,0777);                        |
    |                                                            |
    |        pid = getpid();                                     |
    |        pint = (int *) msg.mtext;                           |
    |        *pint = pid;         /* копирование идентификатора  |
    |                              * процесса в область текста   |
    |                              * сообщения */                |
    |        msg.mtype = 1;                                      |
    |                                                            |
    |        msgsnd(msgid,&msg,sizeof(int),0);                   |
    |        msgrcv(msgid,&msg,256,pid,0);   /* идентификатор    |
    |                               * процесса используется в    |
    |                               * качестве типа сообщения */ |
    |        printf('клиент: получил от процесса с pid %d\n',    |
    |               *pint);                                      |
    | }                                                          |
    --------------------------------------------------------------

              Рисунок 11.6. Пользовательский процесс


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

                                    338

определяет минимальное значение типа сообщений в очереди, и если оно не пре-
вышает  абсолютное значение параметра type, возвращает процессу первое сооб-
щение этого типа. Например, если очередь состоит из трех сообщений,  имеющих
тип  3,  1 и 2, соответственно, а пользователь запрашивает сообщение с типом
-2, ядро возвращает ему сообщение типа 1. Во  всех  случаях,  если  условиям
запроса не удовлетворяет ни одно из сообщений в очереди, ядро переводит про-
цесс в состояние приостанова, разумеется если только в параметре flag не ус-
тановлен бит IPC_NOWAIT (иначе процесс немедленно выходит из функции).
    Рассмотрим  программы, представленные на Рисунках 11.6 и 11.8. Программа
на Рисунке 11.8 осуществляет общее  обслуживание  запросов  пользовательских
процессов  (клиентов).  Запросы, например, могут касаться информации, храня-
щейся в базе данных; обслуживающий процесс  (сервер)  выступает  необходимым
посредником при обращении к базе данных, такой порядок облегчает поддержание
целостности  данных и организацию их защиты от несанкционированного доступа.
Обслуживающий процесс создает сообщение путем установки флага IPC _CREAT при

    --------------------------------------------------------------
    | алгоритм msgrcv     /* получение сообщения */              |
    | входная информация:  (1) дескриптор сообщения              |
    |                      (2) адрес массива, в который заносится|
    |                          сообщение                         |
    |                      (3) размер массива                    |
    |                      (4) тип сообщения в запросе           |
    |                      (5) флаги                             |
    | выходная информация: количество байт в полученном сообщении|
    | {                                                          |
    |      проверить права доступа;                              |
    |   loop:                                                    |
    |      проверить правильность дескриптора сообщения;         |
    |      /* найти сообщение, нужное пользователю */            |
    |      если (тип сообщения в запросе == 0)                   |
    |           рассмотреть первое сообщение в очереди;          |
    |      в противном случае если (тип сообщения в запросе > 0) |
    |           рассмотреть первое сообщение в очереди, имеющее  |
    |            данный тип;                                     |
    |      в противном случае   /* тип сообщения в запросе < 0 */|
    |           рассмотреть первое из сообщений в очереди с наи- |
    |            меньшим значением типа при условии, что его тип |
    |            не превышает абсолютное значение типа, указанно-|
    |            го в запросе;                                   |
    |      если (сообщение найдено)                              |
    |      {                                                     |
    |           переустановить размер сообщения или вернуть ошиб-|
    |            ку, если размер, указанный пользователем слишком|
    |            мал;                                            |
    |           скопировать тип сообщения и его текст из прост-  |
    |            ранства ядра в пространство задачи;             |
    |           разорвать связь сообщения с очередью;            |
    |           вернуть управление;                              |
    |      }                                                     |
    |      /* сообщений нет */                                   |
    |      если (флаги не разрешают приостанавливать работу)     |
    |           вернуть управление с ошибкой;                    |
    |      приостановиться (пока сообщение не появится в очере-  |
    |       ди);                                                 |
    |      перейти на loop;                                      |
    | }                                                          |
    --------------------------------------------------------------
             Рисунок 11.7. Алгоритм получения сообщения

                                    339



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

    --------------------------------------------------------------
    | #include                                      |
    | #include                                        |
    | #include                                        |
    |                                                            |
    | #define MSGKEY     75                                      |
    | struct msgform                                             |
    | {                                                          |
    |        long     mtype;                                     |
    |        char     mtext[256];                                |
    | }msg;                                                      |
    | int msgid;                                                 |
    |                                                            |
    | main()                                                     |
    | {                                                          |
    |       int i,pid,*pint;                                     |
    |       extern cleanup();                                    |
    |                                                            |
    |       for (i = 0; i < 20; i++)                             |
    |            signal(i,cleanup);                              |
    |       msgid = msgget(MSGKEY,0777|IPC_CREAT);               |
    |                                                            |
    |       for (;;)                                             |
    |       {                                                    |
    |            msgrcv(msgid,&msg,256,1,0);                     |
    |            pint = (int *) msg.mtext;                       |
    |            pid = *pint;                                    |
    |            printf('сервер: получил от процесса с pid %d\n',|
    |                   pid);                                    |
    |            msg.mtype = pid;                                |
    |            *pint = getpid();                               |
    |            msgsnd(msgid,&msg,sizeof(int),0);               |
    |       }                                                    |
    | }                                                          |
    |                                                            |
    | cleanup()                                                  |
    | {                                                          |
    |       msgctl(msgid,IPC_RMID,0);                            |
    |       exit();                                              |
    | }                                                          |
    --------------------------------------------------------------

            Рисунок 11.8. Обслуживающий процесс (сервер)


    Сообщения имеют форму 'тип - текст', где текст представляет собой  поток

                                    340

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

    msgctl(id,cmd,mstatbuf)

где id - дескриптор сообщения, cmd - тип команды, mstatbuf - адрес пользова-
тельской структуры, в которой будут храниться управляющие параметры или  ре-
зультаты обработки запроса. Более подробно об аргументах функции пойдет речь
в Приложении.
    Вернемся  к примеру, представленному на Рисунке 11.8. Обслуживающий про-
цесс принимает сигналы и с помощью функции cleanup удаляет очередь сообщений
из системы. Если же им не было поймано ни одного  сигнала  или  был  получен
сигнал  SIGKILL,  очередь  сообщений остается в системе, даже если на нее не
ссылается ни один из процессов. Дальнейшие  попытки  исключительно  создания
новой очереди сообщений с данным ключом (идентификатором) не будут иметь ус-
пех до тех пор, пока старая очередь не будет удалена из системы.


      11.2.2 Разделение памяти  

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

    shmid = shmget(key,size,flag);

где  size - объем области в байтах. Ядро использует key для ведения поиска в
таблице разделяемой памяти: если подходящая запись обнаружена и если  разре-
шение  на  доступ  имеется, ядро возвращает вызывающему процессу указанный в
записи дескриптор. Если запись не найдена и если пользователь установил флаг
IPC_CREAT, указывающий на необходимость создания новой области, ядро  прове-
ряет нахождение размера области в установленных системой пределах и выделяет
область по алгоритму allocreg (раздел 6.5.2). Ядро записывает установки прав
доступа, размер области и указатель на соответствующую запись таблицы облас-
тей в таблицу разделяемой памяти (Рисунок 11.9) и устанавливает флаг, свиде-
тельствующий  о том, что с областью не связана отдельная память. Области вы-
деляется память (таблицы страниц и т.п.) только тогда, когда процесс присое-
диняет область к своему адресному  пространству.  Ядро  устанавливает  также

                                    341

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

    Таблица раз-                             Таблица процессов -
    деляемой па-       Таблица областей      частная таблица об-
       мяти                                    ластей процесса
    ------------       ----------------          -----------
    |      ----+-----  |              |     -----+----     |
    |----------|   -|->|--------------|<-----    |---------|
    |      ----+----|  |              |      ----+----     |
    |----------|    |  |--------------|<-----|   |---------|
    |      ----+--- |  |              |     -|---+----     |
    |----------|  | |  |--------------|      |   |---------|
    |     щ    |  | |  |              |      |   |         |
    |     щ    |  | -->|--------------|      |   |---------|
    |     щ    |  |    |              |      |   |         |
    |     щ    |  ---->|--------------|<------   |---------|
    |     щ    |       |              |  (после  |         |
    |     щ    |       |--------------|   shmat) |---------|
    |     щ    |       |      щ       |          |         |
    |     щ    |       |      щ       |          |---------|
    |     щ    |       ----------------          |    щ    |
    |     щ    |                                 |    щ    |
    ------------                                 -----------

    Рисунок 11.9. Структуры данных, используемые при разделении памяти


    Процесс  присоединяет  область  разделяемой памяти к своему виртуальному
адресному пространству с помощью системной функции shmat:

    virtaddr = shmat(id,addr,flags);

Значение id, возвращаемое функцией shmget, идентифицирует область  разделяе-
мой  памяти, addr является виртуальным адресом, по которому пользователь хо-
чет подключить область, а с помощью флагов (flags) можно указать, предназна-
чена ли область только для чтения и нужно ли ядру округлять значение указан-
ного пользователем адреса. Возвращаемое функцией значение, virtaddr,  предс-
тавляет  собой виртуальный адрес, по которому ядро произвело подключение об-
ласти и который не всегда совпадает с адресом, указанным пользователем.
    В начале выполнения системной функции shmat  ядро  проверяет  наличие  у
процесса  необходимых  прав доступа к области (Рисунок 11.10). Оно исследует
указанный пользователем адрес; если он равен 0,  ядро  выбирает  виртуальный
адрес по своему усмотрению.
    Область разделяемой памяти не должна пересекаться в виртуальном адресном
пространстве  процесса  с  другими областями; следовательно, ее выбор должен
производиться разумно и осторожно. Так, например,  процесс  может  увеличить
размер  принадлежащей  ему области данных с помощью системной функции brk, и
новая область данных будет содержать адреса, смежные с прежней областью; по-
этому, ядру не следует присоединять область разделяемой памяти слишком близ-
ко к области данных процесса. Так же не следует размещать область  разделяе-
мой  памяти вблизи от вершины стека, чтобы стек при своем последующем увели-
чении не залезал за ее пределы. Если, например, стек  растет  в  направлении
увеличения адресов, лучше всего разместить область разделяемой памяти непос-
редственно перед началом области стека.
    Ядро  проверяет  возможность размещения области разделяемой памяти в ад-


                                    342

    --------------------------------------------------------------
    | алгоритм shmat     /* подключить разделяемую память */     |
    | входная информация:  (1) дескриптор области разделяемой    |
    |                          памяти                            |
    |                      (2) виртуальный адрес для подключения |
    |                          области                           |
    |                      (3) флаги                             |
    | выходная информация: виртуальный адрес, по которому область|
    |                      подключена фактически                 |
    | {                                                          |
    |     проверить правильность указания дескриптора, права до- |
    |      ступа к области;                                      |
    |     если (пользователь указал виртуальный адрес)           |
    |     {                                                      |
    |          округлить виртуальный адрес в соответствии с фла- |
    |           гами;                                            |
    |          проверить существование полученного адреса, размер|
    |           области;                                         |
    |     }                                                      |
    |     в противном случае   /* пользователь хочет, чтобы ядро |
    |                           * само нашло подходящий адрес */ |
    |          ядро выбирает адрес: в случае неудачи выдается    |
    |           ошибка;                                          |
    |     присоединить область к адресному пространству процесса |
    |      (алгоритм attachreg);                                 |
    |     если (область присоединяется впервые)                  |
    |          выделить таблицы страниц и отвести память под нее |
    |           (алгоритм growreg);                              |
    |     вернуть (виртуальный адрес фактического присоединения  |
    |      области);                                             |
    | }                                                          |
    --------------------------------------------------------------

       Рисунок 11.10. Алгоритм присоединения разделяемой памяти


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

    shmdt(addr)

где  addr  - виртуальный адрес, возвращенный функцией shmat. Несмотря на то,
что более логичной представляется передача идентификатора, процесс использу-
ет виртуальный адрес разделяемой памяти, поскольку одна и та же область раз-
деляемой памяти может быть подключена к адресному пространству процесса нес-
колько раз, к тому же ее идентификатор может быть удален  из  системы.  Ядро
производит  поиск области по указанному адресу и отсоединяет ее от адресного
пространства процесса, используя алгоритм detachreg (раздел 6.5.7). Посколь-
ку в таблицах областей отсутствуют обратные указатели на таблицу разделяемой
памяти, ядру приходится просматривать таблицу разделяемой памяти  в  поисках
записи,  указывающей  на данную область, и записывать в соответствующее поле
время последнего отключения области.
    Рассмотрим программу, представленную на Рисунке 11.11. В ней описывается

                                    343

процесс, создающий область разделяемой памяти размером 128  Кбайт  и  дважды
присоединяющий  ее к своему адресному пространству по разным виртуальным ад-
ресам. В 'первую' область он записывает данные, а читает их из 'второй'  об-
ласти. На Рисунке 11.12 показан другой процесс, присоединяющий ту же область
(он получает только 64 Кбайта, таким образом, каждый процесс может использо-
вать разный объем области разделяемой памяти); он ждет момента, когда первый
процесс  запишет в первое принадлежащее области слово любое отличное от нуля
значение, и затем принимается считывать данные из  области.  Первый  процесс
делает  'паузу'  (pause), предоставляя второму процессу возможность выполне-
ния; когда первый процесс принимает сигнал, он удаляет  область  разделяемой
памяти из системы.
    Процесс  запрашивает информацию о состоянии области разделяемой памяти и
производит установку параметров для нее с помощью системной функции shmctl:

    shmctl(id,cmd,shmstatbuf);

Значение id идентифицирует запись таблицы разделяемой памяти, cmd определяет
тип операции, а shmstatbuf является адресом  пользовательской  структуры,  в
которую  помещается информация о состоянии области. Ядро трактует тип опера-
ции точно так же, как и при управлении сообщениями. Удаляя область разделяе-
мой памяти, ядро освобождает соответствующую ей запись в таблице разделяемой
памяти и просматривает таблицу областей: если область не  была  присоединена
ни  к  одному из процессов, ядро освобождает запись таблицы и все выделенные
области ресурсы, используя для этого алгоритм freereg (раздел  6.5.6).  Если
же  область  по-прежнему  подключена к каким-то процессам (значение счетчика
ссылок на нее больше 0), ядро только сбрасывает флаг, говорящий о  том,  что
по  завершении  последнего связанного с нею процесса область не должна осво-
бождаться. Процессы, уже использующие область разделяемой памяти, продолжают
работать с ней, новые же процессы не могут присоединить ее. Когда  все  про-
цессы  отключат область, ядро освободит ее. Это похоже на то, как в файловой
системе после разрыва связи с файлом процесс может вновь открыть его и  про-
должать с ним работу.


      11.2.3 Семафоры  

    Системные  функции работы с семафорами обеспечивают синхронизацию выпол-
нения параллельных процессов, производя набор действий единственно над груп-
пой семафоров (средствами низкого уровня). До использования семафоров,  если
процессу нужно было заблокировать некий ресурс, он прибегал к созданию с по-
мощью системной функции creat специального блокирующего файла. Если файл уже
существовал,  функция  creat  завершалась  неудачно, и процесс делал вывод о
том, что ресурс уже заблокирован другим процессом. Главные недостатки такого
подхода заключались в том, что процесс не знал, в какой момент  ему  следует
предпринять следующую попытку, а также в том, что блокирующие файлы случайно
оставались в системе в случае ее
аварийного завершения или перезагрузки.
    Дийкстрой был опубликован алгоритм Деккера, описывающий реализацию сема-
форов  как  целочисленных  объектов, для которых определены две элементарные
операции: P и V (см. [Dijkstra 68]). Операция  P  заключается  в  уменьшении
значения семафора в том случае, если оно больше 0, операция V - в увеличении
этого значения (и там, и там на единицу). Поскольку операции элементарные, в
любой  момент времени для каждого семафора выполняется не более одной опера-
ции P или V. Связанные с семафорами системные  функции  являются  обобщением
операций, предложенных Дийкстрой, в них допускается одновременное выполнение
нескольких операций, причем операции уменьшения и увеличения выполняются над
значениями,  превышающими  1. Ядро выполняет операции комплексно; ни один из
посторонних процессов не сможет переустанавливать значения  семафоров,  пока


                                    344

все  операции  не будут выполнены. Если ядро по каким-либо причинам не может
выполнить все операции, оно не выполняет ни одной; процесс  приостанавливает
свою работу до тех пор, пока эта возможность не будет предоставлена.
    Семафор в версии V системы UNIX состоит из следующих элементов:
  * Значение семафора,
  * Идентификатор последнего из процессов, работавших с семафором,
  * Количество процессов, ожидающих увеличения значения семафора,
  *  Количество процессов, ожидающих момента, когда значение семафора станет
    равным 0.
    Для создания набора семафоров и получения  доступа  к  ним  используется
системная  функция semget, для выполнения различных управляющих операций над
набором - функция semctl, для  работы  со  значениями  семафоров  -  функция
semop.

    --------------------------------------------------------------
    | #include                                      |
    | #include                                        |
    | #include                                        |
    | #define SHMKEY    75                                       |
    | #define    K    1024                                       |
    | int shmid;                                                 |
    |                                                            |
    | main()                                                     |
    | {                                                          |
    |     int i, *pint;                                          |
    |     char *addr1, *addr2;                                   |
    |     extern char *shmat();                                  |
    |     extern cleanup();                                      |
    |                                                            |
    |     for (i = 0; i < 20; i++)                               |
    |          signal(i,cleanup);                                |
    |     shmid = shmget(SHMKEY,128*K,0777|IPC_CREAT);           |
    |     addr1 = shmat(shmid,0,0);                              |
    |     addr2 = shmat(shmid,0,0);                              |
    |     printf('addr1 Ox%x addr2 Ox%x\n',addr1,addr2);         |
    |     pint = (int *) addr1;                                  |
    |                                                            |
    |     for (i = 0; i < 256, i++)                              |
    |          *pint++ = i;                                      |
    |     pint = (int *) addr1;                                  |
    |     *pint = 256;                                           |
    |                                                            |
    |     pint = (int *) addr2;                                  |
    |     for (i = 0; i < 256, i++)                              |
    |          printf('index %d\tvalue %d\n',i,*pint++);         |
    |                                                            |
    |     pause();                                               |
    | }                                                          |
    |                                                            |
    | cleanup()                                                  |
    | {                                                          |
    |     shmctl(shmid,IPC_RMID,0);                              |
    |     exit();                                                |
    | }                                                          |
    --------------------------------------------------------------

    Рисунок 11.11. Присоединение процессом одной и той же области
                   разделяемой памяти дважды


                                    345

      -------------------------------------------------------
      | #include                               |
      | #include                                 |
      | #include                                 |
      |                                                     |
      | #define SHMKEY    75                                |
      | #define    K    1024                                |
      | int shmid;                                          |
      |                                                     |
      | main()                                              |
      | {                                                   |
      |     int i, *pint;                                   |
      |     char *addr;                                     |
      |     extern char *shmat();                           |
      |                                                     |
      |     shmid = shmget(SHMKEY,64*K,0777);               |
      |                                                     |
      |     addr = shmat(shmid,0,0);                        |
      |     pint = (int *) addr;                            |
      |                                                     |
      |     while (*pint == 0)                              |
      |          ;                                          |
      |     for (i = 0; i < 256, i++)                       |
      |          printf('%d\n',*pint++);                    |
      | }                                                   |
      -------------------------------------------------------

         Рисунок 11.12. Разделение памяти между процессами



       Таблица семафоров         Массивы семафоров
           ---------
           |       |        ----т---т---т---т---т---т----
           |       |------->| 0 | 1 | 2 | 3 | 4 | 5 | 6 |
           |       |        -----------------------------
           |-------|
           |       |        ----т---т----
           |       |------->| 0 | 1 | 2 |
           |       |        -------------
           |-------|
           |       |        -----
           |       |------->| 0 |
           |       |        -----
           |-------|
           |       |        ----т---т----
           |       |------->| 0 | 1 | 2 |
           |       |        -------------
           |-------|
           |   щ   |
           |   щ   |
           |   щ   |
           |   щ   |
           |   щ   |
           ---------


    Рисунок 11.13. Структуры данных, используемые в работе над семафорами


                                    346

    Синтаксис вызова системной функции semget:

    id = semget(key,count,flag);

где key, flag и id имеют тот же смысл, что и в других механизмах взаимодейс-
твия  процессов (обмен сообщениями и разделение памяти). В результате выпол-
нения функции ядро выделяет запись, указывающую на массив семафоров и содер-
жащую счетчик count (Рисунок 11.13). В записи также хранится количество  се-
мафоров  в массиве, время последнего выполнения функций semop и semctl. Сис-
темная функция semget на Рисунке 11.14, например, создает  семафор  из  двух
элементов.
    Синтаксис вызова системной функции semop:

    oldval = semop(id,oplist,count);

где  id  -  дескриптор,  возвращаемый функцией semget, oplist - указатель на
список операций, count  -  размер  списка.  Возвращаемое  функцией  значение
oldval является прежним значением семафора, над

    --------------------------------------------------------------
    | #include                                      |
    | #include                                        |
    | #include                                        |
    |                                                            |
    | #define SEMKEY    75                                       |
    | int semid;                                                 |
    | unsigned int count;                                        |
    | /* определение структуры sembuf в файле sys/sem.h          |
    |  * struct sembuf {                                         |
    |  *     unsigned shortsem_num;                              |
    |  *     short sem_op;                                       |
    |  *     short sem_flg;                                      |
    | }; */                                                      |
    | struct sembuf psembuf,vsembuf;    /* операции типа P и V */|
    |                                                            |
    | main(argc,argv)                                            |
    |      int argc;                                             |
    |      char *argv[];                                         |
    | {                                                          |
    |      int i,first,second;                                   |
    |      short initarray[2],outarray[2];                       |
    |      extern cleanup();                                     |
    |                                                            |
    |      if (argc == 1)                                        |
    |      {                                                     |
    |          for (i = 0; i < 20; i++)                          |
    |              signal(i,cleanup);                            |
    |          semid = semget(SEMKEY,2,0777|IPC_CREAT);          |
    |          initarray[0] = initarray[1] = 1;                  |
    |          semctl(semid,2,SETALL,initarray);                 |
    |          semctl(semid,2,GETALL,outarray);                  |
    |          printf('начальные значения семафоров %d %d\n',    |
    |                 outarray[0],outarray[1]);                  |
    |          pause();    /* приостанов до получения сигнала */ |
    |      }                                                     |
    |                                                            |
    |      /* продолжение на следующей странице */               |
    --------------------------------------------------------------

       Рисунок 11.14. Операции установки и снятия блокировки


                                    347

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

    --------------------------------------------------------------
    |      else if (argv[1][0] == 'a')                           |
    |      {                                                     |
    |          first = 0;                                        |
    |          second = 1;                                       |
    |      }                                                     |
    |      else                                                  |
    |      {                                                     |
    |          first = 1;                                        |
    |          second = 0;                                       |
    |      }                                                     |
    |                                                            |
    |      semid = semget(SEMKEY,2,0777);                        |
    |      psembuf.sem_op = -1;                                  |
    |      psembuf.sem_flg = SEM_UNDO;                           |
    |      vsembuf.sem_op = 1;                                   |
    |      vsembuf.sem_flg = SEM_UNDO;                           |
    |                                                            |
    |      for (count = 0; ; count++)                            |
    |      {                                                     |
    |          psembuf.sem_num = first;                          |
    |          semop(semid,&psembuf,1);                          |
    |          psembuf.sem_num = second;                         |
    |          semop(semid,&psembuf,1);                          |
    |          printf('процесс %d счетчик %d\n',getpid(),count); |
    |          vsembuf.sem_num = second;                         |
    |          semop(semid,&vsembuf,1);                          |
    |          vsembuf.sem_num = first;                          |
    |          semop(semid,&vsembuf,1);                          |
    |      }                                                     |
    | }                                                          |
    |                                                            |
    | cleanup()                                                  |
    | {                                                          |
    |          semctl(semid,2,IPC_RMID,0);                       |
    |          exit();                                           |
    | }                                                          |
    --------------------------------------------------------------

       Рисунок 11.14. Операции установки и снятия блокировки (продолжение)


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


                                    348

    --------------------------------------------------------------
    | алгоритм semop           /* операции над семафором */      |
    | входная информация:  (1) дескриптор семафора               |
    |                      (2) список операций над семафором     |
    |                      (3) количество элементов в списке     |
    | выходная информация: исходное значение семафора            |
    | {                                                          |
    |     проверить корректность дескриптора семафора;           |
    | start: считать список операций над семафором из простран-  |
    |         ства задачи в пространство ядра;                   |
    |     проверить наличие разрешений на выполнение всех опера- |
    |      ций;                                                  |
    |                                                            |
    |     для (каждой операции в списке)                         |
    |     {                                                      |
    |         если (код операции имеет положительное значение)   |
    |         {                                                  |
    |             прибавить код операции к значению семафора;    |
    |             если (для данной операции установлен флаг UNDO)|
    |                  скорректировать структуру восстановления  |
    |                   для данного процесса;                    |
    |             вывести из состояния приостанова все процессы, |
    |              ожидающие увеличения значения семафора;       |
    |         }                                                  |
    |         в противном случае если (код операции имеет отрица-|
    |          тельное значение)                                 |
    |         {                                                  |
    |             если (код операции + значение семафора >= 0)   |
    |             {                                              |
    |                  прибавить код операции к значению семафо- |
    |                   ра;                                      |
    |                  если (флаг UNDO установлен)               |
    |                       скорректировать структуру восстанов- |
    |                        ления для данного процесса;         |
    |                  если (значение семафора равно 0)          |
    |                       /* продолжение на следующей страни-  |
    |                        * це */                             |
    --------------------------------------------------------------

      Рисунок 11.15. Алгоритм выполнения операций над семафором


задачи,  когда  перезапускает системную функцию. Таким образом, операции вы-
полняются комплексно - или все за один сеанс или ни одной.
    Ядро меняет значение семафора в зависимости от кода операции.  Если  код
операции  имеет положительное значение, ядро увеличивает значение семафора и
выводит из состояния приостанова все процессы, ожидающие  наступления  этого
события.  Если  код операции равен 0, ядро проверяет значение семафора: если
оно равно 0, ядро переходит к выполнению других операций; в противном случае
ядро увеличивает число приостановленных процессов, ожидающих, когда значение
семафора станет нулевым, и 'засыпает'. Если код операции имеет отрицательное
значение и если его абсолютное значение не превышает значение семафора, ядро
прибавляет код операции (отрицательное число) к значению семафора. Если  ре-
зультат равен 0, ядро выводит из состояния приостанова все процессы, ожидаю-
щие обнуления значения семафора. Если результат меньше абсолютного
значения кода операции, ядро приостанавливает процесс до тех пор, пока  зна-
чение семафора не увеличится. Если процесс приостанавливается посреди опера-
ции, он имеет приоритет, допускающий прерывания; следовательно, получив сиг-
нал, он выходит из этого состояния.

                                    349

    --------------------------------------------------------------
    |                       вывести из состояния приостанова все |
    |                        процессы, ожидающие обнуления значе-|
    |                        ния семафора;                       |
    |                  продолжить;                               |
    |             }                                              |
    |             выполнить все произведенные над семафором в    |
    |              данном сеансе операции в обратной последова-  |
    |              тельности (восстановить старое значение сема- |
    |              фора);                                        |
    |             если (флаги не велят приостанавливаться)       |
    |                  вернуться с ошибкой;                      |
    |             приостановиться (до тех пор, пока значение се- |
    |              мафора не увеличится);                        |
    |             перейти на start;  /* повторить цикл с самого  |
    |                                 * начала * /               |
    |         }                                                  |
    |         в противном случае    /* код операции равен нулю */|
    |         {                                                  |
    |             если (значение семафора отлично от нуля)       |
    |             {                                              |
    |                  выполнить все произведенные над семафором |
    |                   в данном сеансе операции в обратной по-  |
    |                   следовательности (восстановить старое    |
    |                   значение семафора);                      |
    |                  если (флаги не велят приостанавливаться)  |
    |                       вернуться с ошибкой;                 |
    |                  приостановиться (до тех пор, пока значение|
    |                   семафора не станет нулевым);             |
    |                  перейти на start;  /* повторить цикл */   |
    |             }                                              |
    |         }                                                  |
    |     } /* конец цикла */                                    |
    |     /* все операции над семафором выполнены */             |
    |     скорректировать значения полей, в которых хранится вре-|
    |      мя последнего выполнения операций и идентификаторы    |
    |      процессов;                                            |
    |     вернуть исходное значение семафора, существовавшее в   |
    |      момент вызова функции semop;                          |
    | }                                                          |
    --------------------------------------------------------------

  Рисунок 11.15. Алгоритм выполнения операций над семафором (продолжение)


    Перейдем  к  программе,  представленной на Рисунке 11.14, и предположим,
что пользователь исполняет ее (под именем a.out) три раза в следующем поряд-
ке:
    a.out &
    a.out a &
    a.out b &
    Если программа вызывается без параметров, процесс создает набор  семафо-
ров из двух элементов и присваивает каждому семафору значение, равное 1. За-
тем процесс вызывает функцию pause и приостанавливается для получения сигна-
ла, после чего удаляет семафор из системы (cleanup). При выполнении програм-
мы  с  параметром  'a'  процесс (A) производит над семафорами в цикле четыре
операции: он уменьшает на единицу значение семафора 0, то же самое делает  с
семафором 1, выполняет команду вывода на печать и вновь увеличивает значения
семафоров 0 и 1. Если бы процесс попытался уменьшить значение семафора, рав-

                                    350

ное 0, ему пришлось бы приостановиться, следовательно, семафор можно считать
захваченным  (недоступным для уменьшения). Поскольку исходные значения сема-
форов были равны 1 и поскольку к семафорам не было обращений со стороны дру-
гих процессов, процесс A никогда не приостановится, а значения семафоров бу-
дут изменяться только между 1 и 0. При выполнении программы с параметром 'b'
процесс (B) уменьшает значения семафоров 0 и 1 в порядке, обратном ходу  вы-
полнения  процесса  A.  Когда  процессы A и B выполняются параллельно, может
сложиться ситуация, в которой процесс A захватил семафор 0 и хочет захватить
семафор 1, а процесс B захватил семафор 1 и хочет захватить семафор  0.  Оба
процесса  перейдут  в  состояние приостанова, не имея возможности продолжить
свое выполнение. Возникает взаимная блокировка, из  которой  процессы  могут
выйти только по получении сигнала.
    Чтобы  предотвратить  возникновение подобных проблем, процессы могут вы-
полнять одновременно несколько операций над семафорами. В последнем  примере
желаемый эффект достигается благодаря использованию следующих операторов:

    struct sembuf psembuf[2];

    psembuf[0].sem_num = 0;
    psembuf[1].sem_num = 1;
    psembuf[0].sem_op = -1;
    psembuf[1].sem_op = -1;
    semop(semid,psembuf,2);

Psembuf - это список операций, выполняющих одновременное уменьшение значений
семафоров 0 и 1. Если какая-то операция не может выполняться, процесс приос-
танавливается.  Так,  например, если значение семафора 0 равно 1, а значение
семафора 1 равно 0, ядро оставит оба значения неизменными до тех  пор,  пока
не сможет уменьшить и то, и другое.
    Установка  флага  IPC_NOWAIT в функции semop имеет следующий смысл: если
ядро попадает в такую ситуацию, когда процесс должен приостановить свое  вы-
полнение  в  ожидании увеличения значения семафора выше определенного уровня
или, наоборот, снижения этого значения до 0, и если при этом флаг IPC_NOWAIT
установлен, ядро выходит из функции с извещением об ошибке.  Таким  образом,
если не приостанавливать процесс в случае невозможности выполнения отдельной
операции, можно реализовать условный тип семафора.
    Если процесс выполняет операцию над семафором, захватывая при этом неко-
торые  ресурсы,  и  завершает свою работу без приведения семафора в исходное
состояние, могут возникнуть опасные ситуации. Причинами возникновения  таких
ситуаций могут быть как ошибки программирования, так и сигналы, приводящие к
внезапному  завершению  выполнения  процесса.  Если  после того, как процесс
уменьшит значения семафоров, он получит сигнал  kill,  восстановить  прежние
значения процессу уже не удастся, поскольку сигналы данного типа не анализи-
руются процессом. Следовательно, другие процессы, пытаясь обратиться к сема-
форам,  обнаружат,  что последние заблокированы, хотя сам заблокировавший их
процесс уже прекратил свое существование. Чтобы избежать  возникновения  по-
добных  ситуаций,  в  функции  semop процесс может установить флаг SEM_UNDO;
когда процесс завершится, ядро даст обратный ход всем операциям, выполненным
процессом. Для этого в распоряжении у ядра имеется таблица, в которой каждо-
му процессу в системе отведена отдельная  запись.  Запись  таблицы  содержит
указатель на группу структур восстановле-
ния,  по  одной  структуре на каждый используемый процессом семафор (Рисунок
11.16). Каждая структура восстановления состоит из трех элементов -  иденти-
фикатора семафора, его порядкового номера в наборе и установочного значения.
    Ядро выделяет структуры восстановления динамически, во время первого вы-
полнения системной функции semop с установленным флагом SEM_UNDO. При после-
дующих  обращениях  к  функции  с тем же флагом ядро просматривает структуры
восстановления для процесса в поисках структуры с тем же самым идентификато-


                                    351

    Заголовки частных структур
          восстановления            Структуры восстановления
             --------
             |  щ   |
             |  щ   |
             |  щ   |
             |  щ   |   ------------   ------------   ------------
             |------|   |Дескриптор|   |Дескриптор|   |Дескриптор|
             |      |-->|  Номер   |-->|  Номер   |-->|  Номер   |
             |------|   | Значение |   | Значение |   | Значение |
             |      |   ------------   ------------   ------------
             |      |   ------------
             |------|   |Дескриптор|
             |      |-->|  Номер   |
             |------|   | Значение |
             |  щ   |   ------------
             |  щ   |
             |  щ   |
             |  щ   |
             --------

         Рисунок 11.16. Структуры восстановления семафоров


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

  ----------------тт--------  ----------------тт-------т--------
  | идентификатор ||       |  | идентификатор ||       |       |
  |   семафора    || semid |  |   семафора    || semid | semid |
  |---------------++-------|  |---------------++-------+-------|
  | номер семафора||   0   |  | номер семафора||   0   |   1   |
  |---------------++-------|  |---------------++-------+-------|
  |  установочное ||       |  |  установочное ||       |       |
  |    значение   ||   1   |  |    значение   ||   1   |   1   |
  --------------------------  ----------------------------------

   (а) После первой операции       (б) После второй операции

  ----------------тт--------
  | идентификатор ||       |
  |   семафора    || semid |
  |---------------++-------|
  | номер семафора||   0   |                  пусто
  |---------------++-------|
  |  установочное ||       |
  |    значение   ||   1   |
  --------------------------

   (в) После третьей операции      (г) После четвертой операции

 Рисунок 11.17. Последовательность состояний списка структур восстановления


                                    352



ет  специальную  процедуру,  которая просматривает все связанные с процессом
структуры восстановления и выполняет над указанным семафором все обусловлен-
ные действия.
    Ядро создает структуру восстановления всякий раз, когда процесс уменьша-
ет значение семафора, а удаляет ее, когда процесс увеличивает значение сема-
фора, поскольку установочное значение
структуры равно 0. На Рисунке 11.17  показана  последовательность  состояний
списка структур при выполнении программы с параметром 'a'. После первой опе-
рации процесс имеет одну структуру, состоящую из идентификатора semid, номе-
ра  семафора, равного 0, и установочного значения, равного 1, а после второй
операции появляется вторая структура с номером семафора, равным 1, и устано-
вочным значением, равным 1. Если процесс неожиданно завершается, ядро прохо-
дит по всем структурам и прибавляет к каждому семафору по  единице,  восста-
навливая  их значения в 0. В частном случае ядро уменьшает установочное зна-
чение для семафора 1 на третьей операции, в соответствии с увеличением  зна-
чения самого семафора, и удаляет всю структуру целиком, поскольку установоч-
ное  значение становится нулевым. После четвертой операции у процесса больше
нет структур восстановления, поскольку все установочные значения стали нуле-
выми.
    Векторные операции над семафорами позволяют избежать  взаимных  блокиро-
вок, как было показано выше, однако они представляют известную трудность для
понимания  и  реализации, и в большинстве приложений полный набор их возмож-
ностей не является обязательным. Программы, испытывающие потребность  в  ис-
пользовании  набора семафоров, сталкиваются с возникновением взаимных блоки-
ровок на пользовательском уровне, и ядру уже нет необходимости  поддерживать
такие сложные формы системных функций.
    Синтаксис вызова системной функции semctl:

    semctl(id,number,cmd,arg);

    Параметр arg объявлен как объединение типов данных:

    union semunion {
          int val;
          struct semid_ds *semstat; /* описание типов см.  в При-
                                     * ложении */
          unsigned short *array;
    } arg;

    Ядро  интерпретирует  параметр  arg  в зависимости от значения параметра
cmd, подобно тому, как интерпретирует команды ioctl (глава 10). Типы  дейст-
вий,  которые  могут использоваться в параметре cmd: получить или установить
значения управляющих параметров (права доступа и др.),  установить  значения
одного или всех семафоров в наборе, прочитать значения семафоров. Подробнос-
ти  по каждому действию содержатся в Приложении. Если указана команда удале-
ния, IPC_RMID, ядро ведет поиск всех процессов, содержащих структуры восста-
новления для данного семафора, и удаляет соответствующие структуры из систе-
мы. Затем ядро инициализирует используемые семафором структуры данных и  вы-
водит из состояния приостанова все процессы, ожидающие наступления некоторо-
го связанного с семафором события: когда процессы возобновляют свое выполне-
ние, они обнаруживают, что идентификатор семафора больше не является коррек-
тным, и возвращают вызывающей программе ошибку.


      11.2.4 Общие замечания  

    Механизм  функционирования  файловой  системы и механизмы взаимодействия

                                    353

процессов имеют ряд общих черт. Системные функции типа 'get' похожи на функ-
ции creat и open, функции типа 'control' предоставляют  возможность  удалять
дескрипторы  из системы, чем похожи на функцию unlink. Тем не менее, в меха-
низмах взаимодействия процессов отсутствуют операции, аналогичные операциям,
выполняемым системной функцией close.  Следовательно,  ядро  не  располагает
сведениями  о том, какие процессы могут использовать механизм IPC, и, дейст-
вительно, процессы могут прибегать к услугам этого механизма, если правильно
угадывают соответствующий идентификатор и если  у  них  имеются  необходимые
права доступа, даже если они не выполнили предварительно функцию типа 'get'.
Ядро не может автоматически очищать неиспользуемые структуры механизма взаи-
модействия  процессов,  поскольку  ядру  неизвестно,  какие из этих структур
больше не нужны. Таким образом, завершившиеся вследствие возникновения ошиб-
ки процессы могут оставить после себя ненужные и  неиспользуемые  структуры,
перегружающие  и  засоряющие систему. Несмотря на то, что в структурах меха-
низма взаимодействия после завершения существования процесса ядро может сох-
ранить информацию о состоянии и данные, лучше все-таки для  этих  целей  ис-
пользовать файлы.
    Вместо традиционных, получивших широкое распространение файлов механизмы
взаимодействия  процессов  используют  новое пространство имен, состоящее из
ключей (keys). Расширить семантику ключей на всю сеть довольно трудно,  пос-
кольку на разных машинах ключи могут описывать различные объекты. Короче го-
воря, ключи в основном предназначены для использования в одномашинных систе-
мах.  Имена файлов в большей степени подходят для распределенных систем (см.
главу 13). Использование ключей вместо имен файлов также  свидетельствует  о
том, что средства взаимодействия процессов являются 'вещью в себе', полезной
в  специальных приложениях, но не имеющей тех возможностей, которыми облада-
ют, например, каналы и файлы.  Большая  часть  функциональных  возможностей,
предоставляемых  данными средствами, может быть реализована с помощью других
системных средств, поэтому включать их в состав ядра вряд ли  следовало  бы.
Тем не менее, их использование в составе пакетов прикладных программ тесного
взаимодействия дает лучшие результаты по сравнению со стандартными файловыми
средствами (см. Упражнения).


      11.3 ВЗАИМОДЕЙСТВИЕ В СЕТИ  

    Программы, поддерживающие межмашинную связь, такие, как электронная поч-
та, программы дистанционной пересылки файлов и удаленной регистрации, издав-
на используются в качестве специальных средств организации подключений и ин-
формационного  обмена.  Так,  например,  стандартные программы, работающие в
составе электронной почты, сохраняют текст почтовых сообщений пользователя в
отдельном   файле   (для   пользователя   'mjb'   этот   файл   имеет    имя
'/usr/mail/mjb').  Когда один пользователь посылает другому почтовое сообще-
ние на ту же машину, программа mail (почта) добавляет сообщение в конец фай-
ла адресата, используя в целях сохранения целостности различные  блокирующие
и  временные  файлы.  Когда адресат получает почту, программа mail открывает
принадлежащий ему почтовый файл и читает сообщения. Для того, чтобы  послать
сообщение  на другую машину, программа mail должна в конечном итоге отыскать
на ней соответствующий почтовый файл. Поскольку программа не может  работать
с удаленными файлами непосредственно, процесс, протекающий на другой машине,
должен действовать в качестве агента локального почтового процесса; следова-
тельно,  локальному процессу необходим способ связи со своим удаленным аген-
том через межмашинные границы. Локальный процесс является клиентом удаленно-
го обслуживающего (серверного) процесса.
    Поскольку в системе UNIX новые процессы создаются  с  помощью  системной
функции fork, к тому моменту, когда клиент попытается выполнить подключение,
обслуживающий процесс уже должен существовать. Если бы в момент создания но-
вого процесса удаленное ядро получало запрос на подключение (по каналам меж-
машинной связи), возникла бы несогласованность с архитектурой системы. Чтобы

                                    354

избежать этого, некий процесс, обычно init, порождает обслуживающий процесс,
который  ведет чтение из канала связи, пока не получает запрос на обслужива-
ние, после чего в соответствии с некоторым  протоколом  выполняет  установку
соединения.  Выбор  сетевых  средств и протоколов обычно выполняют программы
клиента и сервера, основываясь на информации, хранящейся в прикладных  базах
данных;  с другой стороны, выбранные пользователем средства могут быть зако-
дированы в самих программах.
    В качестве примера рассмотрим программу uucp, которая обслуживает  пере-
сылку  файлов в сети и исполнение команд на удалении (см. [Nowitz 80]). Про-
цесс-клиент запрашивает в базе данных адрес и другую  маршрутную  информацию
(например,  номер телефона), открывает автокоммутатор, записывает или прове-
ряет информацию в дескрипторе открываемого файла и вызывает удаленную  маши-
ну. Удаленная машина может иметь специальные линии, выделенные для использо-
вания  программой  uucp; выполняющийся на этой машине процесс init порождает
getty-процессы - серверы, которые управляют линиями и получают  извещения  о
подключениях. После выполнения аппаратного подключения процесс-клиент регис-
трируется  в  системе  в  соответствии  с  обычным  протоколом  регистрации:
getty-процесс запускает специальный интерпретатор команд, uucico,  указанный
в  файле '/etc/passwd', а процесс-клиент передает на удаленную машину после-
довательность команд, тем самым заставляя ее исполнять процессы от имени ло-
кальной машины.
    Сетевое взаимодействие в системе UNIX представляет  серьезную  проблему,
поскольку  сообщения должны включать в себя как информационную, так и управ-
ляющую части. В управляющей части сообщения может располагаться адрес назна-
чения сообщения. В свою очередь, структура адресных данных зависит  от  типа
сети и используемого протокола. Следовательно, процессам нужно знать тип се-
ти,  а  это идет вразрез с тем принципом, по которому пользователи не должны
обращать внимания на тип файла, ибо все устройства для пользователей  выгля-
дят  как  файлы.  Традиционные методы реализации сетевого взаимодействия при
установке управляющих параметров в сильной степени полагаются на помощь сис-
темной функции ioctl, однако в разных типах сетей  этот  момент  воплощается
по-разному. Отсюда возникает нежелательный побочный эффект, связанный с тем,
что программы, разработанные для одной сети, в других сетях могут не зарабо-
тать.
    Чтобы  разработать сетевые интерфейсы для системы UNIX, были предприняты
значительные усилия. Реализация потоков в последних редакциях версии V  рас-
полагает  элегантным механизмом поддержки сетевого взаимодействия, обеспечи-
вающим гибкое сочетание отдельных модулей протоколов и их согласованное  ис-
пользование на уровне задач. Следующий раздел посвящен краткому описанию ме-
тода  решения  данных  проблем  в  системе BSD, основанного на использовании
гнезд.


      11.4 ГНЕЗДА  

    В предыдущем разделе было показано, каким образом взаимодействуют  между
собой  процессы, протекающие на разных машинах, при этом обращалось внимание
на то, что способы реализации взаимодействия могут быть различаться в  зави-
симости от используемых протоколов и сетевых средств. Более того, эти спосо-
бы  не всегда применимы для обслуживания взаимодействия процессов, выполняю-
щихся на одной и той же машине, поскольку в них предполагается существование
обслуживающего (серверного) процесса, который при выполнении системных функ-
ций open или read будет приостанавливаться драйвером. В целях создания более
универсальных методов взаимодействия процессов на основе использования  мно-
гоуровневых  сетевых протоколов для системы BSD был разработан механизм, по-
лучивший название 'sockets' (гнезда) (см. [Berkeley 83]). В  данном  разделе
мы рассмотрим некоторые аспекты применения гнезд (на пользовательском уровне
представления).


                                    355

               Процесс-клиент       Процесс-сервер
                       |                 |
                       ----           ----
--------------------------+---     ---+---------------------------
| Уровень гнезд              |     |          Уровень гнезд      |
|-------------------------+--|     |--+--------------------------|
|                        TCP |     | TCP                         |
| Уровень протоколов      |  |     |  |       Уровень протоколов |
|                        IP  |     | IP                          |
|-------------------------+--|     |--+--------------------------|
|                     Драйвер|     | Драйвер                     |
| Уровень устройств  Ethernet|     |Ethernet  Уровень устройств  |
--------------------------+---     ---+---------------------------
                          -----   -----
                              |   |
                             С е т ь

           Рисунок 11.18. Модель с использованием гнезд

    Структура  ядра имеет три уровня: гнезд, протоколов и устройств (Рисунок
11.18). Уровень гнезд выполняет функции интерфейса между обращениями к  опе-
рационной  системе (системным функциям) и средствами низких уровней, уровень
протоколов содержит модули, обеспечивающие взаимодействие процессов (на  ри-
сунке  упомянуты протоколы TCP и IP), а уровень устройств содержит драйверы,
управляющие сетевыми устройствами. Допустимые сочетания протоколов и драйве-
ров указываются при построении системы (в секции конфигурации); этот  способ
уступает  по гибкости вышеупомянутому потоковому механизму. Процессы взаимо-
действуют между собой по схеме клиент-сервер: сервер ждет сигнала от гнезда,
находясь на одном конце дуплексной линии связи, а  процессы-клиенты  взаимо-
действуют  с сервером через гнездо, находящееся на другом конце, который мо-
жет располагаться на другой машине. Ядро обеспечивает внутреннюю связь и пе-
редает данные от клиента к серверу.
    Гнезда, обладающие одинаковыми свойствами, например, опирающиеся на  об-
щие  соглашения по идентификации и форматы адресов (в протоколах), группиру-
ются в домены (управляемые одним узлом). В системе  BSD  4.2  поддерживаются
домены:  'UNIX  system' - для взаимодействия процессов внутри одной машины и
'Internet' (межсетевой) - для взаимодействия через сеть с помощью  протокола
DARPA  (Управление перспективных исследований и разработок Министерства обо-
роны США) (см. [Postel 80] и [Postel 81]). Гнезда бывают двух типов:  вирту-
альный  канал  (потоковое  гнездо, если пользоваться терминологией Беркли) и
дейтаграмма. Виртуальный канал обеспечивает надежную доставку данных с  сох-
ранением  исходной  последовательности.  Дейтаграммы не гарантируют надежную
доставку с сохранением уникальности и последовательности, но они более  эко-
номны  в смысле использования ресурсов, поскольку для них не требуются слож-
ные установочные операции; таким образом, дейтаграммы  полезны  в  отдельных
случаях взаимодействия. Для каждой допустимой комбинации типа домен-гнездо в
системе  поддерживается  умолчание  на используемый протокол. Так, например,
для домена 'Internet' услуги виртуального канала выполняет  протокол  транс-
портной  связи (TCP), а функции дейтаграммы - пользовательский дейтаграммный
протокол (UDP).
    Существует несколько системных функций работы с гнездами. Функция socket
устанавливает оконечную точку линии связи.

    sd = socket(format,type,protocol);

Format обозначает домен ('UNIX system' или 'Internet'), type - тип связи че-
рез гнездо (виртуальный канал или дейтаграмма), а protocol - тип  протокола,
управляющего  взаимодействием.  Дескриптор  гнезда sd, возвращаемый функцией
socket, используется другими системными функциями. Закрытие гнезд  выполняет

                                    356

функция close.
    Функция bind связывает дескриптор гнезда с именем:

    bind(sd,address,length);

где  sd - дескриптор гнезда, address - адрес структуры, определяющей иденти-
фикатор, характерный для данной комбинации домена  и  протокола  (в  функции
socket). Length - длина структуры address; без этого параметра ядро не знало
бы,  какова  длина  структуры, поскольку для разных доменов и протоколов она
может быть различной. Например, для домена 'UNIX system' структура  содержит
имя  файла. Процессы-серверы связывают гнезда с именами и объявляют о состо-
явшемся присвоении имен процессам-клиентам.
    С помощью системной функции connect делается запрос на подключение к су-
ществующему гнезду:

    connect(sd,address,length);

Семантический смысл параметров функции остается прежним (см. функцию  bind),
но  address указывает уже на выходное гнездо, образующее противоположный ко-
нец линии связи. Оба гнезда должны использовать одни и те же домен и  прото-
кол связи, и тогда ядро удостоверит правильность установки линии связи. Если
тип  гнезда  - дейтаграмма, сообщаемый функцией connect ядру адрес будет ис-
пользоваться в последующих обращениях к функции send через данное гнездо;  в
момент вызова никаких соединений не производится.
    Пока процесс-сервер готовится к приему связи по виртуальному каналу, яд-
ру  следует  выстроить поступающие запросы в очередь на обслуживание. Макси-
мальная длина очереди задается с помощью системной функции listen:

    listen(sd,qlength)

где sd - дескриптор гнезда, а qlength - максимально-допустимое число  запро-
сов, ожидающих обработки.


    ----------------------         ---------------------------
    |   Процесс-клиент   |         |     Процесс-сервер      |
    |         |          |         |          |  щ           |
    |         |          |         |     ------  щщщщщщ      |
    |         |          |         |     |            щ      |
    |         |          |         |listen addr   accept addr|
    ----------+-----------         ------+------------щ-------
              |                          |            щ
              ----------------------------щщщщщщщщщщщщщ

               Рисунок 11.19. Прием вызова сервером


    Системная  функция  accept принимает запросы на подключение, поступающие
на вход процесса-сервера:

    nsd = accept(sd,address,addrlen);

где sd - дескриптор гнезда, address - указатель на пользовательский  массив,
в  котором  ядро  возвращает  адрес  подключаемого клиента, addrlen - размер
пользовательского массива. По завершении выполнения функции ядро  записывает
в переменную addrlen размер пространства, фактически занятого массивом. Фун-
кция  возвращает  новый дескриптор гнезда (nsd), отличный от дескриптора sd.
Процесс-сервер может продолжать слежение за состоянием объявленного  гнезда,
поддерживая связь с клиентом по отдельному каналу (Рисунок 11.19).

                                    357

    Функции send и recv выполняют передачу данных через подключенное гнездо.
Синтаксис вызова функции send:

    count = send(sd,msg,length,flags);

где  sd  - дескриптор гнезда, msg - указатель на посылаемые данные, length -
размер данных, count - количество фактически переданных байт. Параметр flags
может содержать значение SOF_OOB (послать данные out-of-band - 'через тамож-
ню'), если посылаемые данные не учитываются в  общем  информационном  обмене
между взаимодействующими процессами. Программа удаленной регистрации, напри-
мер,  может послать out-of-band сообщение, имитирующее нажатие на клавиатуре
терминала клавиши 'delete'. Синтаксис вызова системной функции recv:

    count = recv(sd,buf,length,flags);

где buf - массив для приема данных, length - ожидаемый объем данных, count -
количество байт, фактически  переданных  пользовательской  программе.  Флаги
(flags) могут быть установлены таким образом, что поступившее сообщение пос-
ле чтения и анализа его содержимого не будет удалено из очереди, или настро-
ены на получение данных out-of-band. В дейтаграммных версиях указанных функ-
ций, sendto и recvfrom, в качестве дополнительных параметров указываются ад-
реса.  После выполнения подключения к гнездам потокового типа процессы могут
вместо функций send и recv использовать функции read и write. Таким образом,
согласовав тип протокола, серверы могли бы  порождать  процессы,  работающие
только с функциями read и write, словно имеют дело с обычными файлами.
    Функция shutdown закрывает гнездовую связь:

    shutdown(sd,mode)

где mode указывает, какой из сторон (посылающей, принимающей или обеим вмес-
те)  отныне  запрещено  участие в процессе передачи данных. Функция сообщает
используемому протоколу о завершении сеанса сетевого взаимодействия,  остав-
ляя,  тем  не  менее,  дескрипторы гнезд в неприкосновенности. Освобождается
дескриптор гнезда только в результате выполнения функции close.
    Системная функция getsockname получает имя гнездовой связи,  установлен-
ной ранее с помощью функции bind:

    getsockname(sd,name,length);

    Функции  getsockopt  и setsockopt получают и устанавливают значения раз-
личных связанных с гнездом параметров в соответствии с типом домена и прото-
кола.
    Рассмотрим обслуживающую программу,  представленную  на  Рисунке  11.20.
Процесс  создает в домене 'UNIX system' гнездо потокового типа и присваивает
ему имя sockname. Затем с помощью функции listen устанавливается длина  оче-
реди  поступающих сообщений и начинается цикл ожидания поступления запросов.
Функция accept приостанавливает свое выполнение до тех пор, пока  протоколом
не будет зарегистрирован запрос на подключение к гнезду с означенным именем;
после  этого функция завершается, возвращая поступившему запросу новый деск-
риптор гнезда. Процесс-сервер порождает потомка, через которого  будет  под-
держиваться связь с процессом-клиентом; родитель и потомок при этом закрыва-
ют  свои дескрипторы, чтобы они не становились помехой для коммуникационного
траффика другого процесса. Процесс-потомок ведет разговор с клиентом  и  за-
вершается после выхода из функции read. Процесс-сервер возвраща-
ется к началу цикла и ждет поступления следующего запроса на подключение.
    На  Рисунке  11.21  показан  пример процесса-клиента, ведущего общение с
сервером. Клиент создает гнездо в том же домене, что и  сервер,  и  посылает
запрос  на  подключение к гнезду с именем sockname. В результате подключения


                                    358

    --------------------------------------------------------------
    | #include                                      |
    | #include                                     |
    |                                                            |
    | main()                                                     |
    | {                                                          |
    |     int sd,ns;                                             |
    |     char buf[256];                                         |
    |     struct sockaddr sockaddr;                              |
    |     int fromlen;                                           |
    |                                                            |
    |     sd = socket(AF_UNIX,SOCK_STREAM,0);                    |
    |                                                            |
    |     /* имя гнезда - не может включать пустой символ */     |
    |     bind(sd,'sockname',sizeof('sockname') - 1);            |
    |     listen(sd,1);                                          |
    |                                                            |
    |     for (;;)                                               |
    |     {                                                      |
    |                                                            |
    |          ns = accept(sd,&sockaddr,&fromlen);               |
    |          if (fork() == 0)                                  |
    |          {                                                 |
    |              /* потомок */                                 |
    |              close(sd);                                    |
    |              read(ns,buf,sizeof(buf));                     |
    |              printf('сервер читает '%s'\n',buf);           |
    |              exit();                                       |
    |          }                                                 |
    |          close(ns);                                        |
    |     }                                                      |
    | }                                                          |
    --------------------------------------------------------------

        Рисунок 11.20. Процесс-сервер в домене 'UNIX system'

    --------------------------------------------------------------
    | #include                                      |
    | #include                                     |
    |                                                            |
    | main()                                                     |
    | {                                                          |
    |    int sd,ns;                                              |
    |    char buf[256];                                          |
    |    struct sockaddr sockaddr;                               |
    |    int fromlen;                                            |
    |                                                            |
    |    sd = socket(AF_UNIX,SOCK_STREAM,0);                     |
    |                                                            |
    |    /* имя в запросе на подключение не может включать       |
    |    /* пустой символ */                                     |
    |    if (connect(sd,'sockname',sizeof('sockname') - 1) == -1)|
    |         exit();                                            |
    |                                                            |
    |    write(sd,'hi guy',6);                                   |
    | }                                                          |
    --------------------------------------------------------------

         Рисунок 11.21. Процесс-клиент в домене 'UNIX system'

                                    359



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

    socket(AF_INET,SOCK_STREAM,0);

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


      11.5 ВЫВОДЫ  

    Мы  рассмотрели  несколько форм взаимодействия процессов. Первой формой,
положившей начало обсуждению, явилась трассировка процессов - взаимодействие
двух процессов, выступающее в качестве полезного средства отладки  программ.
При  всех своих преимуществах трассировка процессов с помощью функции ptrace
все же достаточно дорогостоящее и примитивное мероприятие, поскольку за один
сеанс функция способна передать строго ограниченный объем данных,  требуется
большое  количество  переключений  контекста,  взаимодействие ограничивается
только формой отношений родитель-потомок, и наконец, сама трассировка произ-
водится только по обоюдному согласию участвующих в ней процессов. В версии V
системы UNIX имеется пакет взаимодействия процессов (IPC), включающий в себя
механизмы обмена сообщениями, работы с семафорами и разделения памяти. К со-
жалению, все эти механизмы имеют узкоспециальное назначение, не имеют  хоро-
шей  стыковки с другими элементами операционной системы и не действуют в се-
ти. Тем не менее, они используются во многих приложениях и  по  сравнению  с
другими схемами отличаются более высокой эффективностью.
    Система  UNIX поддерживает широкий спектр вычислительных сетей. Традици-
онные методы согласования протоколов в сильной степени полагаются на  помощь
системной функции ioctl, однако в разных типах сетей они реализуются по-раз-
ному. В системе BSD имеются системные функции для работы с гнездами, поддер-
живающие  более универсальную структуру сетевого взаимодействия. В будущем в
версию V предполагается включить описанный в главе  10  потоковый  механизм,
повышающий согласованность работы в сети.


      11.6 УПРАЖНЕНИЯ  

  1. Что произойдет в том случае, если в программе debug будет отсутствовать
     вызов функции wait (Рисунок 11.3) ? (Намек: возможны два исхода.)
  2.  С  помощью  функции  ptrace  отладчик считывает данные из пространства
     трассируемого процесса по одному слову за одну операцию. Какие  измене-
     ния следует произвести в ядре операционной системы для того, чтобы уве-
     личить количество считываемых слов ? Какие изменения при этом необходи-
     мо сделать в самой функции ptrace ?
  3.  Расширьте  область действия функции ptrace так, чтобы в качестве пара-
     метра pid можно было указывать идентификатор процесса,  не  являющегося
     потомком текущего процесса. Подумайте над вопросами, связанными с защи-
     той информации: При каких обстоятельствах процессу может быть позволено

                                    360

     читать данные из адресного пространства другого, произвольного процесса
     ?  При каких обстоятельствах разрешается вести запись в адресное прост-
     ранство другого процесса ?
  4. Организуйте из функций работы с сообщениями библиотеку пользовательско-
     го уровня с использованием обычных файлов, поименованных каналов и эле-
     ментов блокировки. Создавая  очередь  сообщений,  откройте  управляющий
     файл  для записи в него информации о состоянии очереди; защитите файл с
     помощью средств захвата файлов и других удобных для вас механизмов. По-
     сылая сообщение данного типа, создавайте поименованный канал  для  всех
     сообщений этого типа, если такого канала еще не было, и передавайте со-
     общение через него (с подсчетом переданных байт). Управляющий файл дол-
     жен соотносить тип сообщения с именем поименованного канала. При чтении
     сообщений управляющий файл направляет процесс к соответствующему поиме-
     нованному  каналу. Сравните эту схему с механизмом, описанным в настоя-
     щей главе, по эффективности, сложности реализации и функциональным воз-
     можностям.
  5. Какие действия пытается выполнить программа, представленная на  Рисунке
     11.22 ?
 *6.  Напишите  программу,  которая подключала бы область разделяемой памяти
     слишком близко к вершине стека задачи и позволяла бы стеку при увеличе-
     нии пересекать границу разделяемой области. В какой  момент  произойдет
     фатальная ошибка памяти ?
  7.   Используйте  в  программе,  представленной  на  Рисунке  11.14,  флаг
     IPC_NOWAIT, реализуя условный тип семафора. Продемонстрируйте,  как  за
     счет этого можно избежать возникновения взаимных блокировок.
  8. Покажите, как операции над семафорами типа P и V реализуются при работе
     с  поименованными  каналами. Как бы вы реализовали операцию P условного
     типа ?
  9. Составьте программы захвата ресурсов,  использующие  (а)  поименованные
     каналы,  (б) системные функции creat и unlink, (в) функции обмена сооб-
     щениями. Проведите сравнительный анализ их эффективности.
 10. На практических примерах работы с поименованными каналами сравните  эф-
     фективность  использования функций обмена сообщениями, с одной стороны,
     с функциями read и write, с другой.
 11. Сравните на конкретных программах скорость передачи данных при работе с
     разделяемой памятью и при использовании механизма  обмена  сообщениями.
     Программы,  использующие разделяемую память, для синхронизации заверше-
     ния операций чтения-записи должны опираться на семафоры.

    --------------------------------------------------------------
    | #include                                      |
    | #include                                        |
    | #include                                        |
    | #define ALLTYPES 0                                         |
    |                                                            |
    | main()                                                     |
    | {                                                          |
    |    struct msgform                                          |
    |    {                                                       |
    |        long mtype;                                         |
    |        char mtext[1024];                                   |
    |    } msg;                                                  |
    |    register unsigned int id;                               |
    |                                                            |
    |    for (id = 0; ; id++)                                    |
    |        while (msgrcv(id,&msg,1024,ALLTYPES,IPC_NOWAIT) > 0)|
    |              ;                                             |
    | }                                                          |
    --------------------------------------------------------------

                                    361


File created by Faq2Site converter. (C) 1998-2002 Edward Grebenyukov
Hosted by uCoz