如果
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节点表
:打开文件表和目录索引。
不同的系统调用使用不同的入口点:
-
-
close(file_descriptor)=>关闭(释放)相关文件描述符表条目和打开文件表中的相关条目(除非有其他引用文件描述符),然后递减相关i节点表项中的ref_计数器(除非打开的文件条目保持打开状态)
-
unlink(file_path)=>没有删除系统调用!通过从目录索引中删除条目来取消i-node表与目录索引的链接。在相关的i-node表项中递减计数器(不知道打开的文件表!)
-
flock(file_descriptor)=>对打开的文件表中的条目应用/删除锁定(不知道目录索引!)
-
当ref_计数器变为零时,i-node表项被删除(实际上是删除一个文件)。它可以在close()或unlink()之后发生
关键是
不一定要立即删除文件(数据)!它只解除目录索引和i节点表的链接。意思是即使在
取消链接
文件可能仍处于打开状态,并带有活动锁!
记住这一点,设想以下两个线程的场景,尝试使用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) 第一个线程得到一个锁,第二个线程被阻塞(或者试图在循环中获得锁)
-
-
(5) 第一个线程关闭文件描述符,结果释放一个锁。因此,第二个线程获得一个锁并开始处理一个任务
-
(6) 第一个线程重复并尝试打开同名的文件。但它和以前一样吗?没有。因为此时目录索引中没有给定名称的文件。所以它会创建一个新文件!新建i-node表项。
-
-
(8) 我们得到了两个线程,两个线程锁定在两个不同的文件上,并且不同步
上述场景中的问题是,打开/取消链接处理目录索引,而锁定/关闭操作的是文件描述符,它们彼此不相关。
为了解决这个问题,我们需要通过一些中央入口点同步这些操作。
它可以通过引入一个单例服务来实现,该服务将使用互斥体或来自
Concurrent Ruby
.
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