Совместный доступ к файлам - блокировки

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

В Unix традиционно используются рекомендательные (advisory) блокировки. Такие блокировки требуют от всех процессов, которые хотят получить доступ к файлу, вызова специальных функций для установки и/или проверки блокировок. Вызовы open(), read() и write() про блокировки ничего не знают, и, в результате, процессы, которые явно не вызывают функции работы с блокировками не узнают об их существовании и могут нарушить логику совместной работы с файлом.

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

Хорошая статья на английском.

Блокировка через вспомогательный файл

Самые ранние версии Unix не поддерживали блокировок, поэтому в различных программах можно встретить создание вспомогательного файла с расширением .lck или .lock. Перед записью в файл somefile.txt программа в цикле пытается создать файл somefile.txt.lck с флагами O_CREAT|O_EXCL. Если файл уже существует, то вызов open() возвращает ошибку и цикл продолжается. Если файл удалось успешно создать, то цикл завершается и можно открывать на запись основной файл. В служебный файл блокировки часто пишут PID процесса, чтобы было понятно, кто его создал.

int fd=-1;
while(fd==-1){
   fd=open("somefile.lck", O_CREAT|O_EXCL|O_WRONLY,0500);
}

//Пишем в файл блокировки свой PID
char pid[6];
itoa(getpid(), pid, 10);
write(fd,pid,strlen(pid));
//этот файловый дескриптор нам больше не нужен
close(fd);

//основная работа
int mainfd=open("somefile", O_WRONLY,0500);
...
close(mainfd);

//Удаляем файл блокировки
unlink("somefile.lck");

Блокировка flock()

Следующим шагом стало появление в BSD системах вызова flock(), который позволял пометить в ядре файл как заблокированный. Этот вызов не стандартизован POSIX, но поддерживается в Linux и во многих версиях Unix. flock() не поддерживается сетевой файловой системой NFS.

#include <sys/file.h>
int flock(int fd, int operation);

Операции:

  • LOCK_EX - если файл не заблокирован, то установить эксклюзивную блокировку. Иначе приостановить выполнение процесса, пока все остальные блокировки не будут сняты. По определению только один процесс может держать эксклюзивную блокировку файла.
  • LOCK_SH - если файл не заблокирован эксклюзивно, то увеличить счётчик разделяемых блокировок. Иначе приостановить процесс до снятия эксклюзивной блокировки. Разделяемую блокировку на заданный файл может держать более чем один процесс.
  • LOCK_UN - удалить блокировку, удерживаемую данным процессом
  • LOCK_NB - (not block) флаг, применяемый вместе с LOCK_EX и LOCK_SH для проверки возможности блокировки. Если блокировка невозможна, то вместо приостановки процесса flock() возвращает -1, а переменная errno устанавливается в значение EWOULDBLOCK.

Блокировки в POSIX fcntl()

Стандарт POSIX определяет операции с блокировками записей - участков файлов. Блокировки производятся универсальный вызов управления открытыми файлами fcntl() (file control) или через функцию locf(). В Linux locf() это библиотечная обёртка для fcntl().

#include <unistd.h>
#include <fcntl.h>
int fcntl(int fd, int cmd, struct flock *lock);

fcntl() - это перегруженный (в смысле C++) вызов ядра с переменным числом параметров. Его поведение определяется значением второго параметра - cmd. За блокировки отвечают команды F_SETLK, F_SETLKW и F_GETLK, которые используются для установки/снятия (SET) и чтения (GET) блокировок файла .

Параметры блокировки задаются/считываются через структуру flock. Блокируемая позиция задаётся параметром, указывающим откуда отсчитывать стартовое смещение, стартовым смещением и размером. Нулевой размер означает блокировку участка файла от стартового смещения и до бесконечности.

