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

时间:2018-10-26 14:45:32

标签: 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()之后是否立即被当前线程真正锁定。我该怎么办?

1 个答案:

答案 0 :(得分:1)

如果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)=>从目录索引中找到索引节点号,并在由文件描述符表引用的打开文件表中创建一个条目(每个进程一个表),然后在相关索引节点表条目中增加ref_counter。 / li>
  • close(file_descriptor)=>从打开文件表中关闭(释放)相关的文件描述符表条目和相关条目(除非有其他引用文件描述符),然后在相关的i节点表条目中减小ref_counter(除非打开文件条目)保持打开状态)
  • unlink(file_path)=>没有删除系统调用!通过从目录索引中删除条目来取消索引节点表与目录索引的链接。相关i节点表条目中的减量计数器(不知道打开文件表!)
  • flock(file_desriptor)=>在打开文件表中的条目上应用/删除锁定(不知道目录索引!)
  • i节点表条目已删除(实际上是删除文件),IFF ref_counter变为零。它可以在close()之后或unlink()之后发生

此处的关键点是 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)两个线程都打开(可能创建)相同的文件。结果,存在从目录索引到索引节点表的链接。每个线程都有自己的文件描述符。
  • (2)两个线程都尝试使用从公开调用中获取的File Descriptor获得排他锁
  • (3)第一个线程获得锁,第二个线程被阻塞(或试图在循环中获得锁)
  • (4)第一个线程完成任务并删除(取消链接)文件。此时,从目录索引到索引节点的链接已删除,我们不会在目录列表中看到它。但是,该文件仍然存在,并在两个线程中使用活动锁打开了!它只是失去了名字。
  • (5)第一个线程关闭文件描述符,结果释放了一个锁。这样,第二个线程将获得一个锁并开始执行任务
  • (6)第一个线程重复并尝试打开一个具有相同名称的文件。但是它和以前的文件一样吗?否。因为此时目录索引中没有具有给定名称的文件。因此,它将创建一个新文件!新的索引节点表条目。
  • (7)第一个线程锁定了一个新文件!
  • (8),我们得到了两个线程,它们在两个不同文件上具有锁,并且是不同步的

在上述情况下,问题在于打开/取消链接对目录索引起作用,而锁定/关闭对文件描述符起作用,而这两个彼此无关。

要解决此问题,我们需要通过某个中央入口点同步这些操作。可以通过引入单例服务来实现,该服务将使用Mutex或{{3}中的基元来提供此同步。 }。

这是一种可能的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