这是 Ruby 代码:
File.open('a.txt', File::CREAT | File::RDWR) do |f|
# Another thread deletes the a.txt file here
f.flock(File::LOCK_EX | File::LOCK_NB)
# How do I check that the file is really locked by my thread?
end
在多线程环境中,当许多线程尝试锁定文件然后将其删除时,一个线程可能会在flock()
之前删除它称呼。在这种情况下,flock()
仍然认为文件就位并返回 true
。
我正在尝试找到一种方法来检查文件是否真的在 flock()
完成后被当前线程锁定。我怎样才能做到这一点?
最佳答案
如果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 表中有两个不同且不相关的入口点:打开文件表和目录索引。不同的系统调用使用不同的入口点:
- open(file_path) => 从目录索引中查找 i 节点编号,并在文件描述符表引用的打开文件表中创建一个条目(每个进程一个表),然后在相关 i 节点表条目中递增 ref_counter。里>
- close(file_descriptor) => 关闭(释放)相关文件描述符表条目和打开文件表中的相关条目(除非有其他引用文件描述符),然后递减相关 i 节点表条目中的 ref_counter(除非打开文件条目)保持开放)
- unlink(file_path) => 没有删除系统调用!通过从目录索引中删除条目,取消 inode 表与目录索引的链接。相关 i 节点表条目中的计数器递减(不知道打开文件表!)
- flock(file_desriptor) => 对打开文件表中的条目应用/删除锁定(不知道目录索引!)
- i-node 表条目被删除(实际上是删除文件)IFF ref_counter 变为零。它可能发生在 close() 之后或 unlink() 之后
这里的关键点是取消链接不一定会立即删除文件(数据)!它仅取消目录索引和 inode 表的链接。这意味着即使在取消链接之后,文件仍可能以事件锁定的方式打开!
请记住这一点,想象以下有 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) 两个线程打开(可能创建)同一个文件。因此,存在从目录索引到 inode 表的链接。每个线程都有自己的文件描述符。
- (2) 两个线程都尝试使用从 open 调用中获取的文件描述符来获取独占锁
- (3) 第一个线程获得锁,而第二个线程被阻塞(或尝试在循环中获得锁)
- (4) 第一个线程完成任务并删除(取消链接)文件。此时,从目录索引到 inode 的链接已删除,我们不会在目录列表中看到它。但是,该文件仍然存在,并且在两个带有事件锁的线程中打开!它只是失去了它的名字。
- (5) 第一个线程关闭文件描述符并因此释放锁。因此第二个线程获得锁并开始处理任务
- (6) 第一个线程重复并尝试打开同名的文件。但它和以前是同一个文件吗?否。因为此时目录索引中不存在给定名称的文件。所以它会创建一个新文件!新的 i 节点表条目。
- (7) 第一个线程获得新文件的锁!
- (8) 我们得到两个线程,它们锁定两个不同的文件并且不同步
上述场景的问题在于,打开/取消链接工作在目录索引上,而锁定/关闭工作在文件描述符上,它们彼此不相关。
为了解决这个问题,我们需要通过一些中央入口点来同步这些操作。它可以通过引入一个单例服务来实现,该服务将使用互斥体或 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
关于ruby - 如何检查文件是否仍被当前线程锁定?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/53011200/