質問

子プロセスを1つだけ生成するプロセスがあるとします。親プロセスが何らかの理由で(通常または異常に、kill、^ C、失敗などをアサートして)終了した場合、子プロセスを停止させます。それを正しく行う方法は?


stackoverflowに関するいくつかの同様の質問:


Windows のstackoverflowに関する同様の質問:

役に立ちましたか?

解決

子は、このように prctl() syscallでオプション PR_SET_PDEATHSIG を指定することにより、親が死んだときにカーネルに SIGHUP (または他のシグナル)を配信するように要求できます:

prctl(PR_SET_PDEATHSIG、SIGHUP);

詳細については、 man 2 prctl を参照してください。

編集:これはLinux専用です

他のヒント

同じ問題を解決しようとしていますが、私のプログラムはOS Xで実行する必要があるため、Linux専用のソリューションはうまくいきませんでした。

このページの他の人々と同じ結論に達しました。親が亡くなったときに子供に通知するPOSIX互換の方法はありません。それで、次善の策を練り上げました-子投票をしました。

(何らかの理由で)親プロセスが停止すると、子の親プロセスはプロセス1になります。子が定期的にポーリングする場合、親が1であるかどうかを確認できます。

これは素晴らしいことではありませんが、動作します。また、このページの他の場所で提案されているTCPソケット/ロックファイルポーリングソリューションよりも簡単です。

過去に「オリジナル」を実行してこれを達成しました。 「子」のコードおよび「生成された」 「親」のコード(つまり、 fork()の後にテストの通常の意味を逆にします)。次に、「spawned」にSIGCHLDをトラップします。コード...

あなたの場合は不可能かもしれませんが、うまくいくとかわいいです。

子プロセスを変更できない場合は、次のようなことを試してください:

int pipes[2];
pipe(pipes)
if (fork() == 0) {
    close(pipes[1]); /* Close the writer end in the child*/
    dup2(0, pipes[0]); /* Use reader end as stdin */
    exec("sh -c 'set -o monitor; child_process & read dummy; kill %1'")
}

close(pipes[0]); /* Close the reader end in the parent */

これは、ジョブ制御を有効にしてシェルプロセス内から子を実行します。子プロセスはバックグラウンドで生成されます。シェルは改行(またはEOF)を待ってから子を殺します。

親が死んだ場合-理由が何であれ-パイプの端を閉じます。子シェルは読み取りからEOFを取得し、バックグラウンドの子プロセスを強制終了します。

完全を期すため。 macOSでは、kqueueを使用できます:

void noteProcDeath(
    CFFileDescriptorRef fdref, 
    CFOptionFlags callBackTypes, 
    void* info) 
{
    // LOG_DEBUG(@"noteProcDeath... ");

    struct kevent kev;
    int fd = CFFileDescriptorGetNativeDescriptor(fdref);
    kevent(fd, NULL, 0, &kev, 1, NULL);
    // take action on death of process here
    unsigned int dead_pid = (unsigned int)kev.ident;

    CFFileDescriptorInvalidate(fdref);
    CFRelease(fdref); // the CFFileDescriptorRef is no longer of any use in this example

    int our_pid = getpid();
    // when our parent dies we die as well.. 
    LOG_INFO(@"exit! parent process (pid %u) died. no need for us (pid %i) to stick around", dead_pid, our_pid);
    exit(EXIT_SUCCESS);
}


void suicide_if_we_become_a_zombie(int parent_pid) {
    // int parent_pid = getppid();
    // int our_pid = getpid();
    // LOG_ERROR(@"suicide_if_we_become_a_zombie(). parent process (pid %u) that we monitor. our pid %i", parent_pid, our_pid);

    int fd = kqueue();
    struct kevent kev;
    EV_SET(&kev, parent_pid, EVFILT_PROC, EV_ADD|EV_ENABLE, NOTE_EXIT, 0, NULL);
    kevent(fd, &kev, 1, NULL, 0, NULL);
    CFFileDescriptorRef fdref = CFFileDescriptorCreate(kCFAllocatorDefault, fd, true, noteProcDeath, NULL);
    CFFileDescriptorEnableCallBacks(fdref, kCFFileDescriptorReadCallBack);
    CFRunLoopSourceRef source = CFFileDescriptorCreateRunLoopSource(kCFAllocatorDefault, fdref, 0);
    CFRunLoopAddSource(CFRunLoopGetMain(), source, kCFRunLoopDefaultMode);
    CFRelease(source);
}

