Программные семафоры с внутренним счетчиком


Программные критические секции

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

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

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

VOID InitializeCriticalSection(CRITICAL_SECTION *pCriticalSection),

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

Вход в критическую секцию задается в программе функцией EnterCriticalSection, которая имеет прототип

VOID EnterCriticalSection(CRITICAL_SECTION *pCriticalSection),

а завершение критической секции программы должно указываться вызовом функции с прототипом

VOID LeaveCriticalSection(CRITICAL_SECTION *pCriticalSection).

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

VOID DeleteCriticalSection(CRITICAL_SECTION *pCriticalSection).

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

 

В операционной системе Windows считающие семафоры создаются функцией CreateSemaphore. Функция имеет прототип

HANDLE CreateSemaphore(

SECURITY_ATTRIBUTES *pSemaphoreAttributes,

LONG lInitialCount, LONG lMaximumCount, LPCTSTR lpName). Эти семафоры могут быть неименованными или именованными и тогда параметр lpName должен задавать адрес имени семафора. Неименованные семафоры задаются путем записи значения NULL в качестве значения параметра lpName. Рассматриваемые семафоры в Windows могут принимать неотрицательные целые значения, но эти значения ограничиваются величиной параметра lMaximumCount, которая задает максимальное значение для данного семафора. Параметр pSemaphoreAttributes задает адрес структуры атрибутов защиты и в простейших случаях берется равным NULL, что заставляет ОС использовать для него атрибуты по умолчанию. Параметр lInitialCount задает начальное значение семафора, точнее его внутреннего счетчика, так как семафор имеет более сложное строение, чем просто переменная (это значение не может быть отрицательным и должно быть не больше значения lMaximumCount). Функция возвращает хэндл созданного семафора при удачном выполнении, и нулевое значение – при неудаче.

Для выполнения операции, аналогичной операции P абстрактных семафоров, следует использовать все ту же универсальную функцию WaiForSingleObject. Каждое выполнение этой функции, когда ее первым аргументом является хэндл считающего семафора, уменьшает внутренний счетчик семафора на единицу, но только в том случае, если он был больше нуля. Блокировка нити при этом не производится. Если же значение внутреннего счетчика семафора на момент выполнения функции WaiForSingleObject было равно нулю, то производится блокировка данной нити. Блокировка действует до тех пор, пока какая-то другая часть текущего комплекса программ (другая нить процесса, другой процесс) не выполнит функцию ReleaseSemaphore, освобождающую семафор. Функция эта имеет прототип

BOOL ReleaseSemaphore(HANDLE hSemaphore,

LONG cReleaseCount, LONG *plPreviousCount).

Функция освобождения считающего семафора увеличивает значение внутреннего счетчика семафора на заданное при вызове функции значение аргумента cReleaseCount, причем параметр plPreviousCount должен содержать адрес переменной, в которой возвращается предыдущее значение этого счетчика или значение NULL, если такое предыдущее значение не требуется в программе. Параметр hSemaphore должен задавать при вызове хэндл семафора, созданного или открытого в данном процессе. Значение аргумента cReleaseCount обязательно должно быть больше нуля. Если это значение такое, что его сумма с предыдущим значением счетчика семафора (значением перед выполнением функции освобождения) больше максимального значения (разрешенного параметром lMaximumCount при создании семафора), то значение счетчика никак не меняется, а функция возвращает значение FALSE, что свидетельствует об ошибке выполнения.

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

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

HANDLE OpenSemaphore(DWORD fdwAccess, // access flag

BOOL fInherit, LPCTSTR lpszSemName).

Необходимым условием применения такой функции к считающему семафору является наличие у семафора имени. При открытии семафора адрес этого имени задается параметром lpszSemName. Параметр fInherit определяет,будет ли наследоваться открытый семафор дочерними процессами для процесса, выполняющего открытие (наследуется при значении параметра TRUE). Параметр fdwAccess задает флаги доступа к открываемому семафору и может принимать значение символической константы SEMAPHORE_ALL_ACCESS или любую комбинацию констант SEMAPHORE_MODIFY_STATE и SYNCHRONIZE. Флаг SEMAPHORE_MODIFY_STATE разрешает использование функции ReleaseSemaphore() для открываемого семафора в данном процессе, а флаг SYNCHRONIZE дает аналогичное разрешение на использование любых функций ожидания, в частности WaiForSingleObject. Флаг SEMAPHORE_ALL_ACCESS заменяет комбинацию двух других флагов и разрешает все виды доступа – любые функции работы с этим семафором.

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

