主进程未终止子进程:Docker容器中大量defunct ssh进程的解决方法
问题描述
我有一个每隔约5分钟执行一次ssh的进程,代码如下:
private String runProcess() throws IOException { ProcessBuilder pb = new ProcessBuilder("sh","-c","ssh hostname echo hello"); Process process = pb.start(); if(!process.waitFor(10, TimeUnit.SECONDS)) { LOGGER.info("Taking too long"); process.destroy(); } StringBuilder output = new StringBuilder(); BufferedReader reader = new BufferedReader( new InputStreamReader(process.getInputStream())); String line; while ((line = reader.readLine()) != null) { output.append(line).append('\n'); } int exitVal = process.waitFor(); if (exitVal != 0) { throw new RuntimeException(String.valueOf(exitVal)); } process.destroy(); return output.toString(); } /// more code to do something with output of the ssh.
目前ssh进程运行正常,能获取预期输出且退出值为0。但在代码运行的Docker容器中执行ps aux后,发现大量ssh命令进程处于'defunct'状态,推测这些是上述方法中启动进程的子进程。请问如何在该方法中同时终止这些子进程?
解决方案
1. 直接启动SSH进程,避免sh -c中转
问题根源在于你通过sh -c启动ssh,此时Java进程的子进程是sh,ssh是sh的子进程。当sh退出后,若Java未正确回收ssh的退出状态,就会产生僵尸进程。直接启动ssh可让Java成为ssh的父进程,waitFor()会自动处理其退出状态:
ProcessBuilder pb = new ProcessBuilder("ssh", "hostname", "echo", "hello");
2. 若必须用sh -c,销毁整个进程组
如果因命令逻辑复杂必须使用sh -c,可将sh及其子进程(ssh)归入同一进程组,销毁时直接杀掉整个进程组,同时确保回收所有进程的退出状态:
private String runProcess() throws IOException, InterruptedException { ProcessBuilder pb = new ProcessBuilder("sh", "-c", "ssh hostname echo hello"); // 合并错误流到输入流,避免进程因输出缓冲区满阻塞 pb.redirectErrorStream(true); Process process = pb.start(); // 读取输出(必须读取,否则进程可能挂起) StringBuilder output = new StringBuilder(); try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) { String line; while ((line = reader.readLine()) != null) { output.append(line).append('\n'); } } // 等待进程结束,超时则销毁整个进程组 if (!process.waitFor(10, TimeUnit.SECONDS)) { LOGGER.info("Taking too long"); // 获取进程PID,发送信号到整个进程组 long pid = ((ProcessHandle) process.toHandle()).pid(); // 先发送SIGTERM优雅终止,无效则换SIGKILL(信号9) Runtime.getRuntime().exec("kill -TERM -" + pid); // 等待进程组完全退出 process.waitFor(); } int exitVal = process.exitValue(); if (exitVal != 0) { throw new RuntimeException(String.valueOf(exitVal)); } return output.toString(); }
说明:kill -TERM -<pid>中的负号表示操作整个进程组,所有属于该组的子进程都会收到终止信号,后续waitFor()会回收它们的退出状态,不会产生僵尸进程。
3. 确保完整读取所有输出流
如果进程的错误流未被读取,可能导致进程阻塞无法正常退出,进而变成僵尸。务必通过redirectErrorStream(true)将错误流合并到输入流,或单独开启线程读取错误流,避免进程挂起。
内容的提问来源于stack exchange,提问作者MKRabbit
相关产品推荐
相关产品推荐