子プロセスには、親プロセスとのパイプがありますか?その場合、書き込み中にSIGPIPEを受信するか、読み取り中にEOFを取得します-これらの状態が検出される可能性があります。

Linuxでは、子に親の死信号をインストールできます。例:

#include <sys/prctl.h> // prctl(), PR_SET_PDEATHSIG
#include <signal.h> // signals
#include <unistd.h> // fork()
#include <stdio.h>  // perror()

// ...

pid_t ppid_before_fork = getpid();
pid_t pid = fork();
if (pid == -1) { perror(0); exit(1); }
if (pid) {
    ; // continue parent execution
} else {
    int r = prctl(PR_SET_PDEATHSIG, SIGTERM);
    if (r == -1) { perror(0); exit(1); }
    // test in case the original parent exited just
    // before the prctl() call
    if (getppid() != ppid_before_fork)
        exit(1);
    // continue child execution ...

フォークの前に親プロセスIDを保存し、 prctl() は、 prctl() および子を呼び出したプロセスの終了。

また、子の親の死亡シグナルは、それ自体の新しく作成された子ではクリアされることに注意してください。 execve()の影響を受けません。

すべての orphansの採用を担当するシステムプロセスが確実である場合、そのテストを簡素化できます。 のPIDは1です。

pid_t pid = fork();
if (pid == -1) { perror(0); exit(1); }
if (pid) {
    ; // continue parent execution
} else {
    int r = prctl(PR_SET_PDEATHSIG, SIGTERM);
    if (r == -1) { perror(0); exit(1); }
    // test in case the original parent exited just
    // before the prctl() call
    if (getppid() == 1)
        exit(1);
    // continue child execution ...

ただし、そのシステムプロセスが init であり、PID 1を使用することは移植性がありません。 POSIX.1-2008の指定

  

既存のすべての子プロセスの親プロセスID、および呼び出しプロセスのゾンビプロセスは、実装定義のシステムプロセスのプロセスIDに設定されます。つまり、これらのプロセスは特別なシステムプロセスに継承されます。

従来、すべてのオーファンを採用しているシステムプロセスはPID 1、つまりinit-すべてのプロセスの祖先です。

Linux または FreeBSD は、別のプロセスにその役割がある可能性があります。たとえば、Linuxでは、プロセスは prctl(PR_SET_CHILD_SUBREAPERを呼び出すことができます。 、1) 、その子孫のすべての孤児を継承するシステムプロセスとして確立する( Fedoraの例 25)。

ここでの別の答えに触発されて、私は次のオールPOSIXソリューションを思いつきました。一般的な考え方は、親と子の間に中間プロセスを作成することです。これには、1つの目的があります。親が死んだときに通知し、明示的に子を殺します。

このタイプのソリューションは、子のコードを変更できない場合に役立ちます。

int p[2];
pipe(p);
pid_t child = fork();
if (child == 0) {
    close(p[1]); // close write end of pipe
    setpgid(0, 0); // prevent ^C in parent from stopping this process
    child = fork();
    if (child == 0) {
        close(p[0]); // close read end of pipe (don't need it here)
        exec(...child process here...);
        exit(1);
    }
    read(p[0], 1); // returns when parent exits for any reason
    kill(child, 9);
    exit(1);
}

この方法には2つの小さな警告があります:

  • 中間プロセスを意図的に強制終了すると、親が死亡しても子は強制終了されません。
  • 子が親の前に終了すると、中間プロセスは元の子pidを強制終了しようとします。これにより、別のプロセスを参照できます。 (これは、中間プロセスでより多くのコードを使用して修正できます。)

さておき、私が使用している実際のコードはPythonです。完全を期すためです:

def run(*args):
    (r, w) = os.pipe()
    child = os.fork()
    if child == 0:
        os.close(w)
        os.setpgid(0, 0)
        child = os.fork()
        if child == 0:
            os.close(r)
            os.execl(args[0], *args)
            os._exit(1)
        os.read(r, 1)
        os.kill(child, 9)
        os._exit(1)
    os.close(r)

標準のPOSIX呼び出しのみを使用することを保証することは不可能だと思います。実生活のように、子供が生まれると、それ自身の生活があります。

親プロセスは、考えられるほとんどの終了イベントをキャッチし、その時点で子プロセスを強制終了することを できますが、キャッチできないものが常にいくつかあります。

たとえば、どのプロセスも SIGKILL をキャッチできません。カーネルがこのシグナルを処理すると、指定されたプロセスを通知せずに、そのプロセスを強制終了します。

類推を拡張する-それを行う唯一の他の標準的な方法は、親がいなくなったことがわかったときに、子供が自殺することです。

prctl(2)でLinux専用の方法があります-他の回答を参照してください。

他の人が指摘したように、親が終了するときに親pidが1になることを頼りにするのは移植性がありません。特定の親プロセスIDを待つ代わりに、IDが変更されるのを待つだけです:

pit_t pid = getpid();
switch (fork())
{
    case -1:
    {
        abort(); /* or whatever... */
    }
    default:
    {
        /* parent */
        exit(0);
    }
    case 0:
    {
        /* child */
        /* ... */
    }
}

/* Wait for parent to exit */
while (getppid() != pid)
    ;

全速でポーリングしたくない場合は、必要に応じてマイクロスリープを追加します。

このオプションは、パイプを使用したり、信号に依存したりするよりも簡単に思えます。

トラップハンドラをインストールして、SIGINTをキャッチします。SIGINTは、子プロセスがまだ生きている場合、それを殺しますが、SIGKILLをキャッチしない他のポスターは正しいです。

排他的アクセスで.lockfileを開き、子がそれを開こうとしてポーリングする-開くことに成功したら、子プロセスは終了するはずです

このソリューションは私のために働いた:

  • 子にstdinパイプを渡す-ストリームにデータを書き込む必要はありません。
  • 子は、EOFまでstdinから無期限に読み取ります。 EOFは、親がなくなったことを通知します。
  • これは、親がいなくなったときにそれを検出するための簡単な方法です。親がクラッシュした場合でも、OSはパイプを閉じます。

これは、親が生存している場合にのみ存在が意味を持つワーカー型プロセス用でした。

手っ取り早い方法は、子と親の間にパイプを作成することだと思います。親が終了すると、子はSIGPIPEを受け取ります。

一部のポスターはすでにパイプと kqueue について言及しています。実際、 socketpair()呼び出しによって、接続された Unixドメインソケットのペアを作成することもできます。ソケットタイプは SOCK_STREAM である必要があります。

2つのソケットファイル記述子fd1、fd2があるとします。 fork()で子プロセスを作成し、fdsを継承します。親ではfd2を閉じ、子ではfd1を閉じます。これで、各プロセスは、 POLLIN イベントのために、残りの開いているfdを独自の端で poll()できます。各サイドが通常のライフタイム中にそのfdを明示的に close()しない限り、 POLLHUP フラグが相手の終了を示すはずです(クリーンに関係なく)か否か)。このイベントが通知されると、子供は何をすべきか(たとえば死ぬこと)を決定できます。

#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <poll.h>
#include <stdio.h>

int main(int argc, char ** argv)
{
    int sv[2];        /* sv[0] for parent, sv[1] for child */
    socketpair(AF_UNIX, SOCK_STREAM, 0, sv);

    pid_t pid = fork();

    if ( pid > 0 ) {  /* parent */
        close(sv[1]);
        fprintf(stderr, "parent: pid = %d\n", getpid());
        sleep(100);
        exit(0);

    } else {          /* child */
        close(sv[0]);
        fprintf(stderr, "child: pid = %d\n", getpid());

        struct pollfd mon;
        mon.fd = sv[1];
        mon.events = POLLIN;

        poll(&mon, 1, -1);
        if ( mon.revents & POLLHUP )
            fprintf(stderr, "child: parent hung up\n");
        exit(0);
    }
}

上記の概念実証コードをコンパイルして、 ./ a.out&amp; などの端末で実行できます。さまざまな信号によって親PIDを強制終了する実験を行うのに約100秒かかります。さもなければ、単に終了します。どちらの場合でも、「子:親がハングアップしました」というメッセージが表示されます。

SIGPIPE ハンドラーを使用するメソッドと比較して、このメソッドは write()呼び出しを試みる必要がありません。

この方法も対称です。つまり、プロセスは同じチャネルを使用して互いの存在を監視できます。

このソリューションは、POSIX関数のみを呼び出します。これをLinuxとFreeBSDで試しました。他のUnixでも動作するはずですが、実際にはテストしていません。

参照:

    Linux manページの
  • unix(7)、FreeBSDの unix(4) poll(2) socketpair( 2)、Linuxの socket(7)

POSIX の下で、 exit() _exit()および _Exit()関数は次のように定義されています:

  • プロセスが制御プロセスである場合、SIGHUPシグナルは、呼び出しプロセスに属する制御端末のフォアグラウンドプロセスグループ内の各プロセスに送信されます。

したがって、親プロセスがそのプロセスグループの制御プロセスになるように調整した場合、親が終了すると、子はSIGHUPシグナルを受け取る必要があります。親がクラッシュしたときにそれが起こるかどうかは確かではありませんが、そうなると思います。確かに、非クラッシュの場合、それはうまく動作するはずです。

基本定義(定義)セクション、および exit()および setsid( )および setpgrp()-完全な画像を取得します。 (そうだ!)

たとえばpid 0に信号を送信する場合、

kill(0, 2); /* SIGINT */

そのシグナルはプロセスグループ全体に送信されるため、子を効果的に殺します。

次のような方法で簡単にテストできます:

(cat && kill 0) | python

次に^ Dを押すと、テキストが&quot; Terminated&quot; として表示されます。これは、stdinが閉じられたために終了するのではなく、Pythonインタープリターが実際に終了したことを示しています。

他の誰かに関連がある場合、C ++からフォークされた子プロセスでJVMインスタンスを生成するとき、親プロセスが完了した後にJVMインスタンスを適切に終了させる唯一の方法は、以下を実行することでしたこれが最善の方法でなかった場合、コメントでフィードバックを提供できることを願っています。

1) execv を介してJavaアプリを起動する前に提案されているように、分岐した子プロセスで prctl(PR_SET_PDEATHSIG、SIGHUP)を呼び出し、