В операционной системе Unix ее классические семафоры представляют собой не отдельные объекты, а целые группы (массивы) счетчиков семафоров, причем эти внутренние счетчики семафоров могут принимать любые неотрицательные значения. Отдельный внутренний семафор в такой группе описывается структурой sem, заданной как

typedef struct sem

{ushort semval; //Значение семафора

pid_t sempid; // ID процесса, выполнившего последнюю операцию

ushort semncnt; // Число процессов,ожидающих увеличения счетчика

ushort semzcnt; // Число процессов, ожидающих обнуления семафора

};

Для получения доступа к семафору и для его создания, если он не существовал, предназначена системная функция semget(), имеющая прототип

int semget (key_t key, int nsems, int semflag).

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

#include <sys/types.h>

#include <sys/ipc.h>

#include <sys/sem.h>

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

Параметр key функции semget() выполняет ту же роль, что и имя семафора в других ОС, практически это просто числовой код, однозначно идентифицирующий конкретный семафор. Этот код следует в простейших случаях просто выбирать согласованно (т.е. с одинаковым значением) для процессов, использующих такой семафор. Для более серьезного использования в Unix имеется специальная функция задания уникального кода для создаваемого семафора. Параметр nsems определяет число счетчиков (внутренних одиночных семафоров), которые требуется создать, а параметр semflag – права доступа к создаваемому семафору и флаги его создания. Права доступа определяются так же, как и для файлов и являются комбинацией битов, задающих возможность чтения, записи и выполнения отдельно для категорий пользователей (владельца, членов группы, всех остальных). Флаги создания семафоров задаются символическими константами IPC_CREAT и IPC_EXCL. Первая из них задает режим создания семафора для функции semget(), а при отсутствии этой константы выполняется открытие семафора с указанным параметром key идентификатором. Флаг IPC_EXCL при создании семафора требует, чтобы при наличии одноименного семафора (уже существующего семафора с тем же значением идентификатора key) функция semget() возвращает ошибку. Неудачное выполнение этой функции влечет возвращаемое значение, равное -1, а при успешном выполнении функция возвращает в качестве своего значения хэндл созданного или открытого (в зависимости от того, что запрашивалось) семафора. Заметим, что в Unix для подобных возвращаемых значений, идентифицирующих системный объект, используется чаще всего термин идентификатор, а не термин хэндл.

Для основных операций над семафорами используется функция с именем semop. Собственно вид операции и характеристики ее выполнения задаются вспомогательной структурой данных, имеющей тип sembuf. Заметим, что такой многоступенчатый подход дает в результате ряд очень общих возможностей, но резко отличается от рассмотренных ранее способов управления системными объектами. Для начинающего он не очень нагляден и довольно сложен в освоении, требуя дополнительного сосредоточения на деталях. Структура sembuf описывается в виде

struct sembuf

{short sem_num; // номер семафора в группе

short sem_op; // операция

short sem_flg; // флаги операции

};

Сама функция операций над семафорами имеет прототип

int semop (int hsem, struct sembuf* opsem, size_t nops),

где hsem – хэндл (идентификатор) семафора, созданного или открытого ранее функцией semget(), opsem – адрес массива описания структур операции над отдельным семафором в группе, а параметр nops задает число внутренних операций, выполняемых в вызове функции, т.е. число используемых элементов в массиве, заданном opsem.

Допускаются три возможные операции над семафорами, определяемые полем sem_op. Если значение поля sem_op положительно, то текущее значение счетчика для указанного номером семафора увеличивается на эту величину ( semval += sem_op), это первая операция. Если значение sem_op равно нулю, то процесс ожидает, пока счетчик семафора не обнулится (вторая операция – ожидание). Если значение поля sem_op отрицательно, то процесс ожидает, пока значение счетчика не станет большим или равным абсолютной величине sem_op, а затем значение этого счетчика уменьшается на эту абсолютную величину (semval -= abs(sem_op) – третья операция). Первой операции в основном соответствует функция ReleaseSemaphore из Windows, второй функции – чистое ожидание обнуления семафора (именно обнуления, а не достижения положительных значений). Третья функция не имеет близких аналогов в других ОС. Первая функция – безусловное изменение семафора, вторая – проверка и ожидание (условное выполнение), третья – проверка и изменение (условное выполнение).

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

Например, можно предложить следующие два варианта двоичного семафора, получающегося из универсального соответствующим подбором структуры sem_op [14].

Сделаем значение 0 разрешающим, а значение 1 – запрещающим. Для этого опишем следующие массивы записей типа sem_op:

struct sembuf sop_lock[2] = { {0, 0, 0), {0, 1, 0}};

struct sembuf sop_unlock[1] = {0, -1, 0};

