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

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

С целью преодоления проблем, присущих блокировочным файлам, в System V и BSD 4.3 была добавлена блокировка записей, реализуемая с помощью системных вызовов lockf() и flock() соответственно. Стандарт POSIX определил третий механизм для блокировки записей, который использует системный вызов fcntl(). Хотя Linux поддерживает все три интерфейса, мы обсудим только интерфейс POSIX, поскольку сейчас его поддерживают почти все платформы Unix. Кроме того, функция lockf() реализована как интерфейс для fcntl(), поэтому оставшаяся часть данного обсуждения касается обоих методов.

Существуют два значительных отличия между блокировками записей и блокировочными файлами. Во-первых, блокировки записей применяются к произвольной части файла. Например, процесс А может заблокировать байты с 50-го по 200-й файла, в то время как другой процесс блокирует байты с 2500-го по 3000-й без конфликта двух блокировок. Мелкомодульное блокирование полезно, когда нескольким процессам необходимо обновить один файл. Еще одно преимущество блокирования записей заключается в том, что блокировки удерживаются в ядре, а не в файловой системе. По окончании процесса все блокировки, которые он содержит, освобождаются.

Как и блокировочные файлы, блокировки POSIX также являются рекомендательными. Linux, как и System V, предоставляет обязательный вариант блокирования записей, который можно использовать, но нарушая при этом переносимость. Блокирование файлов может работать или не работать в сетевой файловой системе (NFS). В последних версиях Linux блокирование файлов работает в NFS, если на всех машинах, участвующих в блокировке, выполняется демон блокировки NFS lockd.

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

Множество блокировок одного процесса никогда не конфликтуют друг с другом[90].

Если процесс имеет блокировку чтения на байтах 200—250 и пытается установить блокировку записи на байты 200–225, ему это удастся. Исходная блокировка смещается и становится блокировкой чтения на байтах 226–250, а новая блокировка записи устанавливается на байты 200–225[91]. Это позволяет предотвратить взаимоблокировку одного процесса (хотя ситуация взаимоблокировки нескольких процессов по-прежнему возможна).

Блокирование записей POSIX осуществляется с помощью системного вызова fcntl(). В главе 11 было показано, что fcntl() выглядит следующим образом.

#include <fcntl.h>

int fcntl (int fd, int command, long arg);

Для всех операций блокировки третий параметр (arg) указывает на структуру struct flock, представленную ниже.

#include <fcntl.h>

struct flock {

 short l_type;

 short l_whence;

 off_t l_start;

 off_t l_len;

 pid_t l_pid;

};

Первый элемент, l_type, определяет тип установленной блокировки.

F_RDLCK Устанавливается блокировка чтения (разделяемая). F_WRLCK Устанавливается блокировка записи (эксклюзивная). F_UNLCK Снимается существующая блокировка.

Следующие два элемента, l_whence и l_start, определяют начало области тем же способом, что и файловые смещения, передаваемые в lseek(). l_whence сообщает о способе интерпретации l_start и принимает одно из значений SEEK_SET, SEEK_CUR или SEEK_END; более подробно эти значения рассматривались в главе 11. Следующий элемент, l_len, сообщает размер блокировки в байтах. Если l_len равно 0, считается, что блокировка распространяется до конца файла. Последний элемент, l_pid, используется только тогда, когда запрашиваются блокировки. Он устанавливается в идентификатор процесса, владеющего запрашиваемой блокировкой.

Существуют три команды fcntl(), относящиеся к блокировке файла. Они передаются fcntl() во втором аргументе, fcntl() возвращает -1 в случае ошибки и 0 — в противном случае. Ниже перечислены допустимые значения параметра command.

