Последнее обновление:
О коварстве функции flock() языка PHP
Эта функция предназначена для блокирования файлов, открытых для чтения и/или для записи. Это бывает необходимо в случаях, когда доступ к этим файлам имеет не один, а несколько процессов, желающих что-то прочитать оттуда или что-то записать. Если в случае чтения все проще – ведь содержимое файла не меняется - то с записью могут возникнуть проблемы. В самом деле, предположим, у Вас на сервере есть файл, в который записывается та или иная информация в результате запроса пользователя (из браузера). Нередко бывает так, что к одному и тому же файлу одновременно имеют доступ несколько пользователей. А все они делают такие запросы, что в результате в файл будет (точнее, должна) записываться информация ото всех их. И – одновременно?...
Как быть в этом случае?
Понятно, что для обработки каждого запроса (т.е. для каждого пользователя, по сути) запускается интерпретатор РНР – в виде еще одного процесса на сервере. Т.е. сколько пользователей взаимодействуют с файлом – столько процессов и будет запущено. В таких ситуациях первой делается та запись, запрос на которую пришел первым.
Точнее, делается попытка записи. А вот удастся ли она – это уже совсем другой вопрос.
Так вот, делается попытка записи. Но, ведь эта операция не может быть выполнена атомарно, т.е. без прерывания. Ведь у сервера – и иной работы много. Есть системные службы, а также, как уже говорилось, другие пользователи. И всем этим сервер занимается постоянно – по мере необходимости.
И вот, предположим, часть информации от запроса очередного пользователя была записана в файл. Указатель (позиции) в файле, соответственно, на том месте, на котором кончилась запись. И интерпретатор РНР готов бы продолжить запись, но операционная система сервера рассуждает иначе: ей ведь надо выполнить еще и массу другой работы. Поэтому она прерывает работу интерпретатора РНР (процесса, запущенного под управлением этого пользователя) и переходит к выполнению следующего задания. А таким заданием вполне может быть и обработка запроса еще одного пользователя, в соответствии с чем необходимо сделать запись, увы, в тот же самый файл.
Хорошо, началась запись. Например, с начала файла. Ведь этот второй процесс (интерпретатор РНР, запущенный для другого пользователя) понятия не имеет, что до него только что шла запись в тот же самый файл для перового пользователя.
Итак, начиная с начала файла, идет запись в тот же самый файл, но в рамках обработки запроса ДРУГОГО пользователя. Соответственно, во-первых, этот файл будет изменен. Во-вторых, после прерывания выполнения этого задания указатель в файле будет установлен уже где-то в другом месте…
И вот, интерпретатор РНР, недавно обрабатывавший запрос первого пользователя, вновь получил квант времени от операционной системы и приступил к продолжению записи… НО: позиция указателя-то в файле – уже иная, так как она изменилась в результате записи от второго (другого) пользователя. Это, естественно, приведет к тому, что интерпретатор РНР, как ни в чем ни бывало, продолжит запись, думая, что он продолжает ее, стартуя не с той позиции, на которой он остановился ранее, а совсем с другой позиции… К чему это приведет, наверное, объяснять не надо: Т.е. информация будет записана интерпретатором РНР в файл, скорее всего, кусками и с разных мест (позиций). И точно так же будет дело обстоять и у других процессов - интерпретаторов РНР, которые ведут запись в этот же файл одновременно с ним (первым процессом – интерпретатором).
Так вот, чтобы не допускать подобной ситуации, во многих языках программирования имеется функция блокировки файла. В языке PHP она звучит так:
flock(…);
Язык дает две возможности для блокировки:
LOCK_SH
для получения разделяемой блокировки (чтение),LOCK_EX
для получения эксклюзивной блокировки (запись).
Подразумевается, что эксклюзивная (исключительная) блокировка надежно блокирует файл. Если, мол, она сделана тем или иным процессом (который успел ее сделать), то писать в файл не смогут другие процессы до тех пор, пока эта блокировка не будет снята. Сделать это можно либо специальной командой, либо дождаться завершения работы процесса – интерпретатора PHP, обрабатывающего запрос того соответствующего пользователя (в последнем случае блокировка будет снята сборщиком мусора).
Таким образом, до тех пор, пока файл заблокирован, никакой другой процесс не сможет получить к нему доступ (для записи), даже в случае, если текущий интерпретатор РНР будет временно приостановлен (прерван). Соответственно, после возобновления работы запись в файл будет производиться именно с той позиции, на которой она окончилась до прерывания.
Т.е. в теории и в самом деле очень все удобно и красиво. Но… лишь в теории.
На самом деле – теория не всегда совпадает с практикой
Как минимум, потому, что в некоторых операционных системах управление блокировками файлом осуществляется на уровне… процессов. А это означает, что даже если кто-то (например, интерпретатор РНР, запущенный для обработки запроса первого пользователя) ЭКСЛЮЗИВНО заблокирует файл, это совсем не помешает записи в этот файл со стороны других процессов, например, интерпретаторов PHP, запущенных под управлением других пользователей.
Более того, даже если блокировка файлов осуществляется не на уровне процессов, а на уровне ядра операционной системы, то и там возможны крайне непредсказуемые результаты. Рассмотрим такой простой пример:
<?php
// Файл test.php
$f = fopen('1.txt', 'r');
flock($f, LOCK_EX);
file_put_contents('1.txt', 'Hello');
flock($f, LOCK_UN);
Также создадим файл с именем 1.txt
, запишем туда что-нибудь, например, символы qwerty. И сохраним его.
Как видно, приведенный выше программный код открывает файл, затем исключительно (эксклюзивно) блокирует его. А дальше – функция file_put_contents()
пытается записать в этот файл слово ‘Hello
’.
Вроде бы, судя по всему, запись в тот файл НЕ ДОЛЖНА бы производиться, пока он не разблокирован, не так ли?
Но, на практике дело обстоит несколько, скажем так, своеобразным образом. Т.е.запись-то не производится… и даже возникает предупреждение, что-то типа:
Warning: file_put_contents() [function.file-put-contents]: Only 0 of 5 bytes written, possibly out of free disk space in Z:\home\site.ru\www\***\test.php on line 4
Таким образом, функция вначале ОЧИЩАЕТ(!) файл. А затем мило сообщает, что, мол, всего 0 байтов-то записано. Ну, раз файл-то заблокирован.
Т.е. как видно, и в самом деле делается запись в размере НОЛЬ символов – в файл. В итоге он, как и положено, становится НУЛЕВОЙ длины. Т.е. усекается. Соответственно, все то, что в нем было до этого момента, теряется. И восстановлению не подлежит.
Кстати, если вместо исключительной блокировки применить LOCK_SH
, ничего не поменяется, файл все равно будет усекаться.
Но, это еще ладно бы. А дальше рассмотрим еще более тяжелый случай.
А что, если в функцию file_put_contents()
добавить флаг FILE_APPEND
?
Этот флаг означает, что функция не перепишет файл заново, а допишет к нему с конца, т.е. добавит новую информацию к уже имеющейся. Так вот, в таком случае НЕ ВОЗНИКНЕТ даже предупреждения. И функция file_put_contents()
«честно» сделает запись, совершенно игнорируя сделанную только что блокировку…
Так что вот так. Это похоже на ситуацию из то ли сказки, то ли анекдота, когда одному дурачку, пришедшему на кухню и увидевшему упавшие на полу пирожки, сказали: « ты аккуратнее с пирожками на полу». В итоге, этот дурачок АККУРАТНО шел по кухне, АККУРАТНО наступая на каждый пирожок.
Это следует иметь в виду при разработке мало-мальски нагруженных приложений и сервисов, посещаемость которых достаточно высока и где могут возникнуть конкурентные процессы записи в один и тот же файл. Как видно, на функцию flock()
в языке РНР надеяться НЕЛЬЗЯ.
Т.е. если в Вашем проекте "вдруг" и "почему-то" начало исчезать содержимое некоторых файлов - вспомните про эту нашу статью.
Примечание
Тестирование указанного программного кода проводилось в виртуальном сервере Denwer, PHP5.3, операционная система Windows 7. Вполне возможно, что в более поздних версиях языка РНР эта досадная оплошность устранена тем или иным образом.