МИНИСТЕРСТВО ОБРАЗОВАНИЯ РОССИЙСКОЙ ФЕДЕРАЦИИ

Московский Государственный институт электроники и математики

(Технический университет)

 

Кафедра Управление и информатика в технических системах

 

 

РАЗДЕЛЯЕМАЯ ПАМЯТЬ И СЕМАФОРЫ

 

Методические указания

к лабораторной работе № 4

по курсу "Системное программное обеспечение"

 

 

 

Москва 2002

 

Составители:

докт. техн. наук Е.А. Саксонов

канд. техн. наук В.Э. Карпов

ст. преподаватель И.П. Карпова

 

УДК 681.3

Разделяемая память и семафоры: Метод. указания к лабораторной работе №4 по курсу " Системное программное обеспечение " / Моск. гос. ин-т электроники и математики; Сост.: Е.А. Саксонов, В.Э.Карпов, И.П.Карпова. М., 2002. – 27с.

 

Лабораторная работа №4: ознакомление с семафорами как средством синхронизации работы параллельных процессов ОС UNIX, с обменом данными между процессами через разделяемую память; приобретение знаний и навыков написания программ работы с конкурирующими процессами.

Методические указания к лабораторным работам являются составной частью учебно-методического комплекса по дисциплине “Системное программное обеспечение”, изучаемой студентами специальности 21.01 “Управление и информатика в технических системах”.

Каждая лабораторная работа выполняется в объеме 3 часов.

 

ОГЛАВЛЕНИЕ

1. ТЕОРЕТИЧЕСКИЕ СВЕДЕНИЯ *

1.1. Взаимодействие процессов в версии V системы UNIX *

1.2. Использование разделяемой памяти *

1.3. Семафоры *

1.3.1. Синхронизация процессов *

1.3.2. Реализация семафоров *

1.4. Общие замечания *

2. СИСТЕМНЫЕ ВЫЗОВЫ *

2.1. Системные вызовы для работы с разделяемой памятью *

2.2. Системные вызовы для работы с семафорами *

3. ПРИМЕРЫ ПРОГРАММ *

ЗАДАНИЯ НА ЛАБОРАТОРНУЮ РАБОТУ № 4 *

БИБЛИОГРАФИЧЕСКИЙ СПИСОК *

Цель лабораторной работы №4 состоит в изучении механизма взаимодействия процессов на основе разделяемой памяти, а также средств их синхронизации с использованием семафоров.

1. ТЕОРЕТИЧЕСКИЕ СВЕДЕНИЯ

1.1. Взаимодействие процессов в версии V системы UNIX

Пакет IPC (Interprocess communication) в версии V системы UNIX включает в себя три механизма. Механизм сообщений дает процессам возможность посылать другим процессам потоки сформатированных данных, механизм разделения памяти позволяет процессам совместно использовать отдельные части виртуального адресного пространства, а семафоры - синхронизировать свое выполнение с выполнением параллельных процессов. Несмотря на то, что они реализуются в виде отдельных блоков, им присущи общие свойства.

1.2. Использование разделяемой памяти

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

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

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

1.3. Семафоры

В настоящее время все большее значение придается крупным вычислительным системам, таким как многопроцессорные вычислительные комплексы и сети ЭВМ. Основным средством увеличения мощностей вычислительных машин является повышение в них уровня параллелизма. Очевидно, что параллельность в аппаратуре отражается и на программном обеспечении. В связи с этим значительно возрастает интерес к параллельным процессам и проблемам их синхронизации.

На сегодняшний день предложено большое количество различных систем синхронизации процессов. К ним относятся:

Один из способов синхронизации параллельных процессов - семафоры Дейкстры, реализованные в ОС UNIX.

1.3.1. Синхронизация процессов

Определим понятие процесса как задания, выполняемого в операционной системе. Поскольку задания в многопользовательской системе должны выполняться независимо и могут выполняться параллельно, то и представляющие их процессы должны быть независимыми и параллельно выполняемыми.

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

Если по условиям работы требуется, чтобы разделяемые ресурсы одномоментно были доступны только одному процессу, то такие ресурсы называются критическими.

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

Элементарные приемы синхронизации, такие как использование общих переменных, имеют ряд недостатков, которые иногда приводят к невозможности получения правильных решений. Поэтому возникла необходимость в создании специальных синхронизирующих примитивов.

