【问题标题】:flock() between PHP and C edge casePHP 和 C 边缘情况之间的flock()
【发布时间】:2020-07-09 08:50:15
【问题描述】:

我有一个 PHP 脚本,它在 Linux 中接收发票并将其保存为文件。之后,一个基于 C++ 无限循环的程序读取每一个并进行一些处理。我希望后者安全地读取每个文件(仅在完全写入之后)。

PHP端代码简化:

file_put_contents("sampleDir/invoice.xml", "contents", LOCK_EX)

在 C++ 方面(使用 C 文件系统 API),我必须首先注意我想保留一个代码,该代码删除指定发票文件夹中的空文件,作为正确处理边缘情况的一种手段从其他来源(不是 PHP 脚本)创建的空文件。

现在,还有一个 C++ 端代码简化:

FILE* pInvoiceFile = fopen("sampleDir/invoice.xml", "r");

if (pInvoiceFile != NULL)
{
    if (flock(pInvoiceFile->_fileno, LOCK_SH) == 0)
    {
        struct stat fileStat;
        fstat(pInvoiceFile->_fileno, &fileStat);
        string invoice;
        invoice.resize(fileStat.st_size);

        if (fread((char*)invoice.data(), 1, fileStat.st_size, pInvoiceFile) < 1)
        {
            remove("sampleDir/invoice.xml"); // Edge case resolution
        }

        flock(pInvoiceFile->_fileno, LOCK_UN);
    }
}

fclose(pInvoiceFile);

如您所见,总结的关键概念是LOCK_EXLOCK_SH 标志的合作。

我的问题是,虽然这种集成工作正常,但昨天我注意到为不应为空的发票执行的边缘案例,因此它被 C++ 程序删除。

file_put_contents 上的 PHP 手册提到了 LOCK_EX 标志的以下内容:

在继续写入时获取文件的排他锁。换句话说,flock() 调用发生在fopen() 调用和fwrite() 调用 之间。这与模式为 "x"fopen() 调用不同。

  • file_put_contents 调用fopen 之前没有建立LOCK_EX 是否会导致问题作为竞争条件?如果是这样,在保留边缘案例删除代码的同时可以做些什么来解决这个问题?
  • 否则,我可能做错了什么吗?

【问题讨论】:

  • 你的代码能在 Linux 上运行吗?
  • 是的,这两种划分都具体运行在 CentOS 7 64 位系统上

标签: php c c++11 flock


【解决方案1】:

您的代码假设file_put_contents() 操作是原子操作,并且使用FLOCK_EXFLOCK_SH 足以确保两个程序之间不会发生竞争条件。 This is not the case.

正如您从 PHP 文档中看到的那样,FLOCK_EX 在打开文件后 应用。这很重要,因为它为 C++ 程序成功打开文件并用FLOCK_SH 锁定它留下了很短的时间窗口。此时该文件已被 PHP 完成的 fopen() 截断,它是空的。

最有可能发生的是:

  1. PHP 代码打开文件以进行写入、截断并有效清除其内容。
  2. C++ 代码打开文件进行读取。
  3. C++ 代码请求文件上的共享锁:已授予锁。
  4. PHP 代码请求文件的排他锁:调用阻塞,等待锁可用。
  5. C++ 代码读取文件内容:无,文件为空。
  6. C++ 代码删除文件。
  7. C++ 代码释放共享锁。
  8. PHP 代码获取排他锁。
  9. PHP 代码写入文件:数据未到达磁盘,因为与打开的文件描述符关联的 inode 不再存在。
  10. 您实际上没有任何文件并且数据丢失。

您的代码的问题在于,您从两个不同的程序对文件执行的操作不是原子的,并且您获取锁的方式无助于确保它们不会重叠。

在符合 POSIX 的系统上保证此类操作的原子性的唯一合理方法是利用 rename(2) 的原子性:

如果newpath 已经存在,它将被自动替换,因此尝试访问newpath 的另一个进程不会发现它丢失。

如果newpath 存在但由于某种原因操作失败,rename() 保证保留newpath 的实例。

在这种情况下,您应该使用等效的rename() PHP 函数。这是保证对文件进行原子更新的最简单方法。

我的建议如下:

  • PHP 代码:

    $tmpfname = tempnam("/tmp", "myprefix");     // Create a temporary file.
    file_put_contents($tmpfname, "contents");    // Write to the temporary file.
    rename($tmpfname, "sampleDir/invoice.xml");  // Atomically replace the contents of invoice.xml by renaming the file.
    
    // TODO: check for errors in all the above calls, most importantly tempnam().
    
  • C++ 代码:

    FILE* pInvoiceFile = fopen("sampleDir/invoice.xml", "r");
    
    if (pInvoiceFile != NULL)
    {
        struct stat fileStat;
        fstat(fileno(pInvoiceFile), &fileStat);
    
        string invoice;
        invoice.resize(fileStat.st_size);
    
        size_t n = fread(&invoice[0], 1, fileStat.st_size, pInvoiceFile);
        fclose(pInvoiceFile);
    
        if (n == 0)
            remove("sampleDir/invoice.xml");
    }
    

这样,C++ 程序要么总是看到旧版本的文件(如果 fopen() 出现在 PHP 的 rename() 之前),要么看到新版本的文件(如果 fopen() 出现在之后),但它会永远不会看到不一致的文件版本。

【讨论】:

  • 嘿,很抱歉恢复这个老话题。最初我认为您的答案是完美的,但事实证明,在所有这些时间发生并且一直在运行您的解决方案之后,我再次从 C++ 端遇到“0字节读取”的情况,导致发票文件被删除和丢失(幸运的是,我们还没有投入生产)。根据我阅读的最新信息(包括您的回答),rename() 仅对文件执行原子更新。我本可以一直假设错误。受影响的案例涉及新发票的创建——我总是需要原子性。那么这是否意味着创造不是原子的呢?谢谢
  • @Adrián 我想如果您的问题与此问题中的问题不同(也许参考这个问题),您应该发布另一个更详细的问题。重命名是 Linux 中文件唯一的原子操作。如果您想要原子创建,唯一的方法是(1)创建一个具有另一个名称的文件(2)将所有内容写入并关闭它(3)将其重命名为您想要的真实名称。这样,您将看到新文件以原子方式显示,其中包含所有内容。
  • 谢谢。我想知道是否要提出一个新问题。但是,如果你确认原子创造是可能的,我相信如此。可能我真正的 PHP 实现,目前依赖于 XML API 来编写临时文件,可能在重命名之前没有关闭它并且它有事情要做,这就是我能想到的唯一原因。然而,我开始采取不同的方式,使用一种更手动的方法,至少是可移植的。祝我好运!
  • @Adrián 我会强烈建议为此使用真正的数据库。使用随机文件并移动它们确实是解决该问题的最笨拙的方法,您绝对不希望在真正的生产服务器中使用类似的东西。
  • 谢谢,我知道这个常见的建议,但我的老板总是谈论使用文件,所以我们对设计很好(而且我只要系统保持对竞争条件的保护这样)
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-12-25
  • 1970-01-01
  • 2012-05-14
  • 1970-01-01
  • 2017-11-23
  • 1970-01-01
相关资源
最近更新 更多