Листинг 19. Программа, реализующая клиент
11.int msgid, length;
12.message.mtype = 1L; /*Тип посылаемого сообщения */
13.
14./*Получим ключ*/
15.if ((key = ftok(“server”, ‘A’)) < 0){
16.printf(“Невозможно получить ключ\n”); exit(1);
17.}
18./* Получим доступ к очереди сообщений, очередь уже *
* должна быть создана сервером |
*/ |
19.if ((msgid = msgget(key, 0)) < 0){
20.printf(“Невозможно получить доступ к очереди\n”); exit(1);
21.}
22./*Поместим строку в сообщение*/
23.if ((length = sprintf(message.buff,
“Здравствуй, Мир!\n”)) < 0){
24.printf(“Ошибка копирования в буфер\n”); exit(1);
25.}
26./*Передадим сообщение*/
27.if (msgsnd(msgid, (void *)&message, length, 0) !=0){
28.printf(“Ошибка записи сообщения в очередь\n”);
29.exit(1);
30.} /*Удалим очередь сообщений*/
31.if (msgctl(msgid, IPC_RMID, 0) < 0){
32.printf(“Ошибка удаления очереди\n”); exit(1);
33.}
34.exit(0);
35.}
6.3.5. Семафоры
Для синхронизации процессов, а точнее для синхронизации доступа нескольких процессов к разделяемым ресурсам, используются семафоры. Являясь одной из форм межпроцессного взаимодействия (IPC), семафоры не предназначены для обмена большими объёмами данных, как в случае FIFO или очередей сообщений. Вместо этого, они выполняют функцию, полностью соответствующую своему названию – разрешать или запрещать процессу использование того или иного разделяемого ресурса.
Применение семафоров поясним на простом примере. Допустим, имеется некий разделяемый ресурс (например, файл). Необходимо блокировать доступ к ресурсу для других процессов, когда некий процесс производит операцию над ресурсом (например, записывает в файл). Для этого свяжем с данным ресурсом некую целочисленную величину – счётчик, доступный для всех процессов. Примем, что значение 1 счётчика означает доступность ресурса, 0 – его недоступность. Тогда перед началом работы с ресурсом процесс должен
101
проверить значение счётчика. Если оно равно 0, ресурс занят и операция недопустима – процессу остаётся ждать.
Если значение счётчика равно 1, можно работать с ресурсом. Для этого, прежде всего, необходимо заблокировать ресурс, т.е. изменить значение счётчика на 0. После выполнения операции для освобождения ресурса значение счётчика необходимо изменить на 1. В приведённом примере счётчик играет роль семафора.
Для нормальной работы необходимо обеспечить выполнение следующих условий:
1)Значение семафора должно быть доступно различным процессам. Поэтому семафор находится не в адресном пространстве процесса, a в адресном пространстве ядра.
2)Операции проверки и изменения значения семафора должны быть реализованы в виде одной атомарной (т.е. непрерываемой другими процессами) по отношению к другим процессам операции. В противном случае возможна ситуация, когда после проверки значения семафора выполнение процесса будет прервано другим процессом, который в свою очередь проверит семафор и изменит его значение.
Единственным способом гарантировать атомарность критических участков операций является выполнение этих операций в режиме ядра. Таким образом, семафоры являются системным ресурсом, действия над которым производятся через интерфейс системных вызовов.
Семафоры в System V обладают следующими характеристиками:
1)Семафор представляет собой не один счётчик, а группу, состоящую из нескольких счётчиков, объединённых общими признаками (например, дескриптором объекта, правами доступа и т.д.).
2)Каждое из этих чисел может принимать любое неотрицательное значение в пределах, определённых системой (а не только значения 0 и 1).
Для получения доступа к семафору (и для его создания, если он не существует) используется системный вызов semget:
#include <sys/types.h> #include <sys/ipc.h> #include <sys/sem.h>
int semget (key_t key, int nsems, int semflag);
В случае успешного завершения операции функция возвращает дескриптор объекта, в случае неудачи – –1. Параметр nsems задаёт число семафоров в группе. В случае, когда мы не создаём, а лишь получаем доступ к существующему семафору, этот аргумент игнорируется. Параметр semflag определяет права доступа к семафору и флажки для его создания (IPC_CREAT,
IPC_EXCL).
102
После получения дескриптора объекта процесс может производить операции над семафором, подобно тому, как после получения файлового дескриптора процесс может читать и записывать данные в файл. Для этого используется системный вызов semop:
#include <sys/types.h> #include <sys/ipc.h> #include <sys/sem.h>
int semop(int semid, struct sembuf *semop, size_t nops);
В качестве второго аргумента функции передаётся указатель на структуру данных, определяющую операции, которые требуется произвести над семафором с дескриптором semid. Операций может быть несколько, и их число указывается в последнем аргументе nops. Важно, что ядро обеспечивает атомарность выполнения критических участков операций (например, проверка значения – изменение значения) по отношению к другим процессам.
Каждый элемент набора операций semop имеет вид:
struct sembuf { |
/*номер |
семафора в группе*/ |
short sem_num; |
||
short sem_op; |
/*операция*/ |
|
short sem_fig; |
/*флаги |
операции*/ |
}; |
|
|
UNIX допускает три возможные операции над семафором, определяемые полем sem_op:
1)Если величина sem_op положительна, то текущее значение семафора увеличивается на эту величину.
2)Если значение sem_op равно нулю, процесс ожидает, пока семафор не обнулится.
3)Если величина sem_op отрицательна, процесс ожидает, пока значение семафора не станет большим или равным абсолютной величине sem_op. Затем абсолютная величина sem_op вычитается из значения семафора.
Можно заметить, что первая операция изменяет значение семафора (безусловное выполнение), вторая операция только проверяет его значение (условное выполнение), а третья проверяет, а затем изменяет значение семафора (условное выполнение).
При работе с семафорами взаимодействующие процессы должны «договориться» об их использовании и кооперативно проводить операции над семафорами. Операционная система не накладывает ограничений на использование семафоров. В частности, процессы вольны решать, какое значение семафора является разрешающим, на какую величину изменяется значение семафора и т.п.
103
Таким образом, при работе с семафорами процессы используют различные комбинации из трёх операций, определённых системой, по-своему трактуя значения семафоров.
В качестве примера рассмотрим два случая использования бинарного семафора (т.е., значения которого могут быть равны только 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);
обеспечивающий атомарное выполнение двух операций. Ядро обеспечивает атомарное выполнение не всего набора операций в целом, а лишь критических участков. Так, например, в процессе ожидания освобождения ресурса (ожидание нулевого значения семафора) выполнение процесса будет (и должно быть) прервано процессом, который освободит ресурс (т.е. установит значение семафора, равным 1). Ожидание семафора соответствует состоянию «сна» процесса, допускающим выполнение других процессов в системе. В противном случае, процесс, ожидающий ресурс, остался бы заблокированным навсегда:
1)Ожидание доступности ресурса. В случае если ресурс уже занят (значение семафора равно 1), выполнение процесса будет приостановлено до освобождения ресурса (значение семафора равно 0).
2)«Запирание» ресурса. Значение семафора устанавливается равным 1.
Для освобождения ресурса процесс должен произвести вызов:
semop(semid, &sop_unlock[0], 1);
который уменьшит текущее значение семафора (равное 1) на 1, и оно станет равным 0, что соответствует освобождению ресурса. Если какой-либо из процессов ожидает ресурс (т.е. произвёл вызов операции sop_lock), он будет «разбужен» системой и сможет в свою очередь запереть ресурс и работать с ним.
104
Во втором примере изменим трактовку значений семафора: значению 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, что повредит нормальной работе с разделяемым ресурсом.
Можно предложить следующее решение данной проблемы:
Листинг 20. Работа с семафорами
1./* Создаём семафор */
2.if ((semid = semget(key, nsems,
perms | IPC_CREAT | IPC_EXCL)) < 0){
3.if (errno == EEXIST){
4.if ((shmid = semget (key, nsems, perms)) < 0)
5.return (-1);
6.}
7.else return (-1);
8.}/*Если семафор создан нами, проинициализируем его*/
9.else
105