Операция блокировки части файла необходима для организации совместного доступа к файлу из нескольких процессов. Главная задача блокировки - обеспечение атомарности чтения данных, т.е. гарантия того, что при чтении (возможно несколькими вызовами 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");
Следующим шагом стало появление в BSD системах вызова flock()
, который позволял пометить в ядре файл как заблокированный. Этот вызов не стандартизован POSIX, но поддерживается в Linux и во многих версиях Unix. flock()
не поддерживается сетевой файловой системой NFS.
#include <sys/file.h>
int flock(int fd, int operation);
Операции:
flock()
возвращает -1, а переменная errno
устанавливается в значение EWOULDBLOCK.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) */
...
};
Тип блокировки:
Команды:
fcntl()
вернёт -1 и установит значение errno в EACCES или EAGAIN.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.
Команды:
При программировании в shell можно воспользоваться командой
flock [options] <file|directory> <command> [command args]
Если отбросить подробности, то flock
пытается установить блокировку (вызовом flock()
, что почти очевидно) на указанный файл или каталог. Если блокировка уже установлена, то flock
уходит в "сон". Как только файл становится доступным, flock
блокирует его, выполняет указанную команду и завершается, освобождая блокировку.
В Linux и некоторых Unix возможна установка обязательных блокировок, которые приостановят вызовы read()
и write()
на заблокированных участках файла (документация по Linux). Для этого файловая система должна быть смонтирована с опцией-o mand, а в правах доступа к файлу должны быть одновременно выставлены флаг запрещения прав на выполнение группой и флаг смены группы процесса при выполнении SGID. В буквенной записи это выглядит так: -rw-r-Sr-- (если бы было право на исполнение группе, то была бы показана маленькая 's') .
write()
, open()
с флагом O_TRUNC и т.п.)read()
и вызовы, изменяющие содержимое файла (write()
и т.д.)