• ГЛАВА 7 Взаимные исключения и условные переменные
  • 7.1. Введение
  • 7.2. Взаимные исключения: установка и снятие блокировки
  • 7.3. Схема производитель-потребитель
  • 7.4. Блокировка и ожидание
  • 7.5. Условные переменные: ожидание и сигнализация
  • Исключение конфликтов блокировок
  • 7.6. Условные переменные: время ожидания и широковещательная передача
  • 7.7. Атрибуты взаимных исключений и условных переменных
  • Завершение процесса, заблокировавшего ресурс
  • 7.8. Резюме
  • Упражнения
  • ГЛАВА 8 Блокировки чтения-записи
  • 8.1. Введение
  • 8.2. Получение и сброс блокировки чтения-записи
  • 8.3. Атрибуты блокировки чтения-записи
  • 8.4. Реализация с использованием взаимных исключений и условных переменных
  • Тип данных pthread_rwlock_t
  • Функция pthread_rwlock_init
  • Функция pthread_rwlock destroy
  • Функция pthread_rwlock_rdlock
  • Функция pthread_rwlock_tryrdlock
  • Функция pthread_rwlock_wrlock
  • Функция pthread_rwlock_trywrlock
  • Функция pthread_rwlock_unlock
  • 8.5. Отмена выполнения потоков
  • Пример
  • 8.6. Резюме
  • Упражнения
  • ГЛАВА 9 Блокирование записей
  • 9.1. Введение
  • 9.2. Блокирование записей и файлов
  • История
  • 9.3. Блокирование записей с помощью fcntl по стандарту Posix
  • Пример
  • Пример: упрощение с помощью макросов
  • 9.4. Рекомендательная блокировка
  • Пример: несотрудничающие процессы
  • 9.5. Обязательная блокировка
  • Пример
  • 9.6. Приоритет чтения и записи
  • Пример: блокировка на чтение при наличии в очереди блокировки на запись
  • Пример: имеют ли приоритет запросы на запись перед запросами на чтение?
  • 9.7. Запуск единственного экземпляра демона
  • 9.8. Блокирование файлов
  • 9.9. Блокирование в NFS
  • 9.10. Резюме
  • Упражнения
  • ГЛАВА 10 Семафоры Posix
  • 10.1.Введение
  • 10.2. Функции sem_open, sem_close и sem_unlink
  • 10.3. Функции sem_wait и sem_trywait
  • 10.4. Функции sem_post и sem_getvalue
  • 10.5. Простые примеры
  • Программа semcreate
  • Программа semunlink
  • Программа semgetvalue
  • Программа semwait
  • Программа sempost
  • Примеры
  • 10.6. Задача производителей и потребителей
  • Зависание
  • 10.7. Блокирование файлов
  • 10.8. Функции sem_init и sem_destroy
  • Пример
  • 10.9. Несколько производителей, один потребитель
  • 10.10. Несколько производителей, несколько потребителей
  • 10.11. Несколько буферов
  • 10.12. Использование семафоров несколькими процессами
  • 10.13. Ограничения на семафоры
  • Пример: программа semsysconf
  • 10.14. Реализация с использованием FIFO
  • Функция sem_open
  • Функция sem_close
  • Функция sem_unlink
  • Функция sem_post
  • Функция sem_wait
  • 10.15. Реализация с помощью отображения в память
  • Функция sem_open
  • Функция sem_close
  • Функция sem_unlink
  • Функция sem_post
  • Функция sem_wait
  • Функция sem_trywait
  • Функция sem_getvalue
  • 10.16. Реализация с использованием семафоров System V
  • Функция sem_open
  • Функция sem_close
  • Функция sem_unlink
  • Функция sem_post
  • Функция sem_wait
  • Функция sem_trywait
  • Функция sem_getvalue
  • 10.17. Резюме
  • Упражнения
  • ГЛАВА 11 Семафоры System V
  • 11.1.Введение
  • 11.2. Функция semget
  • Инициализация значения семафора
  • 11.3. Функция semop
  • 11.4. Функция semctl
  • 11.5. Простые программы
  • Программа semcreate
  • Программа semrmid
  • Программа semsetvalues
  • Программа semgetvalues
  • Программа semops
  • Примеры
  • 11.6. Блокирование файлов
  • 11.7. Ограничения семафоров System V
  • Пример
  • 11.8. Резюме
  • Упражнения
  • ЧАСТЬ 3

    СИНХРОНИЗАЦИЯ

    ГЛАВА 7

    Взаимные исключения и условные переменные

    7.1. Введение

    Эта глава начинается с обсуждения синхронизации — принципов синхронизации действий нескольких программных потоков или процессов. Обычно это требуется для предоставления нескольким потокам или процессам совместного доступа к данным. Взаимные исключения (mutual exclusion — mutex) и условные переменные (conditional variables) являются основными средствами синхронизации.

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

    ПРИМЕЧАНИЕ

    Эта возможность является дополнительной согласно Posix, но обязательной в Unix 98 (см. табл. 1.3).

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

    7.2. Взаимные исключения: установка и снятие блокировки

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

    блокировать_mutex(…);

    критическая область

    разблокировать_mutex(…);

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

    Взаимные исключения по стандарту Posix объявлены как переменные с типом pthread_mutex_t. Если переменная-исключение выделяется статически, ее можно инициализировать константой PTHREAD_MUTEX_INITIALIZER:

    static pthread_mutex_t lock=PTHREAD_MUTEX_INITIALIZER;

    При динамическом выделении памяти под взаимное исключение (например, вызовом mallос) или при помещении его в разделяемую память мы должны инициализировать эту переменную во время выполнения, вызвав функцию pthread_ mutex_init, как показано в разделе 7.7.

    ПРИМЕЧАНИЕ

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

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

    #include <pthread.h>

    int pthread_mutex_lock(pthread_mutex_t *mptr);

    int pthread_mutex_trylock(pthread_mutex_t *mptr);

    int pthread_mutex_unlock(pthread_mutex_t *mptr);

    /* Все три возвращают 0 в случае успешного завершения, положительное значение Еххх – случае ошибки */

    При попытке заблокировать взаимное исключение, которое уже заблокировано другим потоком, функция pthread_mutex_lock будет ожидать его разблокирования, a pthread_mutex_trylock (неблокируемая функция) вернет ошибку с кодом EBUSY.

    ПРИМЕЧАНИЕ

    Если несколько процессов ожидают освобождения взаимного исключения, какой из них начнет выполняться первым? Одна из возможностей, добавленных стандартом 1003.1b-1993, заключается в установке приоритета потоков. Мы не будем углубляться в эту область, отметим лишь, что разным потокам могут быть присвоены разные значения приоритета и функции синхронизации (работающие с взаимными исключениями, блокировками чтения-записи и семафорами) будут передавать управление заблокированному потоку с наивысшим значением приоритета. Раздел 5.5 книги [3] описывает возможности планирования выполнения в Posix.1 более подробно.

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

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

    7.3. Схема производитель-потребитель

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

    С этой задачей мы регулярно сталкиваемся при использовании каналов Unix. Команда интерпретатора, использующая канал

    grep pattern chapters.* | wc -l

    является примером такой задачи. Программа grep выступает как производитель (единственный), a wc — как потребитель (тоже единственный). Канал используется как форма IPC. Требуемая синхронизация между производителем и потребителем обеспечивается ядром, обрабатывающим команды write производителя и read покупателя. Если производитель опережает потребителя (канал переполняется), ядро приостанавливает производителя при вызове write, пока в канале не появится место. Если потребитель опережает производителя (канал опустошается), ядро приостанавливает потребителя при вызове read, пока в канале не появятся данные.

    Такой тип синхронизации называется неявным; производитель и потребитель не знают о том, что синхронизация вообще осуществляется. Если бы мы использовали очередь сообщений Posix или System V в качестве средства IPC между производителем и потребителем, ядро снова взяло бы на себя обеспечение синхронизации.

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

    В одном процессе у нас имеется несколько потоков-производителей и один поток-потребитель. Целочисленный массив buff содержит производимые и потребляемые данные (данные совместного пользования). Для простоты производители просто устанавливают значение buff[0] в 0, buff [1] в 1 и т.д. Потребитель перебирает элементы массива, проверяя правильность записей.

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

    Рис. 7.1. Производители и потребитель


    Листинг 7.1.[1] Функция main

    //mutex/prodcons2.с

    1  #include "unpipc.h"

    2  #define MAXNITEMS 1000000

    3  #define MAXNTHREADS 100

    4  int nitems; /* только для чтения потребителем и производителем */


    5  struct {

    6   pthread_mutex_t mutex;

    7   int buff[MAXNITEMS];

    8   int nput;

    9   int nval;

    10 } shared = {

    11  PTHREAD_MUTEX_INITIALIZER

    12 };


    13 void *produce(void *), *consume(void *);


    14 int

    15 main(int argc, char **argv)

    16 {

    17  int i, nthreads, count[MAXNTHREADS];

    18  pthread_t tid_produce[MAXNTHREADS], tid_consume;

    19  if (argc != 3)

    20   err_quit("usage: prodcons2 <#items> <#threads>");

    21  nitems = min(atoi(argv[1]), MAXNITEMS);

    22  nthreads = min(atoi(argv[2]), MAXNTHREADS);

    23  Set_concurrency(nthreads);

    24  /* запуск всех потоков-производителей */

    25  for (i = 0; i < nthreads; i++) {

    26   count[i] = 0;

    27   Pthread_create(&tid_produce[i], NULL, produce, &count[i]);

    28  }

    29  /* ожидание завершения всех производителей */

    30  for (i = 0; i < nthreads; i++) {

    31   Pthread_join(tid_produce[i], NULL);

    32   printf("count[%d] = %d\n", i, count[i]);

    33  }

    34  /* запуск и ожидание завершения потока-потребителя */

    35  Pthread_create(&tid_consume, NULL, consume, NULL);

    36  Pthread_join(tid_consume, NULL);

    37  exit(0);

    38 }

    Совместное использование глобальных переменных потоками

    4-12 Эти переменные совместно используются потоками. Мы объединяем их в структуру с именем shared вместе с взаимным исключением, чтобы подчеркнуть, что доступ к ним можно получить только вместе с ним. Переменная nput хранит индекс следующего элемента массива buff, подлежащего обработке, a nval содержит следующее значение, которое должно быть в него помещено (0, 1, 2 и т.д.). Мы выделяем память под эту структуру и инициализируем взаимное исключение, используемое для синхронизации потоков-производителей.

    ПРИМЕЧАНИЕ

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

    Аргументы командной строки

    19-22 Первый аргумент командной строки указывает количество элементов, которые будут произведены производителями, а второй — количество запускаемых потоков-производителей.

    Установка уровня параллельности

    23 Функция set_concurrency (наша собственная) указывает подсистеме потоков количество одновременно выполняемых потоков. В Solaris 2.6 она просто вызывает thr_setconcurrency, причем ее запуск необходим, если мы хотим, чтобы у нескольких процессов-производителей была возможность начать выполняться. Если мы не сделаем этого вызова в системе Solaris, будет запущен только первый поток. В Digital Unix 4.0B наша функция set_concurrency не делает ничего, поскольку в этой системе по умолчанию все потоки процесса имеют равные права на вычислительные ресурсы.

    ПРИМЕЧАНИЕ

    Unix 98 требует наличия функции pthread_setconcurrency, выполняющей это же действие. Эта функция требуется для тех реализаций, которые мультиплексируют пользовательские потоки (создаваемые функцией pthread_create) на небольшое множество выполняемых потоков ядра. Такие реализации часто называются «многие-к-немногим» (many-to-few), «двухуровневые» (two-level) или «М-на-N» (M-to-N). В разделе 5.6 книги [3] отношения между пользовательскими потоками и потоками ядра рассматриваются более подробно.

    Создание процессов-производителей

    24-28 Создаются потоки-производители, каждый из которых вызывает функцию produce. Идентификаторы потоков хранятся в массиве tid_produce. Аргументом каждого потока-производителя является указатель на элемент массива count. Счетчики инициализируются значением 0, и каждый поток увеличивает значение своего счетчика на 1 при помещении очередного элемента в буфер. Содержимое массива счетчиков затем выводится на экран, так что мы можем узнать, сколько элементов было помещено в буфер каждым из потоков.

    Ожидание завершения работы производителей, запуск потребителя

    29-36 Мы ожидаем завершения работы всех потоков-производителей, выводя содержимое счетчика для каждого потока, а затем запускаем единственный процесс-потребитель. Таким образом (на данный момент) мы исключаем необходимость синхронизации между потребителем и производителями. Мы ждем завершения работы потребителя, а затем завершаем работу процесса. В листинге 7.2 приведен текст функций produce и consume.

    Листинг 7.2. Функции produce и consume

    //mutex/prodcons2.с

    39 void *

    40 produce(void *arg)

    41 {

    42  for (;;) {

    43   Pthread_mutex_lock(&shared.mutex);

    44   if (shared.nput >= nitems) {

    45    Pthread_mutex_unlock(&shared.mutex);

    46    return(NULL); /* массив полный, готово */

    47   }

    48   shared.buff[shared.nput] = shared.nval;

    49   shared.nput++;

    50   shared.nval++;

    51   Pthread_mutex_unlock(&shared.mutex);

    52   *((int *) arg) += 1;

    53  }

    54 }


    55 void *

    56 consume(void *arg)

    57 {

    58  int i;

    59  for (i = 0; i < nitems; i++) {

    60   if (shared.buff[i] != i)

    61    printf("buff[%d] = %d\n", i, shared.buff[i]);

    62  }

    63  return(NULL);

    64 }

    Формирование данных

    42-53 Критическая область кода производителя состоит из проверки на достижение конца массива (завершение работы)

    if (shared.nput >= nitems)

    и трех строк, помещающих очередное значение в массив:

    shared.buff[shared.nput] = shared.nval;

    shared.nput++;

    shared.nval++;

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

    Потребитель проверяет содержимое массива

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

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

    solaris % prodcons2 1000000 5

    count[0] = 167165

    count[1] = 249891

    count[2] = 194221

    count[3] = 191815

    count[4] = 196908

    Как мы отмечали ранее, если убрать вызов set_concurrency, в системе Solaris 2.6 значение count[0] будет 1000000, а все остальные счетчики будут нулевыми.

    Если убрать из этого примера блокировку с помощью взаимного исключения, он перестанет работать, как и предполагается. Потребитель обнаружит множество элементов buff[i], значения которых будут отличны от i. Также мы можем убедиться, что удаление блокировки ничего не изменит, если будет выполняться только один поток.

    7.4. Блокировка и ожидание

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

    В листинге 7.3 приведен текст функции main. Начало кода (до объявления функции main) не претерпело никаких изменений по сравнению с листингом 7.1.

    Листинг 7.3. Функция main: запуск потребителя сразу после запуска производителей

    //mutex/prodcons3.c

    14 int

    15 main(int argc, char **argv)

    16 {

    17  int i, nthreads, count[MAXNTHREADS];

    18  pthread_t tid_produce[MAXNTHREADS], tid_consume;

    19  if (argc != 3)

    20   err_quit("usage: prodcons3 <#items> <#threads>");

    21  nitems = min(atoi(argv[1]), MAXNITEMS);

    22  nthreads = min(atoi(argv[2]), MAXNTHREADS);

    23  /* создание всех производителей и одного потребителя */

    24  Set_concurrency(nthreads + 1);

    25  for (i = 0; i < nthreads; i++) {

    26   count[i] = 0;

    27   Pthread_create(&tid_produce[i], NULL, produce, &count[i]);

    28  }

    29  Pthread_create(&tid_consume, NULL, consume, NULL);

    30  /* ожидание завершения производителей и потребителя */

    31  for (i = 0; i < nthreads; i++) {

    32   Pthread_join(tid_produce[i], NULL);

    33   printf("count[%d] = %d\n", i, count[i]);

    34  }

    35  Pthread_join(tid_consume, NULL);

    36  exit(0);

    37 }

    24 Мы увеличиваем уровень параллельного выполнения на единицу, чтобы учесть поток-потребитель, выполняемый параллельно с производителями.

    25-29 Поток-потребитель создается сразу же после создания потоков-производителей.

    Функция produce по сравнению с листингом 7.2 не изменяется. В листинге 7.4 приведен текст функции consume, вызывающей новую функцию consume_wait. 

    Листинг 7.4. Функции consume и consume_wait

    //mutex/prodcons3.с

    54 void

    55 consume wait(int i)

    56 {

    57  for (;;) {

    58   Pthread_mutex_lock(&shared.mutex);

    59   if (i < shared.nput) {

    60    Pthread_mutex_unlock(&shared.mutex);

    61    return; /* элемент готов */

    62   }

    63   Pthread_mutex_unlock(&shared.mutex);

    64  }

    65 }


    66 void *

    67 consume(void *arg)

    68 {

    69  int i;

    70  for (i = 0; i < nitems; i++) {

    71   consume_wait(i);

    72   if (shared.buff[i] != i)

    73    printf("buff[%d] = %d\n", i, shared.buff[i]);

    74  }

    75  return(NULL);

    76 }

    Потребитель должен ждать

    71 Единственное изменение в функции consume заключается в добавлении вызова consume_wait перед обработкой следующего элемента массива.

    Ожидание производителей

    57-64 Наша функция consume_wait должна ждать, пока производители не создадут i-й элемент. Для проверки этого условия производится блокировка взаимного исключения и значение i сравнивается с индексом производителя nput. Блокировка необходима, поскольку значение nput может быть изменено одним из производителей в момент его проверки.

    Главная проблема — что делать, если нужный элемент еще не готов. Все, что нам остается и что мы делаем в листинге 7.4, — это повторять операции в цикле, устанавливая и снимая блокировку и проверяя значение индекса. Это называется опросом (spinning или polling) и является лишней тратой времени процессора.

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

    7.5. Условные переменные: ожидание и сигнализация

    Взаимное исключение используется для блокирования, а условная переменная — для ожидания. Это два различных средства синхронизации, и оба они нужны. Условная переменная представляет собой переменную типа pthread_cond_t. Для работы с такими переменными предназначены две функции:

    #include <pthread.h>

    int pthread_cond_wait(pthread_cond_t *cptr, pthread_m_tex_t *mptr);

    int pthread_cond_signal(pthread_cond_t *cptr);

    /* Обе функции возвращают 0 в случае успешного завершения, положительное значение Еххх – в случае ошибки */

    Слово signal в имени второй функции не имеет никакого отношения к сигналам Unix SIGxxx.

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

    Взаимное исключение всегда связывается с условной переменной. При вызове pthread_cond_wait для ожидания выполнения какого-либо условия мы указываем адрес условной переменной и адрес связанного с ней взаимного исключения.

    Мы проиллюстрируем использование условных переменных, переписав пример из предыдущего раздела. В листинге 7.5 объявляются глобальные переменные.

    Переменные производителя и взаимное исключение объединяются в структуру

    7-13 Две переменные nput и rival ассоциируются с mutex, и мы объединяем их в структуру с именем put. Эта структура используется производителями.

    14-20 Другая структура, nready, содержит счетчик, условную переменную и взаимное исключение. Мы инициализируем условную переменную с помощью PTHREAD_ COND_INITIALIZER.

    Функция main по сравнению с листингом 7.3 не изменяется.

    Листинг 7.5. Глобальные переменные: использование условной переменной

    //mutex/prodcons6.c

    1  #include "unpipc.h"

    2  #define MAXNITEMS 1000000

    3  #define MAXNTHREADS 100


    4  /* глобальные переменные для всех потоков */

    5  int nitems; /* только для чтения потребителем и производителем */

    6  int buff[MAXNITEMS];


    7  struct {

    8   pthread_mutex_t mutex;

    9   int nput; /* следующий сохраняемый элемент */

    10  int nval; /* следующее сохраняемое значение */

    11 } put = {

    12  PTHREAD_MUTEX_INITIALIZER

    13 };


    14 struct {

    15  pthread_mutex_t mutex:

    16  pthread_cond_t cond;

    17  int nready; /* количество готовых для потребителя */

    18 } nready = {

    19  PTHREAD_MUTEX_INITIALIZER, PTHREAD_COND_INITIALIZER

    20 };

    Функции produce и consume претерпевают некоторые изменения. Их текст дан в листинге 7.6.

    Листинг 7.6. Функции produce и consume

    //mutex/prodcons6.c

    46 void *

    47 produce(void *arg)

    48 {

    49  for (;;) {

    50   Pthread_mutex_lock(&put.mutex);

    51   if (put.nput >= nitems) {

    52    Pthread_mutex_unlock(&put.mutex);

    53    return(NULL); /* массив заполнен, готово */

    54   }

    55   buff[put.nput] = put.nval;

    56   put.nput++;

    57   put.nval++;

    58   Pthread_mutex_unlock(&put.mutex);

    59   Pthread_mutex_lock(&nready.mutex):

    60   if (nready.nready == 0)

    61    Pthread_cond_signal(&nready.cond);

    62   nready.nready++;

    63   Pthread_mutex_unlock(&nready.mutex);

    64   *((int *) arg) += 1;

    65  }

    66 }


    67 void*

    68 consume(void *arg)

    69 {

    70  int i;

    71  for (i = 0; i < nitems; i++) {

    72   Pthread_mutex_lock(&nready.mutex);

    73   while (nready.nready == 0)

    74    Pthread_cond_wait(&nready.cond, &nready.mutex);

    75   nready.nready--;

    76   Pthread_mutex_unlock(&nready.mutex);

    77   if (buff[i] != i)

    78    printf("buff[%d] = *d\n", i, buff[i]);

    79  }

    80  return(NULL);

    81 }

    Помещение очередного элемента в массив

    50-58 Для блокирования критической области в потоке-производителе теперь используется исключение put.mutex. 

    Уведомление потребителя

    59-64 Мы увеличиваем счетчик nready.nready, в котором хранится количество элементов, готовых для обработки потребителем. Перед его увеличением мы проверяем, не было ли значение счетчика нулевым, и если да, то вызывается функция pthread_cond_signal, позволяющая возобновить выполнение всех потоков (в данном случае потребителя), ожидающих установки ненулевого значения этой переменной. Теперь мы видим, как взаимодействуют взаимное исключение и связанная с ним условная переменная. Счетчик используется совместно потребителем и производителями, поэтому доступ к нему осуществляется с блокировкой соответствующего взаимного исключения (nready.mutex). Условная переменная используется для ожидания и передачи сигнала.

    Потребитель ждет, пока значение nready.nready не станет отличным от нуля

    72-76 Потребитель просто ждет, пока значение счетчика nready. nready не станет отличным от нуля. Поскольку этот счетчик используется совместно с производителями, его значение можно проверять только при блокировке соответствующего взаимного исключения. Если при проверке значение оказывается нулевым, мы вызываем pthread_cond_wait для приостановки процесса. При этом выполняются два атомарных действия:

    1. Разблокируется nready.mutex.

    2. Выполнение потока приостанавливается, пока какой-нибудь другой поток не вызовет pthread_cond_signal для этой условной переменной.

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

    Код, передающий сигнал условной переменной, выглядит следующим образом:

    struct {

     pthread_mutex_t mutex;

     pthread_cond_t cond;

     переменные, для которых устанавливается условие

    } var = { PTHREAD_MUTEX_INITIALIZER, PTHREAD_COND_INITIALIZER, … };


    Pthread_mutex_lock(&var.mutex);

    установка истинного значения условия

    Pthread_cond_signal(&var.cond);

    Pthread_mutex_unlock(&var.mutex);

    В нашем примере переменная, для которой устанавливалось условие, представляла собой целочисленный счетчик, а установка условия означала просто увеличение счетчика. Мы оптимизировали программу, посылая сигнал только при изменении значения счетчика с 0 на 1.

    Код, проверяющий условие и приостанавливающий процесс, если оно не выполняется, обычно выглядит следующим образом:

    Pthread_mutex_lock(&var.mutex);

    while (условие ложно)

     Pthread_cond_wait(&var.cond, &var.mutex);

    изменение условия

    Pthread_mutex_unlock(&var.mutex);

    Исключение конфликтов блокировок

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

    int dosignal;

    Pthread_mutex_lock(nready.mutex);

    dosignal = (nready.nready == 0);

    nready.nready++;

    Pthread_mutex_unlock(&nready.mutex);

    if (dosignal)

     Pthread_cond_signal(&nready.cond);

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

    7.6. Условные переменные: время ожидания и широковещательная передача

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

    ПРИМЕЧАНИЕ

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

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

    #include <pthread.h>

    int pthread_cond_broadcast(pthread_cond_t *cptr);

    int pthread_cond_timedwait(pthread_cond_t *cptr, pthread_mutex_t *mptr, const struct timespec *abstime);

    /* Функции возвращают 0 в случае успешного завершения, положительный код Еххх — в случае ошибки */

    Функция pthread_cond_timedwait позволяет установить ограничение на время блокирования процесса. Аргумент abstime представляет собой структуру timespec:

    struct timespec {

     time_t tv_sec; /* секунды */

     long tv_nsec; /* наносекунды */

    };

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

    Эта величина представляет собой абсолютное значение времени, а не промежуток. Аргумент abstime задает таким образом количество секунд и наносекунд с 1 января 1970 UTC до того момента времени, в который должен произойти возврат из функции. Это отличает функцию от select, pselect и poll (глава 6 [24]), которые в качестве аргумента принимают некоторое количество долей секунды, спустя которое должен произойти возврат. (Функция select принимает количество микросекунд, pselect — наносекунд, a poll — миллисекунд.) Преимущество использования абсолютного времени заключается в том, что если функция возвратится до ожидаемого момента (например, при перехвате сигнала), ее можно будет вызвать еще раз, не изменяя содержимого структуры timespec.

    7.7. Атрибуты взаимных исключений и условных переменных

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

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

    #include <pthread.h>

    int pthread_mutex_imt(pthread_mutex_t *mptr, const pthread_mutexattr_t *attr);

    int pthread_mutex_destroy(pthread_mutex_t *mptr);

    int pthread_cond_init(pthread_cond_t *cрtr, const pthread_condattr_t *attr);

    int pthread_cond_destroy(pthread_cond_t *cptr);

    /* Все четыре функции возвращают 0 в случае успешного завершения работы, положительное значение Еххх – в случае ошибки */

    Рассмотрим, например, взаимное исключение. Аргумент mptr должен указывать на переменную типа pthread_mutex_t, для которой должна быть уже выделена память, и тогда функция pthread_mutex_init инициализирует это взаимное исключение. Значение типа pthread_mutexattr_t, на которое указывает второй аргумент функции pthread_mutex_init(attr ), задает атрибуты этого исключения. Если этот аргумент представляет собой нулевой указатель, используются значения атрибутов по умолчанию.

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

    #include <pthread.h>

    int pthread_mutexattr_init(pthread_mutexattr_t *attr);

    int pthread_mutexattr_destroy(pthread_mutexattr_t *attr);

    int pthread_condattr_init(pthread_condattr_t *attr);

    int pthread_condattr_destroy(pthread_condattr_t *attr);

    /* Все четыре функции возвращают 0 в случае успешного завершения, положительное значение Еххх – в случае ошибки */

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

    #include <pthread.h>

    int pthread_mutexattr_getpshared(const pthread_mutexattr_t *attr, int *valptr);

    int pthread_mutexattr_setpshared(pthread_mutexattr_t *attr, int value);

    int pthread_condattr_getpshared(const pthread_condattr_t *attr, int *valptr);

    int pthread_condattr_setpshared(pthread_condattr_t *attr, int value);

    /* Все четыре функции возвращают 0 в случае успешного завершения, положительное значение Еххх – в случае ошибки */

    Две функции get возвращают текущее значение атрибута через целое, на которое указывает valptr, а две функции set устанавливают значение атрибута равным значению value. Значение value может быть либо PTHREAD_PROCESS_PRIVATE, либо PTHREAD_PROCESS_SHARED. Последнее также называется атрибутом совместного использования процессами. 

    ПРИМЕЧАНИЕ

    Эта возможность поддерживается только в том случае, если константа _POSIX_THREAD_PROCESS_SHARED определена в заголовочном файле <unistd.h>. Она является дополнительной согласно Posix.1 и обязательной по Unix 98 (табл. 1.3). 

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

    pthread_mutex_t *mptr; /* указатель на взаимное исключение, находящееся в разделяемой памяти */

    pthread_mutexattr_t mattr; /* атрибуты взаимного исключения */

    mptr = /* некоторое значение, указывающее на разделяемую память */

    Pthread_mutexattr_init(&mattr);

    #ifdef _POSIX_THREAD_PROCESS_SHARED

     Pthread_mutexattr_setpshared(&mattr, PTHREAD_PROCESS_SHARED);

    #else

     # error Эта реализация не поддерживает _POSIX_THREAD_PROCESS_SHARED

    #endif

    Pthread_mutex_init(mptr, &mattr);

    Мы объявляем переменную mattr типа pthread_mutexattr_t, инициализируем ее значениями атрибутов по умолчанию, а затем устанавливаем атрибут PTHREAD_PROCESS_SHARED, позволяющий совместно использовать взаимное исключение нескольким процессам. Затем pthread_mutex_init инициализирует само исключение с соответствующими атрибутами. Количество разделяемой памяти, которое следует выделить под взаимное исключение, равно sizeof(pthread_mutex_t).

    Практически такая же последовательность команд (с заменой mutex на cond) позволяет установить атрибут PTHREAD_PROCESS_SHARED для условной переменной, хранящейся в разделяемой несколькими процессами памяти.

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

    Завершение процесса, заблокировавшего ресурс

    Когда взаимное исключение используется совместно несколькими процессами, всегда существует возможность, что процесс будет завершен (возможно, принудительно) во время работы с заблокированным им ресурсом. Не существует способа заставить систему автоматически снимать блокировку во время завершения процесса. Мы увидим, что это свойственно и блокировкам чтения-записи, и семафорам Posix. Единственный тип блокировок, автоматически снимаемых системой при завершении процесса, — блокировки записей fcntl (глава 9). При использовании семафоров System V можно специально указать ядру, следует ли автоматически снимать блокировки при завершении работы процесса (функция SEM_UNDO, о которой будет говориться в разделе 11.3).

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

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

    В некоторых случаях автоматическое снятие блокировки (или счетчика — для семафора) при завершении процесса не вызывает проблем. Например, сервер может использовать семафор System V (с функцией SEM_UNDO) для подсчета количества одновременно обслуживаемых клиентов. Каждый раз при порождении процесса вызовом fork он увеличивает значение семафора на единицу, уменьшая его при завершении работы дочернего процесса. Если дочерний процесс завершит работу досрочно, ядро само уменьшит значение семафора. Пример, в котором автоматическое снятие блокировки ядром (а не уменьшение счетчика, как в вышеописанной ситуации) также не вызывает проблем, приведен в разделе 9.7. Демон блокирует один из файлов данных при записи в него и не снимает эту блокировку до завершения работы. Если кто-то попробует запустить копию демона, она завершит работу досрочно, когда обнаружит наличие блокировки на запись. Это гарантирует работу единственного экземпляра демона. Если же демон досрочно завершит работу, ядро само снимет блокировку, что позволит запустить копию демона.

    7.8. Резюме

    Взаимные исключения (mutual exclusion — mutex) используются для защиты критических областей кода, запрещая его одновременное выполнение несколькими потоками. В некоторых случаях потоку, заблокировавшему взаимное исключение, требуется дождаться выполнения какого-либо условия для выполнения последующих действий. В этом случае используется ожидание сигнала по условной переменной. Условная переменная всегда связывается с каким-либо взаимным исключением. Функция pthread_cond_wait, приостанавливающая работу процесса, разблокирует взаимное исключение перед остановкой работы и заново блокирует его при возобновлении работы процесса спустя некоторое время. Сигнал по условной переменной передается каким-либо другим потоком, и этот поток может разбудить либо только один произвольный поток из множества ожидающих (pthread_cond_signal), либо все их одновременно (pthread_cond_broadcast).

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

    Упражнения

    1. Удалите взаимное исключение из листинга 7.2 и убедитесь, что программа работает неправильно, если одновременно запущено более одного производителя.

    2. Что произойдет с листингом 7.1, если убрать вызов Pthread_join для потока-потребителя?

    3. Напишите пpoгрaммy, вызывающую pthread_mutexatt_init и pthread_condattr_init в бесконечном цикле. Следите за используемой этим процессом памятью с помощью какой-нибудь программы, например ps. Что происходит? Теперь добавьте вызовы pthread_mutexattr_destroy и pthread_condattr_destroy и убедитесь, что утечки памяти нет.

    4. В программе из листинга 7.6 производитель вызывает pthread_cond_signal только при изменении nready.nready с 0 на 1. Чтобы убедиться в эффективности этой оптимизации, вызывайте pthread_cond_signal каждый раз, когда nready.nready увеличивается на 1, и выведите его значение в главном потоке после завершения работы потребителя. 

    ГЛАВА 8

    Блокировки чтения-записи

    8.1. Введение

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

    В этой главе описываются блокировки чтения-записи, причем существует различие между получением такой блокировки для считывания и для записи. Правила действуют следующие:

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

    2. Блокировка чтения-записи может быть установлена на запись, только если ни один поток не заблокировал ресурс для чтения или для записи.

    Другими словами, произвольное количество потоков могут считывать данные, если ни один поток не изменяет их в данный момент. Данные могут быть изменены, только если никто другой их не считывает и не изменяет.

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

    Такой вид совместного доступа к ресурсу также носит название совместно-исключающей блокировки (shared-exclusive), поскольку тип используемой блокировки на чтение называется совместной блокировкой (shared lock), а тип используемой блокировки на запись называется исключающей блокировкой (exclusive lock). Существует также специальное название для данной задачи (несколько считывающих процессов и один записывающий): задача читателей и писателей (readers and writers problem), и говорят также о блокировке читателей и писателя (readers-writer lock). В последнем случае слово «читатель» специально употреблено во множественном числе, а «писатель» — в единственном, чтобы подчеркнуть сущность задачи.

    ПРИМЕЧАНИЕ

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

    Функции, описываемые в этой главе, определены стандартом Unix 98, поскольку блокировки чтения-записи не были частью стандарта Posix.1 1996 года. Эти функции были разработаны группой производителей Unix, известной под названием Aspen Group, в 1995 году вместе с другими расширениями, которые еще не были определены Posix.1. Рабочая группа Posix (1003.1j) в настоящее время разрабатывает набор расширений Pthreads, включающий блокировки чтения-записи, который, хочется верить, совпадет с описываемым в этой главе.

    8.2. Получение и сброс блокировки чтения-записи

    Блокировка чтения-записи имеет тип pthread_rwlock_t. Если переменная этого типа является статической, она может быть проинициализирована присваиванием значения константы PTHREAD_RWLOCK_INITIALIZER.

    Функция pthread_rwlock_rdlock позволяет заблокировать ресурс для чтения, причем вызвавший процесс будет заблокирован, если блокировка чтения-записи уже установлена записывающим процессом. Функция pthread_rwlock_wrlock позволяет заблокировать ресурс для записи, причем вызвавший процесс будет заблокирован, если блокировка чтения-записи уже установлена каким-либо другим процессом (считывающим или записывающим). Функция pthread_rwlock_unlock снимает блокировку любого типа (чтения или записи):

    #include <pthread.h>

    int pthread_rwlock_rdlock(pthread_rwlock_t *rwptr);

    int pthread_rwlock_wrlock(pthread_rwlock_t *rwptr);

    int pthread_rwlock_unlock(pthread_rwlock_t *rwptr );

    /* Все функции возвращают 0 в случае успешного завершения, положительное значение Еххх – в случае ошибки */

    Следующие две функции производят попытку заблокировать ресурс для чтения или записи, но если это невозможно, возвращают ошибку с кодом EBUSY, вместо того чтобы приостановить выполнение вызвавшего процесса:

    #include <pthread.h>

    int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwptr);

    int pthread_rwlock_trywrlock(pthread_rwlock_t *rwptr);

    /* Обе функции возвращают 0 в случае успешного завершения, положительное значение Еххх – в случае ошибки */

    8.3. Атрибуты блокировки чтения-записи

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

    Когда поток перестает нуждаться в блокировке, он может вызвать pthread_rwlock_ destroy:

    #include <pthread.h>

    int pthread_rwlock_init(pthread_rwlock_t *rwptr, const pthread_rwlockattr_t *attr);

    int pthread_rwlock_destroy(pthread_rwlock_t *rwptr);

    /* Обе функции возвращают 0 в случае успешного завершения, положительное значение Еххх – в случае ошибки */

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

    #include <pthread.h>

    int pthread_rwlockattr_init(pthread_rwlockattr_t *attr);

    int pthread_rwlockattr_destroy(pthread_rwlockattr_t *attr);

    /* Обе функции возвращают 0 в случае успешного завершения, положительное значение Еххх – в случае ошибки */

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

    #include <pthread.h>

    int pthread_rwlockattr_getpshared(const pthread_rwlockattr_t *attr, int *valptr);

    int pthread_rwlockattr_setpshared(pthread_rwlockattr_t *attr, int value );

    /* Обе функции возвращают 0 в случае успешного завершения, положительное значение Еххх – в случае ошибки */

    Первая функция возвращает текущее значение в целом, на которое указывает аргумент valptr. Вторая функция устанавливает значение этого атрибута равным value, которое может быть либо PTHREAD_PROCESS_PRIVATE, либо PTHREAD_ PROCESS_SHARED.

    8.4. Реализация с использованием взаимных исключений и условных переменных

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

    ПРИМЕЧАНИЕ

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

    Другие реализации блокировок чтения записи заслуживают отдельного изучения. В разделе 7.1.2 книги [3] представлена реализация, в которой приоритет имеют ожидающие записи потоки и предусмотрена обработка отмены выполнения потока (о которой мы вскоре будем говорить подробнее). В разделе В.18.2.3.1 стандарта IEEE 1996 [8] представлена другая реализация, в которой предпочтение имеют ожидающие записи потоки и в которой также предусмотрена обработка отмены. В главе 14 книги [12] также приводится возможная реализация, в которой приоритет имеют ожидающие записи процессы. Реализация, приведенная в этом разделе, взята из пакета АСЕ (http://www.cs.wustl.edu/~schmidt/ACE.html), автором которого является Дуг Шмидт (Doug Schmidt). Аббревиатура АСЕ означает Adaptive Communications Environment. Во всех четырех реализациях используются взаимные исключения и условные переменные. 

    Тип данных pthread_rwlock_t

    В листинге 8.1[1] приведен текст заголовочного файла pthread_rwlock .h, в котором определен основной тип pthread_rwlock_t и прототипы функций, работающих с блокировками чтения и записи. Обычно все это находится в заголовочном файле <pthread.h>.

    Листинг 8.1. Определение типа данных pthread_rwlock_t

    //my_rwlock/pthread_rwlock.h

    1  #ifndef __pthread_rwlock_h

    2  #define __pthread_rwlock_h


    3  typedef struct {

    4   pthread_mutex_t rw_mutex; /* блокировка для структуры */

    5   pthread_cond_t rw_condreaders; /* для ждущих читающих потоков */

    6   pthread_cond_t rw_condwriters; /* для ждущих пишущих потоков */

    7   int rw_magic; /* для проверки ошибок */

    8   int rw_nwaitreaders;/* число ожидающих */

    9   int rw_nwaitwriters;/* число ожидающих */

    10  int rw_refcount;

    11  /* –1, если блокировка на запись, иначе – количество блокировок на чтение */

    12 } pthread_rwlock_t;


    13 #define RW_MAGIC 0x19283746

    14 /* порядок должен быть такой же, как у элементов структуры */

    15 #define PTHREAD_RWLOCK_INITIALIZER { PTHREAD_MUTEX_INITIALIZER, \

    16  PTHREAD_COND_INITIALIZER, PTHREAD_COND_INITIALIZER, \

    17  RW_MAGIC, 0, 0, 0 }


    18 typedef int pthread_rwlockattr_t; /* не поддерживается */


    19 /* прототипы функций */

    20 int pthread_rwlock_destroy(pthread_rwlock_t *);

    21 int pthread_rwlock_init(pthread_rwlock_t *, pthread_rwlockattr_t *);

    22 int pthread_rwlock_rdlock(pthread_rwlock_t *);

    23 int pthread_rwlock_tryrdlock(pthread_rwlock_t *);

    24 int pthread_rwlock_trywrlock(pthread_rwlock_t *);

    25 int pthread_rwlock_unlock(pthread_rwlock_t *);

    26 int pthread_rwlock_wrlock(pthread_rwlock_t *);


    27 /* и наши функции-обертки */

    28 void pthread_rwlock_destroy(pthread_rwlock_t *);

    29 void pthread_rwlock_init(pthread_rwlock_t*, pthread_rwlockattr_t *);

    30 void Pthread_rwlock_rdlock(pthread_rwlock_t *);

    31 int Pthread_rwlock_tryrdlock(pthread_rwlock_t *);

    32 int pthread_rwlock_trywrlock(pthread_rwlock_t *);

    33 void pthread_rwlock_unlock(pthread_rwlock_t *);

    34 void pthread_rwlock_wrlock(pthread_rwlock_t *);


    35 #endif __pthread_rwlock_h

    3-13 Наш тип pthread_rwlock_t содержит одно взаимное исключение, две условные переменные, один флаг и три счетчика. Мы увидим, для чего все это нужно, когда будем разбираться с работой функций нашей программы. При просмотре или изменении содержимого этой структуры мы должны устанавливать блокировку rw_mutex. После успешной инициализации структуры полю rw_magic присваивается значение RW_MAGIC. Значение этого поля проверяется всеми функциями — таким образом гарантируется, что вызвавший поток передал указатель на проинициализированную блокировку. Оно устанавливается в 0 после уничтожения блокировки.

    Обратите внимание, что в счетчике rw_refcount всегда хранится текущий статус блокировки чтения-записи: –1 обозначает блокировку записи (и только одна такая блокировка может существовать в любой момент времени), 0 обозначает, что блокировка доступна и может быть установлена, а любое положительное значение соответствует количеству установленных блокировок на чтение.

    14-17 Мы также определяем константу для статической инициализации нашей структуры.

    Функция pthread_rwlock_init

    Первая функция, pthread_rwlock_init, динамически инициализирует блокировку чтения-записи. Ее текст приведен в листинге 8.2.

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

    9-19 Мы инициализируем взаимное исключение и две условные переменные, которые содержатся в нашей структуре. Все три счетчика устанавливаются в 0, а полю rw_magiс присваивается значение, указывающее на то, что структура была проинициализирована.

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

    Листинг 8.2. Функция pthread_rwlock_init: инициализация блокировки чтения-записи

    //my_rwlock/pthread_rwlock_init.с

    1  #include "unpipc.h"

    2  #include "pthread_rwlock.h"


    3  int

    4  pthread_rwlock_init(pthread_rwlock_t *rw, pthread_rwlockattr_t *attr)

    5  {

    6   int result;

    7   if (attr != NULL)

    8    return(EINVAL); /* not supported */

    9   if ((result = pthread_mutex_init(&rw->rw_mutex, NULL)) != 0)

    10   goto err1;

    11  if ((result = pthread_cond_init(&rw->rw_condreaders, NULL)) != 0)

    12   goto err2;

    13  if ((result = pthread_cond_init(&rw->rw_condwriters, NULL)) != 0)

    14   goto err3;

    15  rw->rw_nwaitreaders = 0;

    16  rw->rw_nwaitwriters = 0;

    17  rw->rw_refcount = 0;

    18  rw->rw_magic = RW_MAGIC;

    19  return(0);

    20 err3:

    21  pthread_cond_destroy(&rw->rw_condreaders);

    22 err2;

    23  pthread_mutex_destroy(&rw->rw_mutex);

    24 err1:

    25  return(result); /* значение errno */

    26 }

    Функция pthread_rwlock destroy

    В листинге 8.3 приведена функция pthread_rwlock_destroy, уничтожающая блокировку чтения записи после окончания работы с ней.

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

    Листинг 8.З. Функция pthread_rwlock_destroy: уничтожение блокировки чтения-записи

    //my_rwlock/pthread_rwlock_destroy.с

    1  #include "unpipc.h"

    2  #include "pthread_rwlock.h"


    3  int

    4  pthread_rwlock_destroy(pthread_rwlock_t *rw)

    5  {

    6   if (rw->rw_magic != RW_MAGIC)

    7    return(EINVAL);

    8   if (rw->rw_refcount != 0 ||

    9    rw->rw_nwaitreaders != 0 || rw->rw_nwaitwriters != 0)

    10   return(EBUSY);

    11  pthread_mutex_destroy(&rw->rw_mutex);

    12  pthread_cond_destroy(&rw->rw_condreaders);

    13  pthread_cond_destroy(&rw->rw_condwriters);

    14  rw->rw_magic = 0;

    15  return(0);

    16 }

    Функция pthread_rwlock_rdlock

    Текст функции pthread_rwlock_rdlock приведен в листинге 8.4.

    Листинг 8.4. Функция pthread_rwlock_rdlock: получение блокировки на чтение

    //my_rwlock/pthread_rwlock_rdlock.с

    1  #include "unpipc.h"

    2  #include "pthread_rwlock.h"


    3  int

    4  pthread_rwlock_rdlock(pthread_rwlock_t *rw)

    5  {

    6   int result;

    7   if (rw->rw_magic != RW_MAGIC)

    8    return(EINVAL);

    9   if ((result = pthread_mutex_lock(&rw->rw_mutex)) != 0)

    10   return(result);

    11  /* предпочтение отдается ожидающим разрешения на запись процессам */

    12  while (rw->rw_refcount < 0 || rw->rw_nwaitwriters > 0) {

    13   rw->rw_nwaitreaders++;

    14   result = pthread_cond_wait(&rw->rw_condreaders, &rw->rw_mutex);

    15   rw->rw_nwaitreaders--;

    16   if (result != 0)

    17    break;

    18  }

    19  if (result == 0)

    20   rw->rw_refcount++; /* блокировка на чтение уже кем-то установлена */

    21  pthread_mutex_unlock(&rw->rw_mutex);

    22  return (result);

    23 }

    9-10 При работе со структурой pthread_rwl ock_t всегда устанавливается блокировка на rw_mutex, являющееся ее полем.

    11-18 Нельзя получить блокировку на чтение, если rw_refcount имеет отрицательное значение (блокировка установлена на запись) или имеются потоки, ожидающие возможности получения блокировки на запись (rw_nwaitwriters больше 0). Если одно из этих условий верно, мы увеличиваем значение rw_nwaitreaders и вызываем pthread_cond_wait для условной переменной rw_condreaders. Вскоре мы увидим, что при разблокировании ресурса прежде всего проверяется наличие процессов, ожидающих возможности установить блокировку на запись, и если таковых не существует, проверяется наличие ожидающих возможности считывания. Если они имеются, для условной переменной rw_condreaders передается широковещательный сигнал.

    19-20 При получении блокировки на чтение мы увеличиваем значение rw_refcount. Блокировка взаимного исключения после этого снимается.

    ПРИМЕЧАНИЕ

    В этой функции есть проблема: если вызвавший поток будет заблокирован в функции pthread_cond_wait и после этого его выполнение будет отменено, он завершит свою работу, не разблокировав взаимное исключение, и значение rw_nwaitreaders окажется неверным. Та же проблема есть и в функции pthread_rwlock_wrlock в листинге 8.6. Эти проблемы будут исправлены в разделе 8.5.

    Функция pthread_rwlock_tryrdlock

    В листинге 8.5 показана наша реализация функции pthread_rwlock_tryrdlock, которая не вызывает приостановления вызвавшего ее потока.

    Листинг 8.5. Функция pthread_rwlock_tryrdlock: попытка заблокировать ресурс для чтения

    //my_rwlock/pthread_rwlock_tryrdlock.с

    1  #include "unpipc.h"

    2  #include "pthread_rwlock.h"


    3  int

    4  pthread_rwlock_tryrdlock(pthread_rwlock_t *rw)

    5  {

    6   int result;

    7   if (rw->rwjnagic != RW_MAGIC)

    8    return(EINVAL);

    9   if ((result = pthread_mutex_lock(&rw->rw_mutex)) != 0)

    10   return(result);

    11  if (rw->rw_refcount < 0 || rw->rw_nwaitwriters > 0)

    12   result = EBUSY; /* блокировка установлена пишущим потоком или есть пишущие потоки, ожидающие освобождения ресурса */

    13  else

    14   rw->rw_refcount++; /* увеличение количества блокировок на чтение */

    15  pthread_mutex_unlock(&rw->rw_mutex);

    16  return(result);

    17 }

    11-14 Если блокировка в данный момент установлена на запись или есть процессы, ожидающие возможности установить ее на запись, возвращается ошибка с кодом EBUSY. В противном случае мы устанавливаем блокировку, увеличивая значение счетчика rw_refcount.

    Функция pthread_rwlock_wrlock

    Текст функции pthread_rwlock_wrlock приведен в листинге 8.6.

    11-17 Если ресурс заблокирован на считывание или запись (значение rw_refcount отлично от 0), мы приостанавливаем выполнение потока. Для этого мы увеличиваем rw_nwaitwriters и вызываем pthread_cond_wait с условной переменной rw_condwriters. Для этой переменной посылается сигнал при снятии блокировки чтения-записи, если имеются ожидающие разрешения на запись процессы.

    18-19 После получения блокировки на запись мы устанавливаем значение rw_refcount в –1.

    Листинг 8.6. Функция pthread_rwlock_wrlock: получение блокировки на запись

    //my_rwlock/pthread_rwlock_wrlock.c

    1  #include "unpipc.h"

    2  #include "pthread_rwlock.h"


    3  int

    4  pthread_rwlock_wrlock(pthread_rwlock_t *rw)

    5  {

    6   int result;

    7   if (rw->rw_magic != RW_MAGIC)

    8    return(EINVAL);

    9   if ((result = pthread_mutex_lock(&rw->rw_mutex)) != 0)

    10   return(result);

    11  while (rw->rw_refcount != 0) {

    12   rw->rw_nwaitwriters++;

    13   result = pthread_cond_wait(&rw->rw_condwriters, &rw->rw_mutex);

    14   rw->rw_nwaitwriters--;

    15   if (result != 0)

    16    break;

    17  }

    18  if (result == 0)

    19   rw->rw_refcount = –1;

    20  pthread_mutex_unlock(&rw->rw_mutex);

    21  return(result);

    22 }

    Функция pthread_rwlock_trywrlock

    Неблокируемая функция pthread_rwlock_trywrlock показана в листинге 8.7.

    11-14 Если значение счетчика rw_refcount отлично от нуля, блокировка в данный момент уже установлена считывающим или записывающим процессом (это безразлично) и мы возвращаем ошибку с кодом EBUSY. В противном случае мы устанавливаем блокировку на запись, присвоив переменной rw_refcount значение –1.

    Листинг 8.7. Функция pthread_rwlock_trywrlock: попытка получения блокировки на запись

    //my_rwlock/pthread_rwlock_trywrlock.c

    1  #include "unpipc.h"

    2  #include "pthread_rwlock.h"


    3  int

    4  pthread_rwlock_trywrlock(pthread_rwlock_t *rw)

    5  {

    6   int result;

    7   if (rw->rw_magic != RW_MAGIC)

    8    return(EINVAL);

    9   if ((result = pthread_mutex_lock(&rw->rw_mutex)) != 0)

    10   return(result);

    11  if (rw->rw_refcount != 0)

    12   result = EBUSY; /* заблокирован пишущим потоком или ожидающим возможности записи */

    13  else

    14   rw->rw_refcount = –1; /* доступна */

    15  pthread_mutex_unlock(&rw->rw_mutex);

    16  return(result);

    17 }

    Функция pthread_rwlock_unlock

    Последняя функция, pthread_rwlock_unlock, приведена в листинге 8.8.

    Листинг 8.8. Функция pthread_rwlock_unlock: разблокирование ресурса

    //my_rwlock/pthread_rwlock_unlock.c

    1  #include "unpipc.h"

    2  #include "pthread_rwlock.h"


    3  int

    4  pthread_rwlock_unlock(pthread_rwlock_t *rw)

    5  {

    6   int result;

    7   if (rw->rw_magic != RW_MAGIC)

    8    return(EINVAL);

    9   if ((result = pthread_mutex_lock(&rw->rw_mutex)) != 0)

    10   return(result);

    11  if (rw->rw_refcount > 0)

    12   rw->rw_refcount--; /* снятие блокировки на чтение */

    13  else if (rw->rw_refcount == –1)

    14   rw->rw_refcount = 0; /* снятие блокировки на запись */

    15  else

    16   err_dump("rw_refcount = %d", rw->rw_refcount);

    17  /* преимущество отдается ожидающим возможности записи потокам */

    18  if (rw->rw_nwaitwriters > 0) {

    19   if (rw->rw_refcount == 0)

    20    result = pthread_cond_signal(&rw->rw_condwriters);

    21  } else if (rw->rw_nwaitreaders > 0)

    22   result = pthread_cond_broadcast(&rw->rw_condreaders);

    23  pthread_mutex_unlock(&rw->rw_mutex);

    24  return(result);

    25 }

    11-16 Если rw_refcount больше 0, считывающий поток снимает блокировку на чтение. Если rw_refcount равно –1, записывающий поток снимает блокировку на запись.

    17-22 Если имеются ожидающие разрешения на запись потоки, по условной переменной rw_condwriters передается сигнал (если блокировка свободна, то есть значение счетчика rw_refcount равно 0). Мы знаем, что только один поток может осуществлять запись, поэтому используем функцию pthread_cond_signal. Если нет потоков, ожидающих возможности записи, но есть потоки, ожидающие возможности чтения, мы вызываем pthread_cond_broadcast для переменной rw_condreaders, поскольку возможно одновременное считывание несколькими потоками. Обратите внимание, что мы перестаем устанавливать блокировку для считывающих потоков, если появляются потоки, ожидающие возможности записи. В противном случае постоянно появляющиеся потоки с запросами на чтение могли бы заставить поток, ожидающий возможности записи, ждать целую вечность. По этой причине мы используем два отдельных оператора if и не можем написать просто:

    /* предпочтение отдается записывающим процессам */

    if (rw->rw_nwaitreaders > 0 && rw->rw_refcount == 0)

     result = pthread_cond_signal(&rw->rw_condwriters);

    else if (rw->rw_nwaitreaders > 0)

     result = pthread_cond_broadcast(&rw->rw_condreaders);

    Мы могли бы исключить и проверку rw->rw_refcount, но это может привести к вызовам pthread_cond_signal даже при наличии блокировок на чтение, что приведет к потере эффективности.

    8.5. Отмена выполнения потоков

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

    #include <pthread.h>

    int pthread_cancel(pthread_t tid);

    /* Возвращает 0 в случае успешного завершения, положительное значение Еххх –в случае ошибки */

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

    Для обработки отмены выполнения поток может установить (push) или снять (pop) обработчик-очиститель (cleanup handler):

    #include <pthread.h>

    void pthread_cleanup_push(void (*function) (void *) void *arg);

    void pthread_cleanup_pop(int execute);

    Эти обработчики представляют собой обычные функции, которые вызываются:

    ■ в случае отмены выполнения потока (другим потоком, вызвавшим pthread_ cancel);

    ■ в случае добровольного завершения работы (вызовом pthread_exit или выходом из начальной функции потока).

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

    Аргумент function представляет собой адрес вызываемой функции, а arg — ее единственный аргумент. Функция pthread_cleanup_pop всегда удаляет обработчик из верхушки стека и вызывает эту функцию, если значение execute отлично от 0.

    ПРИМЕЧАНИЕ

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

    Пример

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

    Рис. 8.1. Временная диаграмма выполнения программы из листинга 8.9


    Создание двух потоков

    10-13 Создаются два потока, первый из которых выполняет функцию thread1, а второй — thread2. После создания первого делается пауза длительностью в одну секунду, чтобы он успел заблокировать ресурс на чтение. 

    Ожидание завершения потоков

    14-23 Мы ожидаем завершения работы второго потока и проверяем, что его статус имеет значение PTHREAD_CANCEL. Затем мы ждем завершения работы первого потока и проверяем, что его статус представляет собой нулевой указатель. Затем мы выводим значение трех счетчиков в структуре pthread_rwlock_t и уничтожаем блокировку.

    Листинг 8.9. Тестовая программа, иллюстрирующая отмену выполнения потока

    //my_rwlock_cancel/testcancel.с

    1  #include "unpipc.h"

    2  #include "pthread_rwlock.h"


    3  pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;

    4  pthread_t tid1, tid2;

    5  void *thread1(void *), *thread2(void *);


    6  int

    7  main(int argc, char **argv)

    8  {

    9   void *status;

    10  Set_concurrency(2);

    11  Pthread_create(&tid1, NULL, thread1, NULL);

    12  sleep(1); /* даем первому потоку возможность получить блокировку */

    13  Pthread_create(&tid2, NULL, thread2, NULL);

    14  Pthread_join(tid2, &status);

    15  if (status != PTHREAD_CANCELED)

    16   printf("thread2 status = %p\n", status);

    17  Pthread_join(tid1, &status);

    18  if (status != NULL)

    19   printf("thread1 status = %p\n", status);

    20  printf("rw_refcount = %d, rw_nwaitreaders = %d, rw_nwaitwriters = %d\n",

    21   rwlock.rw_refcount, rwlock.rw_nwaitreaders,

    22   rwlock.rw_nwaitwriters);

    23  Pthread_rwlock_destroy(&rwlock);

    24  exit(0);

    25 }


    26 void *

    27 thread1(void *arg)

    28 {

    29  Pthread_rwlock_rdlock(&rwlock);

    30  printf("thread1() got a read lock\n");

    31  sleep(3); /* даем второму потоку возможность заблокироваться при вызове pthread_rwlock_wrlock() */

    32  pthread_cancel(tid2);

    33  sleep(3);

    34  Pthread_rwlock_unlock(&rwlock);

    35  return(NULL);

    36 }


    37 void *

    38 thread2(void *arg)

    39 {

    40  printf("thread2() trying to obtain a write lock\n"):

    41  Pthread_rwlock_wrlock(&rwlock);

    42  printf("thread2() got a write lock\n"); /* не будет выполнено */

    43  sleep(1);

    44  Pthread_rwlock_unlock(&rwlock);

    45  return(NULL);

    46 }

    Функция thread1

    26-36 Поток получает блокировку на чтение и ждет 3 секунды. Эта пауза дает возможность другому потоку вызвать pthread_rwlock_wrlock и заблокироваться при вызове pthread_cond_wait, поскольку блокировка на запись не может быть установлена из-за наличия блокировки на чтение. Затем первый поток вызывает pthread_cancel для отмены выполнения второго потока, ждет 3 секунды, освобождает блокировку на чтение и завершает работу.

    Функция thread2

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

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

    solaris % testcancel

    thread1() got a read lock

    thread2() trying to obtain a write lock

    и мы никогда не вернемся к приглашению интерпретатора. Программа зависнет. Произошло вот что:

    1. Второй поток вызвал pthread_rwlock_wrlock (листинг 8.6), которая была заблокирована в вызове pthread_cond_wait.

    2. Первый поток вернулся из вызова slеер(3) и вызвал pthread_cancel.

    3. Второй поток был отменен и завершил работу. При отмене потока, заблокированного в ожидании сигнала по условной переменной, взаимное исключение блокируется до вызова первого обработчика-очистителя. (Мы не устанавливали обработчик, но взаимное исключение все равно блокируется до завершения потока.) Следовательно, при отмене выполнения второго потока взаимное исключение осталось заблокированным и значение rw_nwaitwriters в листинге 8.6 было увеличено.

    4. Первый поток вызывает pthread_rwlock_unlock и блокируется навсегда при вызове pthread_mutex_lock (листинг 8.8), потому что взаимное исключение все еще заблокировано отмененным потоком.

    Если мы уберем вызов pthread_rwlock_unlock в функции thread1, функция main выведет вот что:

    rw_refcount = 1, rw_nwaitreaders = 0, rw_nwaitwriters = 1

    pthread_rwlock_destroy error: Device busy
     

    Первый счетчик имеет значение 1, поскольку мы удалили вызов pthread_rwlock_ unlock, а последний счетчик имеет значение 1, поскольку он был увеличен вторым потоком до того, как тот был отменен.

    Исправить эту проблему просто. Сначала добавим две строки к функции pthread_rwlock_rdlock в листинге 8.4. Строки отмечены знаком +:

      rw->rw_nwaitreaders++;

    + pthread_cleanup_push(rwlock_cancelrdwait, (void *) rw);

      result = pthread_cond_wait(&rw->rw_condreaders, &rw->rw_mutex);

    + pthread_cleanup_pop(0);

      rw->rw_nwaitreaders++;

    Первая новая строка устанавливает обработчик-очиститель (функцию rwlock_cancelrdwait), а его единственным аргументом является указатель rw. После возвращения из pthread_cond_wait вторая новая строка удаляет обработчик. Аргумент функции pthread_cleanup_pop означает, что функцию-обработчик при этом вызывать не следует. Если этот аргумент имеет ненулевое значение, обработчик будет сначала вызван, а затем удален.

    Если поток будет отменен при вызове pthread_cond_wait, возврата из нее не произойдет. Вместо этого будут запущены обработчики (после блокирования соответствующего взаимного исключения, как мы отметили в пункте 3 чуть выше).

    В листинге 8.10 приведен текст функции rwlock_cancelrdwait, являющейся обработчиком-очистителем для phtread_rwlock_rdlock.

    Листинг 8.10. Функция rwlock_cancelrdwait: обработчик для блокировки чтения

    //my_rwlock_cancel/pthread_rwlock_rdlock.с

    3  static void

    4  rwlock_cancelrdwait(void *arg)

    5  {

    6   pthread_rwlock_t *rw;

    7   rw = arg;

    8   rw->rw_nwaitreaders--;

    9   pthread_mutex_unlock(&rw->rw_mutex);

    10 }

    8-9 Счетчик rw_nwaitreaders уменьшается, а затем разблокируется взаимное исключение. Это состояние, которое должно быть восстановлено при отмене потока.

    Аналогично мы исправим текст функции pthread_rwlock_wrlock из листинга 8.6. Сначала добавим две новые строки рядом с вызовом pthread_cond_wait:

      rw->rw_nwaitreaders++;

    + pthread_cleanup_push(rwlock_cancelrwrwait, (void*) rw);

      result = pthread_cond_wait(&rw->rw_condwriters, &rw->rw_mutex);

    + pthread_cleanup_pop(0);

      rw->rw_nwaitreaders--;

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

    Листинг 8.11. Функция rwlock_cancelwrwait: обработчик для блокировки записи

    //my_rwlock_cancel/pthread_rwlock_wrlock.с

    3  static void

    4  rwlock_cancelwrwait(void *arg)

    5  {

    6   pthread_rwlock_t *rw;

    7   rw = arg;

    8   rw->rw_nwaitwriters––;

    9   pthread_mutex_unlock(&rw->rw_mutex);

    10 }

    8-9 Счетчик rw_nwaitwriters уменьшается, и взаимное исключение разблокируется. При запуске нашей тестовой программы из листинга 8.9 с этими новыми функциями мы получим правильные результаты:

    solaris %testcancel

    thread1() got a read lock

    thread2() trying to obtain a write lock

    rw_refcount = 0, rw_nwaitreaders = 0, rw_nwaitwriters = 0

    Теперь три счетчика имеют правильные значения, первый поток возвращается из вызова pthread_rwlock_unlock, а функция pthread_rwlock_destroy не возвращает ошибку EBUSY.

    ПРИМЕЧАНИЕ

    Этот раздел представляет собой обзор вопросов, связанных с отменой выполнения потоков. Для более детального изучения этих проблем можно обратиться, например, к разделу 5.3 книги [3].

    8.6. Резюме

    Блокировки чтения-записи позволяют лучше распараллелить работу с данными, чем обычные взаимные исключения, если защищаемые данные чаще считываются, чем изменяются. Функции для работы с этими блокировками определены стандартом Unix 98, их мы и описываем в этой главе. Аналогичные или подобные им функции должны появиться в новой версии стандарта Posix. По виду функции аналогичны функциям для работы со взаимными исключениями (глава 7).

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

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

    Упражнения

    1. Измените реализацию в разделе 8.4 таким образом, чтобы приоритет имели считывающие, а не записывающие потоки.

    2. Сравните скорость работы нашей реализации из раздела 8.4 с предоставленной производителем. 

    ГЛАВА 9

    Блокирование записей

    9.1. Введение

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

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

    В этой главе мы в первый раз встретимся с нашим примером на увеличение последовательного номера. Рассмотрим следующую ситуацию, с которой столкнулись, например, разработчики спулера печати для Unix (команда lpr в BSD и lp в System V). Процесс, помещающий задания в очередь печати для последующей их обработки другим процессом, должен присваивать каждому из них уникальный последовательный номер. Идентификатор процесса, уникальный во время его выполнения, не может использоваться как последовательный номер, поскольку задание может просуществовать достаточно долго для того, чтобы этот идентификатор был повторно использован другим процессом. Процесс может также отправить на печать несколько заданий, каждому из которых нужно будет присвоить уникальный номер. Метод, используемый спулерами печати, заключается в том, чтобы хранить очередной порядковый номер задания для каждого принтера в отдельном файле. Этот файл содержит всего одну строку с порядковым номером в формате ASCII. Каждый процесс, которому нужно воспользоваться этим номером, должен выполнить следующие три действия:

    1. Считать порядковый номер из файла.

    2. Использовать этот номер.

    3. Увеличить его на единицу и записать обратно в файл.

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

    ПРИМЕЧАНИЕ

    Описанная выше проблема называется проблемой взаимных исключений. Она может быть решена с использованием взаимных исключений из главы 7 или блокировок чтения-записи из главы 8. Различие состоит в том, что здесь мы предполагаем неродственность процессов, что усложняет использование предложенных выше методов. Мы могли бы использовать разделяемую память (подробно об этом говорится в четвертой части книги), поместив в нее переменную синхронизации одного из этих типов, но для неродственных процессов проще воспользоваться блокировкой fcntl. Другим фактором в данном случае стало то, что проблема со спулерами печати возникла задолго до появления взаимных исключений, условных переменных и блокировок чтения-записи. Блокировка записей была добавлена в Unix в начале 80-х, до того как появились концепции разделяемой памяти и программных потоков.

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

    20 Каждый раз при прохождении цикла мы выводим имя программы (argv[0]) перед порядковым номером, поскольку эта функция main будет использоваться с различными версиями функций блокировки и нам бы хотелось видеть, какая версия программы выводит данную последовательность порядковых номеров.

    ПРИМЕЧАНИЕ

    Вывод идентификатора процесса требует преобразования переменной типа pid_t к типу long и последующего использования строки формата %ld. Проблема тут в том, что идентификатор процесса принадлежит к одному из целых типов, но мы не знаем, к какому именно, поэтому предполагается наиболее вместительный — long. Если бы мы предположили, что идентификатор имеет тип int и использовали бы строку %d, a pid_t на самом деле являлся бы типом long, код мог бы работать неправильно.

    Посмотрим, что будет, если не использовать блокировку. В листинге 9.1[1] приведены версии функций my_lock и my_unlock, которые вообще ничего не делают.

    Листинг 9.1. Функции, не осуществляющие блокировку

    //lock/locknone.c

    1  void

    2  my_lock(int fd)

    3  {

    4   return;

    5  }


    6  void

    7  my_unlock(int fd)

    8  {

    9   return;

    10 }

    Листинг 9.2. Функция main для примеров с блокировкой файла

    //lock/lockmain.c

    1  #include "unpipc.h"

    2  #define SEQFILE "seqno" /* имя файла */


    3  void my_lock(int), my_unlock(int);

    4  int

    5  main(int argc, char **argv)

    6  {

    7   int fd;

    8   long i, seqno;

    9   pid_t pid;

    10  ssize_t n;

    11  char line[MAXLINE + 1];

    12  pid = getpid();

    13  fd = Open(SEQFILE, O_RDWR, FILE_MODE);

    14  for (i = 0; i < 20; i++) {

    15   my_lock(fd); /* блокируем файл */

    16   Lseek(fd, 0L, SEEK_SET); /* переходим к его началу */

    17   n = Read(fd, line, MAXLINE);

    18   line[n] = '\0'; /* завершающий 0 для sscanf */

    19   n = sscanf(line, "%ld\n", &seqno);

    20   printf(%s; pid = %ld, seq# = %ld\n", argv[0], (long) pid, seqno);

    21   seqno++; /* увеличиваем порядковый номер */

    22   snprintf(line, sizeof(line), "%ld\n", seqno);

    23   Lseek(fd, 0L, SEEK_SET); /* переходим на начало перед записью */

    24   Write(fd, line, strlen(line));

    25   my_unlock(fd); /* разблокируем файл */

    26  }

    27  exit(0);

    28 }

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

    solaris % locknone

    locknone: pid = 15491, seq# = 1

    locknone: pid = 15491, seq# = 2

    locknone: pid = 15491, seq# = 3

    locknone: pid = 15491, seq# = 4

    locknone: pid = 15491. seq# = 5

    locknone: pid = 15491, seq# = 6

    locknone: pid = 15491, seq# = 7

    locknone: pid = 15491, seq# – 8

    locknone: pid = 15491, seq# = 9

    locknone: pid = 15491, seq# = 10

    locknone: pid = 15491, seq# = 11

    locknone: pid = 15491, seq# = 12

    locknone: pid = 15491, seq# = 13

    locknone: pid = 15491, seq# = 14

    locknone: pid = 15491, seq# = 15

    locknone: pid = 15491, seq# = 16

    locknone: pid = 15491, seq# = 17

    locknone: pid = 15491, seq# = 18

    locknone: pid = 15491, seq# = 19

    locknone: pid = 15491, seq# = 20

    ПРИМЕЧАНИЕ

    Обратите внимание, что функция main хранится в файле lockmain.c, но мы компилируем и компонуем эту программу с функциями, не осуществляющими никакой блокировки (листинг 9.1), поэтому мы называем ее locknone. Ниже будут использоваться другие версии функций my_lock и my_unlock, и исполняемый файл будет называться по-другому в соответствии с используемым методом блокировки.

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

    solaris % locknone & locknone&

    solaris % locknone: pid = 15498, seq# = 1

    locknone: pid = 15498, seq# = 2

    locknone: pid = 15498, seq# = 3

    locknone: pid = 15498, seq# = 4

    locknone: pid = 15498, seq# = 5

    locknone: pid = 15498, seq# = 6

    locknone: pid = 15498, seq# = 7

    locknone: pid = 15498, seq# = 8

    locknone: pid = 15498, seq# = 9

    locknone: pid = 15498, seq# = 10

    locknone: pid = 15498, seq# = 11

    locknone: pid = 15498, seq# = 12

    locknone: pid = 15498, seq# = 13

    locknone: pid = 15498, seq# = 14

    locknone: pid = 15498, seq# = 15

    locknone: pid = 15498, seq# = 16

    locknone: pid = 15498, seq# = 17

    locknone: pid = 15498, seq# = 18

    locknone: pid = 15498, seq# = 19

    locknone: pid = 15498, seq# = 20

    locknone: pid = 15499, seq# = 1

    locknone: pid = 15499, seq# = 2

    locknone: pid = 15499, seq# = 3

    locknone: pid = 15499, seq# = 4

    locknone: pid = 15499, seq# = 5

    locknone: pid = 15499, seq# = 6

    locknone: pid = 15499, seq# = 7

    locknone: pid = 15499, seq# = 8

    locknone: pid = 15499, seq# = 9

    locknone: pid – 15499, seq# = 10

    locknone: pid = 15499, seq# = 11

    locknone: pid = 15499, seq# – 12

    locknone: pid = 15499, seq# = 13

    locknone: pid = 15499, seq# = 14

    locknone: pid = 15499, seq# = 15

    locknone: pid = 15499, seq# = 16

    locknone: pid = 15499, seq# = 17

    locknone: pid = 15499, seq# = 18

    locknone: pid = 15499, seq# = 19

    locknone: pid = 15499, seq# = 20

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

    Первые двадцать строк вывода не содержат ошибок. Они были сформированы первым экземпляром программы (с идентификатором 15 498). Проблема возникает в первой строке, выведенной вторым экземпляром (идентификатор 15499): он напечатал порядковый номер 1. Получилось это, скорее всего, так: второй процесс был запущен ядром, считал из файла порядковый номер (1), а затем управление было передано первому процессу, который работал до завершения. Затем второй процесс снова получил управление и продолжил выполняться с тем значением порядкового номера, которое было им уже считано (1). Это не то, что нам нужно. Каждый процесс считывает значение, увеличивает его и записывает обратно 20 раз (на экран выведено ровно 40 строк), поэтому конечное значение номера должно быть 40.

    Нам нужно каким-то образом предотвратить изменение файла с порядковым номером на протяжении выполнения трех действий одним из процессов. Эти действия должны выполняться как атомарная операция по отношению к другим процессам. Код между вызовами my_lock и my_unlock представляет собой критическую область (глава 7).

    При запуске двух экземпляров программы в фоновом режиме результат на самом деле непредсказуем. Нет никакой гарантии, что при каждом запуске мы будем получать один и тот же результат. Это нормально, если три действия будут выполняться как одна атомарная операция; в этом случае конечное значение порядкового номера все равно будет 40. Однако при неатомарном выполнении конечное значение часто будет отличным от 40, и это нас не устраивает. Например, нам безразлично, будет ли порядковый номер увеличен от 1 до 20 первым процессом и от 21 до 40 вторым или же процессы будут по очереди увеличивать его значение на единицу. Неопределенность не делает результат неправильным, а вот атомарность выполнения операций — делает. Однако неопределенность выполнения усложняет отладку программ.

    9.2. Блокирование записей и файлов

    Ядро Unix никак не интерпретирует содержимое файла, оставляя всю обработку записей приложениям, работающим с этим файлом. Тем не менее для описания предоставляемых возможностей используется термин «блокировка записей». В действительности приложение указывает диапазон байтов файла для блокирования или разблокирования. Сколько логических записей помещается в этот диапазон — значения не имеет.

    Стандарт Posix определяет один специальный диапазон с началом в 0 (начало файла) и длиной 0 байт, который устанавливает блокировку для всего файла целиком. Мы будем говорить о блокировке записей, подразумевая блокировку файла как частный случай.

    Термин «степень детализации» (granularity) используется для описания минимального размера блокируемого объекта. Для стандарта Posix эта величина составляет 1 байт. Обычно степень детализации связана с максимальным количеством одновременных обращений к файлу. Пусть, например, с некоторым файлом одновременно работают пять процессов, из которых три считывают данные из файла и два записывают в него. Предположим также, что каждый процесс работает со своим набором записей и каждый запрос требует примерно одинакового времени для обработки (1 секунда). Если блокировка осуществляется на уровне файла (самый низкий уровень детализации), три считывающих процесса смогут работать со своими записями одновременно, а двум записывающим придется ждать окончания их работы. Затем запись будет произведена сначала одним из оставшихся процессов, а потом другим. Полное затраченное время будет порядка 3 секунд (это, разумеется, очень грубая оценка). Если же уровень детализации соответствует размеру записи (наилучший уровень детализации), все пять процессов смогут работать одновременно, поскольку они обрабатывают разные записи. При этом на выполнение будет затрачена только одна секунда.

    ПРИМЕЧАНИЕ

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

    История

    За долгие годы было разработано множество методов блокировки файлов и записей. Древние программы вроде UUCP и демонов печати играли на реализации файловой системы (три из них описаны в разделе 9.8). Они работали достаточно медленно и не подходили для баз данных, которые стали появляться в начале 80-х.

    Первый раз возможность блокировать файлы и записи появилась в Version 7, куда она была добавлена Джоном Бассом John Bass) в 1980 году в виде нового системного вызова locking. Это блокирование было обязательным (mandatory locking); его унаследовали многие версии System III и Xenix. (Разница между обязательным и рекомендательным блокированием и между блокированием записей и файлов описана далее в этой главе.)

    Версия 4.2BSD предоставила возможность блокирования файлов (а не записей) функцией flock в 1983. В 1984 году стандарт /usr/group (один из предшественников Х/Open) определил функцию lockf, которая осуществляла только исключающую блокировку (на запись), но не совместную.

    В 1984 году в System V Release 2 была добавлена возможность рекомендательной блокировки записей с помощью fcntl. Функция lockf в этой версии также имелась, но она осуществляла просто вызов fcntl. (Многие нынешние версии также реализуют lockf через вызов fcntl.) В 1986 году в версии System V Release 3 появилась обязательная блокировка записей с помощью fcntl. При этом использовался бит set-group-ID (установка идентификатора группы) — об этом методе рассказано в разделе 9.5.

    В 1988 году стандарт Posix.1 включил в себя рекомендательную и обязательную блокировку файлов и записей с помощью функции fcntl, и это именно то, что является предметом обсуждения данной главы. Стандарт X/Open Portability Guide Issue 3 (XPG3, 1988) также указывает на необходимость осуществления блокировки записей через fcntl.

    9.3. Блокирование записей с помощью fcntl по стандарту Posix

    Согласно стандарту Posix, интерфейсом для блокировки записей является функция fcntl:

    #include <fcntl.h>

    int fcntl(int fd, int cmd,… /* struct flock *arg */);

    /* Возвращает –1 в случае ошибки: результат, возвращаемый в случае успешного завершения, зависит от аргумента cmd */

    Для блокировки записей используются три различных значения аргумента cmd. Эти три значения требуют, чтобы третий аргумент, arg, являлся указателем на структуру flock:

    struct flock {

     short l_type;   /* F_RDLCK, F_WRLCK, F_UNLCK */

     short l_whence; /* SEEK_SET, SEEK_CUR, SEEK_END */

     off_t l_start;  /* относительный сдвиг в байтах */

     off_t l_len;    /* количество байтов; 0 означает до конца файла */

     pid_t l_pid;    /* PID, возвращаемый F_GETLK */

    };

    Вот три возможные команды (значения аргумента cmd ):

    ■ F_SETLK — получение блокировки (l_type имеет значение либо F_RDLCK, либо F_WRLCK) или сброс блокировки (l_type имеет значение F_UNLCK), свойства которой определяются структурой flock, на которую указывает arg. Если процесс не может получить блокировку, происходит немедленный возврат с ошибкой EACCESS или EAGAIN.

    ■ F_SETLKW — эта команда идентична предыдущей. Однако при невозможности блокирования ресурса процесс приостанавливается, до тех пор пока блокировка не сможет быть получена (W в конце команды означает «wait»).

    ■ F_GETLK — проверка состояния блокировки, на которую указывает arg. Если в данный момент блокировка не установлена, поле l_type структуры flock, на которую указывает arg, будет иметь значение F_UNLCK. В противном случае в структуре flock, на которую указывает arg, возвращается информация об установленной блокировке, включая идентификатор процесса, заблокировавшего ресурс. 

    Обратите внимание, что последовательный вызов F_GETLK и F_SETLK не является атомарной операцией. Если мы вызвали F_GETLK и она вернула значение F_UNLCK в поле l_type, это не означает, что немедленный вызов F_SETLK будет успешным. Между этими двумя вызовами другой процесс мог уже заблокировать ресурс.

    Причина, по которой была введена команда F_GETLK, — необходимость получения информации о блокировке в том случае, когда F_SETLK возвращает ошибку. Мы можем узнать, кто и каким образом заблокировал ресурс (на чтение или на запись). Но и в этом случае мы должны быть готовы к тому, что F_GETLK вернет результат F_UNLCK, поскольку между двумя вызовами другой процесс мог освободить ресурс.

    Структура flock описывает тип блокировки (чтение или запись) и блокируемый диапазон. Как и в 1 seek, начальный сдвиг представляет собой сдвиг относительно начала файла, текущего положения или конца файла, и интерпретируется в зависимости от значения поля l_whence (SEEK_SET, SEEK_CUR, SEEK_END).

    Поле l_len указывает длину блокируемого диапазона. Значение 0 соответствует блокированию от l_start до конца файла. Существуют, таким образом, два способа заблокировать файл целиком:

    1. Указать l_whence = SEEK_SET, l_start = 0 и l_len = 0.

    2. Перейти к началу файла с помощью lseek, затем указать l_whence = SEEK_CUR, l_start = 0 и l_len = 0.

    Чаще всего используется первый метод, поскольку он предусматривает единственный вызов (fcntl — см. также упражнение 9.10).

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

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

    ПРИМЕЧАНИЕ

    Снятие блокировок при завершении процесса обеспечивается только для блокировок записей fcntl и (в качестве дополнительной возможности) для семафоров System V. Для других средств синхронизации (взаимных исключений, условных переменных, блокировок чтения-записи и семафоров Posix) автоматическое снятие при завершении процесса не предусматривается. Об этом мы говорили в конце раздела 7.7. 

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

    Пример

    Вернемся к нашему примеру из листинга 9.2 и перепишем функции my_lock и my_unlock из листинга 9.1 так, чтобы воспользоваться блокировкой записей Posix. Текст этих функций приведен в листинге 9.3.

    Листинг 9.3. Блокировка записей fcntl по стандарту Posix

    //lock/lockfcntl.c

    1  #include "unpipc.h"


    2  void

    3  my_lock(int fd)

    4  {

    5   struct flock lock;

    6   lock.l_type = F_WRLCK;

    7   lock.l_whence = SEEK_SET;

    8   lock.l_start = 0;

    9   lock.l_len = 0; /* блокирование всего файла на запись */

    10  Fcntl(fd, F_SETLKW, &lock);

    11 }


    12 void

    13 my_unlock(int fd)

    14 {

    15  struct flock lock;

    16  lock.l_type = F_UNLCK;

    17  lock.l_whence = SEEK_SET;

    18  lock.l_start = 0;

    19  lock.l_len = 0; /* разблокирование всего файла */

    20  Fcntl(fd. F_SETLK, &lock);

    21 }

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

    ПРИМЕЧАНИЕ

    Зная определение структуры flock, приведенное выше, мы могли бы проинициализировать структуру my_lock как

    static struct flock lock = { F_WRLCK, SEEK_SET, 0, 0, 0 };

    но это неверно. Posix определяет только обязательные поля структуры, а реализации могут менять их порядок и добавлять к ним дополнительные.

    Мы не приводим результат работы пpoгрaммы, но она, судя по всему, работает правильно. Выполнение этой программы не дает возможности утверждать, что в ней нет ошибок. Если результат оказывается неправильным, то можно сказать с уверенностью, что что-то не так. Но успешное выполнение программы еще ни о чем не говорит. Ядро могло выполнить сначала одну программу, затем другую, и если они не выполнялись параллельно, мы не имеем возможности увидеть ошибку. Увеличить шансы обнаружения ошибки можно, изменив функцию main таким образом, чтобы последовательный номер увеличивался 10000 раз, и запустив 20 экземпляров программы одновременно. Если начальное значение последовательного номера в файле было 1, мы можем ожидать, что после завершения работы всех этих процессов мы увидим в файле число 200001.

    Пример: упрощение с помощью макросов

    В листинге 9.3 установка и снятие блокировки занимали шесть строк кода. Мы должны выделить место под структуру, инициализировать ее и затем вызвать fcntl. Программы можно упростить, если определить следующие семь макросов, которые взяты из раздела 12.3 [21]:

    #define read_lock(fd, offset, whence, len) \

     lock_reg(fd, F_SETLK, F_RDLCK, offset, whence, len)

    #define readw_lock(fd, offset, whence, len) \

     lock_reg(fd, F_SETLKW, F_RDlCK, offset, whence, len)

    #define write_lock(fd, offset, whence, len) \

     lock_reg(fd, F_SETLK, F_WRLCK, offset, whence, len)

    #define writew_lock(fd, offset, whence, len) \

     lock_reg(fd, F_SETLKW, F_WRLCK, offset, whence, len)

    #define un_lock(fd, offset, whence, len) \

     lock_reg(fd, F_SETLK, F_UNLCK, offset, whence, len)

    #define is_read_lockable(fd, offset, whence, len) \

     lock_test(fd, F_RDLCK, offset, whence, len)

    #define is_write_lockable(fd, offset, whence, len) \

     lock_test(fd, F_WRLCK, offset, whence, len)

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

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

    С помощью новых макросов мы можем записать функции my_lock и my_unlock из листинга 9.3 как

    #define my_lock(fd) (Writew_lock(fd, 0, SEEK_SET, 0))

    #define my_unlock(fd) (Un_lock(fd, 0, SEEK_SET, 0))

    Листинг 9.4. Вызов fcntl для получения и снятия блокировки

    //lib/lock_reg.c

    1  #include "unpipc.h"


    2  int

    3  lock_reg(int fd, int cmd, int type, off_t offset, int whence, off_t len)

    4  {

    5   struct flock lock;

    6   lock.l_type = type; /* F_RDLCK, F_WRLCK, F_UNLCK */

    7   lock.l_start = offset; /* сдвиг по отношению к l_whence */

    8   lock.l_whence = whence; /* SEEK SET. SEEK CUR, SEEK END */

    9   lock.l_len = len; /* количество байтов (0 – до конца файла) */

    10  return(fcnt(fd, cmd, &lock)"); /* –1 в случае ошибки */

    11 }

    Листинг 9.5. Вызов fcntl для проверки состояния блокировки

    //lib/lock_test.c

    1  #include "unpipc.h"


    2  pid_t

    3  lock_test(int fd, int type, off_t offset, int whence, off_t len)

    4  {

    5   struct flock lock;

    6   lock.l_type = type; /* F_RDLCK or F_WRLCK */

    7   lock.l_start = offset; /* сдвиг по отношению к l_whence */

    8   lock.l_whence = whence; /* SEEK_SET, SEEK_CUR, SEEK_END */

    9                      lock.l_len = len; /* количество байтов. 0 – до конца файла */

    10 if (fcntl(fd, F_GETLK, &lock) == –1)

    11 return(-1); /* непредвиденная ошибка */

    12 if (lock.l_type == F_UNLCK)

    13 return(0); /* false, область не заблокирована другим процессом */

    14 return(lock.l_pid); /* true, возвращается положительный PID процесса. заблокировавшего ресурс */

    15 }

    9.4. Рекомендательная блокировка

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

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

    Пример: несотрудничающие процессы

    Мы можем проиллюстрировать рекомендательный характер блокировок, запустив два экземпляра нашей программы, один из которых (lockfcntl) использует функции из листинга 9.3 и блокирует файл перед увеличением последовательного номера, а другой (locknone) использует функции из листинга 9.1 и не устанавливает никаких блокировок:

    solaris % lockfcntl & locknone &

    lockfcntl: pid = 18816, seq# = 1

    lockfcntl: pid = 18816, seq# = 2

    lockfcntl: pid = 18816, seq# = 3

    lockfcntl: pid = 18816, seq# = 4

    lockfcntl: pid = 18816, seq# = 5

    lockfcntl: pid = 18816, seq# = 6

    lockfcntl: pid = 18816, seq# = 7

    lockfcntl: pid = 18816, seq# = 8

    lockfcntl: pid = 18816, seq# = 9

    lockfcntl: pid = 18816, seq# = 10

    lockfcntl: pid = 18816, seq# = 11

    locknone: pid = 18817, seq# = 11

    locknone: pid = 18817, seq# = 12

    locknone: pid = 18817, seq# = 13

    locknone: pid = 18817, seq# = 14

    locknone: pid = 18817, seq# = 15

    locknone: pid = 18817, seq# = 16

    locknone: pid = 18817, seq# = 17

    locknone: pid = 18817, seq# = 18

    lockfcntl: pid = 18816, seq# = 12

    lockfcntl: pid = 18816, seq# = 13

    lockfcntl: pid = 18816, seq# = 14

    lockfcntl: pid = 18816, seq# = 15

    lockfcntl: pid = 18816, seq# = 16

    lockfcntl: pid = 18816, seq# = 17

    lockfcntl: pid = 18816, seq# = 18

    lockfcntl: pid = 18816, seq# = 19

    lockfcntl: pid = 18816, seq# = 20

    locknone: pid = 18817, seq# = 19

    locknone: pid = 18817, seq# = 20

    locknone: pid = 18817, seq# = 21

    locknone: pid = 18817, seq# = 22

    locknone: pid = 18817, seq# = 23

    locknone: pid = 18817, seq# = 24

    locknone: pid = 18817, seq# = 25

    locknone: pid = 18817, seq# = 26

    locknone: pid = 18817, seq# = 27

    locknone: pid = 18817, seq# = 28

    locknone: pid = 18817, seq# = 29

    locknone: pid = 18817, seq# = 30

    Программа lockfcntl запускается первой, но в тот момент, когда она выполняет три действия для увеличения порядкового номера с 11 до 12 (в этот момент файл заблокирован), ядро переключается на второй процесс и запускает пpoгрaмму locknone. Этот процесс считывает значение 11 из файла с порядковым номером и использует его. Рекомендательная блокировка, установленная для этого файла пpoгрaммoй lockfcntl, никак не влияет на работу программы locknone.

    9.5. Обязательная блокировка

    Некоторые системы предоставляют возможность установки блокировки другого типа — обязательной (mandatory locking). В этом случае ядро проверяет все вызовы read и write, блокируя их при необходимости. Если для дескриптора установлен флаг O_NONBLOCK, вызов read или write, конфликтующий с установленной блокировкой, вернет ошибку EAGAIN. Если флаг O_NONBLOCK не установлен, выполнение процесса в такой ситуации будет отложено до тех пор, пока ресурс не освободится.

    ПРИМЕЧАНИЕ

    Стандарты Posix.1 и Unix 98 определяют только рекомендательную блокировку. Во многих реализациях, производных от System V, имеется возможность установки как рекомендательной, так и обязательной блокировки. Обязательная блокировка записей впервые появилась в System V Release 3.

    Для установки обязательной блокировки какого-либо файла требуется выполнение двух условий:

    ■ бит group-execute должен быть снят;

    ■ бит set-group–ID должен быть установлен.

    Обратите внимание, что установка бита set-user– ID без установки user-execute смысла не имеет; аналогично и с битами set-group-ID и group-execute. Таким образом, добавление возможности обязательной блокировки никак не повлияло на работу используемого программного обеспечения. Не потребовалось и добавлять новые системные вызовы.

    В системах, поддерживающих обязательную блокировку записей, команда ls просматривает файлы на наличие указанной специальной комбинации битов и выводит буквы l или L, указывающие на то, что для данного файла включена обязательная блокировка. Аналогично команда chmod принимает аргумент l, позволяющий включить для указанного файла обязательную блокировку.

    Пример

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

    Чтобы использовать в нашем примере обязательную блокировку, изменим биты разрешений файла seqno. Кроме того, мы будем использовать новую версию функции main, которая принимает количество проходов цикла for в качестве аргумента командной строки (вместо использования константы 20) и не вызывает printf при каждом проходе цикла:

    solaris % cat > seqno    инициализируем файл единицей

    1

    ^D                      конец файла

    solaris % ls –l seqno

    -rw-r--r-- 1 rstevens other1 2 Oct 7 11:24 seqno

    solaris % chmod +l seqno включение обязательной блокировки

    solaris % ls -l seqno

    -rq-r-lr-- 1 rstevens other1 2 Oct 7 11:24 seqno

    Теперь запустим две программы в качестве фоновых процессов: loopfcntl использует блокировку записей fcntl, а loopnone не использует блокировку вовсе. 

    Укажем в командной строке аргумента 10000 — количество последовательных увеличений порядкового номера.

    solaris % loopfcntl 10000 & loopnone 10000 & запуск фоновых процессов

    solaris % wait                                   ожидание их завершения

    solaris % cat seqno                             вывод последовательного номера

    14378                                            ошибка, должно быть 20001

    Рис. 9.1. Временная диаграмма работы программ loopfcntl и loopnone


    Каждый раз при выполнении этих программ результат будет между 14000 и 16000. Если бы блокировка работала так как надо, он всегда был бы равен 20001. Чтобы понять, где же возникает ошибка, нарисуем временную диaгрaммy выполнения процессов, изображенную на рис. 9.1. 

    Предположим, что loopfcntl запускается первой и выполняет первые восемь действий, изображенных на рисунке. Затем ядро передает управление другому процессу в тот момент, когда установлена блокировка на файл с порядковым номером. Запускается процесс loopnone, но он блокируется в первом вызове read, потому что на файл, который он пытается читать, установлена обязательная блокировка. Затем ядро передает управление первому процессу, который выполняет шаги 13-15. Пока все работает именно так, как мы предполагали, — ядро защищает файл от чтения несотрудничающим процессом, когда этот файл заблокирован.

    Дальше ядро передает управление программе loopnone, которая выполняет шаги 17-23. Вызовы read и write разрешены, поскольку файл был разблокирован на шаге 15. Проблема возникает в тот момент, когда программа считывает значение 5 на шаге 23, а ядро в этот момент передает управление другому процессу. Он устанавливает блокировку и также считывает значение 5. Затем он дважды увеличивает это значение (получается 7), и управление передается loopnone на шаге 36. Однако эта программа записывает в файл значение 6. Так и возникает ошибка.

    На этом примере мы видим, что обязательная блокировка предотвращает доступ к заблокированному файлу (шаг 11), но это не решает проблему. Проблема заключается в том, что левый процесс (на рисунке) может обновить содержимое файла (шаги 25-34) в тот момент, когда процесс справа также находится в состоянии обновления данных (шаги 23, 36 и 37). Если файл обновляется несколькими процессами, все они должны сотрудничать, используя некоторую форму блокировки. Один неподчиняющийся процесс может все разрушить.

    9.6. Приоритет чтения и записи

    В нашей реализации блокировок чтения-записи в разделе 8.4 приоритет предоставлялся ожидающим записи процессам. Теперь мы изучим детали возможного решения задачи читателей и писателей с помощью блокировки записей fcntl. Хочется узнать, как обрабатываются накапливающиеся запросы на блокировку, когда ресурс уже заблокирован. Стандарт Posix этого никак не оговаривает.

    Пример: блокировка на чтение при наличии в очереди блокировки на запись

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

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

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

    Рис. 9.2. Определение возможности установки блокировки на чтение при наличиивочереди блокировки на запись


    Листинг 9.6. Определение возможности установки блокировки на чтение при наличии в очереди блокировки на запись

    //lock/test2.c

    1  #include "unpipc.h"


    2  int

    3  main(int argc, char **argv)

    4  {

    5   int fd;

    6   fd = Open("test1.data", O_RDWR | O_CREAT, FILE_MODE);

    7   Read_lock(fd, 0, SEEK_SET, 0); /* родительский процесс блокирует весь файл на чтение */

    8   printf("%s: parent has read lock\n", Gf_time());

    9   if (Fork() == 0) {

    10   /* первый дочерний процесс */

    11   sleep(1);

    12   printf("%s: first child tries to obtain write lock\n", Gf_time());

    13   Writew_lock(fd, 0, SEEK_SET, 0); /* здесь он будет заблокирован */

    14   printf("%s: first child obtains write lock\n", Gf_time());

    15   sleep(2);

    16   Un_lock(fd, 0, SEEK_SET, 0);

    17   printf("ls: first child releases write lock\n", Gf_time());

    18   exit(0);

    19  }

    20  if (Fork() == 0) {

    21   /* второй дочерний процесс */

    22   sleep(3);

    23   printf("%s: second child tries to obtain read lock\n", Gf_time());

    24   Readw_lock(fd, 0, SEEK_SET, 0);

    25   printf("%s: second child obtains read lock\n", Gf_time());

    26   sleep(4);

    27   Un_lock(fd, 0, SEEK_SET, 0);

    28   printf("%s: second child releases read lock\n", Gf_time());

    29   exit(0);

    30  }

    31  /* родительский процесс */

    32  sleep(5);

    33  Un_lock(fd, 0, SEEK_SET, 0);

    34  printf("%s: parent releases read lock\n", Gf_time());

    35  exit(0);

    36 }

    Родительский процесс открывает файл и получает блокировку на чтение

    6-8 Родительский процесс открывает файл и устанавливает блокировку на чтение для всего файла целиком. Обратите внимание, что мы вызываем read_lock (которая возвращает ошибку в случае недоступности ресурса), а не readw_lock (которая ждет его освобождения), потому что мы ожидаем, что эта блокировка будет установлена немедленно. Мы также выводим значение текущего времени функцией gf_time [24, с. 404], когда получаем блокировку.

    Первый дочерний процесс

    9-19 Порождается первый процесс, который ждет 1 секунду и блокируется в ожидании получения блокировки на запись для всего файла. Затем он устанавливает эту блокировку, ждет 2 секунды, снимает ее и завершает работу.

    Второй дочерний процесс

    20-30 Порождается второй процесс, который ждет 3 секунды, давая возможность первому попытаться установить блокировку на запись, а затем пытается получить блокировку на чтение для всего файла. По моменту возвращения из функции readw_lock мы можем узнать, был ли ресурс предоставлен немедленно или второму процессу пришлось ждать первого. Блокировка снимается через четыре секунды.

    Родительский процесс блокирует ресурс 5 секунд

    31-35 Родительский процесс ждет пять секунд, снимает блокировку и завершает работу.

    На рис. 9.2 приведена временная диаграмма выполнения программы в Solaris 2.6, Digital Unix 4.0B и BSD/OS 3.1. Как видно, блокировка чтения предоставляется второму дочернему процессу немедленно, несмотря на наличие в очереди запроса на блокировку записи. Существует вероятность, что запрос на запись так и не будет выполнен, если будут постоянно поступать новые запросы на чтение. Ниже приведен результат выполнения программы, в который были добавлены пустые строки для улучшения читаемости:

    alpha % test2

    16:32:29.674453: parent has read lock


    16:32:30.709197: first child tries to obtain write lock


    16:32:32.725810: second child tries to obtain read lock

    16:32:32.728739: second child obtains read lock


    16:32:34.722282: parent releases read lock


    16:32:36.729738: second child releases read lock

    16:32:36.735597: first child obtains write lock


    16:32:38.736938: first child releases write lock

    Пример: имеют ли приоритет запросы на запись перед запросами на чтение?

    Следующий вопрос, на который мы попытаемся дать ответ, таков: есть ли приоритет у запросов на блокировку записи перед запросами на блокировку чтения, если все они находятся в очереди? Некоторые решения задачи читателей и писателей предусматривают это.

    В листинге 9.7 приведен текст нашей тестовой программы, а на рис. 9.3 — временная диаграмма ее выполнения.

    Листинг 9.7. Есть ли у писателей приоритет перед читателями

    //lock/test3.c

    1  #include "unpipc.h"


    2  int

    3  main(int argc, char **argv)

    4  {

    5   int fd;

    6   fd = Open("test1.data", O_RDWR | O_CREAT, FILE_MODE);

    7   Write_lock(fd, 0, SEEK_SET, 0); /* родительский процесс блокирует весь файл на запись */

    8   printf("ls: parent has write lock\n", Gf_time());

    9   if (Fork() == 0) {

    10   /* первый дочерний процесс */

    11   sleep(1);

    12   printf("ls: first child tries to obtain write lock\n", Gf_time());

    13   Writew_lock(fd, 0, SEEK_SET, 0); /* здесь процесс будет заблокирован */

    14   printf("%s: first child obtains write lock\n", Gf_time());

    15   sleep(2);

    16   Un_lock(fd, 0, SEEK_LET, 0);

    17   printf("ls: first child releases write lock\n", Gf_time());

    18   exit(0);

    19  }

    20  if (Fork() == 0) {

    21   /* второй дочерний процесс */

    22   sleep(3);

    23   printf("ls: second child tries to obtain read lock\n", Gf_time());

    24   Readw_lock(fd, 0, SEEK_SET, 0);

    25   printf(%s: second child obtains read lock\n", Gf_time());

    26   sleep(4);

    27   Un_lock(fd, 0, SEEK_SET, 0);

    28   printf("ls: second child releases read lock\n", Gf_time());

    29   exit(0);

    30  }

    31  /* родительский процесс */

    32  sleep(5);

    33  Un_lock(fd, 0, SEEK_SET, 0);

    34  printf("ls: parent releases write lock\n", Gf_time());

    35  exit(0);

    36 }

    Родительский процесс создает файл и устанавливает блокировку на запись

    6-8 Родительский процесс создает файл и блокирует его целиком на запись.

    Первый дочерний процесс

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

    Второй дочерний процесс

    20-30 Порождается второй процесс, который ждет три секунды, а затем запрашивает блокировку на чтение на весь файл. Этот запрос будет также помещен в очередь.

    И в Solaris 2.6, и в Digital Unix 4.0B мы видим, что блокировка на запись предоставляется первому процессу, как изображено на рис. 9.3. Но это еще не означает, что у запросов на запись есть приоритет перед запросами на чтение, поскольку, возможно, ядро предоставляет блокировку в порядке очереди вне зависимости от того, на чтение она или на запись. Чтобы проверить это, мы создаем еще одну тестовую программу, практически идентичную приведенной в листинге 9.7, но в ней блокировка на чтение запрашивается через одну секунду, а блокировка на запись — через три секунды. Эти две программы иллюстрируют, что Solaris и Digital Unix обрабатывают запросы в порядке очереди вне зависимости от типа запроса. Однако в BSD/OS 3.1 приоритет имеют запросы на чтение. 

    Рис. 9.3. Есть ли у писателей приоритет перед читателями


    Вот вывод программы из листинга 9.7, на основании которого была составлена временная диaгрaммa на рис. 9.3:

    alpha % test3

    16:34:02.810285: parent has write lock

    16:34:03.848166: first child tries to obtain write lock

    16:34:05.861082: second child tries to obtain read lock

    16:34:07.858393: parent releases write lock

    16:34:07.865222: first child obtains write lock

    16:34:09.865987: first child releases write lock

    16:34:09.872823: second child obtains read lock

    16:34:13.873822: second child releases read lock

    9.7. Запуск единственного экземпляра демона

    Часто блокировки записей используются для обеспечения работы какой-либо пpoгрaммы (например, демона) в единственном экземпляре. Фрагмент кода, приведенный в листинге 9.8, должен выполняться при запуске демона.

    Листинг 9.8. Гарантия выполнения единственного экземпляра программы

    //lock/onedaemon.c

    1  #include "unpipc.h"

    2  #define PATH_PIDFILE "pidfile"


    3  int

    4  main(int argc, char **argv)

    5  {

    6   int pidfd;

    7   char line[MAXLINE];

    8   /* открытие или создание файла с идентификатором процесса */

    9   pidfd = Open(PATH_PIDFILE, O_RDWR | O_CREAT, FILE_MODE);

    10  /* попытка блокирования всего файла на запись */

    11  if (write_lock(pidfd, 0, SEEK_SET, 0) < 0) {

    12   if (errno == EACCES || errno == EAGAIN)

    13    err_quit("unable to lock %s, is %s already running?",

    14     PATH_PIDFILE, argv[0]);

    15   else

    16    err_sys("unable to lock %s", PATH_PIDFILE):

    17  }

    18  /* запись идентификатора: файл остается открытым, чтобы он был заблокирован */

    19  snprintf(line, sizeof(line), "%ld\n", (long) getpid());

    20  Ftruncate(pidfd, 0);

    21  Write(pidfd, line, strlen(line));

    22  /* основной текст программы демона… */

    23  pause();

    24 }

    Открытие и блокирование файла

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

    ПРИМЕЧАНИЕ

    Во многих версиях Unix демоны записывают свои идентификаторы в файл. Solaris 2.6 хранит подобные файлы в каталоге /etc, a Digital Unix 4.0B и BSD/OS — в каталоге /var/run.

    Запись идентификатора процесса в файл

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

    Вот результат работы программы из листинга 9.8:

    solaris % onedaemon&   запускаем первый экземпляр

    [1] 22388

    solaris % cat pidfile проверяем идентификатор

    22388

    solaris % onedaemon   пытаемся запустить второй экземпляр

    unable to lock pidfile, is onedaemon already running?

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

    9.8. Блокирование файлов

    Стандарт Posix.1 гарантирует, что если функция open вызывается с флагами O_CREAT (создать файл, если он еще не существует) и O_EXCL (исключающее открытие), функция возвращает ошибку, если файл уже существует. Более того, проверка существования файла и его создание (если он еще не существует) должны представлять собой атомарную по отношению к другим процессам операцию. Следовательно, мы можем использовать создаваемый таким методом файл как блокировку. Можно быть уверенным, что только один процесс сможет создать файл (то есть получить блокировку), а для снятия этой блокировки файл можно удалить командой unlink.

    В листинге 9.9 приведен текст наших функций установки и снятия блокировки, использующих этот метод. При успешном выполнении функции open мы считаем, что блокировка установлена, и успешно возвращаемся из функции my_lock. Файл мы закрываем, потому что его дескриптор нам не нужен. О наличии блокировки свидетельствует само существование файла вне зависимости от того, открыт он или нет. Если функция open возвращает ошибку EEXIST, значит, файл существует и мы должны еще раз попытаться открыть его. 

    У этого метода есть три недостатка.

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

    В такой ситуации лучше пользоваться блокировкой fcntl, которая автоматически снимается по завершении процесса.

    2. Если файл открыт каким-либо другим процессом, мы должны еще раз вызвать open, повторяя эти вызовы в бесконечном цикле. Это называется опросом и является напрасной тратой времени процессора. Альтернативным методом является вызов sleep на 1 секунду, а затем повторный вызов open (этапроблема обсуждалась в связи с листингом 7.4).

    Эта проблема также исчезает при использовании блокировки fcntl, если использовать команду F_SETLKW. Ядро автоматически приостанавливает выполнение процесса до тех пор, пока ресурс не станет доступен.

    3. Создание и удаление файла вызовом open и unlink приводит к обращению к файловой системе, что обычно занимает существенно больше времени, чем вызов fcntl (обращение производится дважды: один раз для получения блокировки, а второй — для снятия). При использовании fcntl программа выполнила 10000 повторов цикла с увеличением порядкового номера в 75 раз быстрее, чем программа, вызывавшая open и unlink.

    Листинг 9.9. Функции блокировки с использованием open с флагами O_CREAT и O_EXCL

    //lock/lockopen.c

    1  #include "unpipc.h"

    2  #define LOCKFILE "/tmp/seqno.lock"


    3  void

    4  my_lock(int fd)

    5  {


    6   int tempfd;

    7   while ((tempfd = open(LOCKFILE, O_RDWR|O_CREAT|O_EXCL, FILE_MODE)) < 0) {

    8    if (errno != EEXIST)

    9     err_sys("open error for lock file");

    10   /* блокировка установлена кем-то другим, повторяем попытку */

    11  }

    12  Close(tempfd); /* открыли файл, блокировка установлена */

    13 }


    14 void

    15 my_unlock(int fd)

    16 {

    17  Unlink(LOCKFILE); /* снимаем блокировку удалением файла */

    18 }

    Есть еще две особенности файловой системы Unix, которые использовались для реализации блокировок. Первая заключается в том, что функция link возвращает ошибку, если имя новой ссылки уже существует. Для получения блокировки создается уникальный временный файл, полное имя которого содержит в себе его идентификатор процесса (или комбинацию идентификаторов процесса и потока, если требуется осуществлять блокировку между отдельными потоками). Затем вызывается функция link для создания ссылки на этот файл с каким-либо определенным заранее именем. После успешного создания сам файл может быть удален вызовом unlink. После осуществления работы с блокировкой файл с известным именем удаляется командой unlink. Если link возвращает ошибку EEXIST, поток должен попытаться создать ссылку еще раз (аналогично листингу 9.9). Одно из требований к этому методу — необходимо, чтобы и временный файл, и ссылка находились в одной файловой системе, поскольку большинство версий Unix не допускают создания жестких ссылок (результат вызова link) в разных файловых системах.

    Вторая особенность заключается в том, что функция open возвращает ошибку в случае существования файла, если указан флаг O_TRUNC и запрещен доступ на запись. Для получения блокировки мы вызываем open, указывая флаги O_CREAT | O_WRONLY | O_TRUNC и аргумент mode со значением 0 (то есть разрешения на доступ к файлу установлены в 0). Если вызов оказывается успешным, блокировка установлена и мы просто удаляем файл вызовом unlink после завершения работы. Если вызов open возвращает ошибку EACESS, поток должен сделать еще одну попытку (аналогично листингу 9.9). Этот трюк не срабатывает, если поток обладает правами привилегированного пользователя.

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

    9.9. Блокирование в NFS

    Аббревиатура NFS расшифровывается как Network File System (сетевая файловая система); эта система подробно обсуждается в главе 29 [22]. Блокировка записей fcntl представляет собой расширение NFS, поддерживаемое большинством ее реализаций. Обслуживается эта блокировка двумя дополнительными демонами: lockd и statd. При вызове fcntl для получения блокировки ядро обнаруживает, что файл находится в файловой системе NFS. Тогда локальный демон lockd посылает демону lockd сервера запрос на получение блокировки. Демон statd хранит информацию о клиентах, установивших блокировку, и взаимодействует с lockd для обеспечения снятия блокировок в случае завершения процессов.

    Установка блокировки записи в NFS должна занимать в среднем больше времени, чем для локального файла, поскольку для установки и снятия блокировки требуется передача информации по сети. Для проверки работы блокировки NFS нужно всего лишь изменить имя файла, определяемое константой SEQFILE в листинге 9.2. Если измерить время, требуемое для выполнения 10000 операций по увеличению порядкового номера новой версией программы, оно окажется примерно в 80 раз больше, чем для локального файла. Однако нужно понимать, что в этом случае происходит передача информации по сети и при операциях чтения и записи (для изменения порядкового номера).

    ПРИМЕЧАНИЕ

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

    9.10. Резюме

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

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

    Упражнения

    1. Создайте программу locknone из листингов 9.2 и 9.1 и выполните ее много раз. Убедитесь, что программа не работает и результат непредсказуем.

    2. Измените листинг 9.2 так, чтобы стандартный поток вывода не буферизовался. Как это повлияет на работу программы?

    3. Продолжайте изменять программу, вызывая putchar для каждого выводимого символа (вместо printf). Как изменится результат?

    4. Измените блокировку в функции my_lock из листинга 9.3 так, чтобы устанавливалась блокировка на чтение, а не на запись. Что произойдет?

    5. Измените вызов open в программе loopmain.c, указав также флаг O_NONBLOCK. Создайте программу loopfcntlnonb и запустите два экземпляра. Что произойдет? 

    6. Продолжите предыдущий пример, используя неблокируемую версию loopmain.с для создания программы loopnonenonb (используя файл locknone.c). Включите обязательную блокировку для файла seqno. Запустите один экземпляр этой программы и один экземпляр программы loopfcntlnonb из предыдущего примера одновременно. Что произойдет?

    7. Создайте программу loopfcntl и запустите ее 10 раз в фоновом режиме из сценария интерпретатора команд. Каждому из 10 экземпляров следует указать аргумент 10000. Измерьте скорость работы сценария при использовании обязательной и рекомендательной блокировок. Как влияет обязательная блокировка на производительность?

    8. Почему мы вызывали fork в листингах 9.6 и 9.7 для порождения процессов, вместо того чтобы воспользоваться pthread_create для создания потоков?

    9. В листинге 9.9 мы вызываем ftruncate для установки размера файла в 0 байт. Почему бы нам просто не указать флаг O_TRUNC при открытии файла?

    10. Какой из констант — SEEK_SET, SEEK_CUR или SEEK_END — следует пользоваться при указании блокируемого диапазона при написании многопоточного приложения и почему? 

    ГЛАВА 10

    Семафоры Posix

    10.1.Введение

    Семафор представляет собой простейшее средство синхронизации процессов и потоков. Мы рассматриваем три типа семафоров:

    ■ именованные семафоры Posix, идентифицируемые именами, соответствующими стандарту Posix для IPC (см. раздел 2.2);

    ■ размещаемые в разделяемой памяти семафоры Posix;

    ■ семафоры System V (глава 11), обслуживаемые ядром.

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

    Рис. 10.1. Два процесса взаимодействуют с помощью бинарного семафора


    На этом рисунке изображен бинарный семафор, хранящийся в ядре (семафор System V).

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

    Рис. 10.2. Два процесса, использующие бинарный именованный семафор Posix


    ПРИМЕЧАНИЕ

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

    На рис. 10.1 и 10.2 мы указали три операции, которые могут быть применены к семафорам:

    1. Создание семафора. При этом вызвавший процесс должен указать начальное значение (часто 1, но может быть и 0).

    2. Ожидание изменения значения семафора (wait). При этом производится проверка его значения и процесс блокируется, если значение оказывается меньшим либо равным 0, а при превышении 0 значение уменьшается на 1. Это может быть записано на псевдокоде как

    while (semaphore_value <= 0); /* wait: т.е. поток или процесс блокируется */

    semaphore_value--; /* семафор разрешает выполнение операций */

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

    У этой операции есть несколько общеупотребительных имен. Изначально она называлась Р, от голландского proben (проверка, попытка), — это название было введено Эдсгером Дейкстрой. Используются также и термины down (поскольку значение семафора уменьшается) и lock, но мы будем следовать стандарту Posix и говорить об ожидании (wait). 

    3. Установка значения семафора (post). Значение семафора увеличивается одной командой, которая может быть записана на псевдокоде как

    semaphore_value++;

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

    Для этой операции также имеется несколько общеупотребительных терминов. Изначально она называлась V, от голландского verhogen (увеличивать). Называют ее up (значение семафора увеличивается), unlock и signal. Мы, следуя стандарту Posix, называем эту операцию post.

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

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

    ПРИМЕЧАНИЕ

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

    Бинарный семафор может использоваться в качестве средства исключения (подобно взаимному исключению). В листинге 10.1 приведен пример для сравнения этих средств.

    Листинг 10.1. Сравнение бинарных семафоров и взаимных исключений

    инициализация взаимного исключения;  инициализация семафора единицей;

    pthread_mutex_lock(&mutex);          sem_wait(&sem);

    критическая область                  критическая область

    pthread_mutex_unlock(&mutex);        sem_post(&sem);

    Мы инициализируем семафор значением 1. Вызвав sem_wait, мы ожидаем, когда значение семафора окажется больше 0, а затем уменьшаем его на 1. Вызов sem_post увеличивает значение с 0 до 1 и возобновляет выполнение всех потоков, заблокированных в вызове sem_wait для данного семафора.

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

    Рис. 10.3. Задача производителя и потребителя с общим буфером


    В листинге 10.2 приведен текст соответствующей программы на псевдокоде.

    Листинг 10.2. Псевдокод для задачи производителя и потребителя

    Producer                                Consumer

    инициализация семафора get значением 0;

    инициализация семафора put значением 1;

    for (;;) {                              for (;;) {

     sem_wait(&put);                         sem_wait(&get);

     помещение данных в буфер;               обработка данных в буфере;

     sem_post(&get);                         sem_post(&put);

    }                                       }

    Семафор put oгрaничивaeт возможность помещения объекта в общий буфер, а семафор get управляет потребителем при считывании объекта из буфера. Работает эта пpoгрaммa в такой последовательности:

    1. Производитель инициализирует буфер и два семафора.

    2. Пусть после этого запускается потребитель. Он блокируется при вызове sem_wait, поскольку семафор get имеет значение 0.

    3. После этого запускается производитель. При вызове sem_wait значение put уменьшается с 1 до 0, после чего производитель помещает объект в буфер. Вызовом sem_post значение семафора get увеличивается с 0 до 1. Поскольку имеется поток, заблокированный в ожидании изменения значения этого семафора, этот поток помечается как готовый к выполнению. Предположим, тем не менее, что производитель продолжает выполняться. В этом случае он блокируется при вызове sem_wait в начале цикла for, поскольку значение семафора put — 0. Производитель должен подождать, пока потребитель не извлечет данные из буфера.

    4. Потребитель возвращается из sem_wait, уменьшая значение семафора get с 0 до 1. Затем он обрабатывает данные в буфере и вызывает sem_post, увеличивая значение put с 0 до 1. Заблокированный в ожидании изменения значения этого семафора поток-производитель помечается как готовый к выполнению. Предположим опять, что выполнение потребителя продолжается. Тогда он блокируется при вызове sem_wait в начале цикла for, поскольку семафор get имеет значение 0.

    5. Производитель возвращается из sem_wait, помещает данные в буфер, и все повторяется.

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

    Перечислим три главных отличия семафоров и взаимных исключений в паре с условными переменными:

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

    2. Взаимное исключение может быть либо заблокировано, либо разблокировано (пара состояний, аналогично бинарному семафору).

    3. Поскольку состояние семафора хранится в определенной переменной, изменение его значения оказывает влияние на процессы, которые вызовут функцию wait уже после этого изменения, тогда как при отправке сигнала по условной переменной в отсутствие ожидающих его потоков сигнал будет утерян. Взгляните на листинг 10.2 и представьте, что при первом проходе цикла производителем потребитель еще не вызвал sem_wait. Производитель сможет поместить объект в буфер, вызвать sem_post для семафора get (увеличивая его значение с 0 до 1), а затем он заблокируется в вызове sem_wait для семафора put. Через некоторое время потребитель дойдет до цикла for и вызовет sem_wait для переменной get, что уменьшит значение этого семафора с 1 до 0, а затем потребитель приступит к обработке содержимого буфера.

    ПРИМЕЧАНИЕ

    В Обосновании Posix.1 (Rationale) содержится следующий комментарий по поводу добавления семафоров помимо взаимных исключений и условных переменных: «Семафоры включены в стандарт в первую очередь с целью предоставить средства синхронизации выполнения процессов; эти процессы могут и не использовать общий сегмент памяти. Взаимные исключения и условные переменные описаны как средства синхронизации потоков, у которых всегда есть некоторое количество общей памяти. Оба метода широко используются уже много лет. Каждое из этих простейших средств имеет свой предпочтительный круг задач». В разделе 10.15 мы увидим, что для реализации семафоров-счетчиков с живучестью ядра требуется написать около 300 строк кода на С, использующего взаимные исключения и условные переменные. Несмотря на предпочтительность применения семафоров для синхронизации между процессами и взаимных исключений для синхронизации между потоками, и те и другие могут использоваться в обоих случаях. Следует пользоваться тем набором средств, который удобен в данном приложении.

    Выше мы отмечали, что стандартом Posix описано два типа семафоров: именованные (named) и размещаемые в памяти (memory-based или unnamed). На рис. 10.4 сравниваются функции, используемые обоими типами семафоров.

    Именованный семафор Posix был изображен на рис. 10.2. Неименованный, или размещаемый в памяти, семафор, используемый для синхронизации потоков одного процесса, изображен на рис. 10.5.

    Рис. 10.4. Вызовы для семафоров Posix


    Рис. 10.5. Семафор, размещенный в общей памяти двух потоков


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

    Рис. 10.6. Семафор, размещенный в разделяемой двумя процессами памяти


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

    Мы рассмотрим три реализации именованных семафоров Posix: с использованием каналов FIFO, отображаемых в память файлов и семафоров System V. 

    10.2. Функции sem_open, sem_close и sem_unlink

    Функция sem_open создает новый именованный семафор или открывает существующий. Именованный семафор может использоваться для синхронизации выполнения потоков и процессов:

    #include <semaphore.h>

    sem_t *sem_open(const char *name, int oflag, …

     /* mode_t mode, unsigned int value */);

    /* Возвращает указатель на семафор в случае успешного завершения, SEM_FAILED — в случае ошибки */

    Требования к аргументу пате приведены в разделе 2.2.

    Аргумент oflag может принимать значения 0, O_CREAT, O_CREAT | O_EXCL, как описано в разделе 2.3. Если указано значение O_CREAT, третий и четвертый аргументы функции являются обязательными. Аргумент mode указывает биты разрешений доступа (табл. 2.3), a value указывает начальное значение семафора. Это значение не может превышать константу SEM_VALUE_MAX, которая, согласно Posix, должна быть не менее 32767. Бинарные семафоры обычно устанавливаются в 1, тогда как семафоры-счетчики чаще инициализируются большими величинами.

    При указании флага O_CREAT (без O_EXCL) семафор инициализируется только в том случае, если он еще не существует. Если семафор существует, ошибки не возникнет. Ошибка будет возвращена только в том случае, если указаны флаги O_CREAT | O_EXCL.

    Возвращаемое значение представляет собой указатель на тип sem_t. Этот указатель впоследствии передается в качестве аргумента функциям sem_close, sem_wait, sem_trywait, sem_post и sem_getvalue.

    ПРИМЕЧАНИЕ

    Кажется странным возвращать SEM_FAILED в случае ошибки — нулевой указатель был бы более уместен. В ранних версиях стандарта Posix указывалось возвращаемое значение –1, и во многих реализациях константа SEM_FAILED определена как

    #define SEM_FAILED ((sem_t *)(-1))

    В Posix.1 мало говорится о битах разрешений, связываемых с семафором при его создании и открытии. Вспомните, мы говорили в связи с табл. 2.2 о том, что для именованных семафоров не нужно даже указывать флаги O_RDONLY, O_WRONLY и O_RDWR. В системах, на которых мы тестируем все программы этой книги (Digital Unix 4.0B и Solaris 2.6), для работы с семафором (его открытия) необходимо иметь к нему доступ как на чтение, так и на запись. Причина, скорее всего, в том, что обе операции, выполняемые с семафором (post и wait), состоят из считывания текущего значения и последующего его изменения. Отсутствие доступа на чтение или запись в этих реализациях приводит к возвращению функцией sem_open ошибки EACCESS ("Permission denied"). 

    Открыв семафор с помощью sem_open, можно потом закрыть его, вызвав sem_close:

    #include <semaphore.h>

    int sem_close(sem_t *sem);

    /* Возвращает 0 в случае успешного завершения. –1 – в случае ошибки */
     

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

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

    Именованный семафор удаляется из системы вызовом sem_unlink:

    #include <semaphore.h>

    int sem_unlink(const char *name);

    /* Возвращает 0 в случае успешного завершения, –1 – в случае ошибки */

    Для каждого семафора ведется подсчет процессов, в которых он является открытым (как и для файлов), и функция sem_unlink действует аналогично unlink для файлов: объект пате может быть удален из файловой системы, даже если он открыт какими-либо процессами, но реальное удаление семафора не будет осуществлено до тех пор, пока он не будет окончательно закрыт.

    10.3. Функции sem_wait и sem_trywait

    Функция sem_wait проверяет значение заданного семафора на положительность, уменьшает его на единицу и немедленно возвращает управление процессу. Если значение семафора при вызове функции равно нулю, процесс приостанавливается, до тех пор пока оно снова не станет больше нуля, после чего значение семафора будет уменьшено на единицу и произойдет возврат из функции. Ранее мы отметили, что операция «проверка и уменьшение» должна быть атомарной по отношению к другим потокам, работающим с этим семафором:

    #include <semaphore.h>

    int sem_wait(sem_t *sem);

    int sem_trywait(sem_t *sem);

    /* Обе функции возвращают 0 в случае успешного завершения. –1 – в случае ошибки */

    Разница между sem_wait и sem_trywait заключается в том, что последняя не приостанавливает выполнение процесса, если значение семафора равно нулю, а просто немедленно возвращает ошибку EAGAIN.

    Возврат из функции sem_wait может произойти преждевременно, если будет получен сигнал. При этом возвращается ошибка с кодом EINTR.

    10.4. Функции sem_post и sem_getvalue

    После завершения работы с семафором поток вызывает sem_post. Как мы уже говорили в разделе 10.1, этот вызов увеличивает значение семафора на единицу и возобновляет выполнение любых потоков, ожидающих изменения значения семафора:

    #include <semaphore.h>

    int sem_post(sem_t *sem);

    int sem_getvalue(sem_t *sem, int *valp);

    /* Обе функции возвращают 0 в случае успешного завершения. –1 – в случае ошибки */
     

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

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

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

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

    ПРИМЕЧАНИЕ

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

    10.5. Простые примеры

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

    Программа semcreate

    В листинге 10.3 приведен текст программы, создающей именованный семафор. При вызове программы можно указать параметр –е, обеспечивающий исключающее создание (если семафор уже существует, будет выведено сообщение об ошибке), а параметр –i с числовым аргументом позволяет задать начальное значение семафора, отличное от 1. 

    Листинг 10.3.[1] Создание именованного семафора

    //pxsem/semcreate.c

    1  #include "unpipc.h"


    2  int

    3  main(int argc, char **argv)

    4  {

    5   int с, flags;

    6   sem_t *sem;

    7   unsigned int value;

    8   flags = O_RDWR | O_CREAT;

    9   value = 1;

    10  while ((c = Getopt(argc, argv, "ei:")) != –1) {

    11   switch (c) {

    12   case 'e':

    13    flags |= O_EXCL;

    14    break;

    15   case 'i':

    16    value = atoi(optarg);

    17    break;

    18   }

    19  }

    20  if (optind != argc – 1)

    21   err_quit("usage: semcreate [ –e ] [ –i initialvalue ] <name>");

    22  sem = Sem_open(argv[optind], flags, FILE_MODE, value);

    23  Sem_close(sem);

    24  exit(0);

    25 }

    Создание семафора

    22 Поскольку мы всегда указываем флаг O_CREAT, нам приходится вызывать sem_open с четырьмя аргументами. Последние два используются только в том случае, если семафор еще не существует.

    Закрытие семафора

    23 Мы вызываем sem_close, хотя, если бы мы не сделали этот вызов, семафор все равно закрылся бы автоматически при завершении процесса и ресурсы системы были бы высвобождены.

    Программа semunlink

    Программа в листинге 10.4 удаляет именованный семафор.

    Листинг 10.4. Удаление именованного семафора

    //pxsem/semunlink.c

    1 #include "unpipc.h"


    2 int

    3 main(int argc, char **argv)

    4 {

    5  if (argc != 2)

    6   err_quit("usage: semunlink <name>");

    7  Sem_unlink(argv[1]);

    8  exit(0);

    9 }

    Программа semgetvalue

    В листинге 10.5 приведен текст простейшей программы, которая открывает указанный именованный семафор, получает его текущее значение и выводит его.

    Листинг 10.5. Получение и вывод значения семафора

    //pxsem/semgetvalue.с

    1  #include "unpipc.h"


    2  int

    3  main(int argc, char **argv)

    4  {

    5   sem_t *sem;

    6   int val;

    7   if (argc != 2)

    8    err_quit("usage: semgetvalue <name>");

    9   sem = Sem_open(argv[1], 0);

    10  Sem_getvalue(sem, &val);

    11  printf("value = %d\n", val);

    12  exit(0);

    13 }

    Открытие семафора

    9 Семафор, который мы открываем, должен быть заранее создан другой программой. Вторым аргументом sem_open будет 0: мы не указываем флаг O_CREAT и нам не нужно задавать никаких других параметров открытия 0_ххх.

    Программа semwait

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

    Листинг 10.6. Ожидание изменения значения семафора и вывод нового значения

    //pxsem/semwait.c

    1  #include "unpipc.h"


    2  int

    3  main(int argc, char **argv)

    4  {

    5   sem_t *sem;

    6   int val;

    7   if (argc != 2)

    8    err_quit("usage: semwait <name>");

    9   sem = Sem_open(argv[1], 0);

    10  Sem_wait(sem);

    11  Sem_getvalue(sem, &val);

    12  printf("pid %ld has semaphore, value = %d\n", (long) getpid(), val);

    13  pause(); /* блокируется, пока не будет удален */

    14  exit(0);

    15 }

    Программа sempost

    В листинге 10.7 приведена программа, которая выполняет операцию post для указанного семафора (то есть увеличивает его значение на 1), а затем получает значение этого семафора и выводит его.

    Листинг 10.7. Увеличение значения семафора

    //pxsem/sempost.c

    1  #include "unpipc.h"


    2  int

    3  main(int argc, char **argv)

    4  {

    5   sem_t *sem;

    6   int val;

    7   if (argc != 2)

    8    err_quit("usage: sempost <name>");

    9   sem = Sem_open(argv[1], 0);

    10  Sem_post(sem);

    11  Sem_getvalue(sem, &val);

    12  printf("value = %d\n", val);

    13  exit(0);

    14 }

    Примеры

    Для начала мы создадим именованный семафор в Digital Unix 4.0B и выведем его значение, устанавливаемое по умолчанию при инициализации:

    alpha % semcreate /tmp/test1

    alpha % ls-l /tmp/test1

    -rw-r--r-- 1 rstevens system 264 Nov 13 08:51 /tmp/test1

    alpha %semgetvalue /tmp/test1

    value = 1

    Аналогично очередям сообщений Posix система создает файл семафора с тем именем, которое мы указали при вызове функции.

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

    alpha % semwait /tmp/test1

    pid 9702 has semaphore, value = 0 значение после возврата из sem_wait

    ^?                                клавиша прерывания работы в нашей системе

    alpha % semgetvalue /tmp/test1

    value = 0                         значение остается нулевым

    Приведенный пример иллюстрирует упомянутые ранее особенности. Во-первых, значение семафора обладает живучестью ядра. Значение 1, установленное при создании семафора, хранится в ядре даже тогда, когда ни одна программа не пользуется этим семафором. Во-вторых, при выходе из программы semwait, заблокировавшей семафор, значение его не изменяется, то есть ресурс остается заблокированным. Это отличает семафоры от блокировок fcntl, описанных в главе 9, которые снимались автоматически при завершении работы процесса.

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

    alpha % semgetvalue /tmp/test1

    value = 0                          это значение сохранилось с конца предыдущего примера

    alpha % semwait /tmp/test1 &      запуск в фоновом режиме

    [1] 9718                           блокируется в ожидании изменения значения семафора

    alpha % semgetvalue /tmp/test1

    value = –1                         один процесс ожидает изменения семафора

    alpha % semwait /tmp/test1 &       запуск еще одного процесса в фоновом режиме

    [2] 9727                           он также блокируется

    alpha % semgetvalue /tmp/test1

    value = –2                         два процесса ожидают изменения семафора

    alpha % sempost /tmp/test1

    value = –1                         значение после возвращенияиз sem_post

    pid 9718 has semaphore, value = –1 вывод программы semwait

    alpha % sempost /tmp/test1

    value = 0

    pid 9727 has semaphore, value = 0  вывод программы semwait

    При первом вызове sem_post значение семафора изменилось с –2 на –1 и один из процессов, ожидавших изменения значения семафора, был разблокирован.

    Выполним те же действия в Solaris 2.6, обращая внимание на различия в реализации:

    solaris % semcreate /test2

    solaris % ls –l /tmp/.*test2*

    -rw-r--r-- 1 rstevens other1 48 Nov 13 09:11 /tmp/.SEMDtest2

    –rw-rw-rw– 1 rstevens other1  0 Nov 13 09:11 /tmp/.SEMLtest2

    solaris % semgetvalue /test2

    value = 1

    Аналогично очередям сообщений Posix файлы создаются в каталоге /tmp, причем указываемое при вызове имя становится суффиксом имен файлов. Разрешения первого файла соответствуют указанным в вызове sem_open, а второй файл, как можно предположить, используется для блокировки доступа.

    Проверим, что ядро не осуществляет автоматического увеличения значения семафора при завершении работы процесса, установившего блокировку:

    solaris % semwait /test2

    pid 4133 has semaphore, value = 0

    ^? нажимаем клавишу прерывания выполнения

    solaris % semgetvalue /test2

    value = 0

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

    solaris % semgetvalue /test2

    value = 0                       значение сохранилось с конца предыдущего примера

    solaris % semwait /test2&       запуск в фоновом режиме

    [1] 4257                        программа блокируется

    solaris % semgetvalue /test2

    value = 0                        в этой реализации отрицательные значения не используются

    solaris % semwait /test2&        еще один фоновый процесс

    [2] 4263

    solaris % semgetvalue /test2

    value 0                          и для двух ожидающих процессов значение остается нулевым

    solaris % sempost /test2          выполняем операцию post

    pid 4257 has semaphore, value = 0 вывод программы semwait

    value = 0

    solaris % sempost /test2

    pid 4263 has semaphore, value = 0 вывод программы semwait

    value = 0

    Можно заметить отличие по сравнению с результатами выполнения той же последовательности команд в Digital Unix 4.0B: после изменения значения семафора управление сразу же передается ожидающему изменения семафора процессу.

    10.6. Задача производителей и потребителей

    В разделе 7.3 мы описали суть задачи производителей и потребителей и привели несколько возможных ее решений, в которых несколько потоков-производителей заполняли массив, который обрабатывался одним потоком-потребителем.

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

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

    Расширим постановку задачи производителей и потребителей, используя общий буфер в качестве циклического: заполнив последнее поле, производитель (buff[NBUFF-1]) возвращается к его началу и заполняет первое поле (buff[0]), и потребитель действует таким же образом. Возникает еще одно требование к синхронизации: потребитель не должен опережать производителя. Мы все еще предполагаем, что производитель и потребитель представляют собой отдельные потоки одного процесса, но они также могут быть и просто отдельными процессами, если мы сможем создать для них общий буфер (например, используя разделяемую память, часть 4).

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

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

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

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

    Наше решение использует три семафора:

    1. Бинарный семафор с именем mutex защищает критические области кода: помещение данных в буфер (для производителя) и изъятие данных из буфера (для потребителя). Бинарный семафор, используемый в качестве взаимного исключения, инициализируется единицей. (Конечно, мы могли бы воспользоваться и обычным взаимным исключением вместо двоичного семафора. См. упражнение 10.10.)

    2. Семафор-счетчик с именем nempty подсчитывает количество свободных полей в буфере. Он инициализируется значением, равным объему буфера (NBUFF).

    3. Семафор-счетчик с именем nstored подсчитывает количество заполненных полей в буфере. Он инициализируется нулем, поскольку изначально буфер пуст.

    Рис. 10.7. Состояние буфера и двух семафоров-счетчиков после инициализации


    На рис. 10.7 показано состояние буфера и двух семафоров-счетчиков после завершения инициализации. Неиспользуемые элементы массива выделены темным.

    В нашем примере производитель помещает в буфер целые числа от 0 до NLOOP-1 (buff[0] = 0, buff[1] = 1), работая с ним как с циклическим. Потребитель считывает эти числа и проверяет их правильность, выводя сообщения об ошибках в стандартный поток вывода.

    На рис. 10.8 изображено состояние буфера и семафоров-счетчиков после помещения в буфер трех элементов, но до изъятия их потребителем.

    Рис. 10.8. Буфер и семафоры после помещения в буфер трех элементов


    Предположим, что потребитель изъял один элемент из буфера. Новое состояние изображено на рис. 10.9.

    Рис. 10.9. Буфер и семафоры после удаления первого элемента из буфера


    В листинге 10.8 приведен текст функции main, которая создает три семафора, запускает два потока, ожидает их завершения и удаляет семафоры.

    Листинг 10.8. Функция main для решения задачи производителей и потребителей с помощью семафоров

    //pxsem/prodcons1.с

    1  #include "unpipc.h"

    2  #define NBUFF 10

    3  #define SEM_MUTEX "mutex" /* аргументы px_ipc_name() */

    4  #define SEM_NEMPTY "nempty"

    5  #define SEM_NSTORED "nstored"


    6  int nitems; /* read-only для производителя и потребителя */

    7  struct { /* разделяемые производителем и потребителем данные */

    8   int buff[NBUFF];

    9   sem_t *mutex, *nempty, *nstored;

    10 } shared;

    11 void *produce(void *), *consume(void *);


    12 int

    13 main(int argc, char **argv)

    14 {

    15  pthread_t tid_produce, tid_consume;

    16  if (argc != 2)

    17   err_quit("usage: prodcons1 <#items>");

    18  nitems = atoi(argv[1]);

    19  /* создание трех семафоров */

    20  shared.mutex = Sem_open(Px_ipc_name(SEM_MUTEX), O_CREAT | O_EXCL,

    21   FILE_MODE, 1);

    22  shared.nempty = Sem_open(Px_ipc_name(SEM_NEMPTY), 0_CREAT | O_EXCL,

    23   FILE_MODE, NBUFF);

    24  shared.nstored = Sem_open(Px_ipc_name(SEM_NSTORED), O_CREAT | O_EXCL,

    25   FILE_MODE, 0);

    26  /* создание одного потока-производителя и одного потока-потребителя */

    27  Set_concurrency(2);

    28  Pthread_create(&tid_produce, NULL, produce, NULL);

    29  Pthread_create(&tid_consume, NULL, consume, NULL);

    30  /* ожидание завершения работы потоков */

    31  Pthread_join(tid_produce, NULL);

    32  Pthread_join(tid_consume, NULL);

    33  /* удаление семафоров */

    34  Sem_unlink(Px_ipc_name(SEM_MUTEX));

    35  Sem_unlink(Px_ipc_name(SEM_NEMPTY));

    36  Sem_unlink(Px_ipc_name(SEM_NSTORED));

    37  exit(0);

    38 }

    Глобальные переменные

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

    Создание семафоров

    19-25 Мы создаем три семафора, передавая их имена функции px_ipc_name. Флаг O_EXCL мы указываем, для того чтобы гарантировать инициализацию каждого семафора правильным значением. Если после преждевременно завершенного предыдущего запуска программы остались неудаленные семафоры, мы обработаем эту ситуацию, вызвав перед их созданием sem_unlink и игнорируя ошибки. Мы могли бы проверять возвращение ошибки EEXIST при вызове sem_open с флагом O_EXCL, а затем вызывать sem_unlink и еще раз sem_open, но это усложнило бы программу. Если нам нужно проверить, что запущен только один экземпляр программы (что следует сделать перед созданием семафоров), можно обратиться к разделу 9.7, где описаны методы решения этой задачи.

    Создание двух потоков

    26-29 Создаются два потока, один из которых является производителем, а другой — потребителем. При запуске никакие аргументы им не передаются.

    30-36 Главный поток ждет завершения работы производителя и потребителя, а затем удаляет три семафора.

    ПРИМЕЧАНИЕ

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

    В листинге 10.9 приведен текст функций produce и consume.

    Листинг 10.9. Функции produce и consume

    //pxsem/prodcons1.c

    39 void *

    40 produce(void *arg)

    41 {

    42  int i;

    43  for (i = 0; i < nitems; i++) {

    44   Sem_wait(shared.nempty); /* ожидаем освобождения поля */

    45   Sem_wait(shared.mutex);

    46   shared.buff[i % NBUFF] = i; /* помещаем i в циклический буфер */

    47   Sem_post(shared.mutex);

    48   Sem_post(shared.nstored); /* сохраняем еще 1 элемент */

    49  }

    50  return(NULL);

    51 }


    52 void *

    53 consume(void *arg)

    54 {

    55  int i;

    56  for (i = 0; i < nitems; i++) {

    57   Sem_wait(shared.nstored); /* ожидаем появления объекта в буфере */

    58   Sem_wait(shared.mutex);

    59   if (shared.buff[i % NBUFF] != i)

    60    printf("buff[%d] = %d\n", i, shared.buff[i % NBUFF]);

    61   Sem_post(shared.mutex);

    62   Sem_post(shared.nempty); /* еще одно пустое поле */

    63  }

    64  return(NULL);

    65 }

    Производитель ожидает освобождения места в буфере

    44 Производитель вызывает sem_wait для семафора nempty, ожидая появления свободного места. В первый раз при выполнении этой команды значение семафора nempty уменьшится с NBUFF до NBUFF-1.

    Производитель помещает элемент в буфер

    45-48 Перед помещением нового элемента в буфер производитель должен установить блокировку на семафор mutex. В нашем примере, где производитель просто сохраняет значение в элементе массива с индексом i % NBUFF, для описания состояния буфера не используется никаких разделяемых переменных (то есть мы не используем связный список, который нужно было бы обновлять каждый раз при помещении элемента в буфер). Следовательно, установка и снятие семафора mutex не являются обязательными. Тем не менее мы иллюстрируем эту технику, потому что обычно ее применение является необходимым в задачах такого рода (обновление буфера, разделяемого несколькими потоками).

    После помещения элемента в буфер блокировка с семафора mutex снимается (его значение увеличивается с 0 до 1) и увеличивается значение семафора nstored. Первый раз при выполнении этой команды значение nstored изменится с начального значения 0 до 1.

    Потребитель ожидает изменения семафора nstored

    57-62 Если значение семафора nstored больше 0, в буфере имеются объекты для обработки. Потребитель изымает один элемент из буфера и проверяет правильность его значения, защищая буфер в момент доступа к нему с помощью семафора mutex. Затем потребитель увеличивает значение семафора nempty, указывая производителю на наличие свободных полей.

    Зависание

    Что произойдет, если мы по ошибке поменяем местами вызовы Sem_wait в функции consumer (листинг 10.9)? Предположим, что первым запускается производитель (как в решении, предложенном для упражнения 10.1). Он помещает в буфер NBUFF элементов, уменьшая значение семафора nempty от NBUFF до 0 и увеличивая значение семафора nstored от 0 до NBUFF. Затем производитель блокируется в вызове Sem_wait(shared. nempty), поскольку буфер полон и помещать элементы больше некуда.

    Запускается потребитель и проверяет первые NBUFF элементов буфера. Это уменьшает значение семафора nstored от NBUFF до 0 и увеличивает значение семафора nempty от 0 до NBUFF. Затем потребитель блокируется в вызове Sem_wait(shared, nstored) после вызова Sem_wait(shared, mutex). Производитель мог бы продолжать работу, поскольку значение семафора nempty уже отлично от 0, но он вызвал Sem_wait(shared, mutex) и его выполнение было приостановлено. 

    Это называется зависанием программы (deadlock). Производитель ожидает освобождения семафора mutex, а потребитель не снимает с него блокировку, ожидая освобождения семафора nstored. Но производитель не может изменить nstored, пока он не получит семафор mutex. Это одна из проблем, которые часто возникают с семафорами: если в программе сделать ошибку, она будет работать неправильно.

    ПРИМЕЧАНИЕ

    Стандарт Posix позволяет функции sem_wait обнаруживать зависание и возвращать ошибку EDEADLK, но ни одна из систем, использовавшихся для написания примеров (Digital Unix 4.0B и Solaris 2.6), не обнаружила ошибку в данном случае.

    10.7. Блокирование файлов

    Вернемся к задаче о порядковом номере из главы 9. Здесь мы напишем новые версии функций my_lock и my_unlосk, использующие именованные семафоры Posix. В листинге 10.10 приведен текст этих функций.

    Листинг 10.10. Блокирование файла с помощью именованных семафоров Posix

    //lock/lockpxsem.c

    1  #include "unpipc.h"

    2  #define LOCK_PATH "pxsemlock"

    3  sem_t *locksem;

    4  int initflag;


    5  void

    6  my_lock(int fd)

    7  {

    8   if (initflag == 0) {

    9    locksem = Sem_open(Px_ipc_name(LOCK_PATH), O_CREAT, FILE_MODE, 1);

    10   initflag = 1;

    11  }

    12  Sem_wait(locksem);

    13 }


    14 void

    15 my_unlock(int fd)

    16 {

    17  Sem_post(locksem);

    18 }

    Один из семафоров используется для рекомендательной блокировки доступа к файлу и инициализируется единицей при первом вызове функции. Для получения блокировки мы вызываем sem_wait, а для ее снятия — sem_post.

    10.8. Функции sem_init и sem_destroy

    До сих пор мы имели дело только с именованными семафорами Posix. Как мы уже говорили, они идентифицируются аргументом пате, обычно представляющим собой имя файла в файловой системе. Стандарт Posix описывает также семафоры, размещаемые в памяти, память под которые выделяет приложение (тип sem_t), а инициализируются они системой:

    #include <semaphore.h>

    int sem_init(sem_t *sem, int shared, unsigned int value);

    /* Возвращает –1 в случае ошибки */

    int sem_destroy(sem_t *sem);

    /* Возвращает 0 в случае успешного завершения, –1 – в случае ошибки */

    Размещаемый в памяти семафор инициализируется вызовом sem_init. Аргумент sem указывает на переменную типа sem_t, место под которую должно быть выделено приложением. Если аргумент shared равен 0, семафор используется потоками одного процесса, в противном случае доступ к нему могут иметь несколько процессов. Если аргумент shared ненулевой, семафор должен быть размещен в одном из видов разделяемой памяти и должен быть доступен всем процессам, использующим его. Как и в вызове sem_open, аргумент value задает начальное значение семафора.

    После завершения работы с размещаемым в памяти семафором его можно уничтожить, вызвав sem_destroy.

    ПРИМЕЧАНИЕ 1

    Функции sem_open не требуется параметр, аналогичный shared; не требуется ей и атрибут, аналогичный PTHREAD_PROCESS_SHARED (упоминавшийся в связи с взаимными исключениями и условными переменными в главе 7), поскольку именованный семафор всегда используется совместно несколькими процессами.

    ПРИМЕЧАНИЕ 2

    Обратите внимание, что для размещаемого в памяти семафора нет ничего аналогичного флагу O_CREAT: функция sem_init всегда инициализирует значение семафора. Следовательно, нужно быть внимательным, чтобы вызывать sem_init только один раз для каждого семафора. (Упражнение 10.2 иллюстрирует разницу в этом смысле между именованным и размещаемым в памяти семафорами.) При вызове sem_init для уже инициализированного семафора результат непредсказуем.

    ПРИМЕЧАНИЕ 3

    Удостоверьтесь, что вы понимаете фундаментальную разницу между sem_open и sem_init. Первая возвращает указатель на переменную типа sem_t, причем выделение места под переменную и ее инициализация выполняются этой же функцией. Напротив, первый аргумент sem_init представляет собой указатель на переменную типа sem_t, место под которую должен был заранее выделить вызывающий. Функция sem_init только инициализирует эту переменную.

    ПРИМЕЧАНИЕ 4

    Стандарт Posix.1 предупреждает, что для обращения к размещаемым в памяти семафорам можно использовать только указатель, являющийся аргументом при вызове sem_init. Использование копий этого указателя может привести к неопределенным результатам.

    Функция sem_init возвращает –1 в случае ошибки, но она не возвращает 0 в случае успешного завершения. Это действительно странно, и примечание в Обосновании Posix. 1 говорит, что в будущих версиях функция, возможно, начнет возвращать 0 в случае успешного завершения. 

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

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

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

    ■ Если размещаемый в памяти семафор совместно используется несколькими процессами (аргумент shared при вызове seminit равен 1), он должен располагаться в разделяемой памяти, и в этом случае семафор существует столько, сколько существует эта область памяти. Вспомните, что и разделяемая память Posix, и разделяемая память System V обладают живучестью ядра (табл. 1.1). Это значит, что сервер может создать область разделяемой памяти, инициализировать в ней размещаемый в памяти семафор Posix, а затем завершить работу. Некоторое время спустя один или несколько клиентов могут присоединить эту область к своему адресному пространству и получить доступ к хранящемуся в ней семафору.

    Предупреждаем, что нижеследующий код не работает так, как ожидается:

    sem_t mysem;

    Sem_init(&mysem, 1.0); /* 2-й аргумент 1 –> используется процессами */

    if (Fork() == 0) { /* дочерний процесс */

     …

     Sem_post(&mysem);

    }

    Sem_wait(&mysem); /* родительский процесс: ожидание дочернего */

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

    Пример

    В качестве иллюстрации перепишем наш пример решения задачи производителей и потребителей из листингов 10.8 и 10.9 для использования размещаемых в памяти семафоров Posix. В листинге 10.11 приведен текст новой программы.

    Листинг 10.11. Задача производителей и потребителей с использованием размещаемых в памяти семафоров

    //pxsem/prodcons2.c

    1  #include "unpipc.h"

    2  #define NBUFF 10


    3  int nitems; /* только для чтения производителем и потребителем */

    4  struct { /* общие данные производителя и потребителя */

    5   int buff[NBUFF];

    6   sem_t mutex, nempty, nstored; /* семафоры, а не указатели */

    7  } shared;

    8  void *produce(void *), *consume(void *);


    9  int

    10 main(int argc, char **argv)

    11 {

    12  pthread_t tid_produce, tid_consume;

    13  if (argc != 2)

    14   err_quit("usage: prodcons2 <#items>");

    15  nitems = atoi(argv[1]);

    16  /* инициализация трех семафоров */

    17  Sem_init(&shared.mutex, 0, 1);

    18  Sem_init(&shared.nempty, 0, NBUFF);

    19  Sem_init(&shared.nstored, 0, 0);

    20  Set_concurrency(2);

    21  Pthread_create(&tid_produce, NULL, produce, NULL);

    22  Pthread_create(&tid_consume, NULL, consume, NULL);

    23  Pthread_join(tid_produce, NULL);

    24  Pthread_join(tid_consume, NULL):

    25  Sem_destroy(&shared.mutex);

    26  Sem_destroy(&shared.nempty):

    27  Sem_destroy(&shared.nstored);

    28  exit(0);

    29 }


    30 void *

    31 produce(void *arg)

    32 {

    33  int i;

    34  for (i = 0; i < nitems; i++) {

    35   Sem_wait(&shared.nempty); /* ожидание одного свободного поля */

    36   Sem_wait(&shared.mutex);

    37   shared.buff[i % NBUFF] = i; /* помещение i в циклический буфер */

    38   Sem_post(&shared.mutex);

    39   Sem_post(&shared.nstored); /* поместили еще один элемент */

    40  }

    41  return(NULL);

    42 }


    43 void *

    44 consume(void *arg)

    45 {

    46  int i;

    47  for (i = 0; i < nitems; i++) {

    48   Sem_wait(&shared.nstored); /* ожидаем появления хотя бы одного готового для обработки элемента */

    49   Sem_wait(&shared.mutex);

    50   if (shared.buff[i % NBUFF] != i)

    51    printf("buff[*d] = *d\n", i, shared.buff[i % NBUFF]);

    52   Sem_post(&shared.mutex);

    53   Sem_post(&shared.nempty); /* еще одно пустое поле */

    54  }

    55  return(NULL);

    56 }

    Выделение семафоров

    6 Мы объявляем три семафора типа sem_t, и теперь это сами семафоры, а не указатели на них.

    Вызов sem_init

    16-27 Мы вызываем sem_init вместо sem_open* а затем sem_destroy вместо sem_unlink. Вызывать sem_destroy на самом деле не требуется, поскольку программа все равно завершается.

    Остальные изменения обеспечивают передачу указателей на три семафора при вызовах sem_wait и sem_post.

    10.9. Несколько производителей, один потребитель

    Решение в разделе 10.6 относится к классической задаче с одним производителем и одним потребителем. Новая, интересная модификация программы позволит нескольким производителям работать с одним потребителем. Начнем с решения из листинга 10.11, в котором использовались размещаемые в памяти семафоры. В листинге 10.12 приведены объявления глобальных переменных и функция main.

    Листинг 10.12. Функция main задачи с несколькими производителями

    //pxsem/prodcons3.c

    1  #include "unpipc.h"

    2  #define NBUFF 10

    3  #define MAXNTHREADS 100


    4  int nitems, nproducers; /* только для чтения производителем и потребителем */

    5  struct { /* общие данные */

    6   int buff[NBUFF];

    7   int nput;

    8   int nputval;

    9   sem_t mutex, nempty, nstored; /* семафоры, а не указатели */

    10 } shared;


    11 void *produce(void *), *consume(void *);


    12 int

    13 main(int argc, char **argv)

    14 {

    15  int i, count[MAXNTHREADS];

    16  pthread_t tid_produce[MAXNTHREADS], tid_consume;

    17  if (argc != 3)

    18   err_quit("usage: prodcons3 <#items> <#producers>");

    19  nitems = atoi(argv[1]);

    20  nproducers = min(atoi(argv[2]), MAXNTHREADS);

    21  /* инициализация трех семафоров */

    22  Sem_init(&shared.mutex, 0, 1);

    23  Sem_init(&shared.nempty, 0, NBUFF);

    24  Sem_init(&shared.nstored, 0, 0);

    25  /* создание всех производителей и одного потребителя */

    26  Set_concurrency(nproducers + 1);

    27  for (i = 0; i < nproducers; i++) {

    28   count[i] = 0;

    29   Pthread_create(&tid_produce[i], NULL, produce, &count[i]);

    30  }

    31  Pthread_create(&tid_consume, NULL, consume, NULL);

    32  /* ожидание завершения всех производителей и потребителя */

    33  for (i = 0; i < nproducers; i++) {

    34   Pthread_join(tid_produce[i], NULL);

    35   printf("count[%d] = %d\n", i, count[i]);

    36  }

    37  Pthread_join(tid_consume, NULL);

    38  Sem_destroy(&shared.mutex);

    39  Sem_destroy(&shared.nempty);

    40  Sem_destroy(&shared.nstored);

    41  exit(0);

    42 }

    Глобальные переменные

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

    Общая структура

    5-10 В структуру shared добавляются два новых элемента: nput, обозначающий индекс следующего элемента, куда должен быть помещен объект (по модулю BUFF), и nputval следующее значение, которое будет помещено в буфер. Эти две переменные взяты из нашего решения в листингах 7.1 и 7.2. Они нужны для синхронизации нескольких потоков-производителей.

    Новые аргументы командной строки

    17-20 Два новых аргумента командной строки указывают полное количество элементов, которые должны быть помещены в буфер, и количество потоков-производителей. 

    Запуск всех потоков

    21-41 Инициализируем семафоры и запускаем потоки-производители и поток-потребитель. Затем ожидается завершение работы потоков. Эта часть кода практически идентична листингу 7.1.

    В листинге 10.13 приведен текст функции produce, которая выполняется каждым потоком-производителем.

    Листинг 10.13. Функция, выполняемая всеми потоками-производителями

    //pxsem/prodcons3.c

    43 void *

    44 produce(void *arg)

    45 {

    46  for (;;) {

    47   Sem_wait(&shared.nempty); /* ожидание освобождения поля */

    48   Sem_wait(&shared.mutex);

    49   if (shared.nput >= nitems) {

    50    Sem_post(&shared.nempty);

    51    Sem_post(&shared.mutex);

    52    return(NULL); /* готово */

    53   }

    54   shared.buff[shared.nput % NBUFF] = shared.nputval;

    55   shared.nput++;

    56   shared.nputval++;

    57   Sem_post(&shared.mutex);

    58   Sem_post(&shared.nstored); /* еще один элемент */

    59   *((int *) arg) += 1;

    60  }

    61 }

    Взаимное исключение между потоками-производителями

    49-53 Отличие от листинга 10.8 в том, что цикл завершается, когда nitems объектов будет помещено в буфер всеми потоками. Обратите внимание, что потоки-производители могут получить семафор nempty в любой момент, но только один производитель может иметь семафор mutex. Это защищает переменные nput и nval от одновременного изменения несколькими производителями.

    Завершение производителей

    50-51 Нам нужно аккуратно обработать завершение потоков-производителей. После того как последний объект помещен в буфер, каждый поток выполняет

    Sem_wait(&shared.nempty); /* ожидание пустого поля */

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

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

    Листинг 10.14. Функция, выполняемая потоком-потребителем

    //pxsem/prodcons3.с

    62 void *

    63 consume(void *arg)

    64 {

    65  int i;

    66  for (i = 0; i < nitems; i++) {

    67   Sem_wait(&shared.nstored); /* ожидание помещения по крайней мере одного элемента в буфер */

    68   Sem_wait(&shared.mutex);

    69   if (shared.buff[i % NBUFF] != i)

    70    printf("error: buff[%d] = %d\n", i, shared.buff[i % NBUFF]);

    71   Sem_post(&shared.mutex);

    72   Sem_post(&shared.nempty); /* еще одно пустое поле */

    73  }

    74  return(NULL);

    75 }

    Условие завершения единственного потока-потребителя звучит просто: он считает все потребленные объекты и останавливается по достижении nitems.

    10.10. Несколько производителей, несколько потребителей

    Следующее изменение, которое мы внесем в нашу пpoгрaммy, будет заключаться в добавлении возможности одновременной работы нескольких потребителей вместе с несколькими производителями. Есть ли смысл в наличии нескольких потребителей — зависит от приложения. Автор видел два примера, в которых использовался этот метод.

    1. Пpoгрaммa преобразования IP-адресов в имена узлов. Каждый потребитель берет IP-адрес, вызывает gethostbyaddr (раздел 9.6 [24]), затем дописывает имя узла к файлу. Поскольку каждый вызов gethostbyaddr обрабатывается неопределенное время, порядок IP-адресов в буфере будет, скорее всего, отличаться от порядка имен узлов в файле, созданном потоками-потребителями. Преимущество этой схемы в параллельности выполнения вызовов gethostbyaddr (каждый из которых может работать несколько секунд) — по одному на каждый поток-потребитель.

    ПРИМЕЧАНИЕ

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

    2. Программа, принимающая дейтаграммы UDP, обрабатывающая их и записывающая результат в базу данных. Каждая дeйтaгрaммa обрабатывается одним потоком-потребителем, которые выполняются параллельно для ускорения процесса. Хотя дейтаграммы записываются в базу данных в порядке, вообще говоря, отличном от порядка их приема, встроенная схема упорядочения записей в базе данных справляется с этой проблемой.

    В листинге 10.15 приведены глобальные переменные программы.

    Листинг 10.15. Глобальные переменные

    //pxsem/prodcons4.с

    1  #include "unpipc.h"

    2  #define NBUFF 10

    3  #define MAXNTHREADS 100

    4  int nitems, nproducers, nconsumers; /* только для чтения */

    5  struct { /* общие данные производителей и потребителей */

    6   int buff[NBUFF];

    7   int nput; /* номер объекта: 0, 1. 2, … */

    8   int nputval; /* сохраняемое в buff[] значение */

    9   int nget; /* номер объекта: 0, 1, 2, … */

    10  int ngetval; /* получаемое из buff[] значение */

    11  sem_t mutex, nempty, nstored; /* семафоры, а не указатели */

    12 } shared;

    13 void *produce(void *), *consume(void *);

    Глобальные переменные и общая структура

    4-12 Количество потоков-потребителей является глобальной переменной, устанавливаемой из командной строки. В структуру shared добавилось два новых поля: nget — номер следующего объекта, получаемого одним из потоков-потребителей, и ngetval — соответствующее значение.

    Функция main, текст которой приведен в листинге 10.16, запускает несколько потоков-потребителей и потоков-производителей одновременно.

    19-23 Новый аргумент командной строки указывает количество потоков-потребителей. Для хранения идентификаторов потоков-потребителей выделяется место под специальный массив (tid_consume), а для подсчета обработанных каждым потоком объектов выделяется массив conscount.

    24-50 Создаются несколько потоков-производителей и потребителей, после чего основной поток ждет их завершения.

    Листинг 10.16. Функция main для версии с несколькими производителями и потребителями

    //pxsem/prodcons4.с

    14 int

    15 main(int argc, char **argv)

    16 {

    17  int i, prodcount[MAXNTHREADS], conscount[MAXNTHREADS];

    18  pthread_t tid_produce[MAXNTHREADS], tid_consume[MAXNTHREADS];

    19  if (argc != 4)

    20   err_quit("usage: prodcons4 <#items> <#producers> <#consumers>");

    21  nitems = atoi(argv[1]);

    22  nproducers = min(atoi(argv[2]), MAXNTHREADS);

    23  nconsumers = min(atoi(argv[3]), MAXNTHREADS);

    24  /* инициализация трех семафоров */

    25  Sem_init(&shared.mutex, 0, 1);

    26  Sem_init(&shared.nempty, 0, NBUFF);

    27  Sem_init(&shared.nstored, 0, 0);

    28  /* создание производителей и потребителей */

    29  Set_concurrency(nproducers + nconsumers);

    30  for (i = 0; i < nproducers; i++) {

    31   prodcount[i] = 0;

    32   Pthread_create(&tid_produce[i], NULL, produce, &prodcount[i]);

    33  }

    34  for (i = 0; i < nconsumers; i++) {

    35   conscount[i] = 0;

    36   Pthread_create(&tid_consume[i], NULL, consume, &conscount[i]);

    37  }

    38  /* ожидание завершения всех производителей и потребителей */

    39  for (i = 0; i < nproducers: i++) {

    40   Pthread_join(tid_produce[i], NULL);

    41   printf("producer count[%d] = %d\n", i, prodcount[i]);

    42  }

    43  for (i = 0; i < nconsumers; i++) {

    44   Pthread_join(tid_consume[i], NULL);

    45   printf("consumer count[%d] = %d\n", i, conscount[i]);

    46  }

    47  Sem_destroy(&shared.mutex);

    48  Sem_destroy(&shared.nempty);

    49  Sem_destroy(&shared.nstored);

    50  exit(0);

    51 }

    Функция produce содержит одну новую строку по сравнению с листингом 10.13. В части кода, относящейся к завершению потока-производителя, появляется строка, отмеченная знаком +:

     if (shared.nput >= nitems) {

    + Sem_post(&shared.nstored); /* даем возможность потребителям завершить работу */

      Sem_post(&shared.nempty);

      Sem_post(&shared.mutex);

      return(NULL); /* готово */

     }

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

    Sem_wait(&shared.nstored); /* Ожидание помещения объекта в буфер */

    Производителям приходится увеличивать семафор nstored для разблокирования потрeбитeлeй, чтобы они узнали, что работа завершена. Функция consume приведена в листинге 10.17. 

    Листинг 10.17. Функция, выполняемая всеми потоками-потребителями

    //pxsem/prodcons4.c

    72 void *

    73 consume(void *arg)

    74 {

    75  int i;

    76  for (;;) {

    77   Sem_wait(&shared.nstored); /* ожидание помещения объекта в буфер */

    78   Sem_wait(&shared.mutex);

    79   if (shared.nget >= nitems) {

    80    Sem_post(&shared.nstored);

    81    Sem_post(&shared.mutex);

    82    return(NULL); /* готово */

    83   }

    84   i = shared.nget % NBUFF;

    85   if (shared.buff[i] != shared.ngetval)

    86    printf("error: buff[%d] = %d\n", i, shared.buff[i]);

    87   shared.nget++;

    88   shared.ngetval++;

    89   Sem_post(&shared.mutex);

    90   Sem_post(&shared.nempty); /* освобождается место для элемента */

    91   *((int *) arg) += 1;

    92  }

    93 }

    Завершение потоков-потребителей

    79-83 Функция consume сравнивает nget и nitems, чтобы узнать, когда следует остановиться (аналогично функции produce). Обработав последний объект в буфере, потоки-потребители блокируются, ожидая изменения семафора nstored. Когда завершается очередной поток-потребитель, он увеличивает семафор nstored, давая возможность завершить работу другому потоку-потребителю.

    10.11. Несколько буферов

    Во многих программах, обрабатывающих какие-либо данные, можно встретить цикл вида

    while ((n = read(fdin, buff, BUFFSIZE)) > 0) {

     /* обработка данных */

     write(fdout, buff, n);

    }

    Например, программы, обрабатывающие текстовые файлы, считывают строку из входного файла, выполняют с ней некоторые действия, а затем записывают строку в выходной файл. Для текстовых файлов вызовы read и write часто заменяются на функции стандартной библиотеки ввода-вывода fgets и fputs.

    На рис. 10.11 изображена иллюстрация к такой схеме. Здесь функция reader считывает данные из входного файла, а функция writer записывает данные в выходной файл. Используется один буфер.

    Рис. 10.10. Процесс считывает данные в буфер, а потом записывает его содержимое в другой файл


    Рис. 10.11. Один процесс, считывающий данные в буфер и записывающий их в файл


    На рис. 10.10 приведена временная диаграмма работы такой программы. Числа слева проставлены в условных единицах времени. Предполагается, что операция чтения занимает 5 единиц, записи — 7, а обработка данных между считыванием и записью требует 2 единицы времени.

    Можно изменить это приложение, разделив процесс на отдельные потоки, как показано на рис. 10.12. Здесь используется два потока (а не процесса), поскольку глобальный буфер автоматически разделяется между ними. Мы могли бы разделить приложение и на два процесса, но это потребовало бы использования разделяемой памяти, с которой мы еще не знакомы.

    Рис. 10.12. Разделение копирования файла между двумя потоками


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

    Рис. 10.13. Копирование файла двумя потоками


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

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

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

    Рис. 10.14. Копирование файла двумя потоками с двумя буферами


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

    На рис. 10.15 изображена временная диаграмма процесса с двойной буферизацией. Считывающий поток помещает данные в буфер № 1, а затем уведомляет записывающий о том, что буфер готов к обработке. Затем считывающий процесс помещает данные в буфер № 2, а записывающий берет их из буфера № 1.

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

    Обратите внимание, что операции записи выполняются так быстро, как только возможно. Они разделены промежутками времени всего лишь в 2 единицы, тогда как в предыдущих примерах между ними проходило 9 единиц времени (рис. 10.10 и 10.13). Это может оказаться выгодным при работе с некоторыми устройствами типа накопителей на магнитной ленте, которые функционируют быстрее, если данные записываются с максимально возможной скоростью (это называется потоковым режимом — streaming mode).

    Рис. 10.15. Процесс с двойной буферизацией


    Интересно, что задача с двойной буферизацией представляет собой лишь частный случай общей задачи производителей и потребителей.

    Изменим нашу программу так, чтобы использовать несколько буферов. Начнем с решения из листинга 10.11, в котором использовались размещаемые в памяти семафоры. Мы получим даже не двойную буферизацию, а работу с произвольным числом буферов (задается NBUFF). В листинге 10.18 даны глобальные переменные и функция main.

    Листинг 10.18. Глобальные переменные и функция main

    //pxsem/mycat2.c

    1  #include "unpipc.h"

    2  #define NBUFF 8


    3  struct { /* общие данные */

    4   struct {

    5    char data[BUFFSIZE]; /* буфер */

    6    ssize_t n; /* объем буфера */

    7   } buff[NBUFF]; /* количество буферов */

    8   sem_t mutex, nempty, nstored; /* семафоры, а не указатели */

    9  } shared;

    10 int fd; /* входной файл, копируемый в стандартный поток вывода */

    11 void *produce(void *), *consume(void *);


    12 int

    13 main(int argc, char **argv)

    14 {

    15  pthread_t tid_produce, tid_consume;

    16  if (argc != 2)

    17   err_quit("usage: mycat2 <pathname>");

    18  fd = Open(argv[1], O_RDONLY);

    19  /* инициализация трех семафоров */

    20  Sem_init(&shared.mutex, 0, 1);

    21  Sem_init(&shared.nempty, 0, NBUFF);

    22  Sem_init(&shared.nstored, 0, 0);

    23  /* один производитель, один потребитель */

    24  Set_concurrency(2);

    25  Pthread_create(&tid_produce, NULL, produce, NULL); /* reader thread */

    26  Pthread_create(&tid_consume, NULL, consume, NULL); /* writer thread */

    27  Pthread_join(tid_produce, NULL);

    28  Pthread_join(tid_consume, NULL);

    29  Sem_destroy(&shared.mutex);

    30  Sem_destroy(&shared.nempty);

    31  Sem_destroy(&shared.nstored);

    32  exit(0);

    33 }

    Объявление нескольких буферов

    2-9 Структура shared содержит массив структур buff, которые состоят из буфера и его счетчика. Мы создаем NBUFF таких буферов.

    Открытие входного файла

    18 Аргумент командной строки интерпретируется как имя файла, который копируется в стандартный поток вывода.

    В листинге 10.19 приведен текст функций produce и consume.

    Листинг 10.19. Функции produce и consume

    //pxsem/mycat2.c

    34 void *

    35 produce(void *arg)

    36 {

    37  int i;

    38  for (i = 0;;) {

    39   Sem_wait(&shared.nempty); /* Ожидание освобождения места в буфере */

    40   Sem_wait(&shared.mutex);

    41   /* критическая область */

    42   Sem_post(&shared.mutex);

    43   shared.buff[i].n = Read(fd, shared.buff[i].data, BUFFSIZE);

    44   if (shared.buff[i].n == 0) {

    45    Sem_post(&shared.nstored); /* еще один объект */

    46    return(NULL);

    47   }

    48   if (++i >= NBUFF)

    49    i = 0; /* кольцевой буфер */

    50   Sem_post(&shared.nstored); /* еще один объект */

    51  }

    52 }


    53 void *

    54 consume(void *arg)

    55 {

    56  int i;

    57  for (i = 0;;) {

    58   Sem_wait(&shared.nstored); /* ожидание появления объекта для обработки */

    59   Sem_wait(&shared.mutex);

    60   /* критическая область */

    61   Sem_post(&shared.mutex);

    62   if (shared.buff[i].n == 0)

    63    return(NULL);

    64   Write(STDOUT_FILENO, shared.buff[i].data, shared.buff[i].n);

    65   if (++i >= NBUFF)

    66    i=0; /* кольцевой буфер */

    67   Sem_post(&shared.nempty); /* освободилось место для объекта */

    68  }

    69 }

    Пустая критическая область

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

    Считывание данных и увеличение семафора nstored

    43-49 Каждый раз, когда производитель получает пустой буфер, он вызывает функцию read. При возвращении из read увеличивается семафор nstored, уведомляя потребителя о том, что буфер готов. При возвращении функцией read значения 0 (конец файла) семафор увеличивается, а производитель завершает работу.

    Поток-потребитель

    57-68 Поток-потребитель записывает содержимое буферов в стандартный поток вывода. Буфер, содержащий нулевой объем данных, обозначает конец файла. Как и в потоке-производителе, критическая область, защищенная семафором mutex, пуста.

    ПРИМЕЧАНИЕ

    В разделе 22.3 книги [24] мы разработали пример с несколькими буферами. В этом примере производителем был обработчик сигнала SIGIO, а потребитель представлял собой основной цикл обработки (функцию dg_echo). Разделяемой переменной был счетчик nqueue. Потребитель блокировал сигнал SIGIO на время проверки или изменения счетчика.

    10.12. Использование семафоров несколькими процессами

    Правила совместного использования размещаемых в памяти семафоров несколькими процессами просты: сам семафор (переменная типа semt, адрес которой является первым аргументом sem_init) должен находиться в памяти, разделяемой всеми процессами, которые хотят его использовать, а второй аргумент функции sem_init должен быть равен 1.

    ПРИМЕЧАНИЕ

    Эти правила аналогичны требованиям к разделению взаимного исключения, условной переменной или блокировки чтения-записи между процессами: средство синхронизации (переменная типа pthread_mutex_t, pthread_cond_t или pthread_rwlock_t) должно находиться в разделяемой памяти и инициализироваться с атрибутом PTHREAD_PROCESS SHARED.

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

    Что произойдет, если мы вызовем функцию sem_open, возвращающую указатель на тип sem_t, а затем вызовем fork? В описании функции fork в стандарте Posix.1 говорится, что «все открытые родительским процессом семафоры будут открыты и в дочернем процессе». Это означает, что нижеследующий код верен:

    sem_t *mutex; /* глобальный указатель, копируемый, при вызове fork() */

    /* родительский процесс создает именованный семафор */

    mutex = Sem_open(Px_ipc_name(NAME), O_CREAT | O_EXCL, FILE_MODE, 0);

    if ((childpid = Fork()) == 0) {

     /* дочерний процесс */

     …

     Sem_wait(mutex);

     …

    }

    /* родительский процесс */

    Sem_post(mutex);

    ПРИМЕЧАНИЕ

    Причина, по которой следует аккуратно относиться к передаче семафоров при порождении процессов, заключается в том, что состояние семафора может храниться в переменной типа sem_t, но для его работы может требоваться и другая информация (например, дескрипторы файлов). В следующей главе мы увидим, что семафоры System V однозначно определяются их целочисленными идентификаторами, возвращаемыми функцией semget. Любой процесс, которому известен идентификатор, может получить доступ к семафору. Вся информация о семафоре System V хранится в ядре, а целочисленный идентификатор просто указывает номер семафора ядру.

    10.13. Ограничения на семафоры

    Стандартом Posix определены два ограничения на семафоры:

    SEM_NSEMS_MAX — максимальное количество одновременно открытых семафоров для одного процесса (Posix требует, чтобы это значение было не менее 256);

    SEM_VALUE_MAX — максимальное значение семафора (Posix требует, чтобы оно было не меньше 32767).

    Две эти константы обычно определены в заголовочном файле <unistd.h> и могут быть получены во время выполнения вызовом sysconf, как мы показываем ниже.

    Пример: программа semsysconf

    Программа в листинге 10.20 вызывает sysconf и выводит два ограничения на семафоры, зависящие от конкретной реализации. 

    Листинг 10.20. Вызов sysconf для получения ограничений на семафоры

    //pxsem/semsysconf.с

    1 #include "unpipc.h"


    2 int

    3 main(int argc, char **argv)

    4 {

    5  printf("SEM_NSEMS_MAX = %ld, SEM_VALUE_MAX = %ld\n",

    6   Sysconf(_SC_SEM_NSEMS_MAX), Sysconf(_SC_SEM_VALUE_MAX));

    7  exit(0);

    8 }

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

    solaris % semsysconf

    SEMS_NSEMS_MAX = 2147483647, SEM_VALUE_MAX = 2147483647

    alpha % semsysconf

    SEMS_NSEMS_MAX = 256, SEM_VALUE_MAX = 32767

    10.14. Реализация с использованием FIFO

    Займемся реализацией именованных семафоров Posix с помощью каналов FIFO. Именованный семафор реализуется как канал FIFO с конкретным именем. Неотрицательное количество байтов в канале соответствует текущему значению семафора. Функция sem_post помещает 1 байт в канал, a sem_wait считывает его оттуда (приостанавливая выполнение процесса, если канал пуст, а именно этого мы и хотим). Функция sem_open создает канал FIFO, если указан флаг O_CREAT; открывает его дважды (один раз на запись, другой — на чтение) и при создании нового канала FIFO помещает в него некоторое количество байтов, указанное в качестве начального значения.

    ПРИМЕЧАНИЕ

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

    Приведем текст нашего заголовочного файла semaphore.h, определяющего фундаментальный тип sem_t (листинг 10.21).

    Листинг 10.21. Заголовочный файл semaphore.h

    //my_pxsem_fifo/semaphore.h

    1  /* фундаментальный тип */

    2  typedef struct {

    3   int sem_fd[2]; /* два дескриптора fd: [0] для чтения, [1] для записи */

    4   int sem_magic; /* магическое число */

    5  } mysem_t;


    6  #define SEM_MAGIC 0x89674523


    7  #ifdef SEM_FAILED

    8  #undef SEM_FAILED

    9  #define SEM_FAILED ((mysem_t *)(-1)) /* чтобы компилятор не выдавал предупреждений*/


    10 #endif

    Тип данных sem_t

    1-5 Новая структура данных содержит два дескриптора, один из которых предназначен для чтения из FIFO, а другой — для записи. Для единообразия мы храним оба дескриптора в массиве из двух элементов, в котором первый дескриптор всегда открыт на чтение, а второй — на запись.

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

    Функция sem_open

    В листинге 10.22 приведен текст функции sem_open, которая создает новый семафор или открывает существующий.

    Листинг 10.22. Функция sem_open

    //my_pxsem_fifo/sem_open.с

    1  #include "unpipc.h"

    2  #include "semaphore.h"

    3  #include <stdarg.h> /* для произвольного списка аргументов */


    4  mysem_t *

    5  mysem_open(const char *pathname, int oflag, …)

    6  {

    7   int i, flags, save_errno;

    8   char c;

    9   mode_t mode;

    10  va_list ap;

    11  mysem_t *sem;

    12  unsigned int value;

    13  if (oflag & O_CREAT) {

    14   va_start(ap, oflag); /* ар инициализируется последним аргументом */

    15   mode = va_arg(ap, va_mode_t);

    16   value = va_arg(ap, unsigned int);

    17   va_end(ap);

    18   if (mkfifo(pathname, mode) < 0) {

    19    if (errno == EEXIST && (oflag & O_EXCL) == 0)

    20     oflag &= ~O_CREAT; /* уже существует, OK */

    21    else

    22     return(SEM_FAILED);

    23   }

    24  }

    25  if ((sem = malloc(sizeof(mysem_t))) == NULL)

    26   return(SEM_FAILED);

    27  sem->sem_fd[0] = sem->sem_fd[1] = –1;

    28  if ((sem->sem_fd[0] = open(pathname, O_RDONLY | O_NONBLOCK)) < 0)

    29   goto error;

    30  if ((sem->sem_fd[1] = open(pathname, O_WRONLY | O_NONBLOCK)) < 0)

    31   goto error;

    32  /* отключение неблокируемого режима для sem_fd[0] */

    33  if ((flags = fcntl(sem->sem_fd[0], F_GETFL, 0)) < 0)

    34   goto error;

    35  flags &= ~O_NONBLOCK;

    36  if (fcntl(sem->sem_fd[0], F_SETFL, flags) < 0)

    37   goto error;

    38  if (oflag & O_CREAT) { /* инициализация семафора */

    39   for (i = 0; i < value; i++)

    40    if (write(sem->sem_fd[1], &c, 1) != 1)

    41   goto error;

    42  }

    43  sem->sem_magic = SEM_MAGIC;

    44  return(sem);

    45 error:

    46  save_errno = errno;

    47  if (oflag & O_CREAT)

    48   unlink(pathname); /* если мы создали FIFO */

    49  close(sem->sem_fd[0]); /* игнорируем ошибку */

    50  close(sem->sem_fd[1]); /* игнорируем ошибку */

    51  free(sem);

    52  errno = save_errno;

    53  return(SEM_FAILED);

    54 }

    Создание нового sсемафора

    13-17 Если при вызове указан флаг O_CREAT, должно быть указано четыре аргумента, а не два. Мы вызываем va_start, после чего переменная ар указывает на последний явно указанный аргумент (oflag). Затем мы используем ар и функцию va_arg для получения значений третьего и четвертого аргументов. Работу со списком аргументов переменной длины и использование нашего типа va_mode_t мы обсуждали в связи с листингом 5.17.

    Создание нового канала FIFO

    18-23 Создается новый канал FIFO, имя которого было указано при вызове функции. Как мы отмечали в разделе 4.6, эта функция возвращает ошибку EEXIST, если канал уже существует. Если при вызове sem_open флаг O_EXCL не был указан, мы пропускаем эту ошибку; но нам не нужно будет инициализировать этот канал, так что мы при этом сбрасываем флаг O_CREAT.

    Выделение памяти под тип sem_t и открытие FIFO на чтение и запись

    25-37 Мы выделяем место для типа sem_t, который содержит два дескриптора. Затем мы дважды открываем канал FIFO: один раз только на чтение, а другой — только на запись. При этом мы не хотим блокирования при вызове open, поэтому указываем флаги O_NONBLOCK при открытии очереди только для чтения (вспомните табл. 4.1). Мы также указываем флаг O_NONBLOCK при открытии канала на запись, но это предназначено для обнаружения переполнения (на тот случай, если мы попытаемся записать больше, чем позволяет PIPE_BUF). После открытия канала мы отключаем неблокируемый режим для дескриптора, открытого на чтение.

    Инициализация значения созданного семафора

    38-42 Если мы создали семафор, его нужно проинициализировать, записав в канал FIFO value байтов. Если указанное при вызове значение value превышает определенное реализацией ограничение PIPE_BUF, вызов write после переполнения FIFO вернет ошибку с кодом EAGAIN.

    Функция sem_close

    Текст функции sem_close приведен в листинге 10.23.

    11-15 Мы закрываем оба дескриптора и освобождаем память, выделенную под тип sem_t.

    Листинг 10.23. Функция sem_close

    //my_pxsem_fifo/sem_close.с

    1  #include "unpipc.h"

    2  #include "semaphore.h"


    3  int

    4  mysem_close(mysem_t *sem)

    5  {

    6   if (sem->sem_magic != SEM_MAGIC) {

    7    errno = EINVAL;

    8    return(-1);

    9   }

    10  sem->sem_magic = 0; /* чтобы семафор нельзя было больше использовать */

    11  if (close(sem->sem_fd[0]) == –1 || close(sem->sem_fd[1]) == –1) {

    12   free(sem);

    13   return(-1);

    14  }

    15  free(sem);

    16  return(0);

    17 }

    Функция sem_unlink

    Функция sem_unlink, текст которой приведен в листинге 10.24, удаляет из файловой системы наш семафор. Она просто вызывает unlink.

    Листинг 10.24. Функция sem_unlink

    //my_pxsem_fifo/sem_unlink. с

    1 #include "unpipc.h"

    2 #include "semaphore.h"


    3 int

    4 mysem_unlink(const char *pathname)

    5 {

    6  return(unlink(pathname));

    7 }

    Функция sem_post

    В листинге 10.25 приведен текст функции sem_post, которая увеличивает значение семафора.

    11-12 Мы записываем один байт в FIFO. Если канал был пуст, это приведет к возобновлению выполнения всех процессов, заблокированных в вызове read для этого канала.

    Листинг 10.25. Функция sem_post

    //my_pxsem_fifo/sem_post.с

    1  #include "unpipc.h"

    2  #include "semaphore.h"


    3  int

    4  mysem_post(mysem_t *sem)

    5  {

    6   char c;

    7   if (sem->sem_magic != SEM_MAGIC) {

    8    errno = EINVAL;

    9    return(-1);

    10  }

    11  if (write(sem->sem_fd[1], &c, 1) == 1)

    12   return(0);

    13  return(-1);

    14 }

    Функция sem_wait

    Последняя функция для работы с именованными семафорами Posix — sem_wait. Ее текст приведен в листинге 10.26.

    Листинг 10.26. Функция sem_wait

    //my_pxsem_fifo/sem_wait.с

    1  #include "unpipc.h"

    2  #include "semaphore.h"


    3  int

    4  mysem_wait(mysem_t *sem)

    5  {

    6   char c;

    7   if (sem->sem_magic != SEM_MAGIC) {

    8    errno = EINVAL;

    9    return(-1);

    10  }

    11  if (read(sem->sem_fd[0], &c, 1) == 1)

    12   return(0);

    13  return(-1);

    14 }
     

    11-12 Мы считываем 1 байт из канала FIFO, причем работа приостанавливается, если канал пуст.

    Мы еще не реализовали функцию sem_trywait, но это можно сделать, установив флаг отключения блокировки для канала и используя обычный вызов read. Мы также не реализовали функцию sem_getvalue. В некоторых реализациях при вызове функции stat или fstat возвращается количество байтов в именованном или неименованном канале, причем оно помещается в поле st_size структуры stat. Однако это не гарантируется стандартом Posix и, следовательно, не обязательно будет работать в других системах. Пример реализации этих двух функций для работы с семафорами Posix приведен в следующем разделе.

    10.15. Реализация с помощью отображения в память

    Теперь займемся реализацией именованных семафоров Posix с помощью отображаемых в память файлов вместе со взаимными исключениями и условными переменными Posix. Реализация, аналогичная данной, приведена в разделе В.11.3 Обоснования стандарта IEEE 1996 [8].

    ПРИМЕЧАНИЕ

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

    Прежде всего приведем текст нашего заголовочного файла semaphore.h (листинг 10.27), в котором определяется фундаментальный тип sem_t.

    Тип sem_t

    1-7 Структура данных семафора содержит взаимное исключение, условную переменную и беззнаковое целое, в котором хранится текущее значение семафора. Как уже говорилось в связи с листингом 10.21, поле sem_magiс получает значение SEM_MAGIC при инициализации структуры.

    Листинг 10.27. Заголовочный файл semaphore.h

    //my_pxsem_mmap/semaphore.h

    1  /* фундаментальный тип */

    2  typedef struct {

    3   pthread_mutex_t sem_mutex; /* блокируется при проверке и изменении значения семафора */

    4   pthread_cond_t sem_cond; /* при изменении нулевого значения */

    5   unsigned int sem_count; /* значение семафора */

    6   int sem_magic; /* магическое значение, если семафор открыт */

    7  } mysem_t;


    8  #define SEM_MAGIC 0x67458923


    9  #ifdef SEM_FAILED

    10 #undef SEM_FAILED

    11 #define SEM_FAILED ((mysem_t *)(-1)) /* чтобы избежать предупреждений компилятора */

    12 #endif

    Функция sem_open

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

    Листинг 10.28. Функция sem_open: первая половина

    //my_pxsem_mmap/sem_open.с

    1  #include "unpipc.h"

    2  #include "semaphore.h"

    3  #include <stdarg.h> /* для списков аргументов переменной длины */

    4  #define MAX_TRIES 10 /* количество попыток инициализации */


    5  mysem_t *

    6  mysem_open(const char *pathname, int oflag, …)

    7  {

    8   int fd, i, created, save_errno;

    9   mode_t mode;

    10  va_list ap;

    11  mysem_t *sem, seminit;

    12  struct stat statbuff;

    13  unsigned int value;

    14  pthread_mutexattr_t mattr;

    15  pthread_condattr_t cattr;

    16  created = 0;

    17  sem = MAP_FAILED; /* [sic] */

    18 again:

    19  if (oflag & O_CREAT) {

    20   va_start(ap, oflag); /* ар инициализируется последним явно указанным аргументом */

    21   mode = va_arg(ap, va_mode_t) & ~S_IXUSR;

    22   value = va_arg(ap, unsigned int);

    23   va_end(ap);

    24   /* открываем с указанием флага O_EXCL и установкой бита user-execute */

    25   fd = open(pathname, oflag | O_EXCL | O_RDWR, mode | S_IXUSR);

    26   if (fd < 0) {

    27    if (errno == EEXIST && (oflag & O_EXCL) == 0)

    28     goto exists; /* уже существует. OK */

    29    else

    30     return(SEM_FAILED);

    31   }

    32   created = 1;

    33   /* кто создает файл, тот его и инициализирует */

    34   /* установка размера файла */

    35   bzero(&seminit, sizeof(seminit));

    36   if (write(fd, &seminit, sizeof(seminit)) != sizeof(seminit))

    37    goto err;

    38   /* отображение файла в память */

    39   sem = mmap(NULL, sizeof(mysem_t), PROT_READ | PROT_WRITE,

    40    MAP_SHARED, fd, 0);

    41   if (sem == MAP_FAILED)

    42    goto err;

    43   /* инициализация взаимного исключения, условной переменной, значения семафора */

    44   if ((i = pthread_mutexattr_init(&mattr)) != 0)

    45    goto pthreaderr;

    46   pthread_mutexattr_setpshared(&mattr, PTHREAD_PROCESS_SHARED);

    47   i = pthread_mutex_init(&sem->sem_mutex, &mattr);

    48   pthread_mutexattr_destroy(&mattr); /* не забыть удалить */

    49   if (i != 0)

    50    goto pthreaderr;

    51   if ((i = pthread_condattr_init(&cattr)) != 0)

    52    goto pthreaderr;

    53   pthread_condattr_setpshared(&cattr, PTHREAD_PROCESS_SHARED);

    54   i = pthread_cond_init(&sem->sem_cond, &cattr);

    55   pthread_condattr_destroy(&cattr); /* не забыть удалить */

    56   if (i != 0)

    57    goto pthreaderr;

    58   if ((sem->sem_count = value) > sysconf(_SC_SEM_VALUE_MAX)) {

    59    errno = EINVAL;

    60    goto err;

    61   }

    62   /* инициализация завершена, снимаем бит user-execute */

    63   if (fchmod(fd, mode) == –1)

    64    goto err;

    65   close(fd);

    66   sem->sem_magic = SEM_MAGIC;

    67   return(sem);

    68  }

    Работа со списком аргументов переменной длины

    19-23 Если при вызове функции указан флаг O_CREAT, мы должны принять четыре аргумента, а не два. Работа со списком аргументов переменной длины с помощью типа va_mode_t уже обсуждалась в связи с листингом 5.17, где мы использовали метод, аналогичный примененному здесь. Мы сбрасываем бит user-execute переменной mode (S_IXUSR) по причинам, которые вскоре будут раскрыты. Создается файл с указанным именем, и для него устанавливается бит user-execute.

    Создание нового семафора и обработка потенциальной ситуации гонок

    24-32 Если бы при указании флага O_CREAT мы просто открывали файл, отображали в память его содержимое и инициализировали поля структуры sem_t, у нас возникла бы ситуация гонок. Эта ситуация также уже обсуждалась в связи с листингом 5.17, и там мы воспользовались тем же методом, что и сейчас. Такая же ситуация гонок встретится нам, когда мы будем разбираться с листингом 10.37.

    Установка размера файла

    33-37 Мы устанавливаем размер созданного файла, записывая в него заполненную нулями структуру. Поскольку мы знаем, что только что созданный файл имеет размер 0, для установки его размера мы вызываем именно write, но не ftruncate, потому что, как мы отмечаем в разделе 13.3, Posix не гарантирует, что ftruncate срабатывает при увеличении размера обычных файлов. 

    Отображение содержимого файла в память

    38-42 Файл отображается в память вызовом mmap. Этот файл будет содержать текущее значение структуры типа sem_t, хотя, поскольку мы только что отобразили файл в память, мы обращаемся к нему через указатель, возвращаемый mmap, и никогда не вызываем read или write.

    Инициализация структуры sem_t

    43-57 Мы инициализируем три поля структуры sem_t: взаимное исключение, условную переменную и значение семафора. Поскольку именованный семафор Posix может совместно использоваться всеми процессами с соответствующими правами, которым известно его имя, при инициализации взаимного исключения и условной переменной необходимо указать атрибут PTHREAD_PROCESS_SHARED. Чтобы осуществить это для взаимного исключения, нужно сначала проинициализировать атрибуты, вызвав pthread_mutexattr_init, затем установить атрибут совместного использования потоками, вызвав pthread_mutexattr_setpshared, а затем проинициализировать взаимное исключение вызовом pthread_mutex_init. Аналогичные действия придется выполнить и для условной переменной. Необходимо аккуратно уничтожать переменные, в которых хранятся атрибуты, при возникновении ошибок.

    Инициализация значения семафора

    58-61 Наконец мы помещаем в файл начальное значение семафора. Предварительно мы сравниваем его с максимально разрешенным значением семафора, которое может быть получено вызовом sysconf (раздел 10.13).

    Сброс бита user-execute

    62-67 После инициализации семафора мы сбрасываем бит user-execute. Это указывает на то, что семафор был успешно проинициализирован. Затем мы закрываем файл вызовом close, поскольку он уже был отображен в память и нам не нужно держать его открытым.

    В листинге 10.29 приведен текст второй половины функции sem_open. Здесь возникает ситуация гонок, обрабатываемая так же, как уже обсуждавшаяся в связи с листингом 5.19.

    Листинг 10.29. Функция sem_open: вторая половина

    //my_pxsem_mmap/sem_open.с

    69  exists:

    70   if ((fd = open(pathname, O_RDWR)) < 0) {

    71    if (errno == ENOENT && (oflag & O_CREAT))

    72     goto again;

    73    goto err;

    74   }

    75   sem = mmap(NULL, sizeof(mysem_t), PROT_READ | PROT_WRITE,

    76    MAP_SHARED, fd, 0);

    77   if (sem == MAP_FAILED)

    78    goto err;

    79   /* удостоверимся, что инициализация завершена */

    80   for (i = 0; i < MAX TRIES; i++) {

    81    if (stat(pathname, &statbuff) == –1) {

    82     if (errno == ENOENT && (oflag & O_CREAT)) {

    83      close(fd);

    84      goto again;

    85     }

    86     goto err;

    87    }

    88    if ((statbuff.st_mode & S_IXUSR) == 0) {

    89     close(fd);

    90     sem->sem_magic = SEM_MAGIC;

    91     return(sem);

    92    }

    93    sleep(1);

    94   }

    95   errno = ETIMEDOUT;

    96   goto err;

    97  pthreaderr:

    98   errno = i;

    99  err:

    100  /* не даем вызовам unlink и munmap изменить код errno */

    101  save_errno = errno;

    102  if (created)

    103   unlink(pathname);

    104  if (sem != MAP_FAILED)

    105   munmap(sem, sizeof(mysem_t));

    106  close(fd);

    107  errno = save_errno;

    108  return(SEM_FAILED);

    109 }

    Открытие существующего семафора

    69-78 Здесь мы завершаем нашу работу, если либо не указан флаг O_CREAT, либо он указан, но семафор уже существует. В том и в другом случае мы открываем существующий семафор. Мы открываем файл вызовом open для чтения и записи, а затем отображаем его содержимое в адресное пространство процесса вызовом mmap.

    ПРИМЕЧАНИЕ

    Теперь легко понять, почему в Posix.1 сказано, что «обращение к копиям семафора приводит к неопределенным результатам». Если именованный семафор реализован через отображение файла в память, он отображается в адресное пространство всех процессов, в которых он открыт. Это осуществляется функцией sem_open для каждого процесса в отдельности. Изменения, сделанные одним процессом (например, изменение счетчика семафора), становятся доступны другим процессам через отображение в память. Если мы сделаем свою собственную копию структуры sem_t, она уже не будет общей для всех процессов. Хотя нам и может показаться, что вызовы срабатывают (функции для работы с семафором не будут возвращать ошибок, по крайней мере до вызова sem_close, которая не сможет отключить отображение для копии отображенного файла), с другими процессами мы при этом взаимодействовать не сможем. Однако заметьте (табл. 1.4), что области памяти с отображаемыми файлами передаются дочерним процессам при вызове fork, поэтому создание копии семафора ядром при порождении нового процесса проблем не вызовет. 

    Удостоверимся, что семафор проинициализирован

    79-96 Мы должны подождать, пока семафор не будет проинициализирован (если несколько потоков пытаются создать семафор приблизительно одновременно). Для этого мы вызываем stat и проверяем биты разрешений файла (поле st_mode структуры stat). Если бит user-execute снят, структура успешно проинициализирована.

    Возврат кодов ошибок

    97-108 При возникновении ошибки нужно аккуратно вернуть ее код.

    Функция sem_close

    В листинге 10.30 приведен текст нашей функции sem_close, которая просто вызывает munmap для отображенного в память файла. Если вызвавший процесс продолжит пользоваться указателем, который был ранее возвращен sem_open, он получит сигнал SIGSEGV.

    Листинг 10.30. Функция sem_close

    //my_pxsem_mmap/sem_close. с

    1  #include "unpipc.h"

    2  #include "semaphore.h"


    3  int

    4  mysem_close(mysem_t *sem)

    5  {

    6   if (sem->sem_magic != SEM_MAGIC) {

    7    errno = EINVAL;

    8    return(-1);

    9   }

    10  if (munmap(sem, sizeof(mysem_t)) == –1)

    11  return(-1);

    12  return(0);

    13 }

    Функция sem_unlink

    Текст функции sem_unlink приведен в листинге 10.31. Она просто удаляет файл, через который реализован данный семафор, вызывая функцию unlink.

    Листинг 10.31. Функция sem_unlink

    //my_pxsem_mmap/sem_unlink.с

    1 #include "unpipc.h"

    2 #include "semaphore.h"


    3 int

    4 mysem_unlink(const char *pathname)

    5 {

    6  if (unlink(pathname) == –1)

    7   return(-1);

    8  return(0);

    9 }

    Функция sem_post

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

    Листинг 10.32. Функция sem_post

    //my_pxsem_mmap/sem_post.с

    1  #include "unpipc.h"

    2  #include "semaphore.h"


    3  int

    4  mysem_post(mysem_t *sem)

    5  {

    6   int n;

    7   if (sem->sem_magic != SEM_MAGIC) {

    8    errno = EINVAL;

    9    return(-1);

    10  }

    11  if ((n = pthread_mutex_lock(&sem->sem_mutex)) != 0) {

    12   errno = n;

    13   return(-1);

    14  }

    15  if (sem->sem_count == 0)

    16   pthread_cond_signal(&sem->sem_cond);

    17  sem->sem_count++;

    18  pthread_mutex_unlock(&sem->sem_mutex);

    19  return(0);

    20 }

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

    Функция sem_wait

    В листинге 10.33 приведен текст функции sem_wait, которая ожидает изменения значения семафора с 0 на положительное, после чего уменьшает его на 1.

    Листинг 10.33. Функция sem_wait

    //my_pxsem_mmap/sem_wait.с

    1  #include "unpipc.h"

    2  #include "semaphore.h"


    3  int

    4  mysem_wait(mysem_t *sem)

    5  {

    6   int n;

    7   if (setn->sem_magic != SEM_MAGIC) {

    8    errno = EINVAL;

    9    return(-1);

    10  }

    11  if ((n = pthread_mutex_lock(&sem->sem_mutex)) != 0) {

    12   errno = n;

    13   return(-1);

    14  }

    15  while (sem->sem_count == 0)

    16   pthread_cond_wait(&sem->sem_cond, &sem->sem_mutex);

    17  sem->sem_count--;

    18  pthread_mutex_unlock(&sem->sem_mutex);

    19  return(0);

    20 }

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

    Функция sem_trywait

    В листинге 10.34 приведен текст функции sem_trywait, которая представляет собой просто неблокируемый вариант функции sem_wait.

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

    Листинг 10.34. Функция sem_trywait

    //my_pxsem_nmap/sem_trywait.с

    1  #include "unpipc.h"

    2  #include "semaphore.h"


    3  int

    4  mysem_trywait(mysem_t *sem)

    5  {

    6   int n, rc;

    7   if (sem->sem_magic != SEM_MAGIC) {

    8    errno = EINVAL;

    9    return(-1);

    10  }

    11  if ((n = pthread_mutex_lock(&sem->sem_mutex)) != 0) {

    12   errno = n;

    13   return(-1);

    14  }

    15  if (sem->sem_count > 0) {

    16   sem->sem_count--;

    17   rc = 0;

    18  } else {

    19   rc = –1;

    20   errno = EAGAIN;

    21  }

    22  pthread_mutex_unlock(&sem->sem_mutex);

    23  return(rc);

    24 }

    Функция sem_getvalue

    В листинге 10.35 приведен текст последней функции в этой реализации — sem_getvalue. Она возвращает текущее значение семафора.

    11-16 Мы блокируем соответствующее взаимное исключение и считываем значение семафора.

    Листинг 10.35. Функция sem_getvalue

    //my_pxsem_mmap/sem_getvalue.c

    1  #include "unpipc.h"

    2  #include "semaphore.h"


    3  int

    4  mysem_getvalue(mysem_t *sem, int *pvalue)

    5  {

    6   int n;

    7   if (sem->sem_magic != SEM_MAGIC) {

    8    errno = EINVAL;

    9    return(-1);

    10  }

    11  if ((n = pthread_mutex_lock(&sem->sem_mutex)) != 0) {

    12   errno = n;

    13   return(-1);

    14  }

    15  *pvalue = sem->sem_count;

    16  pthread_mutex_unlock(&sem->sem_mutex);

    17  return(0);

    18 }

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

    10.16. Реализация с использованием семафоров System V

    Приведем еще один пример реализации именованных семафоров Posix — на этот раз с использованием семафоров System V. Поскольку семафоры System V появились раньше, чем семафоры Posix, эта реализация позволяет использовать последние в системах, где их поддержка не предусмотрена производителем.

    ПРИМЕЧАНИЕ

    Семафоры System V описаны в главе 11. Этот раздел можно пропустить при первом чтении, с тем чтобы вернуться к нему по прочтении 11 главы.

    Начнем, как обычно, с заголовочного файла semaphore.h (листинг 10.36), который определяет фундаментальный тип данных sem_t.

    Листинг 10.36. Заголовочный файл semaphore.h

    //my_pxsem_svsem/semaphore.h

    1  /* фундаментальный тип данных */

    2  typedef struct {

    3   int sem_semid; /* идентификатор семафора System V */

    4   int sem_magic; /* магическое значение, если семафор открыт */

    5  } mysem_t;


    6  #define SEM_MAGIC 0x45678923


    7  #ifdef SEM_FAILED

    8  #undef SEM_FAILED

    9  #define SEM_FAILED ((mysem_t *)(-1)) /* исключаем предупреждения компилятора */

    10 #endif


    11 #ifndef SEMVMX

    12 #define SEMVMX 32767 /* исторически сложившееся максимальное значение для семафора System V */

    13 #endif

    Тип данных sem_t

    1-5 Мы реализуем именованный семафор Posix с помощью набора семафоров System V, состоящего из одного элемента. Структура данных семафора содержит идентификатор семафора System V и магическое число (обсуждавшееся в связи с листингом 10.21).

    Функция sem_open

    В листинге 10.37 приведен текст первой половины функции sem_open, которая создает новый семафор или открывает существующий.

    Листинг 10.37. Функция sem_open: первая часть

    //my_pxsem_svsem/sem_open. с

    1  #include "unpipc.h"

    2  #include "semaphore.h"

    3  #include <stdarg.h> /* для списков аргументов переменной длины */

    4  #define MAX_TRIES 10 /* количество попыток инициализации */


    5  mysem_t *

    6  mysem_open(const char *pathname, int oflag, … )

    7  {

    8   int i, fd, semflag, semid, save_errno;

    9   key_t key;

    10  mode_t mode;

    11  va_list ap;

    12  mysem_t *sem;

    13  union semun arg;

    14  unsigned int value;

    15  struct semid_ds seminfo;

    16  struct sembuf initop;

    17  /* режим доступа для sem_open() без O_CREAT не указывается; угадываем */

    18  semflag = SVSEM_MODE;

    19  semid = –1;

    20  if (oflag & O_CREAT) {

    21   va_start(ap, oflag); /* инициализируем ар последним явно указанным аргументом */

    22   mode = va_arg(ap, va_mode_t);

    23   value = va_arg(ap, unsigned int);

    24   va_end(ap);

    25   /* преобразуем в ключ, который будет идентифицировать семафор System V */

    26   if ((fd = open(pathname, oflag, mode)) == –1)

    27    return(SEM_FAILED);

    28   close(fd);

    29   if ((key = ftok(pathname, 0)) == (key_t) –1)

    30    return(SEM_FAILED);

    31   semflag = IPC_CREAT | (mode & 0777);

    32   if (oflag & O_EXCL)

    33    semflag |= IPC_EXCL;

    34    /* создаем семафор System V с флагом IPC_EXCL */

    35   if ((semid = semget(key, 1, semflag | IPC_EXCD) >= 0) {

    36    /* OK, мы успели первыми, поэтому инициализируем нулем */

    37    arg.val = 0;

    38    if (semctl(semid, 0, SETVAL, arg) == –1)

    39     goto err;

    40    /* увеличиваем значение, чтобы sem_otime стало ненулевым */

    41    if (value > SEMVMX) {

    42     errno = EINVAL;

    43     goto err;

    44    }

    45    initop.sem_num = 0;

    46    initop.sem_op = value;

    47    initop.sem_flg = 0;

    48    if (semop(semid, &initop, 1) == –1)

    49     goto err;

    50    goto finish;

    51   } else if (errno != EEXIST || (semflag & IPC_EXCL) != 0)

    52    goto err:

    53   /* иначе продолжаем выполнение */

    54  }

    Создание нового семафора и работа со списком аргументов переменной длины

    20-24 Если вызвавший процесс указывает флаг O_CREAT, мы знаем, что функции будут переданы четыре аргумента, а не два. Работа со списком аргументов переменной длины и типом данных va_mode_t обсуждалась в связи с листингом 5.17.

    Создание вспомогательного файла и преобразование полного имени в ключ System V IPC

    25-30 Создается обычный файл с именем, указываемым при вызове функции. Это делается для того, чтобы указать его имя при вызове функции ftok для последующей идентификации семафора. Аргумент oflag, принятый от вызвавшего процесса, передается функции open для дополнительного файла, что позволяет создать его, если он еще не существует, и вернуть ошибку EEXIST, если файл существует и указан флаг O_EXCL. Дескриптор файла затем закрывается, поскольку единственная цель создания файла была в использовании его имени при вызове ftok, преобразующей полное имя в ключ System V IPC (раздел 3.2).

    Создание набора семафоров System V с одним элементом

    32-33 Мы преобразуем константы O_CREAT и O_EXCL в соответствующие константы System V IРС_ххх и вызываем semget для создания набора семафоров System V, состоящего из одного элемента. Флаг IPC_EXCL указывается всегда, чтобы можно было определить, существовал ли семафор до вызова функции или был создан ею.

    Инициализация семафора

    34-50 В разделе 11.2 описана фундаментальная проблема, связанная с инициализацией семафоров System V, а в разделе 11.6 приведен код, позволяющий исключить потенциальную ситуацию гонок. Здесь мы пользуемся аналогичным методом. Первый поток, который создает семафор (вспомните, что мы всегда указываем флаг IPC_EXCL), инициализирует его значением 0 с помощью команды SETVAL при вызове semctl, а затем устанавливает запрошенное вызвавшим процессом начальное значение с помощью semop. Мы можем быть уверены, что значение sem_otime семафора функцией semget устанавливается в 0 и будет изменено на ненулевое вызовом semop. Следовательно, любой поток, работающий с существующим семафором, будет знать, что он уже проинициализирован, если значение sem_otime будет отлично от 0.

    Проверка начального значения

    40-44 Мы проверяем начальное значение, указанное вызвавшим процессом, поскольку семафоры System V обычно хранятся как беззнаковые короткие целые (unsigned short, структура sem в разделе 11.1) с максимальным значением 32767 (раздел 11.7), тогда как семафоры Posix обычно хранятся как целые с максимально возможным размером (раздел 10.13). Константа SEMVMX определяется некоторыми реализациями как максимальное значение семафора System V, а если она не определена, то мы определяем ее равной 32 767 в листинге 10.36.

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

    В листинге 10.38 приведен текст второй половины функции sem_open.

    Листинг 10.38. Функция sem_open: вторая половина

    //my_pxsem_svsem/sem_open.c

    55  /*

    56   * (O_CREAT не указан) или

    57   * (O_CREAT без O_EXCL и семафор уже существует).

    58   * Нужно открыть семафор и проверить, что он уже проинициализирован.

    59   */

    60  if ((key = ftok(pathname, 0)) == (key_t) –1)

    61   goto err;

    62  if ((semid = semget(key, 0, semflag)) == –1)

    63 goto err;

    64  arg.buf = &seminfo;

    65  for (i = 0; i < MAX_TRIES; i++) {

    66   if (semctl(semid, 0, IPC_STAT, arg) == –1)

    67    goto err;

    68   if (arg.buf->sem_otime != 0)

    69    goto finish;

    70   sleep(1);

    71  }

    72  errno = ETIMEDOUT;

    73 err:

    74  save_errno = errno; /* не даем вызову semctl() изменить значение errno */

    75  if (semid != –1)

    76   semctl(semid, 0, IPC_RMID);

    77  errno = save_errno;

    78  return(SEM_FAILED);

    79 finish:

    80  if ((sem = malloc(sizeof(mysem_t))) == NULL)

    81   goto err;

    82  sem->sem_semid = semid;

    83  sem->sem_magic = SEM_MAGIC;

    84  return(sem);

    85 }

    Открытие существующего семафора

    55-63 Если семафор уже создан (флаг O_CREAT не указан или указан, но без O_EXCL, а семафор существует), мы открываем семафор System V с помощью semget. Обратите внимание, что в вызове sem_open указывать аргумент mode не нужно, если не указан флаг O_CREAT, но вызов semget требует указания режима доступа, даже если открывается существующий семафор. Ранее в тексте функции мы присваивали значение по умолчанию (константу SVSEM_MODE из нашего заголовочного файла unpipc.h) переменной, которую теперь передаем semget, если не указан флаг O_CREAT.

    Ожидание инициализации семафора

    64-72 Проверяем, что семафор уже инициализирован, вызывая semctl с командой IPC_STAT и сравнивая значение поля sem_otime возвращаемой структуры с нулем.

    Возврат кода ошибки

    73-78 Когда возникает ошибка, мы аккуратно вызываем все последующие функции, чтобы не изменить значение errno.

    Выделение памяти под sem_t

    79-84 Мы выделяем память под структуру sem_t и помещаем в нее идентификатор семафора System V. Функция возвращает указатель на эту структуру.

    Функция sem_close

    В листинге 10.39 приведен текст функции sem_close, которая вызывает free для освобождения динамически выделенной под структуру sem_t памяти. 

    Листинг 10.39. Функция sem_close

    //my_pxsem_svsem/sem_close.с

    1  #include "unpipc.h"

    2  #include "semaphore.h"


    3  int

    4  mysem_close(mysem_t *sem)

    5  {

    6   if (sem->sem_magic != SEM_MAGIC) {

    7    errno = EINVAL;

    8    return(-1);

    9   }

    10  sem->sem_magic = 0; /* на всякий случай */

    11  free(sem);

    12  return(0);

    13 }

    Функция sem_unlink

    Функция sem_unlink, текст которой приведен в листинге 10.40, удаляет вспомогательный файл и семафор System V, связанные с указанным ей семафором Posix.

    Листинг 10.40. Функция sem_unlink

    //my_pxsem_svsem/sem_unlink.с

    1  #include "unpipc.h"

    2  #include "semaphore.h"


    3  int

    4  mysem_unlink(const char *pathname)

    5  {

    6   int semid;

    7   key_t key;

    8   if ((key = ftok(pathname, 0)) == (key_t) –1)

    9    return(-1);

    10  if (unlink(pathname) == –1)

    11   return(-1);

    12  if ((semid = semget(key, 1, SVSEM_MODE)) == –1)

    13   return(-1);

    14  if (semctl(semid, 0, IPC_RMID) == –1)

    15   return(-1);

    16  return(0);

    17 }

    Получение ключа System V по полному имени

    8-16 Функция ftok преобразует полное имя файла в ключ System V IPC. После этого вспомогательный файл удаляется вызовом unlink (именно в этом месте кода, на тот случай, если одна из последующих функций вернет ошибку). Затем мы открываем семафор System V вызовом semget и удаляем его с помощью команды IPC_RMID для semctl.

    Функция sem_post

    В листинге 10.41 приведен текст функции sem_post, которая увеличивает значение семафора.

    11-16 Мы вызываем semop с операцией, увеличивающей значение семафора на 1.

    Листинг 10.41. Функция sem_post

    //my_pxsem_svsem/sem_post.с

    1  #include "unpipc.h"

    2  #include "semaphore.h"


    3  int

    4  mysem_post(mysem_t *sem)

    5  {

    6   struct sembuf op;

    7   if (sem->sem_magic != SEM_MAGIC) {

    8    errno * EINVAL;

    9    return(-1);

    10  }

    11  op.sem_num = 0;

    12  op.sem_op = 1;

    13  op.sem_flg = 0;

    14  if (semop(sem->sem_semid, &op, 1) < 0)

    15   return(-1);

    16  return(0);

    17 }

    Функция sem_wait

    Следующая функция приведена в листинге 10.42; она называется sem_wait и ожидает изменения значения семафора с нулевого на ненулевое, после чего уменьшает значение семафора на 1.

    11-16 Мы вызываем semop с операцией, уменьшающей значение семафора на 1.

    Листинг 10.42. Функция sem_wait

    //my_pxsem_svsem/sem_wait.c

    1  #include "unpipc.h"

    2  #include "semaphore.h"


    3  int

    4  mysem_wait(mysem_t *sem)

    5  {

    6   struct sembuf op;

    7   if (sem->sem_magic != SEM_MAGIC) {

    8    errno = EINVAL;

    9    return(-1);

    10  }

    11  op.sem_num = 0;

    12  op.sem_op = –1;

    13  op.sem_flg = 0;

    14  if (semop(sem->sem_semid, &op, 1) < 0)

    15   return(-1);

    16  return(0);

    17 }

    Функция sem_trywait

    В листинге 10.43 приведен текст нашей функции sem_trywait, которая представляет собой неблокируемую версию sem_wait.

    13 Единственное отличие от функции sem_wait из листинга 10.42 заключается в том, что флагу sem_flg присваивается значение IPC_NOWAIT. Если операция не может быть завершена без блокирования вызвавшего потока, функция semop возвращает ошибку EAGAIN, а это именно тот код, который должен быть возвращен sem_trywait, если операция не может быть завершена без блокирования потока.

    Листинг 10.43. Функция sem_trywait

    //my_pxsem_svsem/sem_trywait.c

    1  #include "unpipc.h"

    2  #include "semaphore.h"


    3  int

    4  mysem_trywait(mysem_t *sem)

    5  {

    6   struct sembuf op;

    7   if (sem->sem_magic != SEM_MAGIC) {

    8    errno = EINVAL;

    9    return(-1);

    10  }

    11  op.sem_num = 0;

    12  op.sem_op = –1;

    13  op.sem_flg = IPC_NOWAIT;

    14  if (semop(sem->sem_semid, &op, 1) < 0)

    15   return(-1);

    16  return(0);

    17 }

    Функция sem_getvalue

    Последняя функция приведена в листинге 10.44. Это функция sem_getvalue, возвращающая текущее значение семафора.

    11-14 Текущее значение семафора получается отправкой команды GETVAL функции semctl.

    Листинг 10.44. Функция sem_getvalue

    //my_pxsem_svsem/sem_getvalue.с

    1  #include "unpipc.h"

    2  #include "semaphore.h"


    3  int

    4  mysem_getvalue(mysem_t *sem, int *pvalue)

    5  {

    6   int val;

    7   if (sem->sem_magic != SEM_MAGIC) {

    8    errno = EINVAL;

    9    return(-1);

    10  }

    11  if ((val = semctl(sem->sem_semid, 0, GETVAL)) < 0)

    12   return(-1);

    13  *pvalue = val;

    14  return(0);

    15 }

    10.17. Резюме

    Семафоры Posix представляют собой семафоры-счетчики, для которых определены три основные операции:

    1. Создание семафора.

    2. Ожидание изменения значения семафора на ненулевое и последующее уменьшение значения.

    3. Увеличение значения семафора на 1 и возобновление выполнения всех процессов, ожидающих его изменения.

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

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

    В этой главе было приведено три примера возможной реализации семафоров Posix. Первый пример был самым простым, в нем использовались каналы FIFO, а большая часть забот по синхронизации ложилась на ядро (функции read и write). Следующая реализация использовала отображение файлов в память (аналогично реализации очередей сообщений Posix из раздела 5.8), а также взаимное исключение и условную переменную (для синхронизации). Последняя реализация была основана на семафорах System V и представляла собой, по сути, удобный интерфейс для работы с ними.

    Упражнения

    1. Измените функции produce и consume из раздела 10.6 следующим образом. Поменяйте порядок двух вызовов Sem_wait в потребителе, чтобы возникла ситуация зависания (как описано в разделе 10.6). Затем добавьте вызов printf перед каждым Sem_wait, чтобы было ясно, какой из потоков ожидает изменения семафора. Добавьте еще один вызов printf после каждого Sem_wait, чтобы можно было определить, какой поток получил управление. Уменьшите количество буферов до двух, а затем откомпилируйте и выполните эту программу, чтобы убедиться, что она зависнет.

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

    % lockpxsem & lockpxsem & lockpxsem & lockpxsem &

    Каждый из четырех процессов запускается с значением initflag, равным 0, поэтому при вызове sem_open всегда указывается O_CREAT. Нормально ли это?

    3. Что произойдет в предыдущем примере, если одна из четырех программ будет завершена после вызова my_lock, но перед вызовом my_unlock?

    4. Что произошло бы с программой в листинге 10.22, если бы мы не инициализировали оба дескриптора значением –1?

    5. Почему в листинге 10.22 мы сохраняем значение errno, а затем восстанавливаем его, вместо того чтобы написать просто:

    if (sem->fd[0] >= 0) close(sem->fd[0]);

    if (sem->fd[1] >= 0) close(sem->fd[1]);

    6. Что произойдет, если два процесса вызовут нашу реализацию sem_open через FIFO (листинг 10.22) примерно одновременно, указывая флаг O_CREAT и начальное значение 5? Может ли канал быть инициализирован (неправильно) значением 10?

    7. В связи с листингами 10.28 и 10.29 мы описали возможную ситуацию гонок в случае, если два процесса пытаются создать семафор примерно одновременно. Однако решение предыдущей задачи в листинге 10.22 не создавало ситуации гонок. Объясните это.

    8. Стандарт Posix.1 указывает дополнительную возможность для функции semwait: она может прерываться перехватываемым сигналом и возвращать код EINTR. Напишите тестовую программу, которая определяла бы, есть ли такая возможность в вашей реализации.

    Запустите эту тестовую программу с нашими реализациями, использующими FIFO (раздел 10.14), отображение в память (раздел 10.15) и семафоры System V (раздел 10.16).

    9. Какая из трех реализаций sem_post этой главы является функцией типа async-signal-safe (табл. 5.1)?

    10. Измените решение задачи о потребителе и производителе в разделе 10.6 так, чтобы для переменной mutex использовался тип pthread_mutex_t, а не семафор. Заметна ли разница в скорости работы программы?

    11. Сравните быстродействие именованных семафоров (листинги 10.8 и 10.9) и размещаемых в памяти (листинг 10.11).

    ГЛАВА 11

    Семафоры System V

    11.1.Введение

    В главе 10 мы описывали различные виды семафоров, начав с:

    ■ бинарного семафора, который может принимать только два значения: 0 и 1. По своим свойствам такой семафор аналогичен взаимному исключению (глава 7), причем значение 0 для семафора соответствует блокированию ресурса, а 1 — освобождению.

    Далее мы перешли к более сложному виду семафоров:

    ■ семафор-счетчик, значение которого лежит в диапазоне от 0 до некоторого ограничения, которое, согласно Posix, не должно быть меньше 32767. Они использовался для подсчета доступных ресурсов в задаче о производителях и потребителях, причем значение семафора соответствовало количеству доступных ресурсов.

    Для обоих типов семафоров операция wait состояла в ожидании изменения значения семафора с нулевого на ненулевое и последующем уменьшении этого значения на 1. Операция post увеличивала значение семафора на 1, оповещая об этом все процессы, ожидавшие изменения значения семафора.

    Для семафоров System V определен еще один уровень сложности:

    ■ набор семафоров-счетчиков — один или несколько семафоров, каждый из которых является счетчиком. На количество семафоров в наборе существует ограничение (обычно порядка 25 — раздел 11.7). Когда мы говорим о семафоре System V, мы подразумеваем именно набор семафоров-счетчиков, а когда говорим о семафоре Posix, подразумевается ровно один семафор-счетчик.

    Для каждого набора семафоров ядро поддерживает следующую информационную структуру, определенную в файле <sys/sem.h>:

    struct semid_ds {

     struct ipc_perm sem_perm; /* разрешения на операции */

     struct sem *sem_base; /*указатель на массив семафоров в наборе */

     ushort sem_nsems; /* количество семафоров в наборе */

     time_t sem_otime; /* время последнего вызова semop(); */

     time_t sem_ctime; /* время создания последнего IPC_SET */

    };

    Структура ipc_perm была описана в разделе 3.3. Она содержит разрешения доступа для данного семафора.

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

    struct sem {

     ushort_t semval; /* значение семафора, неотрицательно */

     short sempid; /* PID последнего процесса, вызвавшего semop(), SETVAL, SETALL */

     ushort_t semncnt; /* количество ожидающих того, что значение семафора превысит текущее */

     ushort_t semzcnt; /* количество ожидающих того, что значение семафора станет равным 0*/

    };

    Обратите внимание, что sem_base представляет собой указатель на массив структур типа sem — по одному элементу массива на каждый семафор в наборе.

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

    ПРИМЕЧАНИЕ

    В стандарте Unix 98 данная структура не имеет имени. Приведенное выше имя (sem) взято из реализации System V. 

    Любой конкретный семафор в ядре мы можем воспринимать как структуру semid_ds, указывающую на массив структур sem. Если в наборе два элемента, мы получим картину, изображенную на рис. 11.1. На этом рисунке переменная sem_nsems имеет значение 2, а каждый из элементов набора идентифицируется индексом ([0] или [1]). 

    Рис. 11.1. Структуры данных ядра для набора семафоров из двух элементов

    11.2. Функция semget

    Функция semget создает набор семафоров или обеспечивает доступ к существующему.

    #include <sys/sem.h>

    int semget(key_t key, int nsems, int oflag);

    /* Возвращает неотрицательный идентификатор в случае успешного завершения, –1 – в случае ошибки */

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

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

    Аргумент oflag представляет собой комбинацию констант SEM_R и SEM_A из табл. 3.3. Здесь R обозначает Read (чтение), а А — Alter (изменение). К этим константам можно логически прибавить IPC_CREAT или IPC_CREAT | IPC_EXCL, о чем мы уже говорили в связи с рис. 3.2.

    При создании нового семафора инициализируются следующие поля структуры 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 — идентификатор удален).

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

    1. Если значение sem_op положительно, оно добавляется к semval. Такое действие соответствует освобождению ресурсов, управляемых семафором. Если указан флаг SEM_UNDO, значение sem_op вычитается из значения semadj данного семафора.

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

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

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

    Если значение semval больше либо равно модулю sem_op, модуль sem_op вычитается из semval. Если указан флаг SEM_UNDO, модуль sem_op добавляется к значению поля semadj данного семафора.

    Если значение semval меньше модуля sem_op, значение поля semncnt данного семафора увеличивается, а вызвавший поток блокируется до тех пор, пока semval не станет больше либо равно модулю semop. Когда это произойдет, поток будет разблокирован, а модуль sem_op будет отнят от semval и из значения semncnt будет вычтена единица. Если указан флаг SEM_UNDO, модуль sem_op добавляется к значению поля semadj данного семафора. Как отмечалось ранее, поток не будет приостановлен, если указан флаг IPC_NOWAIT. Ожидание завершается преждевременно, если перехватываемый сигнал вызывает прерывание либо семафор удаляется другим потоком.

    ПРИМЕЧАНИЕ

    Если сравнить этот набор операций с теми, которые разрешены для семафоров Posix, мы увидим, что для последних определены только команды –1 (sem_wait) и +1 (sem_post). Для семафоров System V значение семафора может изменяться с шагом, отличным от 1, и кроме того, поток может ожидать, чтобы значение семафора стало нулевым. Эти операции являются более общими, что вместе с возможностью включения нескольких семафоров в набор делает семафоры System V более сложными, чем одиночные семафоры Posix.

    11.4. Функция semctl

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

    #include <sys/sem.h>

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

    /* Возвращает неотрицательное значение в случае успешного завершения (см. в тексте). –1 – в случае ошибки */

    Первый аргумент (semid) представляет собой идентификатор семафора, a semnum указывает элемент набора семафоров (0, 1 и т. д. до nsems –1). Значение semnum используется только командами GETVAL, SETVAL, GETNCNT, GETZCNT и GETPID.

    Четвертый аргумент является дополнительным — он добавляется в зависимости от команды cmd (см. комментарии в описании объединения). Объявляется это объединение следующим образом:

    union semun {

     int val; /* используется только командой SETVAL */

     struct semid_ds *buf; /* используется командами IPC_SET и IPC_STAT */

     ushort *array; /* используется командами GETALL и SETALL */

    };

    Это объединение отсутствует в системных заголовочных файлах и должно декларироваться приложением (мы определяем его в заголовочном файле unpipc.h, листинг B.1). Оно передается по значению, а не по ссылке, то есть аргументом является собственно значение объединения, а не указатель на него. 

    ПРИМЕЧАНИЕ

    К сожалению, в некоторых системах (FreeBSD и Linux) это объединение определено в заголовочном файле <sys/sem.h>, что затрудняет написание переносимых программ. Хотя в объявлении этого объединения в системном заголовочном файле и есть некоторый смысл, стандарт Unix 98 требует, чтобы оно каждый раз явно объявлялось приложением.

    Ниже приведен список поддерживаемых значений аргумента cmd. В случае успешного завершения функция возвращает 0, а в случае ошибки – –1, если в описании команды не сказано что-либо другое.

    ■ GETVAL — возвращает текущее значение semval. Поскольку значение семафора отрицательным быть не может (semval объявляется как unsigned short — беззнаковое короткое целое), в случае успешного возврата значение всегда будет неотрицательным.

    ■ SETVAL — установка значения semval равным arg.val. В случае успешного выполнения корректировочное значение этого семафора (semadj) устанавливается равным нулю для всех процессов.

    ■ GETPID — функция возвращает текущее значение поля sempid.

    ■ GETNCNT — функция возвращает текущее значение поля semncnt.

    ■ GETZCNT — функция возвращает текущее значение поля semzcnt.

    ■ GETALL — возвращаются значения semval для всех элементов набора. Значения возвращаются через указатель arg.array, а сама функция при этом возвращает 0. Обратите внимание, что вызывающий процесс должен самостоятельно выделить массив беззнаковых коротких целых достаточного объема для хранения всех значений семафоров набора, а затем сделать так, чтобы arg.array указывал на этот массив.

    ■ SETALL — установка значений semval для всех элементов набора. Значения задаются через указатель arg.array.

    ■ IPC_RMID — удаление набора семафоров, задаваемого через идентификатор semid.

    ■ IPC_SET — установка трех полей структуры semid_ds равными соответствующим полям структуры arg.buf: sem_perm.uid, sem_perm.gid и sem_perm.mode. Поле sem_ctime структуры semid_ds устанавливается равным текущему времени.

    ■ IPC_STAT — возвращение вызвавшему процессу через аргумент arg.buf текущего значения полей структуры semid_ds для данного набора семафоров. Обратите внимание, что вызывающий процесс должен сначала выделить место под структуру semid_ds и установить на нее указатель arg.buf.

    11.5. Простые программы

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

    Программа semcreate

    Первая программа, текст которой приведен в листинге 11.1,[1] просто создает набор семафоров System V. Параметр командной строки –е соответствует флагу IPC_EXCL при вызове semget, а последним аргументом командной строки является количество семафоров в создаваемом наборе.

    Листинг 11.1. Программа semcreate

    //svsem/semcreate.с

    1  #include "unpipc.h"


    2  int

    3  main(int argc, char **argv)

    4  {

    5   int с, oflag, semid, nsems;

    6   oflag = SVSEM_MODE | IPC_CREAT;

    7   while ((c = Getopt(argc, argv, "e")) != –1) {

    8    switch (c) {

    9    case 'e':

    10    oflag |= IPC_EXCL;

    11    break;

    12   }

    13  }

    14  if (optind != argc – 2)

    15   err_quit("usage: semcreate [ –e ] <pathname> <nsems>");

    16  nsems = atoi(argv[optind + 1]);

    17  semid = Semget(Ftok(argv[optind], 0), nsems, oflag);

    18  exit(0);

    19 }

    Программа semrmid

    Следующая программа, текст которой приведен в листинге 11.2, удаляет набор семафоров из системы. Для этого используется вызов semctl с командой (аргументом cmd) IPC_RMID.

    Листинг 11.2. Программа semrmid

    //svsem/semrmid.c

    1  #include "unpipc.h"


    2  int

    3  main(int argc, char **argv)

    4  {

    5   int semid;

    6   if (argc != 2)

    7    err_quit("usage: semrmid <pathname>"):

    8   semid = Semget(Ftok(argv[1], 0), 0, 0);

    9   Semctl(semid, 0, IPC_RMID);

    10  exit(0);

    11 }

    Программа semsetvalues

    Программа semsetvalues (листинг 11.3) устанавливает значения всех семафоров набора.

    Получение количества семафоров в наборе

    11-15 После получения идентификатора семафора с помощью semget мы вызываем semctl с командой IPC_STAT, чтобы получить значения полей структуры semid_ds для данного семафора. Поле sem_nsems содержит нужную нам информацию о количестве семафоров в наборе.

    Установка всех значений

    19-24 Мы выделяем память под массив беззнаковых коротких целых, по одному элементу на каждый семафор набора, затем копируем полученные из командной строки значения в этот массив. Вызов semctl с командой SETALL позволяет установить все значения семафоров набора одновременно.

    Листинг 11.3. Программа semsetvalues

    //svsem/semsetvalues.с

    1  #include "unpipc.h"


    2  int

    3  main(int argc, char **argv)

    4  {

    5   int semid, nsems, i;

    6   struct semid_ds seminfo;

    7   unsigned short *ptr;

    8   union semun arg;

    9   if (argc < 2)

    10   err_quit("usage: semsetvalues <pathname> [ values … ]");

    11  /* получение количества семафоров в наборе */

    12  semid = Semget(Ftok(argv[1], 0), 0, 0);

    13  arg.buf = &seminfo;

    14  Semctl(semid, 0, IPC_STAT, arg);

    15  nsems = arg.buf->sem_nsems;

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

    17  if (argc != nsems + 2)

    18  err_quit("%d semaphores in set, %d values specified", nsems, argc-2);

    19  /* выделение памяти под значения семафоров набора, помещение этих значений в новый массив */

    20  ptr = Calloc(nsems, sizeof(unsigned short));

    21  arg.array = ptr;

    22  for (i = 0; i < nsems; i++)

    23   ptr[i] = atoi(argv[i +2]);

    24  Semctl(semid, 0, SETALL, arg);

    25  exit(0);

    26 }

    Программа semgetvalues

    В листинге 11.4 приведен текст программы semgetvalues, которая получает и выводит значения всех семафоров набора.

    Получение количества семафоров в наборе

    11-15 После получения идентификатора семафора с помощью semget мы вызываем semctl с командой IPC_STAT для получения значений полей структуры semi d_ds данного семафора. Поле sem_nsems содержит нужную нам информацию о количестве семафоров в наборе.

    Получение всех значений

    16-22 Мы выделяем память под массив беззнаковых коротких целых, по одному элементу на каждый семафор набора. Вызов semctl с командой GETALL позволяет получить все значения семафоров набора одновременно. Каждое значение выводится.

    Листинг 11.4. Программа semgetvalues

    //svsem/semgetvalues.c

    1  #include "unpipc.h"


    2  int

    3  main(int argc, char **argv)

    4  {

    5   int semid, nsems, i;

    6   struct semid_ds seminfo;

    7   unsigned short *ptr;

    8   union semun arg;

    9   if (argc != 2)

    10   err_quit("usage: semgetvalues <pathname>");

    11  /* получаем количество семафоров в наборе */

    12  semid = Semget(Ftok(argv[1], 0), 0, 0);

    13  arg.buf = &seminfo;

    14  Semctl(semid, 0, IPC_STAT, arg);

    15  nsems = arg.buf->sem_nsems;

    16  /* выделяем память под имеющееся количество элементов */

    17  ptr = Calloc(nsems, sizeof(unsigned short));

    18  arg.array = ptr;

    19  /* получаем и выводим значения семафоров */

    20  Semctl(semid, 0, GETALL, arg);

    21  for (i = 0; i < nsems; i++)

    22   printf("semval[%d] = %d\n", i, ptr[i]);

    23  exit(0);

    24 }

    Программа semops

    В листинге 11.5 приведен текст программы semops, позволяющей выполнять последовательность действий над набором семафоров.

    Параметры командной строки

    7-19 Параметр –n соответствует установленному флагу IPC_NOWAIT для каждой операции, а параметр –u аналогичным образом позволяет указать для каждой операции флаг SEM_UNDO. Обратите внимание, что функция semop позволяет указывать свой набор флагов для каждого элемента структуры sembuf (то есть для каждой из операций в отдельности), но для простоты мы в нашей программе задаем одинаковые флаги для всех операций.

    Выделение памяти под набор операций

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

    Выполнение операций

    30 Вызов semop выполняет последовательность операций над семафорами набора.

    Листинг 11.5. Программа semops

    //svsem/semops.c

    1  #include "unpipc.h"


    2  int

    3  main(int argc, char **argv)

    4  {

    5   int с, i, flag, semid, nops;

    6   struct sembuf *ptr;

    7   flag = 0;

    8   while ((c = Getopt(argc, argv, "nu")) != –1) {

    9    switch (c) {

    10   case 'n':

    11    flag |= IPC_NOWAIT; /* для всех операций */

    12    break;

    13   case 'u':

    14    flag |= SEM_UNDO; /* для всех операций */

    15    break;

    16   }

    17  }

    18  if (argc = optind < 2) /* argc – optind = количество оставшихся аргументов */

    19   err_quit("usage: semops [ –n ] [ –u ] <pathname> operation …");

    20  semid = Semget(Ftok(argv[optind], 0), 0, 0);

    21  optind++;

    22  nops = argc – optind;

    23  /* выделение памяти под операции, сохранение их в массиве и выполнение */

    24  ptr = Calloc(nops, sizeof(struct sembuf));

    25  for (i = 0; i < nops; i++) {

    26   ptr[i].sem_num = i;

    27   ptr[i].sem_op = atoi(argv[optind + i]); /* <0, 0, or >0 */

    28   ptr[i].sem_flg = flag;

    29  }

    30  Semop(semid, ptr, nops);

    31  exit(0);

    32 }

    Примеры

    Теперь мы продемонстрируем работу пяти приведенных выше программ и исследуем некоторые свойства семафоров System V:

    solaris % touch /tmp/rich

    solaris % semcreate –e /tmp/rich 3

    solaris % semsetvalues /tmp/rich 1 2 3

    solaris % semgetvalues /tmp/rich

    semval[0] = 1

    semval[1] = 2

    semval[2] = 3

    Сначала мы создали файл с именем /tmp/rich, который использовался при вызове ftok для вычисления идентификатора набора семафоров. Программа semcreate создает набор с тремя элементами. Программа semsetvalues устанавливает значения этих элементов (1, 2 и 3), a semgetvalues выводит их значения.

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

    solaris % semops –n /tmp/rich –1 –2 –4

    semctl error: Resource temporarily unavailable

    solaris % semgetvalues /tmp/rich

    semval[0] = 1

    semval[1] = 2

    semval[2] = 3

    В командной строке мы указываем параметр, отключающий блокировку (-n), и три операции, каждая из которых уменьшает одно из значений набора семафоров. Первая операция завершается успешно (мы можем вычесть 1 из значения первого элемента набора, потому что до вычитания оно равно 1), вторая операция также проходит (вычитаем 2 из значения второго семафора, равного 2), но третья операция выполнена быть не может (мы не можем вычесть 4 из значения третьего семафора, потому что оно равно 3). Поскольку последняя операция последовательности не может быть выполнена и поскольку мы отключили режим блокирования процесса, функция возвращает ошибку EAGAIN. Если бы мы не указали флаг отключения блокировки, выполнение процесса было бы приостановлено до тех пор, пока операция вычитания не стала бы возможной. После этого мы проверяем, чтобы ни одно из значений семафоров набора не изменилось. Хотя первые две операции и могли бы быть выполнены, ни одна из трех на самом деле произведена не была, поскольку последняя операция была некорректной. Атомарность semop и означает, что выполняются либо все операции, либо ни одна из них.

    Теперь продемонстрируем работу флага SEM_UNDO:

    solaris % semsetvalues /tmp/rich 1 2 3 устанавливаем конкретные значения

    solaris % semops –u /tmp/rich -1 –2 –3  для каждой операции указывается флаг SEM_UNDO

    solaris % semgetvalues /tmp/rich

    semval[0] = 1                              все произведенные изменения были сброшены

                                               после завершения работыпрограммы semops

    semval[1] = 2

    semval[2] = 3

    solaris % semops /tmp/rich -1 –2 –3     теперь мы не указываем флаг SEM_UNDO

    solaris % semgetvalues /tmp/rich

    semval[0] = 0

    semval[1] = 0

    semval[2] = 0

    Сначала мы заново устанавливаем значения семафоров в наборе равными 1, 2 и 3 с помощью программы semsetvalues, а затем запускаем программу semops с операциями –1, –2, –3. При этом все три значения семафоров становятся нулевыми, но, так как мы указали параметр –u при вызове semops, для всех трех операций устанавливается флаг SEM_UNDO. При этом значения semadj для элементов набора семафоров становятся равными 1, 2 и 3 соответственно. После завершения программы semops эти значения добавляются к значениям семафоров, в результате чего их значения становятся равными 1, 2 и 3, как будто мы и не запускали программу. В этом мы убеждаемся, запустив semgetvalues. Затем мы снова запускаем semops, но уже без параметра –u, и убеждаемся, что при этом значения семафоров становятся нулевыми и остаются таковыми даже после выхода из программы.

    11.6. Блокирование файлов

    С помощью семафоров System V можно реализовать еще одну версию функций my_lock и my_unlock из листинга 10.10. Новый вариант приведен в листинге 11.6.

    Листинг 11.6. Блокировка файлов с помощью семафоров System V

    //lock/locksvsem.c

    1  #include "unpipc.h"

    2  #define LOCK_PATH "/tmp/svsemlock"

    3  #define MAX_TRIES 10

    4  int semid, initflag;

    5  struct sembuf postop, waitop;


    6  void

    7  my_lock (int fd)

    8  {

    9   int oflag, i;

    10  union semun arg;

    11  struct semid_ds seminfo;

    12  if (initflag == 0) {

    13   oflag = IPC_CREAT | IPC_EXCL | SVSEM_MODE;

    14   if ((semid = semget(Ftok(LOCK_PATH, 0), 1, oflag)) >= 0) {

    15    /* этот процесс создал семафор первым, он же его и инициализирует */

    16    arg.val = 1;

    17    Semctl(semid, 0, SETVAL, arg);

    18   } else if (errno == EEXIST) {

    19    /* семафор создан другим процессом, убедимся, что он проинициализирован */

    20    semid = Semget(Ftok(LOCK_PATH, 0), 1, SVSEM_MODE);

    21    arg.buf = &seminfo;

    22    for (i = 0; i < MAX_TRIES; i++) {

    23     Semctl(semid, 0, IPC_STAT, arg);

    24     if (arg.buf->sem_otime != 0)

    25      goto init;

    26     sleep(1);

    27    }

    28    err_quit("semget OK, but semaphore not initialized");

    29   } else

    30    err_sys("semget error");

    31 init:

    32   initflag = 1;

    33   postop.sem_num = 0; /* инициализируем две структуры semop()*/

    34   postop.sem_op = 1;

    35   postop.sem_flg = SEM_UNDO;

    36   waitop.sem_num = 0;

    37   waitop.sem_op = –1;

    38   waitop.sem_flg = SEM_UNDO;

    39  }

    40  Semop(semid, &waitop, 1); /* уменьшим на 1 */

    41 }


    42 void

    43 my_unlock(int fd)

    44 {

    45  Semop(semid, &postop, 1); /* увеличим на 1*/

    46 }

    Попытка исключающего создания

    13-17 Нам нужно гарантировать, что только один процесс проинициализирует семафор, поэтому при вызове semget мы указываем флаги IPC_CREAT | IPC_EXCL. Если этот вызов оказывается успешным, процесс вызывает semctl для инициализации семафора значением 1. Если мы запустим несколько процессов одновременно и все они вызовут функцию my_lock, только один из них создаст семафор (предполагается, что он еще не существует) и проинициализирует его.

    Семафор уже существует, мы его открываем

    18-20 Если первый вызов semget возвращает ошибку EEXIST, процесс вызывает semget еще раз, но уже без флагов IPC_CREAT и IPC_EXCL.

    Ожидание инициализации семафора

    21-28 В этой программе возникает такая же ситуация гонок, как и обсуждавшаяся в разделе 11.2, когда мы говорили об инициализации семафоров System V вообще. Для исключения такой ситуации все процессы, которые обнаруживают, что семафор уже создан, вызывают semctl с командой IPC_STAT, проверяя значение sem_otime данного семафора. Когда это значение становится ненулевым, мы можем быть уверены, что создавший семафор процесс проинициализировал его и вызвал semop (этот вызов находится в конце функции) успешно. Если значение этого поля оказывается нулевым (что должно происходить крайне редко), мы приостанавливаем выполнение процесса на одну секунду вызовом sleep, а затем повторяем попытку. Число попыток мы ограничиваем, чтобы процесс не «заснул» навсегда.

    Инициализация структур sembuf

    33-38 Как отмечалось ранее, конкретный порядок полей структуры sembuf зависит от реализации, поэтому статически инициализировать ее нельзя. Вместо этого мы выделяем место под две такие структуры и присваиваем значения их полям во время выполнения программы, когда процесс вызывает my_lock в первый раз. При этом мы указываем флаг SEM_UNDO, чтобы ядро сняло блокировку, если процесс завершит свою работу, не сняв ее самостоятельно (см. упражнение 10.3).

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

    11.7. Ограничения семафоров System V

    На семафоры System V накладываются определенные системные ограничения, так же, как и на очереди сообщений. Большинство этих ограничений были связаны с особенностями реализации System V (раздел 3.8). Они показаны в табл. 11.1. Первая колонка содержит традиционное для System V имя переменной ядра, в которой хранится соответствующее oгрaничeниe.


    Таблица 11.1. Типичные значения ограничений для семафоров System V

    Имя Описание DUnix 4.0B Solaris 2.6
    semmni Максимальное количество наборов семафоров в системе 16 10
    semmsl Максимальное количество семафоров в наборе 25 25
    semmns Максимальное количество семафоров в системе 400 60
    semopm Максимальное количество операций за один вызов semop 10 10
    semmnu Максимальное количество структур undo в системе 30
    semume Максимальное количество записей в структуре undo 10 10
    semvmx Максимальное значение семафора 32767 32767
    semaem Максимальное значение корректировки при выходе 16384 16384

    В Digital Unix 4.0B никакого ограничения на semmnu не существует.

    Пример

    Программа в листинге 11.7 позволяет определить ограничения, приведенные в табл. 11.1.

    Листинг 11.7. Определение системных ограничений на семафоры System V

    //svsem/limits.c

    1   #include "unpipc.h"


    2   /* максимальные величины, выше которых ограничение не проверяется */

    3   #define MAX_NIDS 4096 /* максимальное количество идентификаторов семафоров */

    4   #define MAX_VALUE 1024*1024 /* максимальное значение семафора */

    5   #define MAX_MEMBERS 4096 /* максимальное количество семафоров в наборе */

    6   #define MAX_NOPS 4096 /* максимальное количество операций за вызов semop */

    7   #define MAX_NPROC Sysconf(_SC_CHILD_MAX)


    8   int

    9   main(int argc, char **argv)

    10  {

    11   int i, j, semid, sid[MAX_NIDS], pipefd[2];

    12   int semmni, semvmx, semmsl, semmns, semopn, semaem, semume, semmnu;

    13   pid_t *child;

    14   union semun arg;

    15   struct sembuf ops[MAX_NOPS];

    16   /* сколько наборов с одним элементом можно создать? */

    17   for (i = 0; i <= MAX_NIDS; i++) {

    18    sid[i] = semget(IPC_PRIVATE, 1, SVSEM_MODE | IPC_CREAT);

    19    if (sid[i] == –1) {

    20     semmni = i;

    21     printf("%d identifiers open at once\n", semmni);

    22     break;

    23    }

    24   }

    25   /* перед удалением находим максимальное значение, используя sid[0] */

    26   for (j = 7; j < MAX_VALUE; j += 8) {

    27    arg.val = j;

    28    if (semctl(sid[0], 0, SETVAL, arg) == –1) {

    29     semvmx = j – 8;

    30     printf("max semaphore value = %d\n", semvmx);

    31     break;

    32    }

    33   }

    34   for (j = 0; j < i; j++)

    35    Semctl(sid[j], 0, IPC_RMID);

    36   /* определяем максимальное количество семафоров в наборе */

    37   for (i = 1; i <= MAX_MEMBERS; i++) {

    38    semid = semget(IPC_PRIVATE, i, SVSEM_MODE | IPC_CREAT);

    39    if (semid == –1) {

    40     semmsl = i-1;

    41     printf("max of %d members per set\n", semmsl);

    42     break;

    43    }

    44    Semctl(semid, 0, IPC_RMID);

    45   }

    46   /* сколько всего семафоров можно создать? */

    47   semmns = 0;

    48   for (i = 0; i < semmni; i++) {

    49    sid[i] = semget(IPC_PRIVATE, semmsl, SVSEM_MODE | IPC_CREAT);

    50    if (sid[i] == –1) {

    51     /*

    52      До этого в наборе было semmsl элементов,

    53      но теперь мы уменьшаем количество элементов на 1 и смотрим.

    54      не получится ли создать семафор

    55     */

    56     for (j = semmsl-1; j > 0; j--) {

    57      sid[1] = semget(IPC_PRIVATE, j, SVSEM_MODE | IPC_CREAT);

    58      if (sid[i] != –1) {

    59       semmns += j;

    60       printf("max of %d semaphores\n", semmns);

    61       Semctl(sid[i], 0, IPC_RMID);

    62       goto done;

    63      }

    64     }

    65     err_quit("j reached 0, semmns = %d", semmns);

    66    }

    67    semmns += semmsl;

    68   }

    69   printf("max of %d semaphores\n", semns);

    70  done:

    71   for (j = 0; j < i; j++)

    72    Semctl(sid[j], 0, IPC_RMID);

    73   /* определяем количество операций за вызов semop() */

    74   semid = Semget(IPC_PRIVATE, semmsl, SVSEM_MODE | IPC_CREAT);

    75   for (i = 1; i <= MAX_NOPS; i++) {

    76    ops[i-1].sem_num = i-1;

    77    ops[i-1].sem_op = 1;

    78    ops[i-1].sem_flg = 0;

    79    if (semop(semid, ops, i) += –1) {

    80     if (errno != E2BIG)

    81      err_sys("expected E2BIG from semop");

    82     semopn = i-1;

    83     printf("max of %d operations per semop()\n", semopn);

    84     break;

    85    }

    86   }

    87   Semctl(semid, 0, IPC_RMID);

    88   /* определение максимального значения semadj */

    89   /* создание одного набора с одним семафором */

    90   semid = Semget(IPC_PRIVATE, 1, SVSEM_MODE | IPC_CREAT);

    91   arg.val = semvmx;

    92   Semctl(semid, 0, SETVAL, arg); /* устанавливаем значение на максимум */

    93   for (i = semvmx-1; i > 0; i--) {

    94    ops[0].sem_num = 0;

    95    ops[0].sem_op = –i;

    96    ops[0].sem_flg = SEM_UNDO;

    97    if (semop(semid, ops, 1) != –1) {

    98     semaem = i;

    99     printf("max value of adjust-on-exit = %d\n", semaem);

    100    break;

    101   }

    102  }

    103  Semctl(semid, 0, IPC_RMID);

    104  /* определение максимального количества структур UNDO */

    105  /* создаем один набор с одним семафором и инициализируем нулем */

    106  semid = Semget(IPC_PRIVATE, 1, SVSEM_MODE | IPC_CREAT);

    107  arg.val = 0;

    108  Semctl(semid, 0, SETVAL, arg); /* установка значения семафора в 0 */

    109  Pipe(pipefd);

    110  child = Malloc(MAX_NPROC * sizeof(pid_t));

    111  for (i = 0; i < MAX_NPROC; i++) {

    112   if ((child[i] = fork()) == –1) {

    113    semmnu = i – 1;

    114    printf("fork failed, semmnu at least %d\n", semmnu);

    115    break;

    116   } else if (child[i] == 0) {

    117    ops[0].sem_num = 0; /* дочерний процесс вызывает semop() */

    118    ops[0].sem_op = 1;

    119    ops[0].sem_flg = SEM_UNDO;

    120    j = semop(semid, ops, 1); /* 0 в случае успешного завершения. –1 – в случае ошибки */

    121    Write(pipefd[1], &j, sizeof(j));

    122    sleep(30); /* ожидает завершения родительским процессом */

    123    exit(0); /* на всякий случай */

    124   }

    125   /* родительский процесс считывает результат вызова semop() */

    126   Read(pipefd[0], &j, sizeof(j));

    127   if (j == –1) {

    128    semmnu = i;

    129    printf("max # undo structures = %d\n", semmnu);

    130    break;

    131   }

    132  }

    133  Semctl(semid, 0, IPC_RMID);

    134  for (j = 0; j <= i && child[j] > 0; j++)

    135   Kill(child[j], SIGINT);

    136  /* определение максимального количества записей корректировки на процесс */

    137  /* создание одного набора с максимальным количеством семафоров */

    138  semid = Semget(IPC_PRIVATE, semmsl, SVSEM_MODE | IPC_CREAT);

    139  for (i = 0; i < semmsl; i++) {

    140   arg.val = 0;

    141   Semctl(semid, i, SETVAL, arg); /* установка значения семафора в 0 */

    142   ops[i].sem_num = i;

    143   ops[i].sem_op = 1; /* добавляем 1 к значению семафора */

    144   ops[i].sem_flg = SEM_UNDO;

    145   if (semop(semid, ops, i+1) == –1) {

    146    semume = i;

    147    printf("max # undo entries per process = %d\n", semume);

    148    break;

    149   }

    150  }

    151  Semctl(semid, 0, IPC_RMID);

    152  exit(0);

    153 }

    11.8. Резюме

    У семафоров System V имеются следующие отличия от семафоров Posix:

    1. Семафоры System V представляют собой набор значений. Последовательность операций над набором семафоров либо выполняется целиком, либо не выполняется вовсе.

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

    3. Создание семафора System V имеет некоторую особенность, заключающуюся в необходимости выполнения двух вызовов для создания и инициализации семафора, что может привести к ситуации гонок.

    4. Семафоры System V предоставляют возможность отмены операции с ними (undo) после завершения работы процесса.

    Упражнения

    1. Листинг 6.6 представлял собой измененный вариант листинга 6.4, в котором программа принимала идентификатор очереди вместо полного имени файла. Мы продемонстрировали, что для получения доступа к очереди System V достаточно знать только ее идентификатор (предполагается наличие достаточных разрешений). Проделайте аналогичные изменения с программой в листинге 11.5 и посмотрите, верно ли вышесказанное для семафоров System V.

    2. Что произойдет с программой в листинге 11.6, если файл LOCK_PATH не будет существовать?


    Примечания:



    1

    Все исходные тексты, опубликованные в этой книге, вы можете найти по адресу http://www.piter.com/download.








    Главная | В избранное | Наш E-MAIL | Прислать материал | Нашёл ошибку | Наверх