2)親PIDが1になるまでポーリングするシャットダウンフックをJavaアプリケーションに追加し、ハード Runtime.getRuntime()。halt(0)を実行します。ポーリングは、 ps コマンドを実行する別のシェルを起動することで実行されます( LinuxでJavaまたはJRubyでPIDを見つけるにはどうすればよいですか)。

130118を編集:

堅牢なソリューションではなかったようです。私はまだ何が起こっているかの微妙な違いを理解するのに少し苦労していますが、これらのアプリケーションをscreen / SSHセッションで実行すると、時々孤立したJVMプロセスを取得していました。

JavaアプリでPPIDをポーリングする代わりに、シャットダウンフックにクリーンアップを実行させた後、上記のように強制的に停止させました。次に、すべてを終了するときに、生成された子プロセスのC ++親アプリで waitpid を呼び出すようにしました。子プロセスは確実に終了し、親は既存の参照を使用して子プロセスが終了するようにするため、これはより堅牢なソリューションのようです。これを、親プロセスが満足したときに終了し、子プロセスが終了する前に孤立していたかどうかを理解させようとした以前のソリューションと比較してください。

親が亡くなると、孤児のPPIDが1に変わります。自分のPPIDのみを確認する必要があります。 ある意味では、これは前述のポーリングです。 そのためのシェルピースを次に示します。

