UNIX: взаимодействие процессов — страница 11 из 35

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

9.1. Введение

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

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

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

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

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

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

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

ПРИМЕЧАНИЕ

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

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

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

ПРИМЕЧАНИЕ

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

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

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

//lock/locknone.c

1  void

2  my_lock(int fd)

3  {

4   return;

5  }


6  void

7  my_unlock(int fd)

8  {

9   return;

10 }

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

//lock/lockmain.c

1  #include "unpipc.h"

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


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

4  int

5  main(int argc, char **argv)

6  {

7   int fd;

8   long i, seqno;

9   pid_t pid;

10  ssize_t n;

11  char line[MAXLINE + 1];

12  pid = getpid();

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

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

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

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

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

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

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

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

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

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

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

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

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

26  }

27  exit(0);

28 }

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

solaris % locknone

locknone: pid = 15491, seq# = 1

locknone: pid = 15491, seq# = 2

locknone: pid = 15491, seq# = 3

locknone: pid = 15491, seq# = 4

locknone: pid = 15491. seq# = 5

locknone: pid = 15491, seq# = 6

locknone: pid = 15491, seq# = 7

locknone: pid = 15491, seq# – 8

locknone: pid = 15491, seq# = 9

locknone: pid = 15491, seq# = 10

locknone: pid = 15491, seq# = 11

locknone: pid = 15491, seq# = 12

locknone: pid = 15491, seq# = 13

locknone: pid = 15491, seq# = 14

locknone: pid = 15491, seq# = 15

locknone: pid = 15491, seq# = 16

locknone: pid = 15491, seq# = 17

locknone: pid = 15491, seq# = 18

locknone: pid = 15491, seq# = 19

locknone: pid = 15491, seq# = 20

ПРИМЕЧАНИЕ

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

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

solaris % locknone & locknone&

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

locknone: pid = 15498, seq# = 2

locknone: pid = 15498, seq# = 3

locknone: pid = 15498, seq# = 4

locknone: pid = 15498, seq# = 5

locknone: pid = 15498, seq# = 6

locknone: pid = 15498, seq# = 7

locknone: pid = 15498, seq# = 8

locknone: pid = 15498, seq# = 9

locknone: pid = 15498, seq# = 10

locknone: pid = 15498, seq# = 11

locknone: pid = 15498, seq# = 12

locknone: pid = 15498, seq# = 13

locknone: pid = 15498, seq# = 14

locknone: pid = 15498, seq# = 15

locknone: pid = 15498, seq# = 16

locknone: pid = 15498, seq# = 17

locknone: pid = 15498, seq# = 18

locknone: pid = 15498, seq# = 19

locknone: pid = 15498, seq# = 20

locknone: pid = 15499, seq# = 1

locknone: pid = 15499, seq# = 2

locknone: pid = 15499, seq# = 3

locknone: pid = 15499, seq# = 4

locknone: pid = 15499, seq# = 5

locknone: pid = 15499, seq# = 6

locknone: pid = 15499, seq# = 7

locknone: pid = 15499, seq# = 8

locknone: pid = 15499, seq# = 9

locknone: pid – 15499, seq# = 10

locknone: pid = 15499, seq# = 11

locknone: pid = 15499, seq# – 12

locknone: pid = 15499, seq# = 13

locknone: pid = 15499, seq# = 14

locknone: pid = 15499, seq# = 15

locknone: pid = 15499, seq# = 16

locknone: pid = 15499, seq# = 17

locknone: pid = 15499, seq# = 18

locknone: pid = 15499, seq# = 19

locknone: pid = 15499, seq# = 20

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

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

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

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

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

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

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

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

ПРИМЕЧАНИЕ

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

История

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

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

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

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

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

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

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

#include 

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

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

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

struct flock {

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

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

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

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

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

};

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

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

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

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

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

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

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

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

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

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

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

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

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

ПРИМЕЧАНИЕ

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

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

Пример

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

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

//lock/lockfcntl.c

1  #include "unpipc.h"


2  void

3  my_lock(int fd)

4  {

5   struct flock lock;

6   lock.l_type = F_WRLCK;

7   lock.l_whence = SEEK_SET;

8   lock.l_start = 0;

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

10  Fcntl(fd, F_SETLKW, &lock);

11 }


12 void

13 my_unlock(int fd)

14 {

15  struct flock lock;

16  lock.l_type = F_UNLCK;

17  lock.l_whence = SEEK_SET;

18  lock.l_start = 0;

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

20  Fcntl(fd. F_SETLK, &lock);

21 }

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

ПРИМЕЧАНИЕ

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

//lib/lock_reg.c

1  #include "unpipc.h"


2  int

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

4  {

5   struct flock lock;

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

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

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

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

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

11 }

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

//lib/lock_test.c

1  #include "unpipc.h"


2  pid_t

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

4  {

5   struct flock lock;

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

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

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

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

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

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

12 if (lock.l_type == F_UNLCK)

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

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

15 }

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

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

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

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

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

solaris % lockfcntl & locknone &

