碰到这样一个问题——用Java程序来控制shell脚本的运行和停止。具体来讲,这个Java程序至少要有三个功能:
运行Shell脚本;
等待Shell脚本执行结束;
停止运行中的Shell程序;
从功能需求来看,似乎是比较容易做到的。尽管没有写过类似功能的程序,Google一下,很快就有答案了。
用Runtime或者ProcessBuilder可以运行程序,而Process类的waitFor()和destroy()方法分别满足功能2和3。
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
public class ShellRunner extends Thread
{
private Process proc;
private String dir;
private String shell;
public ShellRunner(String dir, String shell)
{
super();
this.proc = null;
this.dir = dir;
this.shell = shell;
}
@Override
public void run() {
try
{
ProcessBuilder builder = new ProcessBuilder("sh", dir + shell);
builder.directory(new File(dir));
proc = builder.start();
System.out.println("Running ...");
int exitValue = proc.waitFor();
System.out.println("Exit Value: " + exitValue);
}
catch (IOException e)
{
e.getLocalizedMessage();
}
catch (InterruptedException e)
{
e.getLocalizedMessage();
}
}
public void kill()
{
if (this.getState() != State.TERMINATED) {
proc.destroy();
}
}
public static void main(String args[]) {
ShellRunner runner = new ShellRunner("/tmp/", "run.sh");
runner.start();
InputStreamReader inputStreamReader = new InputStreamReader(System.in);
BufferedReader reader = new BufferedReader(inputStreamReader);
try
{
String line = null;
while ( (line = reader.readLine()) != null ) {
if (line.equals("kill")) {
runner.kill();
}
else if (line.equals("break")) {
break;
}
else {
System.out.println(runner.getState());
}
}
reader.close();
inputStreamReader.close();
}
catch (IOException e)
{
e.printStackTrace();
}
}
}
跑一下上面这个测试程序,waitFor()方法可以正确等待shell程序退出,但是destroy()方法并没有结束shell脚本相关的进程。
为什么呢?
这是一个BUG。
JDK-bug-4770092:Process.destroy() 不能结束孙子进程(grandchildren)。上述例子中,java程序的子进程是”sh run.sh”,而shell脚本中的任何命令都是”sh run.sh”这个进程的子进程(也可能有孙子进程,或者更远的后代进程)。所以shell脚本中执行的命令并不能随着 Process.destroy()结束。这是一个很老的BUG,但是出于各个平台兼容性的考虑,官方并不准备修复这个BUG。似乎依赖Java程序来完 成功能3的路已经断了。
现在剩下的问题可以归结为:如何结束一颗进程树上的所有进程?其中的某些进程可能已经退出,也就是说进程树的某些分支可能已经断开了。
一个比较自然的想法是:记录所有以”sh run.sh”这个进程为根进程的所有进程号,需要的时候统一kill。这需要一点Linux进程的相关知识:
Linux下每个进程有很多ID属性:PID(进程号)、PPID(父进程号)、PGID(进程组号)、SID(进程所在session的ID)
子进程会继承父进程的进程组信息和会话信息
一个进程只能创建从属于(和它自身)同一个会话的进程组,除非使用setsid系统调用的方式新建一个会话
进程组不能在不同的会话中迁移,进程所属的进程组可以变,但是仅限于同一个会话中的进程组
还要一点Linux命令工具的知识:
用 “kill -9 -1234”可以杀死进程组号为1234的所有进程
strace命令可以跟踪某个进程以及它fork出来的所有子进程产生的系统调用
有了这些知识,思路就比较清晰了:用strace跟踪setsid这个系统调用,记下所有伴随系统调用产生的SID;在需要杀死这棵进程树上的所有进程时,用ps命令把进程树上还没退出的进程组全部找出,一并kill。
形成代码就是:
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
public class ShellRunner2 extends Thread
{
private Process proc;
private String dir;
private String shell;
private File tmpFile;
public ShellRunner2(String dir, String shell) throws IOException
{
super();
this.proc = null;
this.dir = dir;
this.shell = shell;
this.tmpFile = createTempFile(dir, shell);
}
@Override
public void run() {
try
{
ProcessBuilder builder = new ProcessBuilder("sh", "-c",
"strace -o " + tmpFile.getPath() + " -f -e trace=setsid setsid sh " + dir + shell);
builder.directory(new File(dir));
proc = builder.start();
System.out.println("Running ...");
int exitValue = proc.waitFor();
System.out.println("Exit Value: " + exitValue);
}
catch (IOException e)
{
e.getLocalizedMessage();
}
catch (InterruptedException e)
{
e.getLocalizedMessage();
}
}
public void kill()
{
if (this.getState() != State.TERMINATED) {
try
{
ProcessBuilder builder = new ProcessBuilder("sh", "-c",
"ps -o sid,pgid ax | " +
"grep $(grep -e \"setsid()\" -e \"<... setsid resumed>\" " +
tmpFile.getPath() +
" | awk ‘{printf \" -e\" $NF}‘) | awk {‘print $NF‘} | " +
"sort | uniq | sed ‘s/^/-/g‘ | xargs kill -9 2>/dev/null");
builder.directory(new File(dir));
Process proc = builder.start();
proc.waitFor();
}
catch (IOException e)
{
e.printStackTrace();
}
catch (InterruptedException e)
{
e.printStackTrace();
}
}
}
public static void main(String args[]) throws IOException {
ShellRunner2 runner = new ShellRunner2("/tmp/", "a.sh");
runner.start();
InputStreamReader inputStreamReader = new InputStreamReader(System.in);
BufferedReader reader = new BufferedReader(inputStreamReader);
try
{
String line = null;
while ( (line = reader.readLine()) != null ) {
if (line.equals("kill")) {
runner.kill();
}
else if (line.equals("break")) {
break;
}
else {
System.out.println(runner.getState());
}
}
reader.close();
inputStreamReader.close();
}
catch (IOException e)
{
e.printStackTrace();
}
}
private File createTempFile(String dir, String prefix) throws IOException {
String name = "." + prefix + "-" + System.currentTimeMillis();
File tempFile = new File(dir, name);
if (tempFile.createNewFile()) {
return tempFile;
}
throw new IOException("Failed to create file " + tempFile.getPath() + ".");
}
}
设计一个针对性的测试:
下面这个程序fork子程序并设置PGID,测试过成中编译为可执行文件pgid_test。
#include <unistd.h>
#include <sys/types.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
int main(int argc, void *argv[]) {
pid_t pid;
int slp = 10;
if (argc > 1) {
int tmp = atoi(argv[1]);
if (tmp > 0 && tmp < 120) {
slp = tmp;
}
}
if ((pid = fork()) != 0) {
if (setpgid(pid, 0) != 0) {
fprintf(stderr, "setpgid() error - %s", strerror(errno));
}
}
sleep(slp);
return 0;
}
{a,b,c,d}.sh这几个shell脚本之间的调用模拟了一个进程树的生成,并且这些进程有着不同的SID或者PGID。
a.sh
sh b.sh &(sleep 15)&sleep 10
b.sh
(sleep 12)&sh c.sh &
c.sh
./pgid_test 20 &setsid sh d.sh &setsid sh d.sh &
d.sh
./pgid_test 30 &(sleep 30; echo "bad killer " `date` >> /tmp/bad_killer) &
运行java程序,调用kill()后,所有子进程都被成功结束了。
这个方法并不那么优美,也有可能存在问题:
strace的输出sesion id的形式可能不止那么两种;不同版本的strace的输出可能不一样。
shell程序可能以其他用户身份启动了一些进程,而kill又没有权限杀死那些进程。
在ps命令执行之后,kill执行之前,正好有setsid调用,这个调用产生的session的相关进程会被漏掉。这个问题可以通过多次执行解决。
原文:http://my.oschina.net/laigous/blog/531637