check_parent () {
      parent=`ps -f|awk '$2=='$PID'{print $3 }'`
      echo "parent:$parent"
      let parent=$parent+0
      if [[ $parent -eq 1 ]]; then
        echo "parent is dead, exiting"
        exit;
      fi
}


PID=$
cnt=0
while [[ 1 = 1 ]]; do
  check_parent
  ... something
done

2つのソリューションが見つかりましたが、どちらも完璧ではありません。

1。SIGTERMシグナルを受信したときに、kill(-pid)ですべての子を殺します。
明らかに、このソリューションは「kill -9」を処理できませんが、すべての子プロセスを記憶する必要がないため、ほとんどの場合非常に簡単に機能します。


    var childProc = require('child_process').spawn('tail', ['-f', '/dev/null'], {stdio:'ignore'});

    var counter=0;
    setInterval(function(){
      console.log('c  '+(++counter));
    },1000);

    if (process.platform.slice(0,3) != 'win') {
      function killMeAndChildren() {
        /*
        * On Linux/Unix(Include Mac OS X), kill (-pid) will kill process group, usually
        * the process itself and children.
        * On Windows, an JOB object has been applied to current process and children,
        * so all children will be terminated if current process dies by anyway.
        */
        console.log('kill process group');
        process.kill(-process.pid, 'SIGKILL');
      }

      /*
      * When you use "kill pid_of_this_process", this callback will be called
      */
      process.on('SIGTERM', function(err){
        console.log('SIGTERM');
        killMeAndChildren();
      });
    }

