dup2
システムコールのこのマンページは次のように述べています。
EBUSY (Linux のみ) これは、open(2) および dup() との競合状態中に dup2() または dup3() によって返される場合があります。
それはどのような競合状態について話し、エラーが発生した場合dup2
はどうすればよいですか? EBUSY
の場合のように再試行する必要がありEINTR
ますか?
に説明がありfs/file.c
ますdo_dup2()
:
/*
* We need to detect attempts to do dup2() over allocated but still
* not finished descriptor. NB: OpenBSD avoids that at the price of
* extra work in their equivalent of fget() - they insert struct
* file immediately after grabbing descriptor, mark it larval if
* more work (e.g. actual opening) is needed and make sure that
* fget() treats larval files as absent. Potentially interesting,
* but while extra work in fget() is trivial, locking implications
* and amount of surgery on open()-related paths in VFS are not.
* FreeBSD fails with -EBADF in the same situation, NetBSD "solution"
* deadlocks in rather amusing ways, AFAICS. All of that is out of
* scope of POSIX or SUS, since neither considers shared descriptor
* tables and this condition does not arise without those.
*/
fdt = files_fdtable(files);
tofree = fdt->fd[fd];
if (!tofree && fd_is_open(fd, fdt))
goto Ebusy;
EBUSY
解放される記述子がまだ開かれているときに何らかの不完全な状態にある場合に が返されるように見えます (fd_is_open
しかし には存在しませんfdtable
)。
編集(詳細と報奨金が欲しい)
どのように起こるかを理解するために!tofree && fd_is_open(fd, fdt)
、ファイルがどのように開かれるかを見てみましょう。ここに の簡略化されたバージョンがありsys_open
ます:
long do_sys_open(int dfd, const char __user *filename, int flags, umode_t mode)
{
/* ... irrelevant stuff */
/* allocate the fd, uses a lock */
fd = get_unused_fd_flags(flags);
/* HERE the race condition can arise if another thread calls dup2 on fd */
/* do the real VFS stuff for this fd, also uses a lock */
fd_install(fd, f);
/* ... irrelevant stuff again */
return fd;
}
基本的に 2 つの非常に重要なことが発生します。ファイル記述子が割り当てられてから、VFS によって実際に開かれます。これら 2 つの操作fdt
は、プロセスの を変更します。どちらもロックを使用するため、これら 2 つの呼び出し内で期待することは何も悪いことではありません。
どちらが割り当てられたかを記憶するためにfds
、 と呼ばれるビット ベクトルopen_fds
が によって使用されますfdt
。の後get_unused_fd_flags()
、fd
が割り当てられ、対応するビットが に設定されopen_fds
ます。のロックは解除されましたfdt
が、実際の VFS ジョブはまだ完了していません。
この正確な瞬間に、別のスレッド (または shared の場合は別のプロセスfdt
) が dup2 を呼び出すことができます。これは、ロックが解放されているため、ブロックされません。dup2
がここで通常のパスを取った場合、はfd
置き換えられfd_install
ますが、古いファイルに対して実行されます。したがって、のチェックとリターンEbusy
。
fd_install()
私の説明を確認するコメントで、この競合状態に関する追加情報を見つけました。
/* The VFS is full of places where we drop the files lock between
* setting the open_fds bitmap and installing the file in the file
* array. At any such point, we are vulnerable to a dup2() race
* installing a file in the array before us. We need to detect this and
* fput() the struct file we are about to overwrite in this case.
*
* It should never happen - if we allow dup2() do it, _really_ bad things
* will follow. */
私は Linux が行った選択を完全には認識していませんが、他の回答の Linux カーネルからのコメントは、私が 13 年前に OpenBSD で取り組んだことを示しているため、何が起こっていたのかを思い出そうとしました。
実装方法により、open
最初にファイル記述子を割り当て、次にファイル記述子テーブルのロックを解除して実際にオープン操作を終了しようとします。理由の 1 つは、open の副作用 (最も単純なのはファイルの atime を変更することですが、たとえばデバイスを開くと、はるかに重大な副作用が生じる可能性があります) を実際に引き起こしたくないということです。ファイル記述子の。ファイル記述子を割り当てる他のすべての操作にも同じことが当てはまります。以下のテキストを読むと、 open
「ファイル記述子を割り当てるシステムコール」に置き換えてください。これが POSIX によって義務付けられているのか、それとも The Way Things Have Always Been Done だけなのかは覚えていません。
open
メモリを割り当て、ファイル システムにアクセスし、長時間ブロックする可能性のある一連の処理を実行できます。ヒューズのようなファイルシステムの最悪の場合、ユーザーランドに戻ることさえあります。その理由 (およびその他の理由) から、実際には、オープン操作全体でファイル記述子テーブルをロックしたままにしたくありません。カーネル内のロックは、スリープ中に保持するのは非常に悪いため、ロックされた操作の完了にはユーザーランドとの対話が必要になる場合があります[1]。
問題は、誰かがopen
1 つのスレッド (または同じファイル記述子テーブルを共有するプロセス) で呼び出し、ファイル記述子を割り当て、まだ終了していないときに、別のスレッドがdup2
同じファイル記述子を指しているときに発生します。open
ちょうど得た。未完成のファイル記述子はまだ無効であるため (たとえばread
、write
使用しようとすると EBADF が返されます)、まだ実際に閉じることはできません。
OpenBSD では、これは、割り当てられているがまだ開いていないファイル記述子を複雑な参照カウントで追跡することによって解決されます。ほとんどの操作は、ファイル記述子が存在しないかのように見せかけ (ただし、割り当て可能でもありません)、 を返すだけEBADF
です。しかし、dup2
そこにあるからといって、そこにないふりをすることはできません。open
最終的な結果として、2 つのスレッドが同時にandを呼び出した場合dup2
、 open は実際にはファイルに対して完全なオープン操作を実行しますがdup2
、ファイル記述子の競合に勝ったため、最後に行うことopen
は、割り当てたばかりのファイルの参照カウントを減らすことです。もう一度閉じます。その間dup2
、レースに勝ち、open
取得したファイル記述子を閉じるふりをしました(実際には実行しませんでしたopen
それができた)。カーネルがどちらの動作を選択するかは実際には問題ではありません。どちらの場合も、これは または のいずれかの予期しない動作につながる競合であるためopen
ですdup2
。せいぜい、Linux が EBUSY を返すことは競合のウィンドウを縮小するだけですが、競合はまだそこにあり、他のスレッドで戻ってくるのdup2
と同じように呼び出しが発生するのを妨げるものは何もなく、呼び出し元がチャンスを得るopen
前にファイル記述子を置き換えます。open
これを使って。
あなたの質問のエラーは、このレースに参加したときに発生する可能性が最も高いです. それを避けるためにdup2
、同時にファイル記述子テーブルにアクセスする人が他にいないことが確実でない限り、状態がわからないファイル記述子にはしないでください。そして、確実にする唯一の方法は、実行中の唯一のスレッドである (ファイル記述子は常にライブラリによって背後で開かれている) か、上書きしているファイル記述子を正確に把握することです。そもそも割り当てられていないファイル記述子が許可されている理由dup2
は、fds 0、1、および 2 を閉じて dup2 /dev/null をそれらに入れるのが一般的なイディオムだからです。
一方、前にファイル記述子を閉じないdup2
と、 からのエラー リターンが失われclose
ます。close
ただし、からのエラーはばかげており、そもそもそこにあるべきではないため、心配する必要はありません。 C Read Only File Close Errors の処理スレッドの予期しない動作の別の例と、何のためにファイル記述子が奇妙に動作するか私はここで話してきました。この質問を参照してください。
これをトリガーするコードの例を次に示します。
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <err.h>
#include <pthread.h>
static void *
do_bad_things(void *v)
{
int *ip = v;
int fd;
sleep(2); /* pretend this is proper synchronization. */
if ((fd = open("/dev/null", O_RDONLY)) == -1)
err(1, "open 2");
if (dup2(fd, *ip))
warn("dup2");
return NULL;
}
int
main(int argc, char **argv)
{
pthread_t t;
int fd;
/* This will be our next fd. */
if ((fd = open("/dev/null", O_RDONLY)) == -1)
err(1, "open");
close(fd);
if (mkfifo("xxx", 0644))
err(1, "mkfifo");
if (pthread_create(&t, NULL, do_bad_things, &fd))
err(1, "pthread_create");
if (open("xxx", O_RDONLY) == -1)
err(1, "open fifo");
return 0;
}
FIFO は、必要なopen
だけブロックを発生させる標準的な方法です。予想どおり、これは OpenBSD ではサイレントに動作し、Linux ではdup2
EBUSY を返します。MacOSでは、何らかの理由で「echo foo > xxx」を実行したシェルを強制終了しますが、書き込みのためにシェルを開くだけの通常のプログラムは正常に動作しますが、その理由はわかりません。
[1] ここに逸話があります。私は、AFS の実装に使用されるヒューズのようなファイルシステムの作成に携わってきました。私たちが抱えていたバグの 1 つは、ユーザーランドを呼び出している間、ファイル オブジェクトのロックを保持していたことです。ディレクトリ エントリ ルックアップのロック プロトコルでは、ディレクトリ ロックを保持してからディレクトリ エントリを検索し、そのディレクトリ エントリの下のオブジェクトをロックしてから、ディレクトリ ロックを解除する必要があります。ファイル オブジェクト ロックを保持していたため、他のプロセスが入ってきてファイルを検索しようとしたため、そのプロセスはディレクトリ ロックを保持したまま、ファイル ロックのためにスリープ状態になりました。別のプロセスが入ってきて、ディレクトリを検索しようとしましたが、親ディレクトリのロックを保持することになりました。簡単に言うと、ルート ディレクトリに到達するまで一連のロックが保持されることになりました。その間、ファイルシステム デーモンはまだネットワーク経由でサーバーと通信していました。何らかの理由でネットワーク操作が失敗し、ファイルシステム デーモンがエラー メッセージを記録する必要がありました。そのためには、ロケール データベースを読み取る必要がありました。そのためには、フル パスを使用してファイルを開く必要がありました。しかし、ルート ディレクトリが他の誰かによってロックされていたため、デーモンはそのロックを待っていました。そして、8 ロックの長さのデッドロック チェーンがありました。そのため、カーネルは、長時間の操作、特にファイルシステム操作中にロックを保持することを避けるために、しばしば複雑な曲芸師の体操を実行します。デーモンはそのロックを待っていました。そして、8 ロックの長さのデッドロック チェーンがありました。そのため、カーネルは、長時間の操作、特にファイルシステム操作中にロックを保持することを避けるために、しばしば複雑な曲芸師の体操を実行します。デーモンはそのロックを待っていました。そして、8 ロックの長さのデッドロック チェーンがありました。そのため、カーネルは、長時間の操作、特にファイルシステム操作中にロックを保持することを避けるために、しばしば複雑な曲芸師の体操を実行します。