Синхронизация процессов с помощью семафоров
Дейкстра для решения проблемы взаимного исключения предложил концепцию семафоров.
Семафор — это защищенная переменная, значение которой можно опрашивать и менять только при помощи специальных операций (семафорных примитивов) P и V и операции инициализации.
Двоичные (бинарные) семафоры могут принимать только значения 0 или 1 (true или false), то есть могут находиться в двух состояниях: закрыт и открыт. Считающие семафоры (семафоры со счетчиками) могут принимать целые значения. Считающий семафор открыт, если значение счетчика больше 0.
Операции P и V — это семафорные примитивы (неделимые операции), во время выполнения которых процессом к переменной-семафору нет доступа для других процессов. Если одновременно несколько процессов запрашивают выполнение операций P и/или V над одним и тем же семафором, то эти операции будут выполняться последовательно, в произвольном порядке.
Примитив P — процедура, сбрасывающая или уменьшающая значение семафора на 1, эта процедура заключает в себе потенциальное ожидание вызывающих процессов в том случае, если к моменту выполнения этой процедуры процессом соответствующий семафор закрыт.
Процедура V отрывает семафор или увеличивает значение счетчика на 1. Таким образом, вызов этой процедуры может активизировать некоторый ожидающий открытия семафора процесс.
В зависимости от ограничений, установленных для значения счетчика считающего семафора, изменяется смысл семафорной переменной и алгоритм выполнения семафорных примитивов.
Если для счетчика допускаются только целые неотрицательные значения, то смысл его значения — счетчик количества некоторого ресурса. Процедура P — процедура запроса единицы этого ресурса, запрос может быть удовлетворен или может заблокировать процесс, выполнивший его, если количество ресурса равно 0. Процедура V — процедура освобождения единицы ресурса.
Если счетчик может принимать и отрицательные целые значения, то отрицательное значение семафора означает длину очереди процессов, попытавшихся пройти через семафор (запросивших соответствующий ему ресурс).
Операции выборки значения семафорной переменной S, ее изменения и сохранения не могут быть прерваны. Во время выполнения последовательности этих операций в процедурах P и V, вызванных одним процессом, над каким-либо семафором он недоступен для других процессов. Операция hold(S) означает перевод процесса в состояние ожидания на семафоре S и выход из P. Это ожидание не должно быть “занятым” (то есть нельзя зациклить процедуру P на проверке состояния семафора, так как в этом случае ни один другой процесс не смог бы получить доступ к этой защищенной переменной даже для выполнения операции открытия этого семафора). Для устранения занятого ожидания с каждым семафором связывается список блокирования (список процессов, ожидающих открытия семафора). Процесс, который не может продолжиться после выполнения P(S), будет заблокирован с сохранением его состояния в соответствующем S списке блокирования. Операция release(S) — выполняет проверку списка блокирования, связанного с семафором S, если этот список не пуст, то активизируется некоторый процесс из этого списка, пропускается через семафор, а значение семафора изменяется при этом соответствующим образом (бинарный семафор снова закрывается, а для семафора со счетчиком возвращенная процессом, выполнившем операцию V(S), единица ресурса передается очередному процессу из списка).
Ниже приведено описание семафорных примитивов для бинарных и считающих семафоров. Алгоритмы выполнения семафорных операций можно представить так:
| S | Бинарный семафор | Считающий семафор с неотрицательными значениями счетчика | Общий считающий семафор |
| P(S) | if S = 0 then Hold(S) else S := 0 | if S = 0 then Hold(S) else S := S — 1 | S := S — 1; if S < 0 then Hold(S) |
| V(S) | if not Empty(S) then Release (S) else S := 1 ; | if not Empty(S) then Release (S) else S := S + 1 ; | S := S + 1 ; if S<= 0 then Release (S) |
Рассмотрите приведенные ниже варианты примитивов. Нет ли в их текстах ошибок? Все ли свойства примитивов выполняются?
| S | Бинарный семафор | Считающий семафор с неотрицательными значениями счетчика | Общий считающий семафор |
| P(S) | if S = 0 then hold(S); S := 0 | if S = 0 then hold(S); S := S — 1 | S := S — 1; if S < 0 then hold(S) |
| V(S) | S := 1 ; release (S) | S := S + 1 ; release (S) | S := S + 1 ; release (S) |
Использование семафоров значительно упрощает решение проблемы взаимного исключения.
Для синхронизации процессов, рассматривавшихся выше, с помощью семафоров следует выполнить следующие изменения в соответствующих программах:
| Процедура инициализации | Первый процесс | Второй процесс |
| procedure INIT; binary semaphore B ; begin B := 1 ; <Открыт>start(P1) ; start(P2) end INIT . | process P1; binary semaphore B ; begin while true do begin BEFORE1 ; P(B) ; CS1 ; V(B) ; AFTER1 ; end end P1 . | process P2; binary semaphore B ; begin while true do begin BEFORE2 ; P(B) ; CS2 ; V(B) ; AFTER2 ; end end P2 . |
Когда какой-либо процесс подходит к выполнению своей критической секции, он пытается пройти через семафор. Процедура P позволяет продолжить выполнение процесса только в том случае, когда семафор открыт. Причем до выполнения процедуры V семафор оказывается закрытым. Любой процесс, пытающийся выполнить P в то время, как семафор закрыт, будет заблокирован и не сможет продолжить выполнение и попасть в свою критическую секцию. Открыть же семафор может только тот процесс, который выходит из своей критической секции, освобождая разделяемые ресурсы.
Перенести этот механизм синхронизации на случай большего числа процессов не представляет никакой сложности, так как количество процессов, которые могут обращаться к семафорным примитивам практически может быть любым.
Семафоры могут использоваться не только для решения проблемы взаимного исключения, но и как средство синхронизации выполнения процессов, определенные функции которых должны быть согласованы, упорядочены во времени. Например, существуют два процесса P1 и P2.. Первый процесс должен выполнить процедуру BEFORE до того как второй процесс сможет выполнить процедуру AFTER. Если второй процесс подойдет к выполнению процедуры AFTER раньше, чем первый процесс завершит выполнение процедуры BEFORE, он должен ждать ее завершения.
В качестве синхронизирующего сигнала, позволяющего согласовать порядок выполнения процедур, может использоваться сброс/установка значения бинарного семафора.
В процедуре инициализации устанавливается признак того, что процедура BEFORE еще не выполнена первым процессом — закрывается двоичный семафор. Этот семафор проверяется вторым процессом перед выполнением им своей процедуры AFTER. Если при подходе процесса к выполнению этой процедуры семафор оказывается закрытым, первый процесс еще не выполнил свою процедуру BEFORE, выполнение которой должно предшествовать вызову AFTER. Первый процесс, выполнив свою процедуру BEFORE, открывает семафор, передавая таки образом сигнал разрешения выполнения процедуры AFTER во втором процессе.
| Процедура инициализации | Первый процесс | Второй процесс |
| procedure INIT; binary semaphore B ; begin B := 0 ; <Закрыт>start(P1) ; start(P2) end INIT . | process P1; binary semaphore B ; begin . BEFORE ; V(B); <Разрешение для P2 > <выполнить AFTER>. end P1 . | process P2; binary semaphore B ; begin . P(B) ; <Ожидание от P1> <разрешения на выполнение>AFTER ; . end P2 . |
Таким образом, семафоры позволяют решить проблему реализации отношений предшествования для процессов, выполняя роль сигналов.
Семафоры могут также использоваться как счетчики ресурсов и синхронизаторы при решении задачи “производитель-потребитель”, в которой два процесса взаимодействуют через буфер.
Пусть процесс P1 является “производителем”, который вырабатывает информацию и помещает новые записи в буфер. Параллельно с этим процессом в ВС существует процесс-“потребитель” информации P2, считывающий информацию из буфера. Процесс-“писатель” не должен вносить информацию в переполненный буфер и затирать еще не прочитанные “читателем” записи, а процесс-“читатель” не может читать информацию из пустого буфера или повторять чтение уже прочитанных записей.
Пусть емкость буфера составляет N записей.
Для процесса P1 ресурсом являются незаполненные записи в буфере, для их учета можно использовать считающий семафор E, а для процесса P2 необходимым для работы ресурсом являются сформированные процессом P1 записи, для учета их числа можно также использовать считающий семафор F. Кроме того, выполнение операций с буфером, который является разделяемым ресурсом, требует обычно взаимного исключения (последовательность операций над данными в буфере — критическая секция), для реализации которого можно использовать бинарный семафор.
Описание процедуры инициализации и программ, соответствующих процессам P1 и P2, приведено ниже.
| Процедура инициализации | Процесс-производитель | Процесс-потребитель |
| procedure INIT; binary semaphore B ; integer semaphore E, F; begin B := 1 ; <Буфер открыт>E := N ; <Все записи> <в буфере пусты>F := 0 ; <В буфере нет> <ни одной записи>start(P1) ; start(P2) end INIT . | process P1; binary semaphore B ; integer semaphore E, F; begin while true do begin . produce_next_record ; P(E) ; P(B) ; write_record_to_buffer ; V(B); V(F); . end end P1 . | process P2; binary semaphore B ; integer semaphore E, F; begin while true do begin . P(F) ; P(B) ; read_record_from_buffer; V(B); V(E); process_new_record ; . end end P2 . |
Процесс-производитель ресурса может получить доступ к буферу только в том случае, когда есть свободные для добавления информации записи, а процесс-потребитель информации — только при наличии сформированных записей. Причем, если в момент обращения к буферу с ним работает другой процесс, оба процесса переходят в состояние ожидания завершения операции чтения или записи.
Можно ли в приведенных процедурах поменять местами вызовы процедур P(F) и P(B) и P(E) и P(B)?
Рассмотрите варианты процедур для случаев, когда есть несколько процессов-писателей и несколько процессов-читателей, по одному процессу. Как можно сделать процедуры более эффективными?
Для учета номера очередной записи в буфере, которая должна будет считываться или переписываться, процессы могут использовать собственные переменные (номера текущих записей).
6.4.3. Семафоры
Семафоры лучше всего предствлять себе как счетчики, управляющие доступом к общим ресурсам. Чаще всего они используются как блокирующий механизм, не позволяющий одному процессу захватить ресурс, пока этим ресурсом пользуется другой. Семафоры часто подаются как наиболее трудные для восприятия из всех трех видов IPC-объектов. Для полного понимания, что же такое семафор, мы их немного пообсуждаем, прежде чем переходить к системным вызовам и операционной теории.
Слово «семафор» в действительности является старым железнодорожным термином, соответствующим «рукам», не дающим траекториям каров (это то, что у буржуев ездит по рельсам — перев.) пересекаться на перекрестках. То же самое можно сказать и про семафоры. Семафор в положении ON (руки пондяты вверх) если ресурс свободен и в положении OFF (руки опущены) если ресурс недоступен (должны ждать).
Этот пример неплохо показал суть работы семафора, однако важно знать, что в IPC используются множества семафоров, а не отдельные экземпляры. Разумеется, множество может содержать и один семафор, как в нашем железнодорожном примере.
Возможен другой подход к семафорам — как к счетчикам ресурсов. Приведем другой пример из жизни. Вообразим себе спулер, управляющий несколькими принтерами, каждый из которых обрабатывает по нескольку заданий. Гипотетический менеджер печати будет использовать множество семафоров для установления доступа к каждому из принтеров.
Предположим, что в корпоративной комнате имеются 5 работающих принтеров. Наш менеджер конструирует 5 семафоров — по одному на каждый принтер. Поскольку каждый принтер может обрабатывать только по одному запросу за раз, все семафоры устанавливаются в 1, что означает готовность всех принтеров печатать.
Машенька послала запрос на печать. Менеджер смотрит на семафоры и находит первый из них со значением 1. Перед тем, как Машенькин запрос попадет на физическое устройство, менеджер печати уменьшит соответствующий семафор на 1. Теперь значение семафора есть 0. В мире семафоров System V нуль означает стопроцентную занятость ресурса на семафоре. В нашем примере на принтере не будет ничего печататься, пока значение семафора не изменится.
Когда Машенька напечатала все свои плакаты, менеджер печати увеличивает семафор на 1. Теперь его значение вновь равно 1 и принтер может принимать задания снова.
Не смущайтесь тем, что все семафоры инициализируются единицей. Семафоры, трактуемые как счетчики ресурсов, могут изначально устанавливаться в любое натуральное число, не только в 0 или 1. Если бы наши принтеры умели печатать по 500 документов за раз, мы могли бы проинициализировать семафоры значением 500, уменьшая семафор на 1 при каждом поступающем задании и увеличивая после его завершения. Как вы увидите в следующей главе, семафоры имеют очень близкое отношение к разделяемым участкам памяти, играя роль сторожевой собаки, кусающей нескольких писателей в один и тот же сегмент памяти (имеется в виду машинная память).
Перед тем, как копаться в системных вызовах, коротко пробежимся по внутренним структурам данных, с которыми имеют дело семафоры.
Структура semid_ds ядра
Так же, как и для очередей сообщений, ядро отводит часть своего адресного пространства под структуру данных каждого множества семафоров. Структура определена в linux/sem.h:
Так же, как с очередями сообщений, операции с этой структурой проводятся с помощью системных вызовов, а не грязными скальпелями. Вот некоторые описания полей.
Это пример структуры ipc_perm, котораф описана в linux/ipc.h. Она содержит информацию о доступе к множеству семафоров, включая права доступа и информацию о создателе множества (uid и т.д.).
Время последней операции semop() (подробнее чуть позже).
Время последнего изменения структуры.
Указатель на первый семафор в массиве.
Число запросов undo в массиве (подробнее еще чуть позже).
Количество семафоров в массиве.
Структура sem ядра
В sem_ds есть указатель на базу массива семафоров. Каждый элемент массива имеет тип sem, который описан в linux/sem.h:
ID процесса, проделавшего последнюю операцию
Текущее значение семафора
sem_semncnt
Число процессов, ожидающих освобождения требуемых ресурсов
sem_semzcnt
Число процессов, ожидающих освобождения всех ресурсов
Системный вызов semget() используется для того, чтобы создать новое множество семафоров или получить доступ к старому.
Первый аргумент semget() — это ключ (в нашем случае возвращается ftok()-ом). Он сравнивается с ключами остальных множеств семафоров, присутствующих в системе. Вместе с этим решается вопрос о выборе между созданием и подключением к множеству семафоров в зависимости от аргумента msgflg.
Создает множество семафоров, если его еще не было в системе.
При использовании вместе с IPC_CREAT вызывает ошибку, если семафор уже существует.
Если IPC_CREAT используется в одиночку, то semget() возвращает идентификатор множества семафоров — вновь созданного или с таким же ключом. Если IPC_EXCL используется совместно с IPC_CREAT, то либо создается новое множество, либо, если оно уже существует, вызов приводит к ошибке и -1. Сам по себе IPC_EXCL бесполезен, но вместе с IPC_CREAT он дает средство гарантировать, что ни одно из существующих множеств семафоров не открыто для доступа. Как и в других частях System V IPC, восьмеричный режим доступа может быть OR-нут в маску для формирования доступа к множеству семафоров.
Аргумент nems определяет число семафоров, которых требуется породить в новом множестве. Это количество принтеров в нашей корпоративной комнате. Максимальное число семафоров определяется в
Аргумент nsems игнорируется, если вы открвываете существующую очередь.
Напишем функции-переходники для открытия и создания множества семафоров.
Обратите внимание на явное задание доступа 0660. Эта незатейливая функция возвращает идентификатор множества семафоров или -1 в случае ошибки. Должны быть также заданы значение ключа и число семафоров для того, чтобы сосчитать память, необходимую для них. В примере, завершающем этот IPC_EXCL используется для определения существует множество семафоров или нет.
Системный вызов semop()
Первый аргумент semop() есть значение ключа (в нашем случае возвращается semget-ом). Второй аргумент (sops) — это указатель на массив операций, выполняемых над семафорами, третий аргумент (nsops) является количеством операций в этом массиве.
Аргумент sops указывает на массив типа sembuf. Эта структура описана в linux/sem.h следующим образом:
Номер семафора, с которым вы собираетесь иметь дело.
Выполняемая операция (положительное, отрицательное число или нуль).
Флаги операции.
Если sem_op отрицателен, то его значение вычитается из семафора (семафор уменьшается — перев.). Это соответствует получению ресурсов, которые контролирует семафор. Если IPC_NOWAIT не установлен, то вызывающий процесс засыпает, пока семафор не выдаст требуемое количество ресурсов (пока другой процесс не освободит их).
Если sem_op положителен, то его значение добавляется к семафору. Это соответствует возвращению ресурсов множеству семафоров приложения. Ресурсы всегда нужно возвращать множеству семафоров, если они больше не используются!
Наконец, если sem_op равен нулю, то вызывающий процесс будет усыплен (sleep()), пока значение семафора не станет нулем. Это соответствует ожиданию того, что ресурсы будут использованы на 100%. Хорошим примером был бы демон, запущенный с суперпользовательскими правами, динамически регулирующий размеры множества семафоров, если оно достигло стопроцентного использования.
Чтобы пояснить вызов semop, вспомним нашу комнату с принтерами. Пусть мы имеем только один принтер, способный выполнять только одно задание за раз. Мы создаем множество семафоров из одного семафора (только один принтер) и устанавливаем его начальное значение в 1 (только одно задание за раз).
Каждый раз, посылая задание на принтер, нам нужно сначала убедиться, что он свободен. Мы делаем это, пытаясь получить от семафора единицу ресурса. Давайте заполним массив sembuf, необходимый для выполнения операции:
Трансляция вышеописанной инициализации структуры добавит -1 к семафору 0 из множества семафоров. Другими словами, одна единица ресурсов будет получена от конкретного (нулевого) семафора из нашего множества. IPC_NOWAIT установлен, поэтому либо вызов пройдет немедленно, либо будет провален, если принтер занят. Рассмотрим пример инициализации sembuf-а semop-ом:
Третий аргумент (nsops) говорит, что мы выполняем только одну (1) операцию (есть только одна структура sembuf в нашем массиве операций). Аргумент sid является IPC идентификатором для нашего множества семафоров.
Когда задание на принтере выполнится, мы должны вернуть ресурсы обратно множеству семафоров, чтобы принтером могли пользоваться другие.
Трансляция вышеописанной инициализации структуры добавляет 1 к семафору номер 0 множества семафоров. Другими словами, одна единица ресурсов будет возвращена множеству семафоров.
Системный вызов semctl()
Вызов semctl используется для осуществления управления множеством семафоров. Этот вызов аналогичен вызову msgctl для очередей сообщений. Если вы сравните списки аргументов этих двух вызовов, то заметите, что они немного отличаются. Напомним, что семафоры введены скорее как множества, чем как отдельные объекты. С операциями над семафорами требуется посылать не только IPC-ключ, но и конкретный семафор из множества.
Оба системных вызова используют аргумент cmd для определения команды, которая будет выполнена над IPC-объектом. Оставшаяся разница заключается в последнем аргументе. В msgctl он представляет копию внутренней структуры данных ядра. Повторим, что мы используем эту структуру для получения внутренней информации об очереди сообщений либо для установки или изменения прав доступа и владения очередью. Для семафоров поддерживаются дополнительные команды, которые требуют данных более сложного типа в последнем аргументе. Использование объединения (union) огорчает многих новичков до состояния %(. Мы очень внимательно разберем эту структуру, чтобы не возникало никакой путаницы.
Первый аргумент semctl() является ключом (в нашем случае возвращаемым вызовом semget). Второй аргумент (semun) — это номер семафора, над которым совершается операция. По существу, он может быть понят как индекс на множестве семафоров, где первый семафор представлен нулем.
Аргумент cmd представляет собой команду, которая будет выполнена над множеством. Как вы можете заметить, здесь снова присутствуют IPC_STAT/IPC_SET вместе с кучей дополнительных команд, специфичных для множеств семафоров:
Берет структуру semid_ds для множества и запоминает ее по адресу аргумента buf в объединении semun.
Устанавливает значение элемента ipc_perm структуры semid_ds для множества.
Удаляет множество из ядра.
Используется для получения значений всех семафоров множества. Целые значения запоминаются в массиве элементов unsigned short, на который указывает член объединения array.
Выдает число процессов, ожидающих ресурсов в данный момент.
Возвращает PID процесса, выполнившего последний вызов semop.
Возвращает значение одного семафора из множества.
Возвращает число процессов, ожидающих стопроцентного освобождения ресурса.
Устанавливает значения семафоров множества, взятые из элемента array объединения.
Устанавливает значение конкретного семафора множества как элемент val объединения.
Аргумент arg вызова semсtl() является примером объединения semun, описанного в linux/sem.h следующим образом:
Определяет значение, в которое устанавливается семафор командой SETVAL.
Используется командами IPC_STAT/IPC_SET. Представляет копию внутренней структуры данных семафора, находящейся в ядре.
Указатель для команд GETALL/SETALL. Ссылается на массив целых, используемый для установки или получения всех значений семафоров в множестве.
Оставшиеся аргументы __buf и __pad предназначены для ядра и почти, а то и вовсе не нужны разработчику приложения. Эти два аргумента специфичны для LINUX-а, их нет в других системах UNIX-а.
Поскольку этот особенный системный вызов наиболее сложен для восприятия среди всех системных вызовов System V IPC, мы рассмотрим несколько его примеров в действии.
Следующий отрывок выдает значение указанного семафора. Последний аргумент (объединение) игнорируется, если используется команда GETVAL.
Возвращаясь к примеру с принтерами, допустим, что потребовалось определить статус всех пяти принтеров:
Программа пытается создать локальную копию внутренней структуры данных для множества семафоров, изменить права доступа и сIPC_SETить их обратно в ядро. Однако, первый вызов semctl-а немедленно вернет EFAULT или ошибочный адрес для последнего аргумента (объединения!). Кроме того, если бы мы не следили за ошибками для этого вызова, то заработали бы сбой памяти. Почему?
Вспомним, что команды IPC_SET/IPC_STAT используют элемент buf объединения, который является указателем на тип semid_ds. Указатели — это указатели, и ничего кроме указателей! Элемент buf должен ссылаться на некий корректный участок памяти, чтобы наша функция работала как полагается. Рассмотрим исправленную версию:
semtool: Интерактивное средство для работы с семафорами
Поведение semtool()-а зависит от аргументов командной строки, что удобно для вызова из скрипта shell-а. Позволяет делать все, что угодно, от создания и манипулирования до редактирования прав доступа и удаления множества семафоров. Может быть использовано для управления разделяемыми ресурсами через стандартные скрипты shell-а.
Синтаксис командной строки
Создание множества семафоров
semtool c (количество семафоров в множестве)
Запирание семафора
semtool l (номер семафора для запирания)
Отпирание семафора
semtool u (номер семафора для отпирания)
Изменение прав доступа (mode)
semtool m (mode)
Удаление множества семафоров
semtool d
Примеры semtool c 5
semtool l
semtool u
semtool m 660
semtool d
6.4.4. Разделяемая память
Основные понятия
Разделяемая память может быть наилучшим образом описана как отображение участка (сегмента) памяти, которая будет разделена между более чем одним процессом. Это гораздо более быстрая форма IPC, потому что здесь нет никакого посредничества (т.е. каналов, очередей сообщений и т.п.). Вместо этого, информация отображается непосредственно из сегмента памяти в адресное пространство вызывающего процесса. Сегмент может быть создан одним процессом и впоследствии использован для чтения/записи любым количеством процессов.
Внутренние и пользовательские структуры данных
Давайте взглянем на структуру данных, поддерживаемую ядром, для разделяемых сегментов памяти.
Структура ядра shmid_ds
Так же, как для очередей сообщений и множеств семафоров, ядро поддерживает специальную внутреннюю структуру данных для каждого разделяемого сегмента памяти, который существует врутри его адресного пространства. Такая структура имеет тип shmid_ds и определена в Linux/shm.h как следующая:
Операции этой структуры исполняются посредством специального системного вызова и с ними не следует работать непосредственно. Вот описания полей, наиболее относящихся к делу:
Это образец структуры ipc_perm, который определен в Linux/ipc.h. Он содержит информацию о доступе к сегменту, включая права доступа и информацию о создателе сегмента (uid и т.п.).
Размеры сегмента (в байтах).
Время последней привязки к сегменту.
Время последней отвязки процесса от сегмента.
Время последнего изменения этой структуры (изменение mode и т.п.).
PID создавшего процесса.
PID последнего процесса обратившегося к сегменту.
Число процессов, привязанных к сегменту на данный момент.
Системный вызов shmget()
Чтобы создать новый разделяемый сегмент памяти или получить доступ к уже существующему, используется системный вызов shmget().
Этот новый вызов должен выглядеть для вас почти как старые новости. Он поразительно похож на соответствующие вызовы get для очередей сообщений и множеств семафоров.
Первый аргумент для shmget() — это значение ключа (в нашем случае возвращен посредством вызова ftok()-а). Это значение ключа затем сравнивается с существующими значениями, которые находятся внутри ядра для других разделяемых сегментов памяти. В этом отношении операция открытия или получения доступа зависит от содержания аргумента shmflg.
Создает сегмент, если он еще не существует в ядре.
При использовании совместно с IPC_CREAT приводит к ошибке, если сегмент уже существует.
Если используется один IPC_CREAT, то shmget() возвращает либо идентификатор для вновь созданного сегмента, либо идентификатор для сегмента, который уже существует с тем же значением ключа. Если вместе с IPC_CREAT используется IPC_EXCL, тогда либо создается новый сегмент, либо, если сегмент уже существует, вызов «проваливается» с -1. IPC_EXCL сам по себе бесполезен, но если он комбинируется с IPC_CREAT, то может быть использован как способ получения гарантии, что нет уже существующих сегментов, открытых для доступа.
Повторимся, (необязательный) восьмеричный доступ может быть объеденен по ИЛИ в маску доступа.
Давайте создадим функцию-переходник для обнаружения или создания разделяемого сегмента памяти:
Отметьте явное использование 0660 для прав доступа. Эта небольшая функция выдает либо идентификатор разделяемого сегмента памяти (int), либо -1 в случае ошибки. Значение ключа и заказанные размеры сегмента (в байтах) передаются в виде аргументов.
Как только процесс получает действующий идентификатор IPC для выделяемого сегмента, следующим шагом является привязка или размещение сегмента в адресном пространстве процесса.
Системный вызов shmat()
Если аргумент addr является нулем, ядро пытается найти нераспределенную область. Это рекомендуемый метод. Адрес может быть указан, но типично это используется только для облегчения работы аппаратного обеспечения или для разрешения конфликтов с другими приложениями. Флаг SHM_RND может быть OR-нут в аргумент флага, чтобы заставить переданный адрес выровняться по странице (округление до ближайшей страницы).
Кроме того, если устанавливается флаг SHM_RDONLY, то разделяемый сегмент памяти будет распределен, но помечен readonly.
Этот вызов, пожалуй, наиболее прост в использовании. Рассмотрим функцию-переходник, которая по корректному идентификатору сегмента возвращает адрес привязки сегмента:
Если сегмент был правильно привязан, и процесс имеет указатель на начало сегмента, чтение и запись в сегмент становятся настолько же легкими,как манипуляции с указателями. Не потеряйте полученное значение указателя! Иначе у вас не будет способа найти базу (начало) сегмента.
Вызов очень похож на msgctl(), выполняющий подобные задачи для очередей сообщений. Поэтому мы не будем слишком детально его обсуждать. Употребляемые значения команд следующие:
Берет структуру shmid_ds для сегмента и сохрает ее по адресу, указанному buf-ом.
Устанавливает значение ipc_perm-элемента структуры shmid_ds. Сами величины берет из аргумента buf.
Помечает сегмент для удаления.
IPC_RMID в действительности не удаляет сегмент из ядра, а только помечает для удаления. Настоящее же удаление не происходит, пока последний процесс, привязанный к сегменту, не «отвяжется» от него как следует. Конечно, если ни один процесс не привязан к сегменту на данный момент, удаление осуществляется немедленно.
Снятие привязки производит системный вызов shmdt.
После того, как разделяемый сегмент памяти больше не нужен процессу, он должен быть отсоединен вызовом shmdt(). Как уже отмечалось, это не то же самое, что удаление сегмента из ядра. После успешного отсоединения значение элемента shm_nattch структуры shmid_ds уменьшается на 1. Когда оно достигает 0, ядро физически удаляет сегмент.
shmtool: Интерактивный обработчик разделяемых сегментов памяти
Наш последний пример объектов System V IPC — это shmtool: средство командной строки для создания, чтения, записи и удаления разделяемых сегментов памяти. Так же, как и в предыдущий примерах, во время исполнения любой операции сегмент создается, если его прежде не было.
Синтаксис командной строки
Запись строк в сегмент
shmtool w «text»
Получение строк из сегмента
Изменение прав доступа (mode)
shmtool m (mode)
shmtool w test
shmtool w «This is a test»
shmtool r
shmtool d
shmtool m 660
Семафоры
Для синхронизации процессов, а точнее, для синхронизации доступа нескольких процессов к разделяемым ресурсам, используются семафоры. Являясь одной из форм IPC, семафоры не предназначены для обмена большими объемами данных, как в случае FIFO или очередей сообщений. Вместо этого, они выполняют функцию, полностью соответствующую своему названию — разрешать или запрещать процессу использование того или иного разделяемого ресурса.
Применение семафоров поясним на простом примере. Допустим, имеется некий разделяемый ресурс (например, файл). Необходимо блокировать доступ к ресурсу для других процессов, когда некий процесс производит операцию над ресурсом (например, записывает в файл). Для этого свяжем с данным ресурсом некую целочисленную величину — счетчик, доступный для всех процессов. Примем, что значение 1 счетчика означает доступность ресурса, 0 — его недоступность. Тогда перед началом работы с ресурсом процесс должен проверить значение счетчика. Если оно равно 0 — ресурс занят и операция недопустима — процессу остается ждать. Если значение счетчика равно 1 — можно работать с ресурсом. Для этого, прежде всего, необходимо заблокировать ресурс, т. е. изменить значение счетчика на 0. После выполнения операции для освобождения ресурса значение счетчика необходимо изменить на 1. В приведенном примере счетчик играет роль семафора.
Для нормальной работы необходимо обеспечить выполнение следующих условий:
1. Значение семафора должно быть доступно различным процессам. Поэтому семафор находится не в адресном пространстве процесса, а в адресном пространстве ядра.
2. Операция проверки и изменения значения семафора должна быть реализована в виде одной атомарной по отношению к другим процессам (т. е. непрерываемой другими процессами) операции. В противном случае возможна ситуация, когда после проверки значения семафора выполнение процесса будет прервано другим процессом, который в свою очередь проверит семафор и изменит его значение. Единственным способом гарантировать атомарность критических участков операций является выполнение этих операций в режиме ядра (см. режимы выполнения процесса).
Таким образом семафоры являются системным ресурсом, действия над которым производятся через интерфейс системных вызовов.
Семафоры в System V обладают следующими характеристиками:
? Семафор представляет собой не один счетчик, а группу, состоящую из нескольких счетчиков, объединенных общими признаками (например, дескриптором объекта, правами доступа и т.д.).
? Каждое из этих чисел может принимать любое неотрицательное значение в пределах, определенных системой (а не только значения 0 и 1).
Для каждой группы семафоров (в дальнейшем мы будем называть группу просто семафором) ядро поддерживает структуру данных semid_ds, включающую следующие поля:
struct ipc_perm sem_perm Описание прав доступа struct sem *sem_base Указатель на первый элемент массива семафоров ushort sem_nsems Число семафоров в группе time_t sem_otime Время последней операции time_t sem_ctime Время последнего изменения
Значение конкретного семафора из набора хранится во внутренней структуре sem:
ushort semval Значение семафора pid_t sempid Идентификатор процесса, выполнившего последнюю операцию над семафором ushort semncnt Число процессов, ожидающих увеличения значения семафора ushort semzcnt Число процессов, ожидающих обнуления семафора
Помимо собственно значения семафора, в структуре sem хранится идентификатор процесса, вызвавшего последнюю операцию над семафором, число процессов, ожидающих увеличения значения семафора, и число процессов, ожидающих, когда значение семафора станет равным нулю. Эта информация позволяет ядру производить операции над семафорами, которые мы обсудим несколько позже.
Для получения доступа к семафору (и для его создания, если он не существует) используется системный вызов semop(2):
int semget(key_t key, int nsems, int semflag);
В случае успешного завершения операции функция возвращает дескриптор объекта, в случае неудачи — -1. Аргумент nsems задает число семафоров в группе. В случае, когда мы не создаем, а лишь получаем доступ к существующему семафору, этот аргумент игнорируется. Аргумент semflag определяет права доступа к семафору и флажки для его создания (IPC_CREAT, IPC_EXCL).
После получения дескриптора объекта процесс может производить операции над семафором, подобно тому, как после получения файлового дескриптора процесс может читать и записывать данные в файл. Для этого используется системный вызов semop(2):
int semop(int semid, struct sembuf *semop, size_t nops);
В качестве второго аргумента функции передается указатель на структуру данных, определяющую операции, которые требуется произвести над семафором с дескриптором semid. Операций может быть несколько, и их число указывается в последнем аргументе nops. Важно, что ядро обеспечивает атомарность выполнения критических участков операций (например, проверка значения — изменение значения) по отношению к другим процессам.
Каждый элемент набора операций semop имеет вид:
short sem_num; /* номер семафора в группе */
short sem_op; /* операция */
short sem_flg; /* флаги операции */
UNIX допускает три возможные операции над семафором, определяемые полем semop:
1. Если величина semop положительна, то текущее значение семафора увеличивается на эту величину.
2. Если значение semop равно нулю, процесс ожидает, пока семафор не обнулится.
3. Если величина semop отрицательна, процесс ожидает, пока значение семафора не станет большим или равным абсолютной величине semop. Затем абсолютная величина semop вычитается из значения семафора.
Можно заметить, что первая операция изменяет значение семафора (безусловное выполнение), вторая операция только проверяет его значение (условное выполнение), а третья — проверяет, а затем изменяет значение семафора (условное выполнение).
При работе с семафорами взаимодействующие процессы должны договориться об их использовании и кооперативно проводить операции над семафорами. Операционная система не накладывает ограничений на использование семафоров. В частности, процессы вольны решать, какое значение семафора является разрешающим, на какую величину изменяется значение семафора и т.п.
Таким образом, при работе с семафорами процессы используют различные комбинации из трех операций, определенных системой, по-своему трактуя значения семафоров.
В качестве примера рассмотрим два случая использования бинарного семафора (т.е. значения которого могут принимать только 0 и 1). В первом примере значение 0 является разрешающим, а 1 запирает некоторый разделяемый ресурс (файл, разделяемая память и т.п.), ассоциированный с семафором. Определим операции, запирающие ресурс и освобождающие его:
static struct sembuf sop_lock[2] = <
0, 0, 0, /* ожидать обнуления семафора */
0, 1, 0 /* затем увеличить значение семафора на 1 */
static struct sembuf sop_unlock[1] = <
0,-1, 0 /* обнулить значение семафора */
Итак, для запирания ресурса процесс производит вызов:
semop(semid, &sop_lock[0], 2);
обеспечивающий атомарное выполнение двух операций:[43]
1. Ожидание доступности ресурса. В случае, если ресурс уже занят (значение семафора равно 1), выполнение процесса будет приостановлено до освобождения ресурса (значение семафора равно 0).
2. Запирание ресурса. Значение семафора устанавливается равным 1. Для освобождения ресурса процесс должен произвести вызов:
semop(semid, &sop_unlock[0], 1);
который уменьшит текущее значение семафора (равное 1) на 1, и оно станет равным 0, что соответствует освобождению ресурса. Если какой-либо из процессов ожидает ресурса (т. е. произвел вызов операции sop_lock), он будет «разбужен» системой, и сможет в свою очередь запереть ресурс и работать с ним.
Во втором примере изменим трактовку значений семафора: значению 1 семафора соответствует доступность некоторого ассоциированного с семафором ресурса, а нулевому значению — его недоступность. В этом случае содержание операций несколько изменится.
static struct sembuf sop_lock[2] = <
0, -1, 0, /* ожидать разрешающего сигнала (1),
затем обнулить семафор */
static struct sembuf sop_unlock[1] = <
0, 1, 0 /* увеличить значение семафора на 1 */
Процесс запирает ресурс вызовом:
semop(semid, &sop_lock[0], 1);
semop(semid, &sop_unlock[0], 1);
Во втором случае операции получились проще (по крайней мере их код стал компактнее), однако этот подход имеет потенциальную опасность: при создании семафора, его значения устанавливаются равными 0, и во втором случае он сразу же запирает ресурс. Для преодоления данной ситуации процесс, первым создавший семафор, должен вызвать операцию sop_unlock, однако в этом случае процесс инициализации семафора перестанет быть атомарным и может быть прерван другим процессом, который, в свою очередь, изменит значение семафора. В итоге, значение семафора станет равным 2, что повредит нормальной работе с разделяемым ресурсом.
Можно предложить следующее решение данной проблемы:
/* Создаем семафор, если он уже существует semget
возвращает ошибку, поскольку указан флаг IPC_EXCL */
if ((semid = semget(key, nsems, perms | IPC_CREAT | IPC_EXCL)) < 0) <
if (errno = EEXIST) <
/* Действительно, ошибка вызвана существованием объекта */
if ((semid = semget(key, nsems, perms)) < 0)
return(-1); /* Возможно, не хватает системных ресурсов */
return(-1); /* Возможно, не хватает системных ресурсов * /
/* Если семафор создан нами, проинициализируем его */
semop(semid, &sop_unlock[0], 1);
Данный текст является ознакомительным фрагментом.
Продолжение на ЛитРес
Читайте также
Семафоры
Семафоры В операционной системе Linux семафоры (semaphore) — это блокировки, которые переводят процессы в состояние ожидания. Когда задание пытается захватить семафор, который уже удерживается, семафор помещает это задание в очередь ожидания (wait queue) и переводит это задание в
Семафоры чтения-записи
Семафоры чтения-записи Семафоры, так же как и спин-блокировки, могут быть типа чтения-записи. Ситуации, в которых предпочтительнее использовать семафоры чтения-записи такие же как и в случае использования спин-блокировок чтения-записи.Семафоры чтения-записи
Семафоры
Семафоры Давайте переместимся из ванной комнаты на кухню, так как это социально адаптированное помещение для одновременного обитания более чем одного человека. На кухне вы можете не пожелать, чтобы все и каждый находились бы там одновременно. В действительности вы бы,
Семафоры
Семафоры Для синхронизации процессов, а точнее, для синхронизации доступа нескольких процессов к разделяемым ресурсам, используются семафоры. Являясь одной из форм IPC, семафоры не предназначены для обмена большими объемами данных, как в случае FIFO или очередей сообщений.
12.3 СЕМАФОРЫ
12.3 СЕМАФОРЫ Поддержка системы UNIX в многопроцессорной конфигурации может включать в себя разбиение ядра системы на критические участки, параллельное выполнение которых на нескольких процессорах не допускается. Такие системы предназначались для работы на машинах AT amp;T
26.6. Семафоры
26.6. Семафоры Семафор — это объект IPC, управляющий доступом к общим ресурсам (устройствам). Семафоры не позволяют одному процессу захватить устройство до тех пор, пока с этим устройством работает другой процесс. Семафор может находиться в двух положениях: 0 (устройство
10.13. Ограничения на семафоры
10.13. Ограничения на семафоры Стандартом Posix определены два ограничения на семафоры:? SEM_NSEMS_MAX — максимальное количество одновременно открытых семафоров для одного процесса (Posix требует, чтобы это значение было не менее 256);? SEM_VALUE_MAX — максимальное значение семафора (Posix
ГЛАВА 11 Семафоры System V
ГЛАВА 11 Семафоры System V 11.1.Введение В главе 10 мы описывали различные виды семафоров, начав с:? бинарного семафора, который может принимать только два значения: 0 и 1. По своим свойствам такой семафор аналогичен взаимному исключению (глава 7), причем значение 0 для семафора
Семафоры Posix, размещаемые в памяти
Семафоры Posix, размещаемые в памяти Мы измеряем скорость работы семафоров Posix (именованных и размещаемых в памяти). В листинге А.24 приведен текст функции main, а в листинге А.23 — текст функции incr.Листинг А.23. Увеличение счетчика с использованием семафоров Posix в
Именованные семафоры Posix
Именованные семафоры Posix В листинге А.26 приведен текст функции main, измеряющей быстродействие именованных семафоров Posix, а в листинге А.25 — соответствующая функция incr.Листинг А.25. Увеличение общего счетчика с использованием именованного семафора Posix//bench/incr_pxsem2.c40 void
Семафоры System V
Семафоры System V Функция main программы, измеряющей быстродействие семафоров System V, приведена в листинге А.27, а функция incr показана в листинге А.28.Листинг А.27. Функция main для измерения быстродействия семафоров System V//bench/incr_svsem1.c1 #include "unpipc.h"2 #define MAXNTHREADS 1003 int nloop;4 struct <5 int
4.4.2. Исключающие семафоры
4.4.2. Исключающие семафоры Решение проблемы гонки заключается в том, чтобы позволить только одному потоку обращаться к очереди в конкретный момент времени. Когда поток начинает просматривать очередь, все остальные потоки вынуждены дожидаться, пока он удалит очередное
4.4.5. Обычные потоковые семафоры
4.4.5. Обычные потоковые семафоры В предыдущем примере, в котором группа потоков обрабатывает задания из очереди, потоковая функция запрашивает задания до тех пор, пока очередь не опустеет, после чего поток завершается. Эта схема работает в том случае, когда все задания
5.2. Семафоры для процессов
5.2. Семафоры для процессов Как говорилось в предыдущем разделе, процессы должны координировать свои усилия при совместном доступе к памяти. Вспомните: в разделе 4.4.5, "Обычные потоковые семафоры", рассказывалось о семафорах, которые являются счетчиками, позволяющими