🙆

syncコマンドのデータ同期タイミングとI/Oエラー検出

2024/08/25に公開

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の後に、それぞれのファイルシステムに対応するファイル(たとえばマウントポイント)をすべて指定する必要がありますので注意してください。

ソースを確認

coreutilssyncのソースは短いので、これまでに書いたことが本当か、実際に確認してみましょう。以下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においてoktrueなので、I/Oエラーが発生していようといなかろうと、常にEXIT_SUCCESS(0)を返します。

-fオプションを付けた場合、★3を通ってarg_file_systemtrueになります。この後に★4を通ってmodeMODE_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エラーが発生した可能性があることはわかります。

おわりに

データの同期は奥が深いですね。本記事では紹介しきれなかったデータ同期用システムコールfsyncfdatasyncも面白いので、気になるかたはmanページを見てみてください。

Discussion