Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Самоучитель по PHP 4

.pdf
Скачиваний:
82
Добавлен:
02.05.2014
Размер:
4.36 Mб
Скачать

Глава 15. Работа с файлами

261

осуществляется не на уровне ядра системы, а на уровне программы. Поясню на примере.

Однажды я прочитал одно замечательное сравнение рекомендательной блокировки с перекрестком, на котором установилось довольно оживленное движение, регулируемое светофором. Когда горит красный, одни машины стоят, а другие проезжают. В принципе, любая машина может, так сказать, проехать наперекор правилам дорожного движения, не дожидаясь зеленого сигнала, но в таком случае возможны аварии. Рекомендательная блокировка работает точно таким же образом. А именно, процессы, которые ей пользуются, будут работать с разделяемым файлом правильно, а остальные… как-нибудь да будут, пока не произойдет "столкновение".

С другой стороны, "жесткая блокировка" (которая в PHP не поддерживается) подобна шлагбауму: никто не сможет проехать, пока его не поднимут. О жесткой блокировке мы в этой книге говорить не будем.

Единственная функция, которая занимается управлением блокировками в PHP, назы-

вается flock().

bool flock(int $f, int $operation [, int& $wouldblock])

Функция устанавливает для указанного открытого дескриптора файла $f режим блокировки, который бы хотел получить текущий процесс. Этот режим задается аргументом $operation и может быть одной из следующих констант:

rLOCK_SH (или 1) — разделяемая блокировка;

rLOCK_EX (или 2) — исключительная блокировка;

rLOCK_UN (или 3) — снять блокировку;

rLOCK_NB (или 4) — эту константу нужно прибавить к одной из предыдущих, если вы не хотите, чтобы программа "подвисала" на flock() в ожидании своей очереди, а сразу возвращала управление.

Вслучае, если был затребован режим без ожидания, и блокировка не была успешно установлена, в необязательный параметр-переменную $wouldblock будет записано значение истина true.

Вслучае ошибки функция, как всегда, возвращает false, а в случае успешного завершения — true.

Хотя в документации PHP и сказано, что flock() работает во всех операционных системах (в том числе и под Windows), мои тесты показали, что как раз для Windows это не так. А именно, в этой системе функция всегда возвращает индикатор провала, независимо от того, правильно она вызывается, или нет. Возможно, в бу- дущих версиях PHP это досадное недоразумение будет исправлено.

262

Часть IV. Стандартные функции PHP

Типы блокировок

Вспоминаю, когда я впервые столкнулся с описанием возможностей рекомендательных блокировок Си в одном из электронных справочников (кажется, это был man операционной системы FreeBSD), я был неприятно удивлен, — настолько все выглядело сложным и нелогичным. Слава Богу, на самом деле ситуация далеко не так плачевна. Сейчас я постараюсь понятным языком разъяснить все, что касается блокировок в языке PHP. Что же из себя представляют понятия исключительная блокировка и разделяемая блокировка?

Исключительная блокировка

Вернемся к нашему примеру с процессами-писателями. Каждый такой процесс страстно желает, чтобы в некоторый момент (точнее, когда он уже почти готов начать писать) он был единственным, кому разрешена запись в файл.

Он хочет стать исключительным.

Отсюда и название блокировки, которую процесс должен для себя установить. Вызвав функцию flock($f,LOCK_EX), он может быть абсолютно уверен, что все остальные процессы не начнут без разрешения писать в файл, пока он не выполнит все свои действия и не вызовет flock($f, LOCK_UN) или не закроет файл.

Откуда такая уверенность? Дело в том, что если в данный момент наш процесс не единственный претендент на запись, операционная система просто не выпустит его из "внутренностей" функции flock(), т. е. не допустит его продолжения, пока про- цесс-писатель не станет единственным. Момент, когда процесс, использующий исключительную блокировку, становится активным, знаменателен еще и тем, что все остальные процессы-писатели ожидают (все в той же функции flock()), когда же он, наконец, закончит свою работу с файлом. Как только это произойдет, операционная система выберет следующий исключительный процесс, и т. д.

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

15.2).

Листинг 15.2. Модель процесса с исключительной блокировкой

<?

//инициализация

//. . .

$f=fopen($f,"a+") or die("Не могу открыть файл на запись!"); flock($f,LOCK_EX); // ждем, пока мы не станем единственными

//В этой точке мы можем быть уверены, что только эта

//программа работает с файлом

Глава 15. Работа с файлами

263

// . . .

 

 

fflush($f);

// записываем все изменения на диск

 

flock($f,LOCK_UN); // говорим, что мы больше не будем работать с файлом fclose($f);