Такие примитивы под названием P и V операции были предложены Дейкстрой в 1968 году. Эти операции могут выполняться только над специальными переменными, называемыми семафорами или семафорными переменными. Семафоры являются целыми величинами и первоначально были определены как принимающие только неотрицательные значения. Кроме того, если их использовать для решения задач взаимного исключения, то область их значений может быть ограничена лишь “0” или “1”. Однако в дальнейшем была показана важная область применения семафоров, принимающих любые целые положительные значения. Такие семафоры получили название “общих семафоров” в отличие от “двоичных семафоров”, используемых в задачах взаимного исключения. P и V операции являются единственными операциями, выполняемыми над семафорами. Иногда они называются семафорными операциями.

Дадим определение P и V операций в том виде, в котором они были предложены Дейкстрой.

V - операция (V(S)):

операция с одним аргументом, который должен быть семафором.

Эта операция увеличивает значение аргумента на 1.

P - операция (P(S)):

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

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

Эти определения справедливы как для общих, так для двоичных семафоров.

1.3.2. Реализация семафоров

Системные вызовы для работы с семафорами содержатся в пакете IPC (подключаемый файл описаний - <sys/ipc.h>). Эти вызовы обеспечивают синхронизацию выполнения параллельных процессов, производя набор действий только над группой семафоров (средствами низкого уровня).

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

Каждый семафор в системе UNIX представляет собой набор значений (вектор семафоров). Связанные с семафорами системные функции являются обобщением операций P и V семафоров Дейкстры, в них допускается одновременное выполнение нескольких операций (над семафорами, принадлежащими одному вектору, так называемые векторные операции). Ядро выполняет операции комплексно; ни один из посторонних процессов не сможет переустанавливать значения семафоров, пока все операции не будут выполнены. Если ядро по каким-либо причинам не может выполнить все операции, оно не выполняет ни одной; процесс приостанавливает свою работу до тех пор, пока эта возможность не будет предоставлена. (Подробнее о порядке операций над семафорами см. п. 2. “Системные вызовы”).

Семафор в System V состоит из следующих элементов:

Для создания набора семафоров и получения доступа к ним используется системная функция semget, для выполнения различных управляющих операций над набором - функция semctl, для работы со значениями семафоров - функция semop.

1.4. Общие замечания

Механизм функционирования файловой системы и механизмы взаимодействия процессов имеют ряд общих черт. Системные функции типа “get” похожи на функции creat и open, функции типа “control” (ctl) предоставляют возможность удалять дескрипторы из системы, чем похожи на функцию unlink. Тем не менее, в механизмах взаимодействия процессов отсутствуют операции, аналогичные операциям, выполняемым системной функцией close. Следовательно, ядро не располагает сведениями о том, какие процессы используют механизм IPC, и, действительно, процессы могут прибегать к услугам этого механизма, если правильно “угадывают” соответствующий идентификатор и если у них имеются необходимые права доступа, даже если они не выполнили предварительно функцию типа “get”.

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

Вместо традиционных, получивших широкое распространение файлов механизмы взаимодействия процессов используют новое пространство имен, состоящее из ключей (keys). Расширить семантику ключей на всю сеть довольно трудно, поскольку на разных машинах ключи могут описывать различные объекты. Короче говоря, ключи в основном предназначены для использования в одномашинных системах. Имена файлов в большей степени подходят для распределенных систем. Использование ключей вместо имен файлов также свидетельствует о том, что средства взаимодействия процессов являются “вещью в себе”, полезной в специальных приложениях, но не имеющей тех возможностей, которыми обладают, например, каналы и файлы.

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

2. СИСТЕМНЫЕ ВЫЗОВЫ

2.1. Системные вызовы для работы с разделяемой памятью

Системные вызовы для работы с разделяемой памятью в ОС UNIX

описаны в библиотеке <sys/shm.h>.

Функция shmget создает новую область разделяемой памяти или возвращает адрес уже существующей области, функция shmat логически присоединяет область к виртуальному адресному пространству процесса, функция shmdt отсоединяет ее, а функция shmctl позволяет получать информацию о состоянии разделяемой памяти и производить над ней операции.

SHMGET

Создание области разделяемой памяти или получение номера дескриптора существующей области:

int shmget(key_t key, int size, int flag);

id = shmget(key, size, flag);

где id - идентификатор области разделяемой памяти, key - номер области, size - объем области в байтах, flag - параметры создания и права доступа.