同じ方法で、process.exitをどこかで呼び出すと、上記の方法のように 'exit'ハンドラをインストールできます。 注:Ctrl + Cと突然のクラッシュは、プロセスグループを強制終了するためにOSによって自動的に処理されているため、ここではもうありません。

2。 chjj / pty.js を使用して、制御付きのプロセスを生成します。端末が接続されています。
とにかく-9を強制終了することで現在のプロセスを強制終了すると、すべての子プロセスも(OSによって)自動的に強制終了されます。現在のプロセスは端末のもう一方の側を保持しているため、現在のプロセスが停止した場合、子プロセスはSIGPIPEを取得するので停止すると思います。


    var pty = require('pty.js');

    //var term =
    pty.spawn('any_child_process', [/*any arguments*/], {
      name: 'xterm-color',
      cols: 80,
      rows: 30,
      cwd: process.cwd(),
      env: process.env
    });
    /*optionally you can install data handler
    term.on('data', function(data) {
      process.stdout.write(data);
    });
    term.write(.....);
    */

私は、ターミナル制御とセッションを悪用することにより、3つのプロセスを備えたポータブルな非ポーリングソリューションを実行することができました。これはメンタルオナニーですが、動作します。

トリックは次のとおりです:

  • プロセスAが開始されました
  • プロセスAはパイプPを作成します(パイプPからは読み取りません)
  • プロセスAはプロセスBに分岐します
  • プロセスBは新しいセッションを作成します
  • プロセスBは、その新しいセッションに仮想端末を割り当てます
  • プロセスBは、子が終了したときにSIGCHLDハンドラーをインストールして終了します
  • プロセスBはSIGPIPEハンドラーを設定します
  • プロセスBはプロセスCに分岐します
  • プロセスCは必要な処理を行います(たとえば、変更されていないバイナリをexec()するか、ロジックを実行します)
  • プロセスBはパイプPに書き込みます(そしてそのようにブロックします)
  • プロセスAはプロセスBでwait()し、終了すると終了します

その方法:

  • プロセスAが停止した場合:プロセスBはSIGPIPEを取得して停止します
  • プロセスBが停止した場合:プロセスAのwait()が戻って停止し、プロセスCがSIGHUPを取得します(接続された端末とのセッションのセッションリーダーが停止すると、フォアグラウンドプロセスグループのすべてのプロセスがSIGHUPを取得します)
  • プロセスCが停止した場合:プロセスBはSIGCHLDを取得して停止するため、プロセスAは停止します

短所:

  • プロセスCはSIGHUPを処理できません
  • プロセスCは別のセッションで実行されます
  • プロセスCは脆弱なセットアップを壊すため、セッション/プロセスグループAPIを使用できません
  • このような操作ごとに端末を作成することは、これまでで最高のアイデアではありません

7年が過ぎましたが、開発中にwebpack-dev-serverを起動する必要があり、バックエンドプロセスが停止したときにそれを強制終了する必要があるSpringBootアプリケーションを実行しているため、この問題が発生しました。

Runtime.getRuntime()。addShutdownHook を使用しようとしましたが、Windows 10では動作しましたが、Windows 7では動作しませんでした