//Завершение

//. . .

?>

Заметьте, что при открытии файла мы использовали не деструктивный режим w (который удаляет файл, если он существовал), а более "мягкий" — a+. Это неспроста. Посудите сами: удаление файла идеологически есть изменение его содержимого. Но мы не должны этого делать до получения исключительной блокировки (вспомните пример со светофором)! Поэтому, если вам нужно обязательно каждый раз стирать содержимое файла, ни в коем случае не используйте режим открытия w — применяйте a+ и функцию ftruncate(), описанную выше. Например:

$f=fopen($f,"a+") or die("Не могу открыть файл на запись!");

flock($f,LOCK_EX);

//

ждем, пока мы не станем единственными

ftruncate($f,0);

//

очищаем все содержимое файла

Зачем мы используем fflush() перед тем, как разблокировать файл? Все очень просто: отключение блокировки не ведет к сбросу внутреннего файлово- го буфера, т. е. некоторые изменения могут быть "сброшены" в файл уже после того, как блокировка будет снята. Мы, разумеется, этого не хотим, вот и за- ставляем PHP принудительно записать все изменения на диск.

Устанавливайте исключительную блокировку, когда вы собираетесь изменять файл. Всегда используйте при этом режим открытия r, r+ или a+. Никогда не применяйте режим w. Снимайте блокировку так рано, как только сможете, и не забывайте перед этим вызвать fflush().

Разделяемая блокировка

Мы решили ровно половину нашей задачи. Действительно, теперь данные из нескольких процессов-писателей не будут перемешиваться, но как быть с читателями? А вдруг процесс-читатель захочет прочитать как раз из того места, куда пишет про- цесс-писатель? В этом случае он, очевидно, получит "половинчатые" данные. То есть, данные неверные. Как же быть?

Существуют два метода обхода этой проблемы. Первый — это использовать все ту же исключительную блокировку. Действительно, кто сказал, что исключительную

264

Часть IV. Стандартные функции PHP

блокировку можно применять только в процессах, изменяющих файл? Ведь функция flock() не знает, что будет выполнено с файлом, для которого она вызвана. Однако этот метод довольно-таки неудачен, и вот по какой причине. Представьте, что про- цессов-читателей много, а писателей — мало, и к тому же писатели еще и вызываются, скажем, раз в пару минут, а не постоянно, как читатели. В случае использования исключительной блокировки для процессов-читателей, довольно интенсивно обращающихся к файлу, мы очень скоро получим целый их рой, висящий, недовольно гудя, в очереди, пока очередному процессу разрешат читать. Но ведь никакой "аварии" не случится, если один и тот же файл будут читать и сразу все процессы этого роя, правда? Ведь чтение из файла его не изменяет. Итак, предоставив исключительную блокировку для читателей, мы потенциально получаем проблемы с производительностью, перерастающие в катастрофу, когда процессов-читателей становится больше некоторого определенного порога.

Второй (и лучший) способ подразумевает использование разделяемой блокировки. Процесс, который устанавливает этот вид блокировки, будет приостановлен только в одном случае: когда активен другой процесс, установивший исключительную блокировку. В нашем примере процессы-читатели будут "поставлены в очередь" только тогда, когда активизируется процесс-писатель. И это правильно. Посудите сами: зачем зажигать красный свет на перекрестке, если поперечного движения заведомо нет?

Теперь давайте посмотрим на разделяемую блокировку читателей с точки зрения процесса-писателя. Что он должен делать, если кто-то читает из файла, в который он как раз собирается записывать? Очевидно, он должен дождаться, пока читатель не закончит работу. Иными словами, вызов flock($f,LOCK_EX) обязан подождать, пока активна хотя бы одна разделяемая блокировка. Это и происходит в действительности.

Возможно, вам на ум пришла аналогия с перекрестком, по одной дороге кото- рого движется почти непрерывный поток машин, и поперечное движение при этом блокируется навсегда, — так что у водителей нет никаких шансов про- биться через сплошной поток. В реальном мире это действительно иногда про- исходит (потому-то любой светофор всегда представляет собой исключитель- ную блокировку), но только не в мире PHP. Дело в том, что, если почти всегда активна разделяемая блокировка, операционная система все равно так рас- пределяет кванты времени, что в некоторые из них можно "включить" исключи- тельную блокировку. То есть "поток машин" становится не сплошным, а с "про- белами" — ровно такого размера, чтобы в них могли "прошмыгнуть" машины, едущие в перпендикулярном направлении.

В листинге 15.3 представлена модель процесса, использующего разделяемую блокировку.

