22

どうやらPOSIXは次のように述べています

ファイル記述子またはストリームは、それが参照する開いているファイルの説明では「ハンドル」と呼ばれます。開いているファイルの説明には、複数のハンドルが含まれる場合があります。[…]最初のハンドルのファイルオフセットに影響を与えるアプリケーションによるすべてのアクティビティは、それが再びアクティブなファイルハンドルになるまで中断されます。[…]これらのルールを適用するために、ハンドルが同じプロセスにある必要はありません。--POSIX.1-2008 _

2つのスレッドがそれぞれ[write()関数]を呼び出す場合、各呼び出しは、他の呼び出しの指定された効果をすべて見るか、まったく見ないかのどちらかです。--POSIX.1-2008 _

これについての私の理解は、最初のプロセスがaを発行し write(handle, data1, size1)、2番目のプロセスが発行する write(handle, data2, size2)場合、書き込みは任意の順序で発生する可能性がありますが、data1とは元の状態で連続しているdata2 必要があります。

しかし、次のコードを実行すると、予期しない結果が得られます。

#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/wait.h>
die(char *s)
{
  perror(s);
  abort();
}

main()
{
  unsigned char buffer[3];
  char *filename = "/tmp/atomic-write.log";
  int fd, i, j;
  pid_t pid;
  unlink(filename);
  /* XXX Adding O_APPEND to the flags cures it. Why? */
  fd = open(filename, O_CREAT|O_WRONLY/*|O_APPEND*/, 0644);
  if (fd < 0)
    die("open failed");
  for (i = 0; i < 10; i++) {
    pid = fork();
    if (pid < 0)
      die("fork failed");
    else if (! pid) {
      j = 3 + i % (sizeof(buffer) - 2);
      memset(buffer, i % 26 + 'A', sizeof(buffer));
      buffer[0] = '-';
      buffer[j - 1] = '\n';
      for (i = 0; i < 1000; i++)
        if (write(fd, buffer, j) != j)
          die("write failed");
      exit(0);
    }
  }
  while (wait(NULL) != -1)
    /* NOOP */;
  exit(0);
}

LinuxとMacOSX 10.7.4でこれを実行してみましたgrep -a '^[^-]\|^..*-' /tmp/atomic-write.logが、一部の書き込みが連続していないか、重複していない(Linux)か、単純に破損している(Mac OS X)ことを示しています。

呼び出しにフラグO_APPENDを追加すると、open(2)この問題が修正されます。いいですが、理由がわかりません。POSIXは言う

O_APPEND設定されている場合、ファイルオフセットは、各書き込みの前にファイルの最後に設定されます。

しかし、これはここでは問題ではありません。私のサンプルプログラムは lseek(2)、同じファイル記述を共有するだけで、同じファイルオフセットを共有します。

Stackoverflowで同様の質問をすでに読んだことがありますが、それでも私の質問に完全には答えていません。

2つのプロセスからのファイルへのアトミック書き込みは、プロセスが(同じファイルではなく)同じファイル記述を共有する場合に特に対処していません 。

「書き込み」システムコールが特定のファイルでアトミックであるかどうかをプログラムでどのように判断しますか?と言う

POSIXで定義されているwrite呼び出しには、アトミック性の保証はまったくありません。

しかし、上で引用したように、それはいくつかあります。さらに、 このアトミック性のO_APPEND保証は、なしでも存在するはずだと私には思えますが、このアトミック性の保証をトリガーするようですO_APPEND

この動作をさらに説明できますか?

4

4 に答える 4

15

man 2 write私のシステムではそれをうまくまとめています:

すべてのファイルシステムがPOSIXに準拠しているわけではないことに注意してください。

メーリングリストでの最近の議論からの引用は次のとおりです。ext4

現在、同時読み取り/書き込みは個々のページに対してのみアトミックですが、システムコールには含まれていません。これによりread()、いくつかの異なる書き込みから混合されたデータが返される可能性がありますが、これは適切なアプローチではないと思います。これを行うアプリケーションは壊れていると主張するかもしれませんが、実際には、これはファイルシステムレベルで、重大なパフォーマンスの問題なしに簡単に実行できるものであるため、一貫性を保つことができます。また、POSIXはこれについても言及しており、XFSファイルシステムにはすでにこの機能があります。

ext4これは、最新のファイルシステムを1つだけ挙げると、この点でPOSIX.1-2008に準拠していないことを明確に示しています。

于 2012-05-18T10:40:30.157 に答える
14

編集: OSの動作の最新の変更で2017年8月に更新されました。

まず、WindowsのO_APPENDまたは同等のFILE_APPEND_DATAは、最大ファイル範囲(ファイル「長さ」)の増分が同時ライターの下でアトミックであることを意味します。これはPOSIXによって保証されており、Linux、FreeBSD、OS X、およびWindowsはすべて正しく実装されています。Sambaもそれを正しく実装しますが、v5より前のNFSは、アトミックに追加するワイヤーフォーマット機能がないため、実装していません。したがって、append-onlyでファイルを開くと、NFSが関与していない限り、主要なOSで同時書き込みが互いに引き裂かれることはありません。

