ГЛАВА 11. ВЗАИМОДЕЙСТВИЕ ПРОЦЕССОВ
Оцените этот текст
Наличие механизмов взаимодействия дает произвольным процессам возмож- ность осуществлять обмен данными и синхронизировать свое выполнение с други- ми процессами. Мы уже рассмотрели несколько форм взаимодействия процессов, такие как канальная связь, использование поименованных каналов и посылка сигналов. Каналы (непоименованные) имеют недостаток, связанный с тем, что они известны только потомкам процесса, вызвавшего системную функцию pipe: не имеющие родственных связей процессы не могут взаимодействовать между собой с помощью непоименованных каналов. Несмотря на то, что поименованные каналы позволяют взаимодействовать между собой процессам, не имеющим родственных связей, они не могут использоваться ни в сети (см. главу 13), ни в организа- ции множественных связей между различными группами взаимодействующих процес- сов: поименованный канал не поддается такому мультиплексированию, при кото- ром у каждой пары взаимодействующих процессов имелся бы свой выделенный ка- нал. Произвольные процессы могут также связываться между собой благодаря по- сылке сигналов с помощью системной функции kill, однако такое "сообщение" состоит из одного только номера сигнала. В данной главе описываются другие формы взаимодействия процессов. В на- чале речь идет о трассировке процессов, о том, каким образом один процесс следит за ходом выполнения другого процесса, затем рассматривается пакет IPC: сообщения, разделяемая память и семафоры. Делается обзор традиционных методов сетевого взаимодействия процессов, выполняющихся на разных машинах, и, наконец, дается представление о "гнездах", применяющихся в системе BSD. Вопросы сетевого взаимодействия, имеющие специальный характер, такие как протоколы, адресация и др., не рассматриваются, поскольку они выходят за рамки настоящей работы. В системе 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-программ, предоставляемая только суперпользовате- лю, реализуется как составная часть традиционного механизма защиты файлов. Пакет 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", запрашиваю- щая информацию о состоянии записи, изменяющая эту информацию или удаляю- щая запись из системы. Когда процесс запрашивает информацию о состоянии записи, ядро проверяет, имеет ли процесс разрешение на чтение записи, после чего копирует данные из записи таблицы по адресу, указанному поль- зователем. При установке значений принадлежащих записи параметров ядро проверяет, совпадают ли между собой пользовательский код идентификации процесса и идентификатор пользователя (или создателя), указанный в запи- си, не запущен ли процесс под управлением суперпользователя; одного раз- решения на запись недостаточно для установки параметров. Ядро копирует сообщенную пользователем информацию в запись таблицы, устанавливая зна- чения пользовательского и группового кодов идентификации, режимы доступа и другие параметры (в зависимости от типа механизма). Ядро не изменяет значения полей, описывающих пользовательский и групповой коды идентифи- кации создателя записи, поэтому пользователь, создавший запись, сохраня- ет управляющие права на нее. Пользователь может удалить запись, либо ес- ли он является суперпользователем, либо если идентификатор процесса сов- падает с любым из идентификаторов, указанных в структуре записи. Ядро увеличивает номер дескриптора, чтобы при следующем назначении записи ей был присвоен новый дескриптор. Следовательно, как уже ранее говорилось, если процесс попытается обратиться к записи по старому дескриптору, выз- ванная им функция получит отказ. С сообщениями работают четыре системных функции: 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
Last-modified: Thu, 12-Feb-98 07:20:36 GMT