lockfcntl: pid = 18816, seq# = 1

lockfcntl: pid = 18816, seq# = 2

lockfcntl: pid = 18816, seq# = 3

lockfcntl: pid = 18816, seq# = 4

lockfcntl: pid = 18816, seq# = 5

lockfcntl: pid = 18816, seq# = 6

lockfcntl: pid = 18816, seq# = 7

lockfcntl: pid = 18816, seq# = 8

lockfcntl: pid = 18816, seq# = 9

lockfcntl: pid = 18816, seq# = 10

lockfcntl: pid = 18816, seq# = 11

locknone: pid = 18817, seq# = 11

locknone: pid = 18817, seq# = 12

locknone: pid = 18817, seq# = 13

locknone: pid = 18817, seq# = 14

locknone: pid = 18817, seq# = 15

locknone: pid = 18817, seq# = 16

locknone: pid = 18817, seq# = 17

locknone: pid = 18817, seq# = 18

lockfcntl: pid = 18816, seq# = 12

lockfcntl: pid = 18816, seq# = 13

lockfcntl: pid = 18816, seq# = 14

lockfcntl: pid = 18816, seq# = 15

lockfcntl: pid = 18816, seq# = 16

lockfcntl: pid = 18816, seq# = 17

lockfcntl: pid = 18816, seq# = 18

lockfcntl: pid = 18816, seq# = 19

lockfcntl: pid = 18816, seq# = 20

locknone: pid = 18817, seq# = 19

locknone: pid = 18817, seq# = 20

locknone: pid = 18817, seq# = 21

locknone: pid = 18817, seq# = 22

locknone: pid = 18817, seq# = 23

locknone: pid = 18817, seq# = 24

locknone: pid = 18817, seq# = 25

locknone: pid = 18817, seq# = 26

locknone: pid = 18817, seq# = 27

locknone: pid = 18817, seq# = 28

locknone: pid = 18817, seq# = 29

locknone: pid = 18817, seq# = 30

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

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

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

ПРИМЕЧАНИЕ

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

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

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

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

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

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

Пример

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

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

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

1

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

solaris % ls –l seqno

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

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

solaris % ls -l seqno

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

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

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

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

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

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

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

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


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

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

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

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

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

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

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

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

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

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

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


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

//lock/test2.c

1  #include "unpipc.h"


2  int

3  main(int argc, char **argv)

4  {

5   int fd;

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

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

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

9   if (Fork() == 0) {

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

11   sleep(1);

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

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

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

15   sleep(2);

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

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

18   exit(0);

19  }

20  if (Fork() == 0) {

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

22   sleep(3);

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

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

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

26   sleep(4);

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

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

29   exit(0);

30  }

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

32  sleep(5);

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

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

35  exit(0);

36 }

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

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

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

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

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

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

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

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

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

alpha % test2

16:32:29.674453: parent has read lock


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


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

16:32:32.728739: second child obtains read lock


16:32:34.722282: parent releases read lock


16:32:36.729738: second child releases read lock

16:32:36.735597: first child obtains write lock


16:32:38.736938: first child releases write lock

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

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

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

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

//lock/test3.c

1  #include "unpipc.h"


2  int

3  main(int argc, char **argv)

4  {

5   int fd;

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

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

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

9   if (Fork() == 0) {

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

11   sleep(1);

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

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

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

15   sleep(2);

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

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

18   exit(0);

19  }

20  if (Fork() == 0) {

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

22   sleep(3);

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

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

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

26   sleep(4);

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

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

29   exit(0);

30  }

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

32  sleep(5);

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

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

35  exit(0);

36 }

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

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

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

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

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

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

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

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


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

alpha % test3

16:34:02.810285: parent has write lock

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

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

16:34:07.858393: parent releases write lock

16:34:07.865222: first child obtains write lock

16:34:09.865987: first child releases write lock

16:34:09.872823: second child obtains read lock

16:34:13.873822: second child releases read lock

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

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

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

//lock/onedaemon.c

1  #include "unpipc.h"

2  #define PATH_PIDFILE "pidfile"


3  int

4  main(int argc, char **argv)

5  {

6   int pidfd;

7   char line[MAXLINE];

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

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

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

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

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

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

14     PATH_PIDFILE, argv[0]);

15   else

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

17  }

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

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

20  Ftruncate(pidfd, 0);

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

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

23  pause();

24 }

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

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

ПРИМЕЧАНИЕ

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

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

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

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

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

[1] 22388

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

22388

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

unable to lock pidfile, is onedaemon already running?

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

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

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

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

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

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

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

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

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

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

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

//lock/lockopen.c

1  #include "unpipc.h"

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


3  void

4  my_lock(int fd)

5  {


6   int tempfd;

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

8    if (errno != EEXIST)

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

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

11  }

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

13 }


14 void

15 my_unlock(int fd)

16 {

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

18 }

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

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

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

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

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

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

ПРИМЕЧАНИЕ

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

9.10. Резюме

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

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

Упражнения

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

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

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

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

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

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

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

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

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

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

ГЛАВА 10