F_SETLK Устанавливает блокировку, описанную в arg. Если блокировку невозможно выдать из-за конфликта с блокировками других процессов, возвращается EAGAIN. Если l_type устанавливается в F_UNLCK, существующая блокировка снимается. F_SETLKW Подобно F_SETLK, но блокирует только при условии предоставления блокировки. Если сигнал поступает во время блокирования процесса, вызов fcntl() возвращает EAGAIN. F_GETLK Проверяет возможность выдачи описанной в arg блокировки. Если блокировка предоставляется, содержимое struct flock не меняется, кроме l_type, который устанавливается в F_UNLCK. Если блокировка не выдается, l_pid устанавливается в идентификатор процесса, содержащего конфликтующую блокировку. Значение 0 возвращается независимо от того, будет ли предоставлена блокировка.

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

fcntl(fd, F_GETLK, &lockinfo);

if (lockinfо.l_type != F_UNLCK) {

 fprintf(stderr, "конфликт блокировок ");

 return 1;

}

lockinfо.l_type = F_RDLCK;

fcntl(fd, F_SETLK, &lockinfo);

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

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

 1: /* lock.с */

 2:

 3: #include <errno.h>

 4: #include <fcntl.h>

 5: #include <stdio.h>

 6: #include <unistd.h>

 7:

 8: /* выводит сообщение и ожидает нажатия

 9:    пользователем клавиши <Enter> */

10: void waitforuser(char * message) {

11:  char buf[10];

12:

13:  printf("%s", message);

14:  fflush(stdout);

15:

16:  fgets(buf, 9, stdin);

17: }

18:

19: /* Получает блокировку заданного типа на файловом дескрипторе fd.

20:    Типом блокировки может быть F_UNLCK, F_RDLCK или F_WRLCK */

21: void getlock(int fd, int type) {

22:  struct flock lockinfo;

23:  char message[80];

24:

25:  /* будет блокироваться весь файл */

26:  lockinfo.l_whence = SEEK_SET;

27:  lockinfo.l_start = 0;

28:  lockinfo.l_len = 0;

29:

30:  /* продолжать попытки, пока того желает пользователь */

31:  while (1) {

32:   lockinfo.l_type = type;

33:   /* если блокировка получена, немедленно возвратиться */

34:   if (!fcntl(fd, F_SETLK, &lockinfo)) return;

35:

36:   /* найти, кто удерживает конфликтующую блокировку */

37:   fcntl(fd, F_GETLK, &lockinfo);

38:

39:   /* есть шанс, что блокировка освобождена между F_SETLK

40:      и F_GETLK; проверить, существует ли еще конфликт

41:      перед тем, как сообщать об этом */

42:   if (lockinfo.l_type != F_UNLCK) {

43:    sprintf (message, "конфликт с процессом %d... нажмите "

44:     "<enter> для повторения:", lockinfo.l_pid);

45:    waitforuser(message);

46:   }

47:  }

48: }

49:

50: int main(void) {

51:  int fd;

52:

53:  /* подготовить файл для блокировки */

54:  fd = open("testlockfile", O_RDWR | O_CREAT, 0666);

55:  if (fd < 0) {

56:   perror("open");

57:   return 1;

58:  }

59:

60:  printf("получение блокировки чтения ");

61:  getlock(fd, F_RDLCK);

62:  printf("блокировка чтения получена ");

63:

64:  waitforuser(" для продолжения нажмите <enter>:");

65:

66:  printf("освобождение блокировки ");

67:  getlock(fd, F_UNLCK);

68:

69:  printf("получение блокировки записи ");

70:  getlock(fd, F_WRLCK);

71:  printf("блокировка записи получена ");

72:

73:  waitforuser(" для завершения нажмите <enter>:");

74:

75:  /* при закрытии файла блокировки освобождаются */

76:

77:  return 0;

78: }

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

1. Открытие одного файла дважды, что дает два разных файловым дескриптора.

2. Поучение блокировки чтения на одной области в обоих файловых дескрипторах.

3. Закрытие одного из файловых дескрипторов.

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

После fork() родительский процесс сохраняет свои файловые блокировки, но дочерний процесс — нет. Если бы дочерние процессы наследовали блокировки, два процесса пришли бы, в конечном счете, к блокировке записи на одной области файла.

Однако файловые блокировки наследуются в exec(). Поскольку в POSIX не определено, что происходит с блокировками после exec(), все варианты Unix сохраняют их[92].