Наличие механизмов взаимодействия дает произвольным процессам возмож-
ность осуществлять обмен данными и синхронизировать свое выполнение с други-
ми процессами. Мы уже рассмотрели несколько форм взаимодействия процессов,
такие как канальная связь, использование поименованных каналов и посылка
сигналов. Каналы (непоименованные) имеют недостаток, связанный с тем, что
они известны только потомкам процесса, вызвавшего системную функцию 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