struct flock {
    ...
    short l_type;    /* Тип блокировки: F_RDLCK,
                        F_WRLCK, F_UNLCK */
    short l_whence;  /* Как интерпретировать l_start:
                        SEEK_SET, SEEK_CUR, SEEK_END */
    off_t l_start;   /* Начальное смещение для блокировки */
    off_t l_len;     /* Количество байт для блокировки */
    pid_t l_pid;     /* PID процесса блокирующего нашу блокировку
                        (F_GETLK only) */
    ...
};

Тип блокировки:

  • F_RDLCK - разделяемая блокировка. Мы собираемся только читать файл и другие читатели могут к нам присоединиться. В это время никто не должен писать в файл.
  • F_WRLCK - эксклюзивная блокировка. Мы собираемся менять файл и никто не должен его читать или в него писать.
  • F_UNLCK - снятие блокировки

Команды:

  • F_SETLK - установить блокировку (если l_type установлен в значение F_RDLCK или F_WRLCK) или снять блокировку (если l_type установлен в значение F_UNLCK). Если другой процесс заблокировал указанный участок или его часть, то fcntl() вернёт -1 и установит значение errno в EACCES или EAGAIN.
  • F_SETLKW - (setlk + wait) тоже, что и предыдущий случай, но с приостановкой текущего процесса до освобождения внешней блокировки
  • F_GETLK - проверка наличия блокировки. На вход подаётся параметры интересующего участка и тип блокировки, на выходе - l_type==F_UNLCK если конфликтов не обнаружено или параметры блокировки, которая пересекается с интересующей.

Блокировки в POSIX lockf()

Функция lockf() в Linux реализуется через fcntl() и представляет из себя упрощённый интерфейс для работы с блокировками.

#include <unistd.h>
int lockf(int fd, int cmd, off_t len);

lockf() работает с участком файла начиная с текущей позиции чтения/записи и длиною len байт если len > 0; c позиции раньше текущей на len и до текущей если len < 0; с текущей позиции до конца файла если len = 0.

Команды:

  • F_LOCK - Устанавливает исключительную блокировку указанной области файла. Если эта область (или её часть) уже блокирована другим процессом, то вызов приостановит выполнение текущего процесса до тех пор, пока не будет снята предыдущая блокировка. Если эта область перекрывается с ранее заблокированной в этом процессе областью, то они объединяются. Файловые блокировки снимаются сразу после того, как установивший их процесс закрывает файловый дескриптор. Дочерние процессы не наследуют подобные блокировки.
  • F_TLOCK - То же самое, что и F_LOCK, но вызов никогда не блокирует выполнение и возвращает ошибку, если файл уже заблокирован.
  • F_ULOCK - Снимает блокировку с заданной области файла. Может привести к тому, что блокируемая область будет поделена на две заблокированные области.
  • F_TEST - Проверяет наличие блокировки: возвращает 0, если указанная область не заблокирована или заблокирована вызвавшим процессом; возвращает -1, меняет значение errno на EAGAIN (в некоторых системах на EACCES), если блокировка установлена другим процессом.

Блокировки в shell

При программировании в shell можно воспользоваться командой

flock [options] <file|directory> <command> [command args]

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

Обязательные (mandatory) блокировки

В Linux и некоторых Unix возможна установка обязательных блокировок, которые приостановят вызовы read() и write() на заблокированных участках файла (документация по Linux). Для этого файловая система должна быть смонтирована с опцией-o mand, а в правах доступа к файлу должны быть одновременно выставлены флаг запрещения прав на выполнение группой и флаг смены группы процесса при выполнении SGID. В буквенной записи это выглядит так: -rw-r-Sr-- (если бы было право на исполнение группе, то была бы показана маленькая 's') .

  • Если на файл установлена разделяемая блокировка F_RDLCK то блокируются все вызовы, которые изменяют файл (write(), open() с флагом O_TRUNC и т.п.)
  • Если на файл установлена разделяемая блокировка F_WRLCK то блокируются вызовы чтения read() и вызовы, изменяющие содержимое файла (write() и т.д.)
  • Обязательная блокировка несовместима с отображением файла в память.