Unix / Linuxでは、フォークするとアクティブなRAIIオブジェクトはどうなりますか?二重削除はありますか?コピーの作成と割り当てとは何ですか?何も悪いことが起こらないようにする方法は?
4 に答える
fork(2)
すべてのメモリを含む、プロセスの完全なコピーを作成します。はい、自動オブジェクトのデストラクタは、親プロセスと子プロセスの別々の仮想メモリ空間で2回実行されます。「悪い」ことは何も起こりません(もちろん、デストラクタのアカウントからお金を差し引く場合を除いて)、あなたはただ事実を知っている必要があります。
主に、これらの関数をC ++で使用することは問題ありませんが、どのデータがどのように共有されるかを知っておく必要があります。
の時点で、新しいプロセスが親のメモリの完全なコピーを取得することを考慮してくださいfork()
(コピーオンライトを使用)。メモリは状態であるため、クリーンな状態を残す必要がある2つの独立したプロセスがあります。
さて、あなたがあなたに与えられた記憶の範囲内にとどまっている限り、あなたはまったく問題がないはずです:
#include <iostream>
#include <unistd.h>
class Foo {
public:
Foo () { std::cout << "Foo():" << this << std::endl; }
~Foo() { std::cout << "~Foo():" << this << std::endl; }
Foo (Foo const &) {
std::cout << "Foo::Foo():" << this << std::endl;
}
Foo& operator= (Foo const &) {
std::cout << "Foo::operator=():" << this<< std::endl;
return *this;
}
};
int main () {
Foo foo;
int pid = fork();
if (pid > 0) {
// We are parent.
int childExitStatus;
waitpid(pid, &childExitStatus, 0); // wait until child exits
} else if (pid == 0) {
// We are the new process.
} else {
// fork() failed.
}
}
上記のプログラムは大まかに印刷されます:
Foo():0xbfb8b26f
~Foo():0xbfb8b26f
~Foo():0xbfb8b26f
コピーの構築やコピーの割り当ては行われず、OSはビット単位のコピーを作成します。アドレスは物理アドレスではなく、各プロセスの仮想メモリ空間へのポインタであるため、同じです。
2つのインスタンスが情報を共有する場合、たとえば、終了する前にフラッシュして閉じる必要がある開いているファイルなど、より困難になります。
#include <iostream>
#include <fstream>
int main () {
std::ofstream of ("meh");
srand(clock());
int pid = fork();
if (pid > 0) {
// We are parent.
sleep(rand()%3);
of << "parent" << std::endl;
int childExitStatus;
waitpid(pid, &childExitStatus, 0); // wait until child exits
} else if (pid == 0) {
// We are the new process.
sleep(rand()%3);
of << "child" << std::endl;
} else {
// fork() failed.
}
}
これは印刷されるかもしれません
parent
また
child
parent
または、他の何か。
問題は、2つのインスタンスが同じファイルへのアクセスを調整するのに十分ではなく、の実装の詳細がわからないことですstd::ofstream
。
(可能な)解決策は、「プロセス間通信」または「IPC」という用語で見つけることができます。最も近いものは次のwaitpid()
とおりです。
#include <unistd.h>
#include <sys/wait.h>
int main () {
pid_t pid = fork();
if (pid > 0) {
int childExitStatus;
waitpid(pid, &childExitStatus, 0); // wait until child exits
} else if (pid == 0) {
...
} else {
// fork() failed.
}
}
最も簡単な解決策は、各プロセスが独自の仮想メモリのみを使用し、他には何も使用しないようにすることです。
もう1つの解決策は、Linux固有の解決策です。サブプロセスがクリーンアップしないことを確認します。オペレーティングシステムは、取得したすべてのメモリの未加工の非RAIIクリーンアップを実行し、開いているすべてのファイルをフラッシュせずに閉じます。fork()
これは、を使用しexec()
て別のプロセスを実行する場合に役立ちます。
#include <unistd.h>
#include <sys/wait.h>
int main () {
pid_t pid = fork();
if (pid > 0) {
// We are parent.
int childExitStatus;
waitpid(pid, &childExitStatus, 0);
} else if (pid == 0) {
// We are the new process.
execlp("echo", "echo", "hello, exec", (char*)0);
// only here if exec failed
} else {
// fork() failed.
}
}
デストラクタをトリガーせずに終了するもう1つの方法は、exit()
関数です。私は一般的にC++で使用しないことをお勧めしますが、フォークするときはその場所があります。
参照:
現在受け入れられている回答は、同期の問題を示しています。これは、RAIIが実際に引き起こす可能性のある問題とは何の関係もありません。つまり、RAIIを使用するかどうかに関係なく、親と子の間で同期の問題が発生します。2つの異なるコンソールで同じプロセスを実行すると、まったく同じ同期の問題が発生します。(つまりfork()
、プログラムに関与せず、プログラムを2回並行して実行するだけです。)
同期の問題を解決するには、セマフォを使用できます。sema_open(3)
および関連する機能を参照してください。スレッドはまったく同じ同期の問題を生成することに注意してください。ミューテックスを使用して複数のスレッドを同期できるのはあなただけであり、ほとんどの場合、ミューテックスはセマフォよりもはるかに高速です。
したがって、RAIIで問題が発生するのは、RAIIを使用して、私が外部リソースと呼んでいるものを保持する場合です。ただし、すべての外部リソースが同じように影響を受けるわけではありません。私は2つの状況で問題が発生したので、ここで両方を示します。
ソケットをshutdown()しないでください
独自のソケットクラスがあるとします。デストラクタでは、シャットダウンを実行します。結局のところ、完了したら、接続が完了したことを示すメッセージをソケットのもう一方の端に送信することもできます。
class my_socket
{
public:
my_socket(char * addr)
{
socket_ = socket(s)
...bind, connect...
}
~my_socket()
{
if(_socket != -1)
{
shutdown(socket_, SHUT_RDWR);
close(socket_);
}
}
private:
int socket_ = -1;
};
このRAIIクラスを使用すると、shutdown()
関数は親と子のソケットに影響します。つまり、親と子の両方がそのソケットの読み取りも書き込みもできなくなります。ここでは、子がソケットをまったく使用していないと仮定します(したがって、同期の問題はまったくありません)が、子が死ぬと、RAIIクラスが起動し、デストラクタが呼び出されます。その時点でソケットをシャットダウンし、使用できなくなります。
{
my_socket soc("127.0.0.1:1234");
// do something with soc in parent
...
pid_t const pid(fork());
if(pid == 0)
{
int status(0);
waitpid(pid, &status, 0);
}
else if(pid > 0)
{
// the fork() "duplicated" all memory (with copy-on-write for most)
// and duplicated all descriptors (see dup(2)) which is why
// calling 'close(s)' is perfectly safe in the child process.
// child does some work
...
// here 'soc' calls my_socket::~my_socket()
return;
}
else
{
// fork did not work
...
}
// here my_socket::~my_socket() was called in child and
// the socket was shutdown -- therefore it cannot be used
// anymore!
// do more work in parent, but cannot use 'soc'
// (which is probably not the wanted behavior!)
...
}
親と子でソケットを使用しないでください
もう1つの可能性は、まだソケットを使用している場合(ただし、外部との通信に使用されるパイプやその他のメカニズムでも同じ効果が得られる可能性があります)、「BYE」コマンドを2回送信することになる可能性があります。これは実際には同期の問題に非常に近いですが、この場合、その同期はRAIIオブジェクトが破棄されたときに発生します。
たとえば、ソケットを作成してオブジェクトで管理するとします。オブジェクトが破壊されたときはいつでも、「BYE」コマンドを送信して反対側に伝えたいと思います。
class communicator
{
public:
communicator()
{
socket_ = socket();
...bind, connect...
}
~communicator()
{
write(socket_, "BYE\n", 4);
// shutdown(socket_); -- now we know not to do that!
close(socket_);
}
private
int socket_ = -1;
};
この場合、もう一方の端は「BYE」コマンドを受け取り、接続を閉じます。これで、もう一方の端で閉じられたため、親はそのソケットを使用して通信できなくなります。
これは、phresnelが彼のofstreamの例で話していることと非常に似ています。ただ、同期を修正するのは簡単ではありません。「BYE\n」または別のコマンドをソケットに書き込む順序は、最終的にソケットが反対側から閉じられるという事実を変更しません(つまり、同期はプロセス間ロックを使用して実現できますが、 、その"BYE"
コマンドはコマンドに似ていshutdown()
ます、それはそのトラックで通信を停止します!)
解決策
簡単だったshutdown()
ので、関数を呼び出さないだけです。そうは言っても、あなたはまだshutdown()
子供ではなく、親で起こりたいと思っていたのかもしれません。
問題を解決する方法はいくつかありますが、そのうちの1つは、pidを記憶し、それを使用して、これらの破壊的な関数呼び出しを呼び出す必要があるかどうかを知ることです。可能な修正があります:
class communicator
{
communicator()
: pid_(getpid())
{
socket_ = socket();
...bind, connect...
}
~communicator()
{
if(socket_ != -1)
{
if(pid_ == getpid())
{
write(socket_, "BYE\n", 4);
shutdown(socket_, SHUT_RDWR);
}
close(socket_);
}
}
private:
pid_t pid_;
int socket_;
};
ここでは、親にいる場合write()
にshutdown()
のみ、を実行します。
子はすべての記述子で呼び出されるclose()
ため、ソケット記述子でを実行できる(そして実行することが期待されている)ため、子は保持するファイルごとに異なるファイル記述子を持っていることに注意してください。fork()
dup()
別の警備員
これで、RAIIオブジェクトが親のずっと上に作成され、子がとにかくそのRAIIオブジェクトのデストラクタを呼び出すというもっと複雑なケースがあるかもしれません。roemckeが述べたように、呼び出す_exit()
のがおそらく最も安全な方法です(exit()
ほとんどの場合は機能しますが、親に望ましくない副作用が生じる可能性があります。同時にexit()
、子が正常に終了するために必要になる場合があります。つまりtmpfile()
、作成されたものを削除します。 !)。つまり、を使用する代わりに、をreturn
呼び出します_exit()
。
pid_t r(fork());
if(r == 0)
{
try
{
...child do work here...
}
catch(...)
{
// you probably want to log a message here...
}
_exit(0); // prevent stack unfolding and calls to atexit() functions
/* NOT REACHED */
}
これはとにかく、他の多くのことが起こる可能性のある「親のコード」に子供が戻ってほしくないという理由だけで、はるかに安全です。スタック展開だけではありません。(つまりfor()
、子が継続することになっていないループを継続する...)
_exit()
関数は返されないため、スタックで定義されたオブジェクトのデストラクタは呼び出されません。子が例外を発生させた場合には呼び出されないため、ここではtry / catchが非常に重要です。ただし、ヒープに割り当てられたすべてのオブジェクトを破棄しない関数を呼び出す必要がありますが、スタックを展開した後に関数を呼び出します。したがって、おそらくすべてのRAIIデストラクタと呼ばれます...そしてまた、あなたが期待するものではありません。_exit()
terminate()
terminate()
exit()
との違い_exit()
は、前者がatexit()
関数を呼び出すことです。あなたが子供や親でそれをする必要があることは比較的めったにありません。少なくとも、奇妙な副作用はありませんでした。ただし、一部のライブラリは、が呼び出されるatexit()
可能性を考慮せずに使用します。fork()
関数で自分自身を保護する1つの方法atexit()
は、関数を必要とするプロセスのPIDを記録することatexit()
です。関数が呼び出されたときにPIDが一致しない場合は、戻って他に何もしません。
pid_t cleanup_pid = -1;
void cleanup()
{
if(cleanup_pid != getpid())
{
return;
}
... do your clean up here ...
}
void some_function_requiring_cleanup()
{
if(cleanup_pid != getpid())
{
cleanup_pid = getpid();
atexit(cleanup);
}
... do work requiring cleanup ...
}
明らかに、atexit()
それを正しく実行するライブラリの数はおそらく0に非常に近いでしょう。したがって...そのようなライブラリは避ける必要があります。
execve()
またはを呼び出した場合_exit()
、クリーンアップは発生しないことに注意してください。したがってtmpfile()
、子+での呼び出しの場合_exit()
、その一時ファイルは自動的に削除されません...
何をしているのかわからない限り、子プロセスは、処理が完了した後、常に_exit()を呼び出す必要があります。
pid_t pid = fork()
if (pid == 0)
{
do_some_stuff(); // Make sure this doesn't throw anything
_exit(0);
}
アンダースコアは重要です。子プロセスでexit()を呼び出さないでください。これにより、ストリームバッファがディスク(またはファイル記述子が指している場所)にフラッシュされ、2回書き込まれることになります。