flock
(PHP 4, PHP 5, PHP 7, PHP 8)
flock — 汎用のファイルロックを行う
説明
flock() を使うと、(ほとんどの Unix、そして Windows さえ含む) 事実上すべてのプラットフォームで使用可能な、簡易な読み手/書き手モデルを実現できます。
ロックの解放は、fclose() が実行されるか、
stream
がガベージコレクションされた段階で行われます。
PHP は、恣意的にファイルをロックする汎用の手段を提供します
(これは、アクセスする全プログラムが同一のロックの方法を使用する必要があり、
そうでない場合は動作しないことを意味します)。
デフォルトでは、要求したロックが確保されるまでこの関数はブロックします。
以下で説明する LOCK_NB
オプションでこの挙動を制御することができます。
パラメータ
stream
-
fopen() を使用して作成したファイルシステムポインタリソース。
operation
-
operation
は以下のいずれかとなります。-
共有ロック(読み手)とするには
LOCK_SH
をセットします。 -
排他的ロック(書き手)とするには
LOCK_EX
をセットします。 -
(共有または排他的)ロックを開放するには
LOCK_UN
をセットします。
ロックを試みている間に flock() がブロックすべきでない場合は、上の操作のいずれかに
LOCK_NB
をビットマスクとして追加できます。 -
共有ロック(読み手)とするには
would_block
-
ロックがブロックされた (errno が EWOULDBLOCK となった) 場合に、オプションの 3 番目の引数に 1 が設定されます。
例
例1 flock() の例
<?php
$fp = fopen("/tmp/lock.txt", "r+");
if (flock($fp, LOCK_EX)) { // 排他ロックを確保します
ftruncate($fp, 0); // ファイルを切り詰めます
fwrite($fp, "ここで何かを書きます\n");
fflush($fp); // 出力をフラッシュしてからロックを解放します
flock($fp, LOCK_UN); // ロックを解放します
} else {
echo "ファイルを取得できません!";
}
fclose($fp);
?>
例2 flock() で LOCK_NB
オプションを使う例
<?php
$fp = fopen('/tmp/lock.txt', 'r+');
/* LOCK_NB オプションを LOCK_EX で有効にします */
if(!flock($fp, LOCK_EX | LOCK_NB)) {
echo 'Unable to obtain lock';
exit(-1);
}
/* ... */
fclose($fp);
?>
注意
注意:
flock() は、Windows 上ではアドバイザリロックではなく 強制ロックを使います。強制ロックは Linux や System V 系の OS でもサポートされています。 これは、そのファイルに setgid パーミッションが設定されていて グループの実行ビットがクリアされている場合に fcntl() システムコールが通常サポートしている方式です。 Linux では、これを行うには mand オプションつきでファイルシステムをマウントしておく必要があります。
注意:
flock()は、ファイルポインタを必要とするため、 (fopen()へ引数"w"または"w+"を指定して)書き込 みモードでオープンすることにより丸めるファイルにアクセス保護する 特別なロックファイルを使用する必要があるかもしれません。
注意:
fopen() が返すローカルファイルへのポインタ、あるいは streamWrapper::stream_lock() メソッドを実装した ユーザー空間のストリームを指すファイルポインタに対してのみ使うことができます。
一連のコードで別の値を stream
引数に代入すると、
それ以降のコードでロックを解放します。
いくつかのオーペレーティングシステムでflock() はプロセスレベルで実装されています。マルチスレッド 型のサーバーAPIを使用している場合、同じサーバーインスタンスの並 列スレッドで実行されている他のPHPスクリプトに対してファイルを保 護する際に flock()を使用することはできません!
flock()はFAT
のような
旧式のファイルシステムではサポートされていないため、
これらの環境の場合は常にfalse
を返すことになります。
注意:
Windows では、 ロックするプロセスが同じファイルを二回オープンする場合、 ファイルをアンロックするまで二番目のハンドルではアクセスできません。
User Contributed Notes 39 notes
The supplied documentation is vague, ambiguous and lacking, and the user comments contain erroneous information! The flock function follows the semantics of the Unix system call bearing the same name. Flock utilizes ADVISORY locking only; that is, other processes may ignore the lock completely; it only affects those that call the flock call.
LOCK_SH means SHARED LOCK. Any number of processes MAY HAVE A SHARED LOCK simultaneously. It is commonly called a reader lock.
LOCK_EX means EXCLUSIVE LOCK. Only a single process may possess an exclusive lock to a given file at a time.
If the file has been LOCKED with LOCK_SH in another process, flock with LOCK_SH will SUCCEED. flock with LOCK_EX will BLOCK UNTIL ALL READER LOCKS HAVE BEEN RELEASED.
If the file has been locked with LOCK_EX in another process, the CALL WILL BLOCK UNTIL ALL OTHER LOCKS have been released.
If however, you call flock on a file on which you possess the lock, it will try to change it. So: flock(LOCK_EX) followed by flock(LOCK_SH) will get you a SHARED lock, not "read-write" lock.
Note that Example #1 contains a bug: ftruncate() does *not* re-set the file pointer to the beginning of the file. You need to execute a call to rewind() afterward. I realize that the ftruncate page does mention this, but if anybody copies the example above (as I did), their program will not work correctly unless they fix this.
Further information on flock: The system is not restarted if a signal is delivered to the process, so flock will happily return false in case of SIGALRM, SIGFPE or something else.
When a file is closed the lock will be released by the system anyway, even if PHP doesn't do it explicitly anymore since 5.3.2 (the documentation is very confusing about this).
However, I had a situation on an apache/PHP server where an out-of-memory error in PHP caused file handles to not be closed and therefore the locks where kept even thought PHP execution had ended and the process had returned to apache to serve other requests. The lock was kept alive until apache recycled those processes.
This lack of proper clean up basically makes flock() completely unreliable.
Simple Helper for lock files creation
<?php
class FileLocker {
protected static $loc_files = array();
public static function lockFile($file_name, $wait = false) {
$loc_file = fopen($file_name, 'c');
if ( !$loc_file ) {
throw new \Exception('Can\'t create lock file!');
}
if ( $wait ) {
$lock = flock($loc_file, LOCK_EX);
} else {
$lock = flock($loc_file, LOCK_EX | LOCK_NB);
}
if ( $lock ) {
self::$loc_files[$file_name] = $loc_file;
fprintf($loc_file, "%s\n", getmypid());
return $loc_file;
} else if ( $wait ) {
throw new \Exception('Can\'t lock file!');
} else {
return false;
}
}
public static function unlockFile($file_name) {
fclose(self::$loc_files[$file_name]);
@unlink($file_name);
unset(self::$loc_files[$file_name]);
}
}
if ( !FileLocker::lockFile('/tmp/1.lock') ) {
echo "Can't lock file\n";
die();
}
sleep(10);
FileLocker::unlockFile('/tmp/1.lock');
echo "All Ok\n";
Regarding the change in PHP 5.3.2 with locked files:
Without having studied the PHP source code in detail, the situation appears to be as follows when the PHP function fclose() is called:
Before 5.3.2 PHP would check if the file was locked, then release the lock, and then close the file.
From 5.3.2 PHP just closes the file.
But note, that the operating system releases the lock automatically when the file is closed. Therefore a call to fclose() STILL releases the lock (this is tested with PHP 5.3.2, Linux, x64).
When writing to a file, you should avoid using w+ because it would erase the contents of the file before locking
If you need to write the complete file again you could use the following instead:
<?php
$fp = fopen('yourfile.txt', 'a') ;
if (flock($fp, LOCK_EX)) {
ftruncate($fp, 0) ; // <-- this will erase the contents such as 'w+'
fputs($fp, 'test string') ;
flock($fp, LOCK_UN) ;
}
fclose($fp) ;
?>
Best,
Fernando Gabrieli
I just spent some time (again) to understand why a reading with file_get_contents() and file was returning me an empty string "" or array() whereas the file was existing and the contents not empty.
In fact, i was locking file when writing it (file_put_contents third arg) but not testing if file was locked when reading it (and the file was accessed a lot).
So, please pay attention that file_get_contents(), file() and maybe others php files functions are going to return empty data like if the contents of the file was an empty string.
To avoid this problem, you have to set a LOCK_SH on your file before reading it (and then waiting if locked).
Something like this :
<?php
public static function getContents($path, $waitIfLocked = true) {
if(!file_exists($path)) {
throw new Exception('File "'.$path.'" does not exists');
}
else {
$fo = fopen($path, 'r');
$locked = flock($fo, LOCK_SH, $waitIfLocked);
if(!$locked) {
return false;
}
else {
$cts = file_get_contents($path);
flock($fo, LOCK_UN);
fclose($fo);
return $cts;
}
}
}
?>
Code to test by yourself :
abc.txt :
someText
file.php :
<?php
$fo = fopen('abc.txt', 'r+');
flock($fo, LOCK_EX);
sleep(10);
flock($fo, LOCK_UN);
?>
file2.php :
<?php
var_dump(file_get_contents('abc.txt'));
var_dump(file('abc.txt'));
?>
Then launch file.php and switch to file2.php during the 10 seconds and see the difference before/after
Actually, there is no use of the while loop with the usleep. My testing has revealed the following:
<?php
//some code here
flock($file_handle, LOCK_EX) // <- Your code will pause here untill you get the lock for indefinite amount of time or till your script times out
//some code here
?>
This will actually check for the lock without pausing and then it will sleep:
<?php
//code here
while (!flock($file_handle, LOCK_EX | LOCK_NB)) {
//Lock not acquired, try again in:
usleep(round(rand(0, 100)*1000)) //0-100 miliseconds
}
//lock acquired
//rest of the code
?>
The problem is, if you have a busy site and a lots of locking, the while loop may not acquire the lock for some time. Locking without LOCK_NB is much more persistent and it will wait for the lock for as long as it takes. It is almose guaranteed that the file will be locked, unless the script times out or something.
Consider these two scripts: 1st one is ran, and the second one is ran 5 seconds after the first.
<?php
//1st script
$file_handle = fopen('file.test', 'r+');
flock($file_handle, LOCK_EX); //lock the file
sleep(10); //sleep 10 seconds
fclose($file_handle); //close and unlock the file
?>
<?php
//2nd script
$file_handle = fopen('file.test', 'r+');
flock($file_handle, LOCK_EX); //lock the file
fclose($file_handle); //close and unlock the file
?>
If you run 1st and then the 2nd script,the 2nd script will wait untill the 1st has finished. As soon as the first script finishes, the second one will acquire the lock and finish the execution. If you use flock($file_handle, LOCK_EX | LOCK_NB) in the 2nd script while the 1st script is running, it would finish execution immediately and you would not get the lock.
you can wrap a lock as an object to make it a scope-based lock. when the lock object is no longer referenced, like when it's unset or the owner returns, the destructor will call unlock.
this way you can just create a lock object and forget about it.
<?php
class lock {
private $handle;
public static function read ( $handle ) {
$lock = new static();
$lock->handle = $handle;
return flock($handle,LOCK_SH) ? $lock : false;
}
public static function write ( $handle ) {
$lock = new static();
$lock->handle = $handle;
return flock($handle,LOCK_EX) ? $lock : false;
}
public function __destruct ( ) {
flock($this->handle,LOCK_UN);
}
}
?>
I have found that if you open a currently locked file with 'w' or 'w+' ("file pointer at the beginning of the file and truncate the file to zero length") then it will not truncate the file when the lock is released and the file available.
Example I used to test it:
<?php
// a.php
$fp = fopen( "/tmp/lock.txt", "w+" );
flock( $fp, LOCK_EX ); // exclusive lock
$steps = 10;
// write to the file
for ($i=0; $i< $steps; $i++) {
fwrite($fp, 'a '.time().' test '. $i."\n");
sleep(1);
}
flock( $fp, LOCK_UN ); // release the lock
fclose( $fp );
?>
----------
<?php
// b.php
$fp = fopen( "/tmp/lock.txt", "w+" );
flock( $fp, LOCK_EX ); // exclusive lock
// ftruncate($fp, 0) is needed here! <----
$steps = 5;
// write to the file
for ($i=0; $i< $steps; $i++) {
fwrite($fp, 'b '.time().' test '. $i."\n");
sleep(1);
}
flock( $fp, LOCK_UN ); // release the lock
fclose( $fp );
?>
Loading a.php then loading b.php right after will result in:
b 1054075769 test 0
b 1054075770 test 1
b 1054075771 test 2
b 1054075772 test 3
b 1054075773 test 4
a 1054075764 test 5
a 1054075765 test 6
a 1054075766 test 7
a 1054075767 test 8
a 1054075768 test 9
As you can see, b.php does not truncate the file as the w+ would suggest if the file were instantly available. But only moves the pointer to the begining of the file. If b.php was loaded after a.php finished then there would be no "a ..." lines in the file, since it would be truncated.
To fix this you have to add ftruncate($fp, 0) right after the flock.
'r+' and 'a' seem to work fine, though.
I just want to add a note about making atomic lock on NFS, there is only two
ways:
- 1 (the most robust but the most complicate) - It's to use link() to create a
hard link to a file you want to lock (on the same FS of course).
(On most NFS implementations, Link() is atomic)
Once you created a hard link (not a symbolic link), with a unique randomly
generated name, call stat() on it and count the number of link (nlink), if there
is only 2 then the file is locked.
If there is more than two you have to unlink() the link you just created and
create a new one with a new unique name (else NFS will use its cache and stat
will return wrong data) then call stat() on the new link and test the number of
links again, repeat this operation until you get the lock.
You have to use usleep() between the link() attempts with a fixed + random
sleep value to avoid dead lock situations (link() and unlink() may be atomic
but not instantaneous)
Also note than when you unlink a file through NFS, if NFS think that the file
is still in use, it will create a .nfs link to this file until it realizes the
file is no longer in use... A wrong timing could generate thousands of those
files and a deadlock situation. Because of this when a deadlock situation
occurs or if your stat() command returns a very high number of links, you have
to look for .nfs file in the same directory you created your links and unlink
all the .nfs file you find (sometimes NFS take its time to remove them)
- 2 (the simplest) - the second method is to use a lock server and lock daemons
on each client that will forward lock request to the server... (this is more
dangerous than the first method because the daemons may be killed...)
Here is for reference the function I created to make atomic locks through NFS
(this function is in production since at least 4 years now), it's just for
reference because it uses many external functions to do its job but you can see
the principle:
http://pastey.net/85793
I'm thinking that a good way to ensure that no data is lost would be to create a buffer directory that could store the instructions for what is to be written to a file, then whenever the file is decidedly unlocked, a single execution could loop through every file in that directory and apply the indicated changes to the file.
I'm working on writing this for a flat-file based database. The way it works is, whenever a command is issued (addline, removeline, editline), the command is stored in a flat file stored in a folder named a shortened version of the filename to be edited and named by the time and a random number. In that file is a standardized set of commands that define what is to be done to what file (the likes of "file: SecuraLog/index_uid" new line "editline: 14").
Each execution will check every folder in that directory for files and a certain amount of time (I don't know how long, maybe 1-2 seconds) is spent making pending changes to unlocked files. This way no changes will be lost (i.e. person 1 makes a change at the same time as person 2, and person 1 loses the race by just enough to have their changed version of the file overwritten by person 2's version) and there will be no problems with opening an empty open file.
And here's the timeout template for UNIX:
<?php
pcntl_signal(SIGALRM, function() {});
pcntl_alarm(3);
try {
if (!flock($handle, LOCK_EX)) {
throw new \Exception("Timeout");
}
} finally {
pcntl_alarm(0);
pcntl_signal_dispatch();
pcntl_signal(SIGALRM, SIG_DFL);
}
?>