|
||||||||||||||||||||||||||||||||||||||
|
ЧАСТЬ 3СИНХРОНИЗАЦИЯ ГЛАВА 7Взаимные исключения и условные переменные 7.1. ВведениеЭта глава начинается с обсуждения синхронизации — принципов синхронизации действий нескольких программных потоков или процессов. Обычно это требуется для предоставления нескольким потокам или процессам совместного доступа к данным. Взаимные исключения (mutual exclusion — mutex) и условные переменные (conditional variables) являются основными средствами синхронизации. Взаимные исключения и условные переменные появились в стандарте Posix.1 для программных потоков, и всегда могут быть использованы для синхронизации отдельных потоков одного процесса. Стандарт Posix также разрешает использовать взаимное исключение или условную переменную и для синхронизации нескольких процессов, если это исключение или переменная хранится в области памяти, совместно используемой процессами.
В этой главе мы разберем классическую схему производитель-потребитель, используя взаимные исключения и условные переменные. В примере будут использоваться программные потоки, а не процессы, поскольку предоставить потокам общий буфер данных, предполагаемый в этой задаче, легко, а вот создать буфер данных между процессами можно только с помощью одной из форм разделяемой памяти (которая будет описана только в четвертой части книги). Еще одно решение этой задачи, уже с использованием семафоров, появится в главе 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.
Следующие три функции используются для установки и снятия блокировки взаимного исключения: #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.
Хотя мы говорим о защите критической области кода программы, на самом деле речь идет о защите данных, с которыми работает эта часть кода. То есть взаимное исключение обычно используется для защиты совместно используемых несколькими потоками или процессами данных. Взаимные исключения представляют собой блокировку коллективного пользования. Это значит, что если совместно используемые данные представляют собой, например, связный список, то все потоки, работающие с этим списком, должны блокировать взаимное исключение. Ничто не может помешать потоку работать со списком, не заблокировав взаимное исключение. Взаимные исключения предполагают добровольное сотрудничество потоков. 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 и т.д.). Мы выделяем память под эту структуру и инициализируем взаимное исключение, используемое для синхронизации потоков-производителей. Аргументы командной строки 19-22 Первый аргумент командной строки указывает количество элементов, которые будут произведены производителями, а второй — количество запускаемых потоков-производителей. Установка уровня параллельности23 Функция set_concurrency (наша собственная) указывает подсистеме потоков количество одновременно выполняемых потоков. В Solaris 2.6 она просто вызывает thr_setconcurrency, причем ее запуск необходим, если мы хотим, чтобы у нескольких процессов-производителей была возможность начать выполняться. Если мы не сделаем этого вызова в системе Solaris, будет запущен только первый поток. В Digital Unix 4.0B наша функция set_concurrency не делает ничего, поскольку в этой системе по умолчанию все потоки процесса имеют равные права на вычислительные ресурсы. Создание процессов-производителей 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 для пробуждения всех процессов, заблокированных в ожидании сигнала данной условной переменной.
#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. Последнее также называется атрибутом совместного использования процессами.
Нижеследующий фрагмент кода показывает, как нужно инициализировать взаимное исключение, чтобы его можно было совместно использовать нескольким процессам: 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). В последнем случае слово «читатель» специально употреблено во множественном числе, а «писатель» — в единственном, чтобы подчеркнуть сущность задачи.
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. Реализация с использованием взаимных исключений и условных переменныхДля реализации блокировок чтения-записи достаточно использовать взаимные исключения и условные переменные. В этом разделе мы рассмотрим одну из возможных реализаций, в которой предпочтение отдается ожидающим записи потокам. Это не является обязательным; возможны альтернативы.
Тип данных 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_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.
ПримерЛегче всего продемонстрировать проблему нашей реализации из предыдущего раздела с помощью примера. На рис. 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 секунды, освобождает блокировку на чтение и завершает работу. Функция thread237-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.
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. Увеличить его на единицу и записать обратно в файл. Проблема в том, что пока один процесс выполняет эти три действия, другой процесс может параллельно делать то же самое. В итоге возникнет полный беспорядок с номерами, как мы увидим в следующих примерах.
Таким образом, процессу нужно заблокировать файл, чтобы никакой другой процесс не мог получить к нему доступ, пока первый выполняет свои три действия. В листинге 9.2 приведен текст простой программы, выполняющей соответствующие действия. Функции my_lock и my_unlock обеспечивают блокирование и разблокирование файла в соответствующие моменты. Мы приведем несколько возможных вариантов реализации этих функций. 20 Каждый раз при прохождении цикла мы выводим имя программы (argv[0]) перед порядковым номером, поскольку эта функция main будет использоваться с различными версиями функций блокировки и нам бы хотелось видеть, какая версия программы выводит данную последовательность порядковых номеров.
Посмотрим, что будет, если не использовать блокировку. В листинге 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
Установим значение последовательного номера в файле обратно в единицу и запустим программу в двух экземплярах в фоновом режиме. Результат будет такой: 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 секунд (это, разумеется, очень грубая оценка). Если же уровень детализации соответствует размеру записи (наилучший уровень детализации), все пять процессов смогут работать одновременно, поскольку они обрабатывают разные записи. При этом на выполнение будет затрачена только одна секунда.
ИсторияЗа долгие годы было разработано множество методов блокировки файлов и записей. Древние программы вроде 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.
Блокировка записей не должна использоваться со стандартной библиотекой ввода-вывода, поскольку функции из этой библиотеки осуществляют внутреннюю буферизацию. С заблокированными файлами следует использовать функции 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, чтобы приостановить выполнение процесса при невозможности установки блокировки.
Мы не приводим результат работы п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 не установлен, выполнение процесса в такой ситуации будет отложено до тех пор, пока ресурс не освободится.
Для установки обязательной блокировки какого-либо файла требуется выполнение двух условий: ■ бит 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 Демон создает однострочный файл, в который записывает свой идентификатор процесса. Этот файл открывается или создается, а затем делается попытка Заблокировать его на запись целиком. Если блокировку установить не удается, мы понимаем, что один экземпляр демона уже запущен, поэтому выводится сообщение об ошибке и программа завершает работу. Запись идентификатора процесса в файл 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 раз больше, чем для локального файла. Однако нужно понимать, что в этом случае происходит передача информации по сети и при операциях чтения и записи (для изменения порядкового номера).
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.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 описано два типа семафоров: именованные (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_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.
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 Главный поток ждет завершения работы производителя и потребителя, а затем удаляет три семафора.
В листинге 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. Потребитель ожидает изменения семафора nstored57-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. Это одна из проблем, которые часто возникают с семафорами: если в программе сделать ошибку, она будет работать неправильно.
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.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_init16-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 (каждый из которых может работать несколько секунд) — по одному на каждый поток-потребитель.
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, в данном примере пуста. Если бы буферы данных представляли собой связный список, здесь мы могли бы удалять буфер из списка, не конфликтуя при этом с производителем. Но в нашем примере, где мы просто переходим к следующему буферу с единственным потоком-производителем, защищать нам просто нечего. Тем не менее мы оставляем операции установки и снятия блокировки, подчеркивая, что они могут потребоваться в новых версиях кода. Считывание данных и увеличение семафора nstored43-49 Каждый раз, когда производитель получает пустой буфер, он вызывает функцию read. При возвращении из read увеличивается семафор nstored, уведомляя потребителя о том, что буфер готов. При возвращении функцией read значения 0 (конец файла) семафор увеличивается, а производитель завершает работу. Поток-потребитель57-68 Поток-потребитель записывает содержимое буферов в стандартный поток вывода. Буфер, содержащий нулевой объем данных, обозначает конец файла. Как и в потоке-производителе, критическая область, защищенная семафором mutex, пуста.
10.12. Использование семафоров несколькими процессамиПравила совместного использования размещаемых в памяти семафоров несколькими процессами просты: сам семафор (переменная типа semt, адрес которой является первым аргументом sem_init) должен находиться в памяти, разделяемой всеми процессами, которые хотят его использовать, а второй аргумент функции sem_init должен быть равен 1.
Что касается именованных семафоров, процессы всегда могут обратиться к одному и тому же семафору, указав одинаковое имя при вызове 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); …
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. Создание нового канала FIFO18-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].
Прежде всего приведем текст нашего заголовочного файла semaphore.h (листинг 10.27), в котором определяется фундаментальный тип sem_t. Тип sem_t1-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_t43-57 Мы инициализируем три поля структуры sem_t: взаимное исключение, условную переменную и значение семафора. Поскольку именованный семафор Posix может совместно использоваться всеми процессами с соответствующими правами, которым известно его имя, при инициализации взаимного исключения и условной переменной необходимо указать атрибут PTHREAD_PROCESS_SHARED. Чтобы осуществить это для взаимного исключения, нужно сначала проинициализировать атрибуты, вызвав pthread_mutexattr_init, затем установить атрибут совместного использования потоками, вызвав pthread_mutexattr_setpshared, а затем проинициализировать взаимное исключение вызовом pthread_mutex_init. Аналогичные действия придется выполнить и для условной переменной. Необходимо аккуратно уничтожать переменные, в которых хранятся атрибуты, при возникновении ошибок. Инициализация значения семафора58-61 Наконец мы помещаем в файл начальное значение семафора. Предварительно мы сравниваем его с максимально разрешенным значением семафора, которое может быть получено вызовом sysconf (раздел 10.13). Сброс бита user-execute62-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. Удостоверимся, что семафор проинициализирован 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, эта реализация позволяет использовать последние в системах, где их поддержка не предусмотрена производителем.
Начнем, как обычно, с заголовочного файла 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 IPC25-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_t79-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 — по одному элементу массива на каждый семафор в наборе. Помимо текущих значений всех семафоров набора в ядре хранятся еще три поля данных для каждого семафора: идентификатор процесса, изменившего значение семафора последним, количество процессов, ожидающих увеличения значения семафора, и количество процессов, ожидающих того, что значение семафора станет нулевым.
Любой конкретный семафор в ядре мы можем воспринимать как структуру 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 для второго процесса будет не определено.
К счастью, существует способ исключить в данном случае ситуацию гонок. Стандарт гарантирует, что при создании набора семафоров поле 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 при создании семафора).
Весь массив операций, передаваемый функции 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. Ожидание завершается преждевременно, если перехватываемый сигнал вызывает прерывание либо семафор удаляется другим потоком.
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). Оно передается по значению, а не по ссылке, то есть аргументом является собственно значение объединения, а не указатель на него.
Ниже приведен список поддерживаемых значений аргумента 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, а затем повторяем попытку. Число попыток мы ограничиваем, чтобы процесс не «заснул» навсегда. Инициализация структур sembuf33-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
В 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 | Прислать материал | Нашёл ошибку | Наверх |
||||||||||||||||||||||||||||||||||||||
|