ただし、これは、読み取りで書き込みが破損するかどうかについては何も述べていません。その上で、POSIXは、通常のファイルに対するread()およびwrite()のアトミック性について次のように述べています。

以下の関数はすべて、通常のファイルまたはシンボリックリンクを操作するときにPOSIX.1-2008で指定された効果において、相互にアトミックである必要があります...[多くの関数]... read()... write( )... 2つのスレッドがそれぞれこれらの関数のいずれかを呼び出す場合、各呼び出しは、他の呼び出しの指定された効果をすべて表示するか、いずれも表示しないものとします。[ソース]

書き込みは、他の読み取りおよび書き込みに関してシリアル化できます。ファイルデータのread()が(何らかの方法で)データのwrite()の後に発生することが証明できる場合は、呼び出しが異なるプロセスによって行われた場合でも、そのwrite()を反映する必要があります。[ソース]

しかし逆に:

POSIX.1-2008のこのボリュームは、複数のプロセスからのファイルへの同時書き込みの動作を指定していません。アプリケーションは、何らかの形式の同時実行制御を使用する必要があります。[ソース]

これら3つの要件すべてを安全に解釈すると、同じファイル内のエクステントと重複するすべての書き込みを相互にシリアル化し、引き裂かれた書き込みがリーダーに表示されないように読み取る必要があります。

安全性は劣りますが、それでも許可されている解釈は、同じプロセス内のスレッド間で読み取りと書き込みが相互にシリアル化されるだけであり、プロセス間で書き込みが読み取り専用に関してシリアル化されることです(つまり、プロセスですが、プロセス間のI / Oは取得リリースのみです)。

では、人気のあるOSとファイルシステムはこれに対してどのように機能するのでしょうか。提案されたBoost.AFIOの非同期ファイルシステムおよびファイルI/O C ++ライブラリの作成者として、私は経験的なテスターを作成することにしました。1つのプロセスの多くのスレッドの結果は次のとおりです。


O_DIRECT / FILE_FLAG_NO_BUFFERINGなし:

NTFSを使用するMicrosoftWindows10:アトミック性= 1バイトを10.0.10240まで更新し、10.0.14393から少なくとも1Mb、おそらくPOSIX仕様に従って無限に更新します。

Linux 4.2.6 with ext4:更新アトミック性=1バイト

FreeBSD 10.2とZFS:更新アトミック性=少なくとも1Mb、おそらくPOSIX仕様に従って無限大。

O_DIRECT / FILE_FLAG_NO_BUFFERING:

NTFSを使用するMicrosoftWindows10:アトミック性=まで、10.0.10240までを更新します。ページが整列している場合のみ最大4096バイト、それ以外の場合はFILE_FLAG_WRITE_THROUGHがオフの場合は512バイト、それ以外の場合は64バイト。このアトミック性は、設計ではなくPCIe DMAの機能である可能性があることに注意してください。10.0.14393以降、POSIX仕様では、少なくとも1Mb、おそらく無限大です。

Linux 4.2.6とext4:更新アトミック性=少なくとも1Mb、おそらくPOSIX仕様に従って無限大。ext4を搭載した以前のLinuxは間違いなく4096バイトを超えていなかったことに注意してください。XFSは確かにカスタムロックを使用していましたが、最近のLinuxがext4でこの問題を最終的に修正したようです。

FreeBSD 10.2とZFS:更新アトミック性=少なくとも1Mb、おそらくPOSIX仕様に従って無限大。


したがって、要約すると、ZFSを使用するFreeBSDおよびNTFSを使用するごく最近のWindowsはPOSIXに準拠しています。ext4を搭載したごく最近のLinuxは、POSIXがO_DIRECTのみに準拠しています。

生の経験的テスト結果はhttps://github.com/ned14/afio/tree/master/programs/fs-probeで確認できます。引き裂かれたオフセットは512バイトの倍数でのみテストするため、512バイトのセクターの部分的な更新が読み取り-変更-書き込みサイクル中に破損するかどうかはわかりません。

于 2016-02-07T20:20:51.587 に答える
8

ここで標準が義務付けていることのいくつかの誤解は、プロセスとスレッドの使用に由来し、それがあなたが話している「ハンドル」状況にとって何を意味するのか。特に、あなたはこの部分を逃しました:

ハンドルは、基になる開いているファイルの説明に影響を与えることなく、明示的なユーザーアクションによって作成または破棄できます。それらを作成する方法には、fcntl()、dup()、fdopen()、fileno()、およびが含まれfork()ます。それらは、少なくともfclose()、close()、およびexec関数によって破棄できます。[...] fork()の後に、前に1つ存在していた場所に2つのハンドルが存在することに注意してください。