Ядро использует key для ведения поиска в таблице разделяемой памяти: если подходящая запись обнаружена и если разрешение на доступ имеется, ядро возвращает вызывающему процессу указанный в записи дескриптор. Если запись не найдена и пользователь установил флаг IPC_CREAT, указывающий на необходимость создания новой области, ядро проверяет нахождение размера области в установленных системой пределах и выделяет область.

Ядро записывает установки прав доступа, размер области и указатель на соответствующую запись таблицы областей в таблицу разделяемой памяти (Рис.1) и устанавливает флаг, свидетельствующий о том, что с областью не связана отдельная память.

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

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

SHMAT

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

void *shmat(int id, void *addr, int flag);

virtaddr = shmat(id, addr, flag);

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

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

SHMDT

Отсоединение области разделяемой памяти от виртуального адресного пространства процесса:

int shmdt(void *addr);

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

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

SHMCTL

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

int shmctl(int id, int cmd, struct shmid_ds *buf);

Значение id (возвращаемое функцией shmget) идентифицирует запись таблицы разделяемой памяти, cmd определяет тип операции, а buf является адресом пользовательской структуры, хранящей информацию о состоянии области. Типы операций описываются списком определений в файле “sys/ipc.h”:

#define IPC_RMID 10 /* удалить идентификатор (область) */

#define IPC_SET 11 /* установить параметры */

#define IPC_STAT 12 /* получить параметры */

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

2.2. Системные вызовы для работы с семафорами

Системные вызовы для работы с семафорами в ОС UNIX описаны в

библиотеке <sys/sem.h>.

SEMGET

Создание набора семафоров и получение доступа к ним:

int semget(key_t key, int count, int semflg);

semid = semget(key, count, semflg);

где key - номер семафора, count - количество семафоров, semflg - параметры создания и права доступа. Ядро использует key для ведения поиска в таблице семафоров: если подходящая запись обнаружена и разрешение на доступ имеется, ядро возвращает вызывающему процессу указанный в записи дескриптор. Если запись не найдена, а пользователь установил флаг IPC_CREAT - создание нового семафора, - ядро проверяет возможность его создания и выделяет запись в таблице семафоров. Запись указывает на массив семафоров и содержит счетчик count (Рис.1). В записи также хранится количество семафоров в массиве, время последнего выполнения функций semop и semctl.

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

SEMOP

Установка или проверка значения семафора:

int semop(int semid, struct sembuf *oplist, unsigned nsops);

где semid - дескриптор, возвращаемый функцией semget, oplist - указатель на список операций, nsops - размер списка. Возвращаемое функцией значение является прежним значением семафора, над которым производилась операция. Каждый элемент списка операций имеет следующий формат (определение структуры sembuf в файле sys/sem.h):

struct sembuf

{ unsigned short sem_num;

short sem_op;

short sem_flg;

}

где shortsem_num - номер семафора, идентифицирующий элемент массива семафоров, над которым выполняется операция; sem_op - код операции; sem_fl – флаги операции. Ядро считывает список операций oplist из адресного пространства задачи и проверяет корректность номеров семафоров, а также наличие у процесса необходимых разрешений на чтение и корректировку семафоров. Если таких разрешений не имеется, системная функция завершается неудачно (res = -1). Если ядру приходится приостанавливать свою работу при обращении к списку операций, оно возвращает семафорам их прежние значения и находится в состоянии приостанова до наступления ожидаемого события, после чего системная функция запускается вновь. Поскольку ядро хранит коды операций над семафорами в глобальном списке, оно вновь считывает этот список из пространства задачи, когда перезапускает системную функцию. Таким образом, операции выполняются комплексно - или все за один сеанс, или ни одной.

Установка флага IPC_NOWAIT в функции semop имеет следующий смысл: если ядро попадает в такую ситуацию, когда процесс должен приостановить свое выполнение в ожидании увеличения значения семафора выше определенного уровня или, наоборот, снижения этого значения до 0, и если при этом флаг IPC_NOWAIT установлен, ядро выходит из функции с извещением об ошибке. (Таким образом, если не приостанавливать процесс в случае невозможности выполнения отдельной операции, можно реализовать условный тип семафора). Флаг SEM_UNDO позволяет избежать блокирования семафора процессом, который закончил свою работу прежде, чем освободил захваченный им семафор. Если процесс установил флаг SEM_UNDO, то при завершении этого процесса ядро даст обратный ход всем операциям, выполненным процессом. Для этого в распоряжении у ядра имеется таблица, в которой каждому процессу отведена отдельная запись. Запись содержит указатель на группу структур восстановления, по одной структуре на каждый используемый процессом семафор (Рис.2).

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

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

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

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

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

