ruby - 如何检查文件是否仍被当前线程锁定?

标签 ruby multithreading

这是 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/

相关文章:

ruby - Codekit文件权限问题(只读文件系统@dir_s_mkdir -/.sass-cache)

ruby-on-rails - Rails 欢迎页面尝试在 Nginx + Passenger 中获取 rails.png 时出现错误 404

c - 在信号处理程序中暂停线程执行

multithreading - 无法通过 channel 发送结构:mpsc::Sender 无法在线程之间安全共享

silverlight - Silverlight:线程/延迟的操作/异步调用/事件

c++ - 是否有嵌套类的用例?

ruby - 如何获取字符串中可用键的所有组合

java - 为什么这种锁变化对 JDBC 性能的影响如此之大?

java - 重绘仅在调整大小后有效

ruby - 正则表达式匹配在相对路径中不在域中包含字符串的 url