上で引用したPOSIX仕様のセクションから。「create[handlesusing] fork」への参照については、このセクションではこれ以上詳しく説明しませんが、の仕様にfork()少し詳細が追加されています。

子プロセスには、親のファイル記述子の独自のコピーが必要です。子の各ファイル記述子は、親の対応するファイル記述子と同じオープンファイル記述を参照する必要があります。

ここでの関連ビットは次のとおりです。

  • 子は親のファイル記述子のコピーを持っています
  • 子のコピーは、親が上記のfdsを介してアクセスできるのと同じ「もの」を参照します
  • ファイル記述orとファイル記述イオンは同じものではありません。特に、ファイル記述子は上記の意味でのハンドルです。

これは、最初の引用が「fork()creates [...] handles」と言っているときに参照するものです。これらはコピーとして作成されるため、その時点から切り離され、ロックステップで更新されなくなります。

サンプルプログラムでは、すべての子プロセスが同じ状態で開始する独自のコピーを取得しますが、コピーの実行後、これらのファイル記述子/ハンドルは独立したインスタンスになり、書き込みは互いに競合します。write()保証者のみであるため、これは標準に関して完全に受け入れられます。

通常のファイルまたはシーク可能な他のファイルでは、データの実際の書き込みは、ファイルに関連付けられたファイルオフセットによって示されるファイル内の位置から続行する必要があります。write()から正常に戻る前に、ファイルオフセットは実際に書き込まれたバイト数だけインクリメントされます。

これは、すべてが同じオフセットで書き込みを開始する一方で(fdコピーがそのように初期化されたため)、成功したとしても、すべてが異なる量を書き込む可能性があることを意味します(Nバイトの書き込み要求が正確に 書き込むという標準による保証はありません)バイト;実際Nのどの場合でも成功する可能性があります)。書き込みの順序が指定されていないため、上記のサンプルプログラム全体の結果は指定されていません。要求された合計量が書き込まれたとしても、上記のすべての標準では、ファイルオフセットが増分されると述べています。これは、アトミックに(1回だけ)インクリメントされるとは言っておらず、実際のデータの書き込みがアトミックな方法で行われるとも言っていません。0 <=<= N

ただし、保証されていることが1つあります。ファイル内に、書き込みの前に存在していなかったもの、または書き込みによって書き込まれたデータのいずれからも取得されていないものが表示されないようにする必要があります。そうした場合、それは破損であり、ファイルシステムの実装にバグがあります。上で観察したことは、おそらく...書き込みの一部を並べ替えても最終結果を説明できない場合です。

を使用すると、O_APPENDこれが修正されます。これを使用すると、次のようになります。を参照write()してください。

ファイルステータスフラグのO_APPENDフラグが設定されている場合、ファイルオフセットは各書き込みの前にファイルの終わりに設定され、ファイルオフセットの変更と書き込み操作の間にファイル変更操作は発生しません。

これは、あなたが求める「前」/「介入なし」のシリアル化動作です。

スレッドを使用すると、動作が部分的に変更されます。スレッドは、作成時にファイル記述子/ハンドルのコピーを受信せず、実際の(共有)ものを操作するためです。スレッドは(必然的に)すべてが同じオフセットで書き込みを開始するわけではありません。ただし、partial-write-successのオプションは、見たくない方法でインターリーブが表示される可能性があることを意味します。それでも、それでも完全に標準に準拠している可能性があります。

道徳:デフォルトで制限されているPOSIX/UNIX標準を期待しないでください。一般的なケースでは、仕様は意図的に緩和されており、プログラマーとしての意図を明確にする必要があります。

于 2012-05-21T11:01:42.100 に答える
6

引用した仕様の最初の部分を誤って解釈しています。

ファイル記述子またはストリームは、それが参照する開いているファイルの説明では「ハンドル」と呼ばれます。開いているファイルの説明には、複数のハンドルが含まれる場合があります。[…]最初のハンドルのファイルオフセットに影響を与えるアプリケーションによるすべてのアクティビティは、それが再びアクティブなファイルハンドルになるまで中断されます。[…]これらのルールを適用するために、ハンドルが同じプロセスにある必要はありません。

これは、同時アクセスを処理するための実装に要件を課しません。代わりに、出力と副作用の明確な順序が必要な場合は、異なるプロセスからでも同時アクセスを行わないようにアプリケーションに要件を課します。

アトミック性が保証されるのは、書き込みサイズがに収まるパイプの場合のみPIPE_BUFです。

ちなみに、への呼び出しが通常のファイルに対してアトミックであったとしても、にwrite収まるパイプへの書き込みの場合を除いて、常に部分的な書き込み(つまり、要求されたバイト数より少ない書き込み)で戻ることができます。この場合、要求よりも小さい書き込みはアトミックになりますが、操作全体のアトミック性に関してはまったく役に立ちません(アプリケーションを再呼び出しして終了する必要があります)。PIPE_BUFwritewrite

于 2012-05-18T13:02:51.680 に答える