close(2) が完了しないのはなぜですか?マルチスレッドで発生する可能性のある問題

close(2) が完了しないのはなぜですか?マルチスレッドで発生する可能性のある問題

RedHat Linux 7.9、カーネル3.10.0-1160.90.1.el7.x86_64、glibc-2.17-326.el7_9.x86_64。はい、古いです。申し訳ありません。

Linuxカーネルでclose(2)カーネル関数が時々1回の呼び出しで完了しないのはなぜですか?マルチスレッドに問題がある可能性があることがわかりました。スレッドがファイル記述子を開いたり、書き込んだり、閉じたりした場合、クローズ中にファイル記述子が open() の他のスレッドで再使用されることを個人的に目撃しました。その後、close()が完了して書き込みを試みると、無効なファイル記述子が表示されます。

これは機能ですか、それともバグですか?複数のスレッドがファイルディスクリプタテーブルを共有していることを知っているので、ファイルディスクリプタテーブルをできるだけ早く処理するときに未定義の動作(以下を参照)が発生する可能性がありますが、close()が完了する前にファイルディスクリプタがどのように解放されるのか疑問に思います。

スレッドが開かれたときに呼び出されるclone()は、実際にはすべてのスレッド間でファイル記述子テーブル全体を共有していることを知らないため、各スレッドのコードは自分が開くファイル記述子に書き込みます。また、あるスレッドの open()/write()/close() ワークセットが別のスレッドの close() の途中で open() を実行するため、競合が発生する可能性があるとは予想されません。クリーンアップが完了して返される前に使用できるファイル記述子。

以下は、この動作を示すstraceのいくつかの出力です。これには58506と58508という2つのスレッドがあります。 PID 58506は、タイムスタンプ532769の終わりから終了12に移動します。 532791 - 25マイクロ秒後 - 終了は完了です。ただし、タイムスタンプ532775-532769-PID 58508以降、8つのマイクがファイルを開きます。 532803(オープン開始後28マイクロ秒)でオープンが完了し、ファイル記述子12(現在クローズ)がPID 58508に割り当てられます。 532949でファイル記述子が正しくないため、書き込みに失敗しました。これは、PID 58508がファイル記述子を取得した532803以降の146マイクロ秒です。

これは、一対のSSDでサポートされているハードウェアRAIDカードを持つDell R640サーバーのローカルファイルシステムにあります(価値があるだけ)。

58506 08:58:34.532769 close(12 <unfinished ...>

58508 08:58:34.532775 open("/path/to/dir1/file1", O_WRONLY|O_CREAT|O_TRUNC, 0664 <unfinished ...>

58506 08:58:34.532791 <... close resumed>) = 0

58508 08:58:34.532803 <... open resumed>) = 12

58506 08:58:34.532808 close(12)         = 0

58508 08:58:34.532936 write(12, “datadatadatadata"..., 1572 <unfinished ...>

58506 08:58:34.532943 <... write resumed>) = 258

58508 08:58:34.532949 <... write resumed>) = -1 EBADF (Bad file descriptor)

58506 08:58:34.532963 stat("/path/to/dir2", {st_mode=S_IFDIR|0755, st_size=4096, ...})
 = 0
58506 08:58:34.532995 open("/path/to/dir2/file2", O_WRONLY|O_CREAT|O_TRUNC, 0664) = 12


58508 08:58:34.533286 close(12)         = 0
58508 08:58:34.533311 close(12)         = -1 EBADF (Bad file descriptor)

58508 08:58:34.533595 write(1, "[2023-10-04 08:58:34.533588 E] P"..., 216) = 216

58506 08:58:34.533634 write(12, "moredatadatadatadata"..., 6139) = -1 EBADF (Bad file descriptor)
58506 08:58:34.533660 close(12)         = -1 EBADF (Bad file descriptor)

58508 08:58:34.533681 nanosleep({tv_sec=0, tv_nsec=100000000},  <unfinished ...>

58506 08:58:34.533688 close(12)         = -1 EBADF (Bad file descriptor)
58506 08:58:34.533709 stat("/path/to/dir2/file2", {st_mode=S_IFREG|0644, st_size=0, ...}) = 0
58506 08:58:34.533760 write(1, "[2023-10-04 08:58:34.533754 E] P"..., 233) = 233
58506 08:58:34.533786 nanosleep({tv_sec=0, tv_nsec=100000000},  <unfinished ...>

タイムスタンプ533709では、開発者はファイルの長さがゼロであることを確認します。ファイルは実際に作成されましたが、書き込みに失敗してファイルにデータがありません。したがって、コードはループに陥り、ファイルを再構築しようとします。

POSIXでは、開かれたファイル記述子をサブスレッドにコピーするにはpthread()が必要です(https://linux.die.net/man/7/pthreads)、しかし、サブオープンファイル記述子は言及されていません。

私はclose()が「オペレーティングシステムの観点から見ると、この関数の実行中にスレッドをキャンセルするのが安全であることを意味する」キャンセルポイントであることに少し不安です(https://stackoverflow.com/questions/27374707/what-exactly-is-a-cancellation-point、許可された回答を参照してください)。しかし、close()スレッドがキャンセルされると、他のスレッドに無効なファイル記述子がある可能性があります...?オペレーティングシステムは、当時正しいファイル記述子テーブルを持っていることを確認しますか?そうですが、close()が完了する前にファイル記述子を解放すると緊張します。

関連情報