プロセスが終了するのを待つ、または両方のWindowsバージョンで正常に動作する InterruptedException を待つ専用スレッドを使用するように変更しました。

private void startWebpackDevServer() {
    String cmd = isWindows() ? "cmd /c gradlew webPackStart" : "gradlew webPackStart";
    logger.info("webpack dev-server " + cmd);

    Thread thread = new Thread(() -> {

        ProcessBuilder pb = new ProcessBuilder(cmd.split(" "));
        pb.redirectOutput(ProcessBuilder.Redirect.INHERIT);
        pb.redirectError(ProcessBuilder.Redirect.INHERIT);
        pb.directory(new File("."));

        Process process = null;
        try {
            // Start the node process
            process = pb.start();

            // Wait for the node process to quit (blocking)
            process.waitFor();

            // Ensure the node process is killed
            process.destroyForcibly();
            System.setProperty(WEBPACK_SERVER_PROPERTY, "true");
        } catch (InterruptedException | IOException e) {
            // Ensure the node process is killed.
            // InterruptedException is thrown when the main process exit.
            logger.info("killing webpack dev-server", e);
            if (process != null) {
                process.destroyForcibly();
            }
        }

    });

    thread.start();
}

歴史的に、UNIX v7から、プロセスシステムはプロセスの親IDをチェックすることでプロセスの孤立を検出しました。私が言うように、歴史的に、 init(8)システムプロセスは、1つの理由だけで特別なプロセスです。新しい親プロセスIDの割り当てを処理するカーネルアルゴリズムはこの事実に依存するため、死ぬことはありません。プロセスがその exit(2)呼び出しを実行すると(プロセスシステムコールまたは信号を送信するなどの外部タスクによって)、カーネルはこのプロセスのすべての子にidを再割り当てします。親プロセスIDとしてのinitプロセス。これにより、最も簡単なテストになり、プロセスが孤立しているかどうかを知る最も移植性の高い方法になります。 getppid(2)システムコールの結果を確認し、それが init(2)プロセスのプロセスIDである場合、プロセスはシステムコールの前に孤立しました。

このアプローチから、問題につながる可能性のある2つの問題が発生します。

  • まず、 init プロセスを任意のユーザープロセスに変更する可能性があるので、initプロセスが常にすべての孤立プロセスの親になることを保証するにはどうすればよいでしょうか?さて、 exit システムコールコードには、呼び出しを実行しているプロセスがinitプロセス(pidが1に等しいプロセス)であるかどうかを確認する明示的なチェックがあり、その場合はカーネルパニック(プロセス階層を維持できなくなるはずです)。そのため、initプロセスが exit(2)呼び出しを行うことは許可されません。
  • 2番目に、上記の基本的なテストに競合状態があります。 InitプロセスのIDは、歴史的には 1 であると想定されていますが、POSIXアプローチでは保証されていません。これを行うPOSIX実装はほとんどありません。元のUNIX派生システムでは、 getppid(2)システムコールの応答として 1 があれば、プロセスが孤立していると見なすことができます。 。確認する別の方法は、フォークの直後に getppid(2)を作成し、その値を新しい呼び出しの結果と比較することです。両方の呼び出しがアトミックではないため、これは単にすべてのケースで機能するわけではなく、親プロセスは fork(2)の後、最初の getppid(2)システムコール。 process parent idは、親が exit(2)呼び出しを行うときに一度だけ変更されるため、 getppid(2) result親プロセスが終了したことを確認するために呼び出し間で変更このテストは、initプロセスの実際の子に対しては有効ではありません。なぜなら、それらは常にinit(8) `の子であるためです。しかし、これらのプロセスは、初期化プロセス)

これを行うLinux固有の別の方法は、新しいPID名前空間に親を作成することです。その名前空間ではPID 1になり、その名前空間を終了すると、そのすべての子が SIGKILL で即座に殺されます。

残念ながら、新しいPID名前空間を作成するには、 CAP_SYS_ADMIN が必要です。ただし、この方法は非常に効果的であり、親の最初の起動以降、親または子に実際の変更を加える必要はありません。

clone(2) pid_namespaces(7)、および unshare(2)

ライセンス: CC-BY-SA帰属
所属していません StackOverflow
scroll top