当父进程退出时,我如何使子进程退出?

49

我正在使用ProcessBuilder启动一个子进程,并且需要在父进程退出时子进程也退出。通常情况下,我的代码可以正确停止子进程。但是如果我导致操作系统杀死父进程,子进程将继续运行。

有没有办法将子进程与父进程"绑定"在一起,使得当父进程被杀死时子进程也会退出?


类似问题:

10个回答

43

虽然您无法保护自己免受硬件中止(例如在Unix上的SIGKILL)的影响,但可以防范其他导致父进程关闭(例如SIGINT)并清理子进程的信号。 您可以通过使用关机挂钩来实现:请参见Runtime#addShutdownHook,以及一个相关的SO问题此处

您的代码可能看起来像这样:

String[] command;
final Process childProcess = new ProcessBuilder(command).start();

Thread closeChildThread = new Thread() {
    public void run() {
        childProcess.destroy();
    }
};

Runtime.getRuntime().addShutdownHook(closeChildThread); 

3
Runtime.getRuntime().addShutdownHook(new Thread(childProcess::destroy)); - Carsen Daniel Yates
对于我来说,在Groovy中这是:Process proc = cmd.execute() addShutdownHook { proc.destroy() } - Shane Gannon

20

子进程与父进程之间没有强连接。虽然它们可能知道彼此的进程ID,但它们之间没有硬连接。你所说的是一个孤儿进程,这是一个操作系统级别的问题。这意味着任何解决方案可能都与平台有关。

我能想到的唯一方法就是让子进程定期检查其父进程的状态,如果父进程关闭,则退出。不过我认为这并不是很可靠的方法。


12
如果您定期检查父进程的状态,以查看子进程的PPID是否变为1,则可以可靠地进行轮询,因为孤儿进程的PPID会变为init进程(PID=1)。 - Aaron
1
如果有人想知道,在Android系统中,当父进程死亡时,pid似乎是0(进程System pid),而不是1。 - Rui Marques
11
我刚刚实现了“父进程状态检查”的功能,有一个简单的改进 - 我只是将原始的父进程ID传递给子进程,让它们检查是否保持相同。这样无论是init、systemd或任何pid数值都可以使用该方法。 - Jan Spurny
3
如果我们启动一些第三方进程,例如Word、Excel等,我们无法编写这些子进程来轮询其父进程,对吗? - NDestiny

19

在子进程中,你可以简单地启动一个从System.in读取的线程。如果你没有向子进程的标准输入写入数据,那么也没有其他人会这样做,这个线程会被“永久”阻塞。但是如果父进程被杀死或以其他方式死亡,你将收到EOF(或异常)。因此,你也可以关闭子进程。(使用java.lang.ProcessBuilder启动子进程)


这是一个很好的解决方案。如果阅读操作在一个线程中进行,请记得将线程设置为守护线程(true),否则线程会阻止程序正常关闭。 - Esben Skov Pedersen
这个解决方案的开销是多少? - ed22

9

提供一种类似于@tomkri回答的黑客方式,并提供演示代码。

如果您的子进程不需要使用输入流,请将子进程的输入流重定向到其父进程的输入流。然后在子进程中添加一个线程来始终读取输入流,当该线程无法从输入流中读取任何内容时,该子进程退出。因此,父进程退出->父进程的输入流不存在->子进程的输入流不存在->子进程退出。

这是全部使用Java编写的演示代码。

父进程:

package process.parent_child;

import java.io.File;
import java.io.IOException;
import java.lang.ProcessBuilder.Redirect;

public class ParentProc {

    public static void main(String[] args) {
        System.out.println("I'm parent.");

        String javaHome = System.getProperty("java.home");
        String javaBin = javaHome + File.separator + "bin" + File.separator + "java";
        ProcessBuilder builder = new ProcessBuilder(javaBin, "process.parent_child.ChildProc");

        // Redirect subprocess's input stream to this parent process's input stream.
        builder.redirectInput(Redirect.INHERIT);
        // This is just for see the output of child process much more easily.
        builder.redirectOutput(Redirect.INHERIT);
        try {
            Process process = builder.start();
            Thread.sleep(5000);
        } catch (IOException e) {
            e.printStackTrace();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Parent exits.");
    }
}

子进程:



package process.parent_child;

import java.io.IOException;
import java.util.Scanner;

public class ChildProc {


