Если f.flock(File::LOCK_EX | File::LOCK_NB)
возвращает не false
значение, то f
заблокировано.Он будет держать блокировку до тех пор, пока вы не закроете файл или явно не вызовете f.flock(File::LOCK_UN)
.Вам не нужно проверять, заблокирован ли он снова.Чтобы объяснить, что на самом деле там происходит, нам нужно сначала посмотреть на внутренние компоненты файловой системы и связанные системные вызовы:
File Descriptor Table Open File Table i-node Table Directory Index
╒════════════════════╕ ╒═════════════╕ ╒════════════╕ ╒═════════════╕
┃3 ..................┣━━━━━━▷┃ open file1 ┣━━┳━━━▷┃ /tmp/file1 ┃◃━━━━┫ file1 ┃
┃4 ..................┣━━━━━━▷┃ open file1 ┣━━┚ ┏━▷┃ /tmp/file2 ┃◃━━━━┫ file2 ┃
┃5 ..................┣━━━┳━━▷┃ open file2 ┣━━━━┚
┃6 ..................┣━━━┚
Ключевой момент на этой диаграмме состоит в том, что есть две разные и не связанные точки входа в Таблица i-узла : таблица открытых файлов и индекс каталога. Различные системные вызовы работают с разными точками входа:
- open (file_path) => находит i-узелномер из индекса каталога и создает запись в таблице открытых файлов, на которую ссылается таблица дескрипторов файлов (одна таблица на процесс), затем увеличивает ref_counter в соответствующей записи таблицы i-узла.
- close (file_descriptor) => closes (освобождает) связанную запись таблицы дескрипторов файлов и связанную запись из таблицы открытых файлов (если не существует других ссылающихся дескрипторов файлов), затем уменьшает значение ref_counter в связанной записи таблицы i-узла (если запись открытого файла не остается открытой)
- unlink (file_path) => нет системного вызова Delete!Отключение таблицы i-node от индекса каталога путем удаления записи из индекса каталога.Счетчик уменьшений в соответствующей записи таблицы i-узла (не знает об Open File Table!)
- flock (file_desriptor) => применить / удалить блокировку записей в Open File Table (не знает об индексе каталога!)
- Удалена запись таблицы i-узла (практически удаляется файл). IFF ref_counter становится нулевым.Это может произойти после close () или после unlink ()
Ключевым моментом здесь является то, что unlink не обязательно удаляет файл (данные) немедленно!Он отменяет связь только с индексом каталога и таблицей i-узлаЭто означает, что даже после unlink файл все еще может быть открыт с активными блокировками!
Имея это в виду, представьте следующий сценарий с двумя потоками, пытаясь синхронизироваться пофайл с использованием open / flock / close и попыткой очистки с использованием unlink:
THREAD 1 THREAD 2
==================================================
| |
| |
(1) OPEN (file1, CREATE) |
| (1) OPEN (file1, CREATE)
| |
(2) LOCK-EX (FD1->i-node-1) |
[start work] (2) LOCK-EX (FD2->i-node-1) <---
| . |
| . |
(3) work . |
| (3) waiting loop |
| . |
[end work] . |
(4) UNLINK (file1) . -----------------------
(5) CLOSE (FD1)--------unlocked------> [start work]
| |
| |
(6) OPEN (file1, CREATE) |
| |
| (5) work
(7) LOCK-EX (FD1->i-node-2) |
[start work] !!! does not wait |
| |
(8) work |
| |
- (1) оба потока открывают (потенциально создают) один и тот же файл.В результате появляется ссылка из индекса каталога на таблицу i-узла.Каждый поток получает свой дескриптор файла.
- (2) оба потока пытаются получить эксклюзивную блокировку, используя дескриптор файла, который они получают при открытом вызове
- (3) первый поток получает блокировку, а второйпоток заблокирован (или пытается получить блокировку в цикле)
- (4) первый поток завершает задачу и удаляет (отменяет связь) файл.На этом этапе ссылка из индекса каталога на i-узел удалена, и мы не увидим его в списке каталогов.НО, файл все еще там и открыт в двух потоках с активной блокировкой!Он просто потерял свое имя.
- (5) первый поток закрывает дескриптор файла и в результате снимает блокировку.Таким образом, второй поток получает блокировку и начинает работать над задачей
- (6), первый поток повторяется и пытается открыть файл с тем же именем.Но это тот же файл, что и раньше?Нет. Потому что на данный момент в каталоге индексов нет файла с данным именем.Таким образом, он создает новый файл вместо этого!новая запись таблицы i-узла.
- (7) первый поток получает блокировку для нового файла!
- (8), и мы получаем два потока с блокировкой для двух разных файлов и несинхронизированные
Проблема в приведенном выше сценарии заключается в том, что операции открытия / отмены связи работают с индексом каталога, а операции блокировки / закрытия - с дескрипторами файлов, которые не связаны друг с другом.
Чтобы решить эту проблему, нам нужно синхронизировать эти операции через некоторую центральную точку входа. Это можно реализовать, внедрив одноэлементную службу, которая будет обеспечивать эту синхронизацию, используя Mutex или примитивы из Concurrent Ruby .
Вот одна из возможных реализаций PoC:
class FS
include Singleton
def initialize
@mutex = Mutex.new
@files = {}
end
def open(path)
path = File.absolute_path(path)
file = nil
@mutex.synchronize do
file = File.open(path, File::CREAT | File::RDWR)
ref_count = @files[path] || 0
@files[path] = ref_count + 1
end
yield file
ensure
@mutex.synchronize do
file.close
ref_count = @files[path] - 1
if ref_count.zero?
FileUtils.rm(path, force: true)
@files.delete(path)
else
@files[path] = ref_count
end
end
end
end
А вот ваш переписанный пример из вопроса:
FS.instance.open('a.txt') do |f|
if f.flock(File::LOCK_EX | File::LOCK_NB)
# you can be sure that you have a lock
end
# 'a.txt' will finally be deleted
end