+------+ | | |
| | +------+ | | |
| --+---->| | | | |
| | +---+--+ | | |
+------+ | | | |
| - | | | +------+
| - | +-----------|------------------>+------+
| - | | | |
| - | | | |
| - | +------------------>+------+
| - | | |
| - | +------+
| - | | - |
| - | | - |
| - | | - |
+------+ +------+

Рисунок 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() |