Элемент sop_lock[0] задает ожидание обнуления семафора (в нем sem_op=0), элемент sop_lock[1] – увеличение семафора на 1 (положительное, т.е. увеличивающее значение sem_op равно 1). Оба действия предполагается выполнять над начальным (нулевым по индексу) семафором в группе, т.к. поле sem_num равно 0, последнее поле – для флага – не используется (также нулевое). Элемент sop_unlock[0] задает обнуление значения семафора

Теперь для запирания ресурса (операции P над семафором) следует выполнить вызов

semop(hsem, &sop_lock[0], 2),

а для освобождения ресурса (операции V над семафором) следует выполнить вызов

semop(hsem, &sop_unlock[0], 1).

В первом вызове указано, что число внутренних операций равно 2, именно столько записей подготовлено для действий в массиве sop_lock, во втором вызове третий параметр задан числом 1, т.е. предполагается выполнение только одной внутренней операции, которую описывает одноэлементный массив sop_unlock.

В другом варианте изменим смысл значений счетчика семафора: значению 0 будет отвечать доступ к ресурсу (семафор открыт), а значению 1 – закрытый доступ к ресурсу (семафор закрыт). Для этого опишем структуры операций для семафора следующим образом:

struct sembuf sopp_lock[2]= {0, -1, 0};

struct sembuf unsopp_lock[1]= {0, 1, 0};

Теперь ресурс запирается (операция P над семафором) вызовом функции в виде

semop(hsem, &sopp_lock[0], 1),

а освобождается (операция V над семафором) вызовом

semop(hsem, &unsopp_lock[0], 1).

Второй вариант кажется проще, но на самом деле в его использовании имеется потенциальная опасность, так как в Unix семафоры создаются с начальными нулевыми значениями внутренних счетчиков. Задание произвольных начальных значений здесь не предусмотрено. Поэтому сразу после создания второй вариант семафора запирает ресурс, что не всегда удобно для программиста. Использование операции unsopp_lock сразу после создания семафора не снимает всех проблем, так как между вызовами создания и освобождения возможно переключение процессора на другой процесс, который, в свою очередь, может вызвать ту же операцию освобождения семафора. Последнее приведет после операции unsopp_lock в процессе, создавшем семафор, к значению внутреннего счетчика, ставшего в результате двух таких последовательных вызовов (хотя и в разных процессах) равным 2, а не 1, как можно было рассчитывать.

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

int semctl(int semid, int num, int cmd, union semun arg),

где semid задает хэндл (идентификатор) набора семафоров, num – число семафоров в наборе, над которым выполняется операция, а cmd – тип операции над семафором. Этот аргумент cmd может задаваться константами IPC_STAT, IPC_SET и IPC_RMID. Первая из них задает операцию получения информации о конкретном семафоре, вторая служит для операции установки значения отдельного семафора в группе, а последняя предназначена для операции удаления семафора. Кроме того константой SETALL можно задать операцию установки значений всех семафоров в группе.

Вспомогательная структура данных типа semun определяется как

union semun {

int val;

struct semid_ds *buf ;

unsigned short int *array;

struct seminfo *__buf ;

};

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

В частности, установка единичного начального значения в массиве семафоров с хэндлом hsem, где массив состоит всего из одного элемента задается фрагментом

union semun semar;

unsigned short val[1]={1};

. . .

semar.array=val;

semctl(hsem, 0, SETALL, semar);

 

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

semctl(hsem, 1, IPC_RMID, semar),

где содержимое экземпляра структуры semar совершенно не существенно.

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

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

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

int sem_init(sem_t *sem, int pshared, unsigned int value),

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

По завершению использования рассматриваемый семафор следует уничтожить вызовом функции sem_destroy с прототипом

int sem_destroy(sem_t *sem),

где единственным аргументом служит указатель на созданный ранее семафор.

Операции P(sem) и V(sem) для данных семафоров задаются программными функциями с прототипами

int sem_wait(sem_t *sem) и

int sem_post(sem_t *sem).

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

Дополнительно предлагается еще две программных функции с прототипами

int sem_trywait(sem_t *sem),

int sem_getvalue(sem_t *sem, int *value).

Первая из них подобна функции sem_wait, но не выполняет приостановку нити, когда семафор, указанный ее параметром, занят. Но в этом случае она возвращает ненулевой код возврата (в остальных случаях этот код – нулевой). Заметим, что само ненулевое значение кода возврата этой функции в указанной ситуации задается символической константой EAGAIN. Функция sem_getvalue() возвращает текущее – на момент запроса – внутреннее значение семафора с помощью своего второго параметра.