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

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