PHP8.1でついにファイルへの書き込みを強制する関数 fsync() が追加されました。
『いや、fwrite()があるよね?』と思った人は、半分正解で半分不正解。
ファイルの同期については、プログラムからどうやってファイルが操作されるのかの知識が必要です。
1行ちがうだけで結果も同じ。意味あるのか? ファイル書き込み処理
まずは、サンプルプログラムを動かしてみましょうか?
従来のファイル書き込み処理も入れてちがいを見てみましょう。
// ファイル同期を使う
$file = 'test1.txt';
$stream = fopen($file, 'w');
fwrite($stream, 'test data');
fwrite($stream, "\n");
fsync($stream);
fclose($stream);
// ファイル同期の代わりにファイル・ストリームのフラッシュを使う
$file = 'test2.txt';
$stream = fopen($file, 'w');
fwrite($stream, 'test data');
fwrite($stream, "\n");
fflush($stream);
fclose($stream);
// ファイルオープン・書き込み・クローズのみ
$file = 'test3.txt';
$stream = fopen($file, 'w');
fwrite($stream, 'test data');
fwrite($stream, "\n");
fclose($stream);
ls -ltr
-rw-rw-r--. 1 user user 10 Aug 8 20:35 test1.txt
-rw-rw-r--. 1 user user 10 Aug 8 20:35 test3.txt
-rw-rw-r--. 1 user user 10 Aug 8 20:35 test2.txt
結果を見ると、すべて同じファイル書き込み処理をしているようにしか見えません。
『ファイル操作にちがいが見えないんだから、fsync() っている?』と思うでしょう。
また、fflush() もいる? と思ったかもしれませんが、結果は同じでも処理はまったくの別物です。
たった1行のちがいでも。
また、fflush() を使うと、あとに実行されたファイル書き込みだけの処理よりも遅くファイル作成されているのも気になりますね?
これは、fflush(), fsync() の特性が影響しています。
ファイル操作の道順
プログラミングではかんたんにファイル書き込み・読み込みをしているように見えますが、それまでには3ステップの処理が動いています。
プログラムからファイル操作の命令があると、ファイルの読み書きをするまでの間に、ファイル・ストリームというバッファが存在します。
バッファとはメモリ上にデータを貯める倉庫みたいなもの。
プログラムのファイルデータは、倉庫に預けたり倉庫から引き取ったりしているだけで、直接、ストレージ上のファイルとやり取りしているわけではありません。
またファイルは、プログラムと直接やりとりしていません。ファイルは倉庫のストリームと行っています。
なんか社会経済の流通システムと似ていますね?
ファイル・ストリームとは流通業でいう卸売業者です。その卸売業者に何かを命令するのが fflush() やPHP8.1 で追加された fsync() です。
倉庫へデータを預けてるだけの fwrite()
プログラムのファイル操作では、倉庫のファイル・ストリームとのデータやり取りをしているだけで、ストリームとストレージ上のファイルのやりとりは関知しません。
つまり、fwrite() はファイルへ書き込むように思われますが、じっさいは、書き込みデータを倉庫(ストリーム)に預けてるだけです。
fwrite() の戻り値の true, false は、ストリームへのデータお預けが成功したかを見ているだけで、じっさいにファイルへ書き込んだかは見ていません。
『それでいいの?』と思うかもしれませんが、倉庫に預けたデータはOS のシステム・コールで確実にストレージへ反映されるようになってるので、プログラム上はそれでOKとすることが多いです。
(OSシステムに高負荷がかかったときなど、まれに失敗することがあるが。)
fwrite() では、実際のファイルへの書き込みはOSシステムの仕事。
また、ストリームがじっさいにファイルへ書き込むタイミングもOSシステムの範疇です。
プログラム上の fclose() でストリーム操作を終了するのをきっかけとして、OSシステムがファイルへの書き込みのタイミングを見計らって処理をします。
ストリームとストレージ上のファイルとのやりとりをプログラムが関知してない分、処理は一番速いです。
ファイル書き込みまでの処理 | fwrite | fflush | fsync | |
---|---|---|---|---|
① | ストリームへ書き出し | ○ | - | - |
PHP公式ドキュメント
fopen(), fclose() はファイル・ストリームへのアクセス開始・終了の合図
さっきも少し触れましたが、メモリ上のバッファであるファイル・ストリームへのアクセス開始は fopen() で行い、終了は fclose() で行います。
fopen() の戻り値がそのメモリへアクセスする変数になるんですが、それをファイルポインタやストリームと呼ぶ。
これを見ても、プログラムはストレージ上のファイルへはアクセスしていないことが分かる。
ストリームに繋いで、編集して、閉じる、なので、ストリームだけにアクセスしてるのが一目瞭然ですね?
倉庫のデータの吐き出しを明示する fflush()
flush は、『流れる』『(水などを)勢いよく流す』などの意味で、プログラミングでは、溜まったデータを吐き出す意味があります。
fflush() は、ファイル・ストリームに溜まったデータを吐き出す命令のこと。
ファイルへの書き込みでは、これをきっかけにストレージ上のファイルへ書き込みが始まります。
fwrite() では関知していない、ファイルへ書き出すタイミングをプログラミングで調整できる。
ただしfflush() は、メモリ上の倉庫を空っぽにできたかを見ているだけで、吐き出したデータがじっさいにファイルへ書き込まれたかどうかまでは見ていません。
荷物を積んだトラックが倉庫を出発したところまでは確認して、きちんと店舗に搬入されたかまで見ていないんですね?
ファイル書き込みまでの処理 | fwrite | fflush | fsync | |
---|---|---|---|---|
① | ストリームへ書き出し | ○ | - | - |
② | ストリームからストレージへの書き出し命令 | ✗ | ○ | ○ |
fflush() はストリームを空っぽにする命令なので、事前に fwirte() でストリームへデータを書き込まないといけない。
サンプルプログラムで fwrite() -> fflush() の順で書いているのはそういうこと。
PHP公式ドキュメント
倉庫からの搬出から店舗の搬入まで確認する fsync()
一方、PHP8.1 で追加された fsync() は、fflush() よりももっと先まで見ています。
fflush() は倉庫からトラックが出発したところまでしか見てませんが、fsync() はそのトラックが店舗に着いて、荷物が搬入されたところまで見ています。
つまりfsync() は、ストレージ上のファイルにじっさいに書き込みが行われ完了するところまで関知しています。
fsync() の戻り値の true, false は、ストレージ上のファイルに処理内容が反映されたかどうかを示すので、fwrite(), fflush() の戻り値よりも確実です。
ファイル書き込みまでの処理 | fwrite | fflush | fsync | |
---|---|---|---|---|
① | ストリームへ書き出し | ○ | - | - |
② | ストリームからストレージへの書き出し命令 | ✗ | ○ | ○ |
③ | ストレージへの書き出し(ファイル作成・更新) | ✗ | ✗ | ○ |
fsync() の処理が一番遅い。
でも、fwrite() がOSシステムに任せて早めに次の処理をするのに対し、最後まで見ているfsync() は、次の処理の開始が待たされます。つまり、その分だけ処理が遅い。
処理が速い順に、fwrite()のみ -> fflush() -> fsync()になる。
OSシステムに任せられるところは任せて処理を速くするか、確実にファイル操作ができる代わりに若干、時間がかかるのを取るか? という使い分けをします。
fsync は『ファイルの同期』ですが、その意味は分かりますよね?
ファイル操作の最後まで見ているよ? ということ。
ファイル操作を細かく指定できるようになったPHP8.1
fsync() という名の関数はPHPでは初ですが、C言語では昔からあるもので、ファイル操作関数としてはとても有名です。
また、fsync() とセットで fdatasync() という関数もあります。かんたんにいうと、fsync() の一部のデータの同期をやめて処理の時間短縮を図ったもの。
PHPの公式ドキュメントでは、ファイル・ストリーム(バッファ)とか、プログラム -> ストリーム -> ストレージ の流れについて触れてませんが、C言語のマニュアルではもっとくわしく書かれています。
(といっても文章なのでイメージしづらいが。)
man fsync
またストリームのバッファは、書き込みだけに使うものじゃありません。読み込んだときもバッファに貯めて、プログラムはそのメモリからデータを読みます。
ファイル操作では必ずプログラムとストレージの間にバッファを挟むと覚えましょう。
これはソケット通信などでも同じです。
OSシステム・コールを介在しデータを扱うものは、ストリームを使うと覚えてもいい。
fsync(), fdatasync() は fwrite()のあとに使うもので、ファイル読み込みには使わない。
読むだけではファイルを同期する必要がないから。
同期が必要なのは内容を変更したとき。