Листинг 15.3. Модель процесса с разделяемой блокировкой

<?

Глава 15. Работа с файлами

265

//инициализация

//. . .

$f=fopen($f,"r") or die("Не могу открыть файл на чтение!"); flock($f,LOCK_SH); // ждем, когда процессы-писатели угомонятся

//В этой точке мы можем быть уверены, что эта программа работает

//с файлом, когда ни одна другая программа в него не пишет

//. . .

flock($f,LOCK_UN); // говорим, что мы больше не будем работать с файлом fclose($f);

//Завершение

//. . .

?>

Устанавливайте разделяемую блокировку, когда вы собираетесь только читать из файла, не изменяя его. Всегда используйте при этом режим открытия r, и никакой другой. Снимайте блокировку так рано, как только сможете.

Блокировки с запретом "подвисания"

Как следует из описания функции flock(), к ее второму параметру можно прибавить константу LOCK_NB для того, чтобы функция не ожидала, когда программа может "двинуться в путь", а сразу же возвращала управление в основную программу. Это может пригодиться, если вы не хотите, чтобы ваш сценарий бесполезно простаивал, ожидая, пока ему разрешат обратиться к файлу. В эти моменты, возможно, лучше будет заняться какой-нибудь полезной работой — например, почистить временные файлы, память, или же просто сообщить пользователю, что файл заблокирован, чтобы он подождал и не думал, что программа зависла. Вот пример использования исключительной блокировки в совокупности с LOCK_NB:

$f=fopen("file.txt","a+"); while(!flock($f,LOCK_EX+LOCK_NB)) {

echo "Пытаемся получить доступ к файлу <br>"; sleep(1); // ждем 1 секунду

}

// Работаем

Эта программа основывается на том факте, что выход из flock() может произойти либо в результате отказа блокировки, либо после того, как блокировка будет установлена — но не до того! Таким образом, когда мы наконец-то дождемся разрешения

266

Часть IV. Стандартные функции PHP

доступа к файлу и произойдет выход из цикла while, мы уже будем иметь исключительную блокировку, закрепленную за нашим файлом.

Пример счетчика

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

Итак, нам нужен сценарий, который бы при каждом своем запуске увеличивал число, хранящееся в файле, и выводил его в браузер. Несложная, казалось бы, задача сильно осложняется тем, что при большой посещаемости сервера могут быть запущены сразу несколько процессов-счетчиков, которые попытаются обратиться к одному и тому же файлу. Если не принять мер, это приведет к тому, что счетчик рано или поздно "обнулится".

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

Листинг 15.4. Простейший текстовый счетчик

<? $f=fopen("counter.dat","a+");

flock($f,LOCK_EX);

// Говорим, что дальше будем работать только мы

$count=fread($f,100);

// Читаем значение, сохраненное в файле

@$count=$count+1;

// Увеличиваем его на 1 (пустая строка = 0)

ftruncate($f,0);

// Стираем файл

fwrite($f,$count);

// Записываем новое значение

fflush($f);

// Сбрасываем файловый буфер

flock($f,LOCK_UN);

// Отключаемся от блокировки

fclose($f);

// Закрываем файл

echo $count;

// Печатаем величину счетчика

?>

 

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

Глава 16

Работа с каталогами

Сточки зрения операционной системы каталоги — это те же самые файлы, только со специальным именем. То есть директорию можно представить себе как файл, в котором хранятся имена и местоположения других файлов и каталогов. Этим обеспечивается традиционная древовидность организации файловой системы в различных ОС.

Скаждым процессом (в частности, и с работающим сценарием) ассоциирован свой так называемый текущий каталог. Все действия по работе с файлами и каталогами осуществляются по умолчанию именно в ней. Например, если мы открываем файл, указав только его имя, PHP будет искать этот файл именно в текущем каталоге. Существуют также и функции, которые могут сделать текущим любой указанный каталог.

Манипулирование каталогами

Вот несколько полезных функций для работы с каталогами.

bool mkdir(string $name, int $perms)

Создает каталог с именем $name и правами доступа $perms. Права доступа для каталогов указываются точно так же, как и для файлов. Чаще всего значение $perms устанавливают равным 0770 (предваряющий ноль обязателен — он указывает PHP на то, что это — восьмеричная константа, а не десятичное число). Например:

mkdir("my_directory",0755); // создает подкаталог в текущем каталоге mkdir("/data"); // создает подкаталог data в корневом каталоге

В случае успеха функция возвращает true, иначе — false. Необходимо заметить, что пользователь не может создать подкаталог в родительском каталоге, права на запись в который у него отсутствуют. Здесь точно такая же ситуация, как и с файлами.

