如果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-node Table有两个不同且不相关的入口点:打开文件表和目录索引。不同的系统调用使用不同的入口点:
- open(file_path) => 从目录索引中找到 i-node 编号,并在文件描述符表引用的 Open File Table 中创建一个条目(每个进程一个表),然后在相关 i-node Table 条目中增加 ref_counter。
- close(file_descriptor) => 关闭(释放)相关文件描述符表条目和打开文件表中的相关条目(除非有其他引用文件描述符),然后减少相关 i-node 表条目中的 ref_counter(除非打开文件条目保持打开状态)
- unlink(file_path) => 没有删除系统调用!通过从目录索引中删除条目,从目录索引中取消链接 i-node 表。相关 i-node 表条目中的递减计数器(不知道打开文件表!)
- flock(file_desriptor) => 对打开文件表中的条目应用/删除锁定(不知道目录索引!)
- i-node 表条目被删除(实际上是删除一个文件)IFF ref_counter 变为零。它可以在 close() 或 unlink() 之后发生
这里的重点是unlink不一定会立即删除文件(数据)!它仅取消链接目录索引和 i-node 表。这意味着即使在 unlink 之后,该文件仍可能打开并带有活动锁!
记住这一点,想象一下下面的场景,有 2 个线程,尝试使用 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-node 表的链接。每个线程都有自己的文件描述符。
- (2) 两个线程都尝试使用从打开调用中获得的文件描述符获取排他锁
- (3) 第一个线程获得锁,而第二个线程被阻塞(或试图在循环中获得锁)
- (4) 第一个线程完成任务并删除(取消链接)文件。此时从目录索引到 i-node 的链接被删除,我们不会在目录列表中看到它。但是,该文件仍然存在,并且在两个线程中打开并带有活动锁!它只是失去了它的名字。
- (5) 第一个线程关闭文件描述符并因此释放锁。因此第二个线程获得了锁并开始执行任务
- (6) 第一个线程重复并尝试打开同名文件。但它和以前的文件一样吗?不,因为此时目录索引中没有给定名称的文件。所以它会创建一个新文件!新的 i-node 表条目。
- (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