SEMCTL

Выполнение управляющих операций над набором семафоров:

int semctl(int semid, int semnum, int cmd, union semun arg);

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

union semunion

{ int val; // используется только для SETVAL

struct semid_ds *semstat; // для IPC_STAT и IPC_SET

unsigned short *array;

} arg;

Ядро интерпретирует параметр arg в зависимости от значения параметра cmd, который может принимать следующие значения:

GETVAL - вернуть значение того семафора, на который указывает параметр num.

SETVAL - установить значение семафора, на который указывает параметр num, равным значению arg.val.

GETPID - вернуть идентификатор процесса, выполнявшего последним функцию semop по отношению к тому семафору, на который указывает параметр semnum.

GETNCNT - вернуть число процессов, ожидающих того момента, когда значение семафора станет положительным.

GETZCNT - вернуть число процессов, ожидающих того момента, когда значение семафора станет нулевым.

GETALL - вернуть значения всех семафоров в массиве arg.array.

SETALL - установить значения всех семафоров в соответствии с содержимым массива arg.array.

IPC_STAT - считать структуру заголовка семафора с идентификатором id в буфер arg.buf. Аргумент semnum игнорируется.

IPC_SET – запись структуры семафора из буфера arg.buf.

IPC_RMID - удалить семафоры, связанные с идентификатором id, из системы.

Если указана команда удаления IPC_RMID, ядро ведет поиск всех процессов, содержащих структуры восстановления для данного семафора, и удаляет соответствующие структуры из системы. Затем ядро инициализирует используемые семафором структуры данных и выводит из состояния приостанова все процессы, ожидающие наступления некоторого связанного с семафором события: когда процессы возобновляют свое выполнение, они обнаруживают, что идентификатор семафора больше не является корректным, и возвращают вызывающей программе ошибку. Если возвращаемое функцией число равно 0, то функция завершилась успешно, иначе (возвращаемое значение равно -1) произошла ошибка. Код ошибки хранится в переменной errno.

3. ПРИМЕРЫ ПРОГРАММ

Пример 1. Запись в область разделяемой памяти и чтение из нее.

Первая из программ описывает процесс, в котором создается область разделяемой памяти размером 128 Кбайт и производится запись и считывание данных из этой области.

В соответствии со второй программой другой процесс присоединяет ту же область (он получает только 64 Кбайта, таким образом, каждый процесс может использовать разный объем области разделяемой памяти); он ждет момента, когда первый процесс запишет в первое принадлежащее области слово любое отличное от нуля значение, и затем принимается считывать данные из области.

Первый процесс делает “паузу” (pause), предоставляя второму процессу возможность выполнения; когда первый процесс принимает сигнал, он удаляет область разделяемой памяти из системы.

/*

Запись в разделяемую память и чтение из нее

*/

#include <sys/types.h>

#include <sys/ipc.h>

#include <sys/shm.h>

#define SHMKEY 5

#define K 1024

int shmid;

main()

{ int i, *pint;

char *addr;

extern char *shmat();

extern cleanup();

/* определение реакции на все сигналы */

for (i = 0; i < 20; i++) signal(i, cleanup);

/* создание общедоступной разделяемой области памяти размером 128*K (или получение ее идентификатора, если она уже существует) */

shmid = shmget(SHMKEY,128*K,0777¦IPC_CREAT);

addr = shmat(shmid,0,0);

pint = (int *) addr;

for (i = 0; i < 256; i++) *pint++ = i;

pint = (int *) addr;

*pint = 256;

pint = (int *) addr;

for (i = 0; i < 256; i++)

printf("index %d\tvalue %d\n",i,*pint++);

/* ожидание сигнала */

pause();

}

/* удаление разделяемой памяти */

cleanup()

{

shmctl(shmid,IPC_RMID,0);

exit();

}

/*

Чтение из разделяемой памяти данных, записанных первым

процессом

*/

#include <sys/types.h>

#include <sys/ipc.h>

#include <sys/shm.h>

#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++);

}

Пример 2. Работа двух параллельных процессов в одном критическом интервале времени.

Для организации работы двух процессов в одном критическом интервале времени необходимо время работы одного процесса сделать недоступным для другого (т.е. второй процесс не может выполняться одновременно с первым). Для этого используем средство синхронизации - семафор. В данном случае нам потребуется один семафор. Опишем его с помощью системной функции ОС UNIX:

lsid = semget(75, 1, 0777 | IPC_CREAT);