    private static class StdinListenerThread extends Thread {

        public void run() {
            int c;
            try {
                c = System.in.read();
                while ( c != -1 ) {
                    System.out.print(c);
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
            System.out.println("\nChild exits.");
            System.exit(0);
        }
    }

    public static void main(String[] args) throws InterruptedException {
        System.out.println("I'm child process.");
        StdinListenerThread thread = new StdinListenerThread();
        thread.start();
        Thread.sleep(10000);
    }
}

执行以下命令运行此父进程:

    java process.parent_child.ParentProc

你会看到
    I'm parent.
    I'm child process.
    Parent exits.
    xmpy-mbp:bin zhaoxm$
    Child exits

父进程退出时,子进程立即退出。

很棒的解决方案,非常有帮助。 - user4559332
1
这个很好用,可以让你优雅地退出并避免在父Java进程被Eclipse(例如)使用SIGKILL终止时出现子Java进程的孤儿问题。 - Jon N

4

正如您所发现的那样,操作系统允许您解决此问题。相反,创建一个被两个进程共享的资源。当父进程放弃该资源时,子进程会响应并关闭。例如:

在父进程上创建一个带有服务器端TCP/IP套接字的线程,以随机高端口号为接受模式。当子进程启动时,将端口号作为参数传递(环境变量、数据库条目等)。让它创建一个线程并打开该套接字。然后让线程永远停留在套接字上。如果连接断开,使子进程退出。

或者

在父进程上创建一个不断更新文件更新日期的线程。(更新频率取决于您需要杀死和关闭之间的粒度。)子进程有一个线程来监视同一文件的更新时间。如果在特定时间间隔后它没有更新,则自动关机。


2

对于单个子进程,您可以通过反转子/父关系来管理此过程。请参见我的答案以回答该问题的较新版本。

Translated: 对于单个子进程,您可以通过反转子/父关系来进行管理。请参考我在此问题的后续版本中的回答。

1
对于Windows,我想出了这个投票黑客攻击:
static int getPpid(int pid) throws IOException {
    Process p = Runtime.getRuntime().exec("C:\\Windows\\System32\\wbem\\WMIC.exe process where (processid="+pid+") get parentprocessid");
    BufferedReader br = new BufferedReader(new InputStreamReader(p.getInputStream()));
    br.readLine();
    br.readLine();
    String ppid= br.readLine().replaceAll(" ","");
    return Integer.parseInt(ppid);
}
static boolean shouldExit() {
    try {
        String pid = ManagementFactory.getRuntimeMXBean().getName().split("@")[0];
        int ppid = getPpid(Integer.parseInt(pid));
        /* pppid */ getPpid(ppid);
    } catch (Exception e) {
        return true;
    } 
    return false;
}

0
尝试在父进程停止时向子进程发送终止信号。

1
杀死一个进程通常不允许进行任何清理。这正是问题所在。 - sblundy
这取决于您如何终止进程以及在哪个操作系统下。在Unix上,使用SIGTERM允许程序进行清理,而使用SIGKILL则不允许。 - Powerlord
你没有回答“如何”这个问题;https://dev59.com/EnVC5IYBdhLWcg3whBej#272728已经回答了(而且我认为它应该成为被接受的答案)。 - mirabilos

-1

一旦我们获得了父进程的进程ID(PID),我们就可以杀死所有子进程。我们可以使用Windows的taskkill命令来杀死由相应PID启动的任何进程。

您可以根据您的用例从以下帖子中找到PID:https://stackoverflow.com/Questions/4750470/how-to-get-pid-of-process-ive-just-started-within-java-program

一旦您拥有PID,您所需要做的就是通过发送以下命令要求操作系统为您杀死该进程:"taskkill /F /T /PID P_id"。在此处将P_id替换为您的进程ID。这里/T会杀死相应PID的进程树中的所有进程。

您可以使用java.lang.Runtime.exec函数执行上述命令。

PS:此解决方案仅适用于Windows操作系统。


-1

根据我的测试,如果父进程被杀死,那么子进程的ppid将变为1。因此,我们可能可以杀死任何ppid = 1的进程。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接