Уильям Стивенс - UNIX: взаимодействие процессов
При создании нового семафора инициализируются следующие поля структуры semid_ds:
■ поля uid и cuid структуры sem_perm устанавливаются равными действующему идентификатору пользователя процесса, а поля guid и cgid устанавливаются равными действующему идентификатору группы процесса;
■ биты разрешений чтения-записи аргумента oflag сохраняются в sem_perm.mode;
■ поле sem_otime устанавливается в 0, а поле sem_ctime устанавливается равным текущему времени;
■ значение sem_nsems устанавливается равным nsems;
■ структуры sem для каждого из семафоров набора не инициализируются. Это происходит лишь при вызове semctl с командами SETVAL или SETALL.
Инициализация значения семафора
В комментариях к исходному коду в издании этой книги 1990 года неправильно утверждалось, что значения семафоров набора инициализируются нулем при вызове semget с созданием нового семафора. Хотя в некоторых системах это действительно происходит, гарантировать подобное поведение ядра нельзя. Более старые реализации System V вообще не инициализировали значения семафоров, оставляя их содержимое таким, каким оно было до выделения памяти.
В большинстве версий документации ничего не говорится о начальных значениях семафоров при создании нового набора. Руководство по написанию переносимых программ X/Open XPG3 (1989) и стандарт Unix 98 исправляют это упущение и открыто утверждают, что значения семафоров не инициализируются вызовом semget, а устанавливаются только при вызове semctl (вскоре мы опишем эту функцию) с командами SETVAL (установка значения одного из семафоров набора) и SETALL (установка значений всех семафоров набора).
Необходимость вызова двух функций для создания (semget) и инициализации (semctl) набора семафоров является неисправимым недостатком семафоров System V. Эту проблему можно решить частично, указывая флаги IPC_CREAT | IPC_EXCL при вызове semget, чтобы только один процесс, вызвавший semget первым, создавал семафор, и этот же процесс должен семафор инициализировать.
Другие процессы получают при вызове semget ошибку EEXIST, так что им приходится еще раз вызывать semget, уже не указывая флагов IPC_CREAT или IPC_EXCL.
Однако ситуация гонок все еще не устранена. Предположим, что два процесса попытаются создать и инициализировать набор семафоров с одним элементом приблизительно в один и тот же момент времени, причем оба они будут выполнять один и тот же фрагмент кода:
1 oflag = IPC_CREAT | IPC_EXCL | SVSEM_MODE;
2 if ((semid = semget(key, 1, oflag)) >= 0) { /* успешное завершение, этот процесс должен инициализировать семафор */
3 arg.val = 1;
4 Semctl(semid, 0, SETVAL, arg);
5 } else if (errno == EEXIST) { /* уже существует, поэтому просто открываем семафор */
6 semid = Semget(key, 1, SVSEM_MODE);
7 } else
8 err_sys("semget error");
9 Semop(semid, …); /* уменьшаем значение семафора на 1 */
При этом может произойти вот что:
1. Первый процесс выполняет строки 1-3, а затем останавливается ядром.
2. Ядро запускает второй процесс, который выполняет строки 1, 2, 5, 6 и 9.
Хотя первый процесс, создавший семафор, и будет единственным процессом, который проинициализирует семафор, ядро может переключиться на другой процесс в промежутке между созданием и инициализацией семафора, и тогда второй процесс сможет обратиться к семафору (строка 9), который еще не был проинициализирован. Значение семафора после выполнения строки 9 для второго процесса будет не определено.
ПРИМЕЧАНИЕ
В семафорах Posix эта проблема исключается благодаря тому, что семафоры создаются и инициализируются единственным вызовом — sem_open. Более того, даже если указан флаг O_CREAT, семафор будет проинициализирован только в том случае, если он еще не существовал на момент вызова функции.
Будет ли обсуждавшаяся выше ситуация гонок создавать какие-то проблемы — зависит от приложения. В некоторых приложениях (например, задача производителей и потребителей в листинге 10.12) единственный процесс всегда создает и инициализирует семафор. В этом варианте ситуация гонок возникать не будет. В других приложениях (пример с блокировкой файлов в листинге 10.10) нет такого единственного процесса, который бы создавал и инициализировал семафор: первый процесс, открывающий семафор, должен создать его и проинициализировать, так что в этом случае ситуацию гонок следует исключать.
К счастью, существует способ исключить в данном случае ситуацию гонок. Стандарт гарантирует, что при создании набора семафоров поле sem_otime структуры semid_ds инициализируется нулем. (Руководства System V с давних пор говорят об этом, это утверждается и в стандартах XPG3 и Unix 98.) Это поле устанавливается равным текущему времени только при успешном вызове semop. Следовательно, второй процесс в приведенном выше примере должен просто вызвать semctl с командой IPC_STAT после второго вызова semget (строка 6). Затем этот процесс должен ожидать изменения значения sem_otime на ненулевое, после чего он может быть уверен в том, что семафор был успешно проинициализирован другим процессом. Это значит, что создавший семафор процесс должен проинициализировать его значение и успешно вызвать semop, прежде чем другие процессы смогут воспользоваться этим семафором. Мы используем этот метод в листингах 10.37 и 11.6.
11.3. Функция semop
После инициализации семафора вызовом semget с одним или несколькими семафорами набора можно выполнять некоторые действия с помощью функции semop:
#include <sys/sem.h>
int semop(int semid, struct sembuf *opsptr, size_t nops);
/* Возвращает 0 в случае успешного завершения, –1 – в случае ошибки */
Указатель opsptr указывает на массив структур вида
struct sembuf {
short sem_num; /* номер семафора: 0, 1,… nsems-1 */
short sem_op; /* операция с семафором: <0, 0, >0 */
short sem_flg; /* флаги операции: 0, IPC_NOWAIT, SEM_UNDO */
};
Количество элементов в массиве структур sembuf, на который указывает opsptr, задается аргументом nops. Каждый элемент этого массива определяет операцию с одним конкретным семафором набора. Номер семафора указывается в поле sen_num и принимает значение 0 для первого семафора, 1 для второго и т. д., до nsems-1, где nsems соответствует количеству семафоров в наборе (второй аргумент в вызове semget при создании семафора).
ПРИМЕЧАНИЕ
В структуре гарантированно содержатся только три указанных выше поля. Однако в ней могут быть и другие поля, причем порядок их может быть совершенно произвольным. Поэтому не следует статически инициализировать эту структуру кодом наподобие
struct sembuf ops[2] = {
0, 0, 0, /* ждем, пока первый элемент не станет равен нулю */
0, 1, SEM_UNDO /* затем увеличиваем [0] на 1 */
};
Вместо этого следует инициализировать ее динамически, как в нижеследующем примере:
struct sembuf ops[2];
ops[0].sem_num = 0; /* ждем, пока первый элемент не станет равен нулю */
ops[0].sem_op = 0;
ops[0].sem_flg = 0;
ops[1].sem_num = 0; /* затем увеличиваем [0] на 1 */
ops[1].sem_op = 1;
ops[1].sem_flg = SEM_UNDO;
Весь массив операций, передаваемый функции semop, выполняется ядром как одна операция; атомарность при этом гарантируется. Ядро выполняет все указанные операции или ни одну из них. Пример на эту тему приведен в разделе 11.5.
Каждая операция задается значением sem_op, которое может быть отрицательным, нулевым или положительным. Сделаем несколько утверждений, которыми будем пользоваться при дальнейшем обсуждении:
■ semval — текущее значение семафора (рис. 11.1);
■ semncnt — количество потоков, ожидающих, пока значение семафора не станет больше текущего (рис. 11.1);
■ semzcnt — количество потоков, ожидающих, пока значение семафора не станет нулевым (рис. 11.1);
■ semadj — корректировочное значение данного семафора для вызвавшего процесса. Это значение обновляется, только если для данной операции указан флаг SEM_UNDO в поле sem_flg структуры sembuf. Эта переменная создается в ядре для каждого указавшего флаг SEM_UNDO процесса в отдельности; поле структуры с именем semadj не обязательно должно существовать;
■ когда выполнение потока приостанавливается до завершения операции с семафором (мы увидим, что поток может ожидать либо обнуления семафора, либо получения семафором положительного значения), поток перехватывает сигнал и происходит возвращение из обработчика сигнала, функция semop возвращает ошибку EINTR. Используя терминологию, введенную в книге [24, с. 124], можно сказать, что функция semop представляет собой медленный системный вызов, который прерывается перехватываемыми сигналами;
■ когда выполнение потока приостанавливается до завершения операции с семафором и этот семафор удаляется из системы другим потоком или процессом, функция semop возвращает ошибку EIDRM (identifier removed — идентификатор удален).