где lsid - это идентификатор семафора;75 - ключ пользовательского дескриптора (если он занят, система создаст свой); 1 - количество семафоров в массиве; IPC_CREAT - флаг для создания новой записи в таблице дескрипторов (описан с правами доступа 0777).

Для установки начального значения семафора используем структуру sem. В ней присваиваем значение:

sem.array[0] = 1;

то есть семафор открыт для пользования.

Завершающим шагом является инициализация массива (в данном случае массив состоит из одного элемента):

semctl(lsid,1,SETALL,sem);

где lsid - идентификатор семафора (выделенная строка в дескрипторе); 1 - количество семафоров; SETALL - команда “установить все семафоры”; sem - указатель на структуру.

Устанавливаем флаг SEM_UNDO в структуре sop для работы с функцией semop (значение этого флага не меняется в процессе работы).

Далее в программе организуются два параллельных процесса (потомки “главной” программы) с помощью системной функции fork(). Один процесс-потомок записывает данные в разделяемую память, второй считывает эти данные. При этом процессы-потомки синхронизируют доступ к разделяемой памяти с помощью семафоров.

Функция p() описывается следующим образом:

int p(int sid)

{ sop.sem_num = 0; /* номер семафора */

sop.sem_op = -1;

semop(sid, &sop, 1);

}

В структуру sop заносится номер семафора, над которым будет произведена операция и значение самой операции (в данном случае это уменьшение значения на 1). Флаг был установлен заранее, поэтому функция в процессе всегда находится в ожидании свободного семафора. (Функция v() работает аналогично, но sop.sem_op = 1).

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

#include <stdio.h>

#include <sys/types.h>

#include <sys/ipc.h>

#include <sys/shm.h>

#include <sys/sem.h>

#include <unistd.h>

#include <errno.h>

int shmid, lsid, x;

struct sembuf sop;

union semun

{ int val;

struct semid_ds *buf;

ushort *array;

} sem;

int p(int sid)

{ sop.sem_num = 1;

sop.sem_op = -1;

semop(sid, &sop, 1);

}

int v(int sid)

{ sop.sem_num = 1;

sop.sem_op = 1;

semop(sid, &sop, 1);

}

main()

{ int j, i, id, id1, n;

lsid = semget(75, 1, 0777 | IPC_CREAT);

sem.array = (ushort*)malloc(sizeof(ushort));

sem.array[0] = 1;

sop.sem_flg = SEM_UNDO;

semctl(lsid, 1, SETALL, sem);

printf(" n= ");

scanf("%d", &n);

id = fork();

if (id == 0) /* первый процесс */

{ for(i = 0; i < n; i++)

{ p(lsid);

puts("\n Работает процесс 1");

v(lsid);

}

exit(0);

}

id1 = fork();

if (id1 == 0) /* второй процесс */

{ for (j = 0; j < n; j++)

{ p(lsid);

puts("\n Работает процесс 2");

v(lsid);

}

exit(0);

}

wait(&x);

wait(&x);

exit(0);

}

Пример 3. Векторные операции над семафорами.

#include <sys/types.h>

#include <sys/ipc.h>

#include <sys/sem.h>

#define SEMKEY 75

int semid; /* идентификатор семафора */

unsigned int count; /* количество семафоров */

struct sembuf psembuf, vsembuf; /* операции типа P и V */

cleanup()

{ semctl(semid, 2, IPC_RMID, 0);

exit();

}

main(int argc, char *argv[])

{ int i, first, second;

short initarray[2], outarray[2];

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(); /* приостанов до получения сигнала */

}

else

if (argv[1][0] == 'a')

{ first = 0;

second = 1;

}

else

{ first = 1;

second = 0;

}

/* получение доступа к ранее созданному семафору */

semid = semget(SEMKEY, 2, 0777);

/* определение операций P и V */

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);

}

}

Предположим, что пользователь исполняет данную программу (под именем a.out) три раза в следующем порядке:

a.out &

a.out a &

a.out b &

Если программа вызывается без параметров, процесс создает набор семафоров из двух элементов и присваивает каждому семафору значение, равное 1. Затем процесс вызывает функцию pause() и приостанавливается для получения сигнала, после чего удаляет семафор из системы (cleanup).

При выполнении программы с параметром 'a' процесс (A) производит над семафорами в цикле четыре операции: он уменьшает на единицу значение семафора 0, то же самое делает с семафором 1, выполняет команду вывода на печать и вновь увеличивает значения семафоров 0 и 1. Если бы процесс попытался уменьшить значение семафора, равное 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, ядро оставит оба значения неизменными до тех пор, пока не сможет уменьшить и то, и другое.

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

