代码之家  ›  专栏  ›  技术社区  ›  yegor256

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

  •  2
  • yegor256  · 技术社区  · 6 年前

    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() 打电话。在这种情况下, 羊群() 仍然认为文件已经到位并返回 true

    我试图找到一种方法来检查文件是否真的被当前线程锁定 羊群() 完成。我怎么能做到呢?

    1 回复  |  直到 6 年前
        1
  •  2
  •   Vitaliy Tsvayer    6 年前

    如果 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