Вы, наверное, заметили, что атрибуты доступа 0770 означают "доступен для чтения, записи и исполнения для владельца и его группы". Что означает атри- бут исполнения, установленный для каталога? Может быть, он разрешает пользователям запускать из него программы? А вот и нет. Право на "исполне-

Глава 16. Работа с каталогами

269

ние" показывает, что пользователь сможет просмотреть содержимое катало- га. Конечно, все это специфично для операционных систем семейства Unix.

bool rmdir(string $name)

Удаляет каталог с именем $name. В случае успеха возвращает true, иначе — false. Как всегда, действуют стандартные ограничения файловой системы на эту операцию.

bool chdir(string $path)

Сменяет текущий каталог на указанный. Если такого каталога не существует, возвращает false. Параметр $path может определять и относительный путь, задающийся от текущего каталога. Вот несколько примеров:

chdir("/tmp/data");

// переходим по абсолютному пути

chdir("./somathing");

// переходим в подкаталог текущего каталога

chdir("something");

// то же самое

chdir("..");

// переходим в родительский каталог

chdir("~/data");

// переходим в /home/ПОЛЬЗОВАТЕЛЬ/data (для Unix)

string getcwd()

 

Возвращает полный путь к текущему каталогу, начиная от "корня" (/). Если такой путь не может быть отслежен (это иногда бывает в Unix из-за того, что права на чтение для родительских каталогов могут быть сняты), вызов "проваливается" и возвра-

щает false.

Эта функция появилась в PHP совсем недавно. Так что если ее не окажется в вашей версии, обновите ее поскорее, либо напишите заменитель (что не так-то просто).

Работа с записями

Дальше описываются функции, которые позволяют узнать, какие объекты находятся в указанном каталоге. Например, с их помощью можно вывести содержимое текущего каталога. Механизм работы этих функций базируется примерно на тех же принципах, что и применяемых для файловых операций: сначала интересующий каталог открывается, затем из него производится считывание записей, и под конец каталог нужно закрыть. Правила интуитивно понятны и, наверное, хорошо вам знакомы.

int opendir(string $path)

Открывает каталог $path для дальнейшего считывания из него информации о файлах и подкаталогах и возвращает его идентификатор. Дальнейшие вызовы readdir() с идентификатором в параметрах будут обращены именно к этому каталогу. Функция возвращает false, если произошла ошибка.

270

Часть IV. Стандартные функции PHP

string readdir(int $handle)

Считывает очередное имя файла или подкаталога из открытого ранее каталога с идентификатором $handle и возвращает его в виде строки. Порядок следования файлов в каталоге зависит от операционной системы — скорее всего, он будет совпадать с тем порядком, в котором эти файлы создавались, но не всегда. Вместе с именами подкаталогов и файлов будут также получены два специальных элемента: это . (ссылка на текущий каталог) и .. (ссылка на родительский каталог). В подавляющем большинстве случаев нам нужно их игнорировать, что и сделано в примере из листинга 16.1 при помощи инструкции continue.

PHP версии 3 позволял опускать параметр $handle в этом случае, кажется, подразумевался последний открытый каталог. Сценарий "собирался с силами", "вздыхал" икое-как работал. PHP версии 4 более строг: в нем вы обязатель- но должны указывать параметр $handle для функции readdir(), в против- ном случае вам гарантированы сюрпризы.

В случае, если в каталоге все файлы уже считаны, функция возвращает ложное значение. Но не позволяйте себе привыкнуть к конструкции такого вида:

$d=opendir("somewhere"); while($e=readdir($d)) { . . .}

Она заставит цикл прерваться в его середине в случае обнаружения файла с именем "0", чего нам бы, конечно, не хотелось. Вместо этого пользуйтесь следующим методом:

$d=opendir("somewhere"); while(($e=readdir($d))!==false) { . . .}

Оператор !== позволяет точно проверить, была ли возвращена величина false.

void closedir(int $handle)

Закрывает ранее открытый каталог с идентификатором $handle. Не возвращает ничего. В принципе, можно и не закрывать каталоги, т. к. это делается автоматически при завершении программы, но лучше все-таки такой легкостью не обольщаться.

void rewinddir(int $handle)

"Перематывает" внутренний указатель открытого каталога на начало. После этого можно воспользоваться readdir(), чтобы заново начать считывать содержимое каталога.

Пример: печать дерева каталогов

В заключение приведу пример программы, которая рекурсивно распечатывает список всех каталогов (доступных сценарию) в вашей системе, начиная от корневого.