Идентификатор

семафора

semid

 

Идентификатор

семафора

semid

semid

Номер семафора

0

Номер семафора

0

1

Установленное

значение

1

Установленное

значение

1

1

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

Идентификатор

семафора

semid

 

 

 

Номер семафора

0

Пусто

 

Установленное

значение

1

 

 

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

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

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

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

4. ЗАДАНИЯ НА ЛАБОРАТОРНУЮ РАБОТУ № 4

  1. Процесс 1 порождает потомков 2 и 3, все они присоединяют к себе разделяемую память объемом (2*sizeof(int)). Процессы 1 и 2 по очереди пишут в эту память число, равное своему номеру (1 или 2). После этого один из процессов удаляет разделяемую память, затем процесс 3 считывает содержимое области разделяемой памяти и записывает в файл. Используя семафоры, обеспечить следующее содержимое файла:
  2. а) 1 2 1 2 1 2 1 2

    б) 1 1 2 2 1 1 2 2

    в) 1 1 2 1 1 2 1 1 2

    г) 2 1 1 2 1 1 2 1 1

    д) 1 2 2 1 2 2 1 2 2

  3. Процесс 1 порождает потомков 2 и 3. Все процессы записывают в общую разделяемую память число, равное своему номеру. Используя семафоры, обеспечить следующее содержимое области памяти:
  4. а) 1 2 3 1 2 3 1 2 3

    б) 1 1 2 2 3 3 1 1 2 2 3 3

    в) 1 2 1 3 1 2 1 3 1 2 1 3

    г) 2 1 1 3 2 1 1 3 2 1 1 3

    д) 3 1 2 3 1 2 3 1 2

    Последний процесс считывает содержимое разделяемой памяти, выводит его на экран и удаляет разделяемую память.

  5. Процесс 1 порождает потомков 2 и 3, все они присоединяют к себе две области разделяемой памяти M1 и M2 объемом (N1*sizeof(int)) и (N2*sizeof(int)) соответственно. Процесс 1 пишет в M1 число, которое после каждой записи увеличивается на 1; процесс 2 переписывает k2 чисел из M1 в M2, а процесс 3 переписывает k3 чисел из M2 в файл. После каждого этапа работы процесс 1 засыпает на t1 секунд, процесс 2 - на t2 секунд, а процесс 3 - на t3 секунд. Процессу 1 запрещается записывать в занятую область M1; процесс 2 может переписать данные, если была произведена запись в M1 и M2 свободна; процесс 3 может переписывать данные из M2, только если была осуществлена запись в M2. Используя семафоры, обеспечить синхронизацию работы процессов в соответствии с заданными условиями. Параметры N1, N2, k1, k2, k3, t1, t2, t3 задаются в виде аргументов командной строки.
  6. Процесс 1 порождает потомка 2, они присоединяют к себе разделяемую память объемом (N*sizeof(int)). Процесс 1 пишет в нее k1 чисел сразу, процесс 2 переписывает k2 чисел из памяти в файл. Процесс 1 может производить запись, только если в памяти достаточно места, а процесс 2 переписывать, только если имеется не меньше, чем k2 чисел. После каждой записи (чтения) процессы засыпают на t секунд. После каждой записи процесс 1 увеличивает значение записываемых чисел на 1. Используя семафоры, обеспечить синхронизацию работы процессов в соответствии с заданными условиями. Параметры N, k1, k2, t задаются в виде аргументов командной строки.
  7. Реализовать библиотеку системных вызовов разделяемой памяти и семафоров стандарта POSIX. Для этого использовать лишь механизм отображения файлов в память.
  8. Написать программу "Морской бой" с архитектурой клиент–сервер.
  9. Написать программу "Крестики–нолики" с архитектурой клиент–сервер.
  10. Создать систему вида "локальный форум".

 

БИБЛИОГРАФИЧЕСКИЙ СПИСОК

  1. Дансмур М., Дейвис Г. Операционная система UNIX и программирование на языке Си: Пер. с англ. – М.: Радио и связь, 1989. – 192 с.
  2. Керниган Б.В., Пайк Р. UNIX – универсальная среда программирования: Пер. с англ. – М.: Финансы и статистика, 1992. – 304 с.
  3. Робачевский А.М. Операционная система UNIX. – СПб.: BHV – Санкт-Петербург, 1997. – 528 с.