syncコマンドのデータ同期タイミングとI/Oエラー検出
sync
コマンドはストレージデバイスへのI/Oをキャッシュしているページキャッシュとバッファキャッシュのデータをデバイスに同期(以下「同期」と記載)します。このコマンドについては「sync
は同期処理を開始するだけで完了を待たずに終了する」「sync
を3回実行すると同期が完了する」などという話が有名です。本記事ではLinux環境においてsync
コマンドの終了時に同期がされるのかについて述べます。sync
コマンドはGNU coreutils
が提供するものを対象とします。
本記事ではsync
コマンドによる同期のタイミングに加えてもう一つ、同期処理の実行中にI/Oエラーが発生した場合に、エラーを検出できるのかについても述べます。
まとめ
-
sync
コマンド実行後に同期は完了している。ただし仕様上はそうなると保証されていない。 - 同期中のディスクI/Oエラーは
-f
オプションを付ければ検出できる。
調査環境
- Linuxディストリビューション: Ubuntu 20.04
- Linuxカーネル: 5.15.0-119-generic
- GNU coreutils: 8.32
仕様を確認
まずsync
コマンドのmanページ(man 1 sync
)の中から同期タイミングについて述べられている箇所を抜粋します。
Persistence guarantees vary per system. See the system calls below for more details.
意訳すると「システムによって同期を保証するかどうかが違う。それぞれのシステムのシステムコールに依存する」と書いています。ここでいう「システム」とはLinuxかFreeBSDかSolarisかあるは…というOSの違いだと思ってください。GNU coreutilsはLinux以外にも大量のシステム上で動作するのです。
後述するようにsync
コマンドをオプション無しで実行すると、Cライブラリ(glibc)のsync
ライブラリ関数を呼び出し、その中でsync
システムコールを呼び出します。ライブラリ関数の仕様については、manpages-posix-dev
パッケージをインストールすると、Linuxがおおむね満たすことを目指しているPOSIXの仕様(man 3p sync
)を見られます。以下、関係する記述を抜粋します。
The writing, although scheduled, is not necessarily complete upon return from sync().
意訳すると「sync
ライブラリ関数を呼び出すと、同期処理をスケジュールはするが、完了を待つ必要はない」と書いています。
ではLinuxにおいてsync
ライブラリ関数の中で呼ばれるsync
システムコールの仕様を確認してみましょう。以下man 2 sync
からの抜粋です。
According to the standard specification (e.g., POSIX.1-2001), sync() schedules the writes,
but may return before the actual writing is done.
However Linux waits for I/O completions, and thus sync() or syncfs() provide
the same guarantees as fsync() called on every file in the system or filesystem respectively.
意訳すると「POSIXにはsync
は同期を待たなくてよいと書いているが、Linuxは待っている」です。つまり「仕様上はsync
呼び出し後に同期されていることを保証しないが、呼び出し後に同期は終わっているという実装になっている」ということです。つまり他のOSはさておき、Linuxではsync
コマンド実行後に同期は完了している、ということがわかりました。
I/Oエラーの検出
sync
コマンドはデフォルトでは動作中にI/Oエラーが発生しても、エラーが発生したことを呼び出し側に通知しません。というよりも、通知できません。なぜかというとsync
ライブラリ関数およびsync
システムコールは戻り値が無い(void
を返す)からです。検出しても防ぎようがありません。知りたければ別途dmesg
を実行するなりしてカーネルのエラーログにI/Oエラーの発生を示すものが存在するかどうかを確認しなければいけません。
前の段落で「デフォルトでは」と書いたのがポイントです。sync
コマンドは-f
オプションを付ければ同期処理中にI/Oエラーが起きたかどうかを検出可能です。-f
オプションを付ける場合はオプションの後に1つ以上のファイル名を指定します。そうすると、引数で指定したファイルが存在するファイルシステムのデータを同期します。このとき呼び出すシステムコールはsync
ではなくsyncfs
です。
syncfs
システムコールの定義は以下の通りです(man 2 syncfs
より抜粋)。
int syncfs(int fd);
...
syncfs() is like sync(), but synchronizes just the filesystem containing file referred to by the open file descriptor fd.
syncfs
システムコールはfd
で示されるファイルが存在するファイルシステムのデータを同期した後に、int
型の戻り値を返します。戻り値がEIO
の場合は同期中にI/Oエラーが発生したといえます。
syncfs
システムコールは同期対象がファイルシステムなので、全ファイルシステムのデータを同期するためには、sync -f
の後に、それぞれのファイルシステムに対応するファイル(たとえばマウントポイント)をすべて指定する必要がありますので注意してください。
ソースを確認
coreutils
のsync
のソースは短いので、これまでに書いたことが本当か、実際に確認してみましょう。以下v8.32のソースのmain()
関数を引用します。重要なところに★を付けています。
int
main (int argc, char **argv)
{
int c;
bool args_specified;
bool arg_data = false, arg_file_system = false;
enum sync_mode mode; ★1
bool ok = true; ★2
initialize_main (&argc, &argv);
set_program_name (argv[0]);
setlocale (LC_ALL, "");
bindtextdomain (PACKAGE, LOCALEDIR);
textdomain (PACKAGE);
atexit (close_stdout);
while ((c = getopt_long (argc, argv, "df", long_options, NULL))
!= -1)
{
switch (c)
{
case 'd':
arg_data = true;
break;
case 'f':
arg_file_system = true; ★3
break;
case_GETOPT_HELP_CHAR;
case_GETOPT_VERSION_CHAR (PROGRAM_NAME, AUTHORS);
default:
usage (EXIT_FAILURE);
}
}
args_specified = optind < argc;
if (arg_data && arg_file_system)
{
die (EXIT_FAILURE, 0,
_("cannot specify both --data and --file-system"));
}
if (!args_specified && arg_data)
die (EXIT_FAILURE, 0, _("--data needs at least one argument"));
if (! args_specified || (arg_file_system && ! HAVE_SYNCFS))
mode = MODE_SYNC;
else if (arg_file_system)
mode = MODE_FILE_SYSTEM; ★4
else if (! arg_data)
mode = MODE_FILE;
else
mode = MODE_DATA;
if (mode == MODE_SYNC)
sync (); ★5
else
{
for (; optind < argc; optind++)
ok &= sync_arg (mode, argv[optind]); ★6
}
return ok ? EXIT_SUCCESS : EXIT_FAILURE; ★7
}
★1で定義しているsync_mode
は動作モードを表します。ここで重要なのはデフォルトのMODE_SYNC
と、-f
オプションを指定した場合のMODE_FILE_SYSTEM
です。★2で定義しているok
は同期が成功したかどうかを示します。デフォルト値は成功を示すtrue
です。
MODE_SYNC
の場合は、★5のルートを通ってsync
ライブラリ関数を呼び出します。内部ではsync
システムコールを呼び出します。sync
ライブラリ関数には戻り値が無いのでmain
関数から復帰する★7においてok
はtrue
なので、I/Oエラーが発生していようといなかろうと、常にEXIT_SUCCESS
(0)を返します。
-f
オプションを付けた場合、★3を通ってarg_file_system
がtrue
になります。この後に★4を通ってmode
がMODE_FILE_SYSTEM
になります。さらにその後、★6でコマンドライン引数で指定したファイルごとにsync_arg
関数を呼び出します。
以下sync_arg
関数の定義から本記事に関係のある部分を抜粋します。
static bool
sync_arg (enum sync_mode mode, char const *file)
{
bool ret = true;
...
if (ret == true)
{
int sync_status = -1;
switch (mode)
...
#if HAVE_SYNCFS
case MODE_FILE_SYSTEM:
sync_status = syncfs (fd); ★8
break;
#endif
...
if (sync_status < 0)
{
error (0, errno, _("error syncing %s"), quoteaf (file));
ret = false; ★9
}
}
...
return ret; ★10
}
MODE_FILE_SYSTEM
の場合は★8を通り、syncfs
ライブラリ関数を呼び、内部でsyncfs
システムコールを呼び出しています。syncfs
システムコールの戻り値が0以外、つまり失敗した場合は★9でret
がfalseになり、★10でsync_arg
関数自体の戻り値がfalse
になります。この場合main
関数の終了時に★7でEXIT_FAILURE
(0以外の値)を返し、異常終了します。
syncfs
システムコールとは違いsync -f
コマンドの場合は失敗しても何故失敗したのかはわかりませんが、少なくとも失敗した場合にI/Oエラーが発生した可能性があることはわかります。
おわりに
データの同期は奥が深いですね。本記事では紹介しきれなかったデータ同期用システムコールfsync
やfdatasync
も面白いので、気になるかたはmanページを見てみてください。
Discussion