flock () между PHP и C крайний регистр - PullRequest
0 голосов
/ 09 июля 2020

У меня есть сценарий PHP, который получает и сохраняет счета в виде файлов в Linux. Позже программа на C ++ с бесконечным l oop читает каждую и выполняет некоторую обработку. Я хочу, чтобы последний безопасно читал каждый файл (только после полной записи).

PHP упрощение бокового кода:

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

На стороне C ++ (с C API файловой системы) , Я должен сначала отметить, что я хочу сохранить код, который удаляет файлы в указанной папке счетов-фактур, которые пусты, так же как средство для правильной работы с крайним случаем пустого файла, создаваемого из других источников (не PHP script).

А вот и упрощение бокового кода 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_EX и LOCK_SH flags.

Моя проблема в том, что, хотя эта интеграция работала нормально, вчера я заметил крайний случай, выполненный для счета-фактуры, который не должен быть пустым, и поэтому он был удален программой C ++.

PHP руководство по file_put_contents упоминает следующее для флага LOCK_EX:

Получите исключительную блокировку файла при переходе к записи. Другими словами, вызов flock() происходит между вызовом fopen() и вызовом fwrite() . Это не идентично вызову fopen() в режиме "x".

  • Может ли проблема быть вызвана состоянием гонки из-за того, что LOCK_EX не был установлен непосредственно перед file_put_contents звонки fopen? Если да, то что можно сделать, чтобы решить эту проблему, сохранив код удаления краевого корпуса?
  • В противном случае, могу ли я сделать что-то в целом не так?

1 Ответ

1 голос
/ 09 июля 2020

Ваш код предполагает, что операция file_put_contents() - это atomi c, и что использования FLOCK_EX и FLOCK_SH достаточно, чтобы гарантировать отсутствие условий гонки между двумя программами. Это не тот случай .

Как видно из PHP do c, FLOCK_EX применяется после открытия файла. Это важно, потому что у программы на C ++ остается короткое время, чтобы успешно открыть файл и заблокировать его с помощью FLOCK_SH. В этот момент файл уже был усечен командой fopen(), выполненной PHP, и он пуст.

Скорее всего, происходит следующее:

  1. PHP код открывает файл для записи, усечения и эффективного уничтожения его содержимого.
  2. Код C ++ открывает файл для чтения.
  3. Код C ++ запрашивает общую блокировку файла: блокировка предоставляется.
  4. PHP код запрашивает монопольную блокировку файла: вызовы блокируются, ожидая, пока блокировка станет доступной.
  5. Код C ++ читает содержимое файла: ничего, файл пуст.
  6. Код C ++ удаляет файл.
  7. Код C ++ освобождает разделяемую блокировку.
  8. PHP код получает исключительную блокировку.
  9. PHP код записывает в файл: данные не достигают диска, потому что индексный дескриптор, связанный с дескриптором открытого файла, больше не существует.
  10. Фактически у вас нет файла, и данные теряются.

Проблема с вашим кодом в том, что t Операции, которые вы выполняете с файлом из двух разных программ, не являются atomi c, и способ, которым вы устанавливаете блокировки, не помогает гарантировать, что они не перекрываются.

Единственный разумный способ гарантировать атомарность такой операции в POSIX-совместимой системе, даже не беспокоясь о блокировке файлов, заключается в использовании преимущества атомарности rename(2):

If newpath уже существует, он будет заменен атомарно, чтобы не было точки, в которой другой процесс, пытающийся получить доступ к newpath, не обнаружит, что он отсутствует.

Если newpath существует, но операция по какой-то причине завершается неудачно, rename() гарантирует, что экземпляр newpath останется на месте.

Эквивалентная функция rename() PHP - это то, что вы должны использовать в этом случае. Это самый простой способ гарантировать обновления файла atomi c.

Я бы предложил следующее:

  • 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() происходит после), но он никогда не увидит несовместимую версию файла.

...