Runtime.getRuntime.exec()執行linux腳本導致程序卡死問題
問題:
在Java程序中,通過Runtime.getRuntime().exec()執行一個Linux腳本導致程序被掛住,而在終端上直接執行這個腳本則沒有任何問題。
原因:
先來看Java代碼:
public final static void process1(String[] cmdarray) {
Process p = null;
BufferedReader br = null;
try {
p = Runtime.getRuntime().exec(cmdarray);
br = new BufferedReader(new InputStreamReader(p.getInputStream()));
String line = null;
while ((line = br.readLine()) != null) {
System.out.println(line);
}
p.waitFor();
} catch (Exception e) {
e.printStackTrace();
} finally {
if (br != null) {
try {
br.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (p != null) {
p.destroy();
}
}
}
腳本內容很簡單,主要內容是將一個指定的tar.gz文件解壓到指定目錄中。
程序被掛住后,查看進程列表,發現了幾個可疑點:
neil@-bash:~/work/tgz$ps ux | grep dowjones
neil 2079 0.0 0.0 2435492 264 s001 R+ 10:56上午 0:00.00 egrep dowjones
neil 2077 0.0 0.0 2435080 652 ?? S 10:56上午 0:00.24 tar xvf dowjones.tar.gz
neil 2073 0.0 0.0 2435488 792 ?? S 10:56上午 0:00.00 /bin/bash /Users/neil/bin/genova/genova_crm.sh /Users/neil/work/tgz/dowjones.tar.gz /Users/neil/work/dest/dowj
其中genova_crm.sh 就是要執行的腳本,tar xvf dowjones.tar.gz 就是執行解壓的命令。
可以看到,程序卡在tar命令上,這個命令被掛住了,非常奇怪的事情。。。
再次查看JDK文檔,發現Process的文檔上說標准緩沖區大小有限,不正確操作輸入輸出流時可能導致程序掛住。
單獨執行tar xvf dowjones.tar.gz命令時,發現有N多輸出,而通過Java執行時,沒有看到那些輸出。
Java程序中只獲取了標准輸出流,沒有獲取錯誤輸出流,那么有可能是錯誤輸出緩沖區滿而導致tar命令掛住。
解決方法:
修改Java程序,標准輸出流與錯誤輸出流均要處理,保證輸出緩沖區不會被堵住。具體作法是用一個異步線程讀取標准輸出,讀完即扔,讓主線程讀取錯誤輸出流:
public final static void process1(String[] cmdarray) {
try {
final Process p = Runtime.getRuntime().exec(cmdarray);
new Thread(new Runnable() {
@Override
publicvoid run() {
BufferedReader br = new BufferedReader(
new InputStreamReader(p.getInputStream()));
try {
while (br.readLine() != null)
;
br.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}).start();
BufferedReader br = null;
br = new BufferedReader(new InputStreamReader(p.getErrorStream()));
String line = null;
while ((line = br.readLine()) != null) {
System.out.println(line);
}
p.waitFor();
br.close();
p.destroy();
} catch (Exception e) {
e.printStackTrace();
}
}
重新執行,發現程序可以正常執行了,tar命令的回顯被打印出來了。問題解決。
這可能跟特定的tar包有關,執行tar解壓時,明顯可以看到回顯字符串中有些亂碼,回顯全部被輸出到錯誤流了。
上述方法可以避免標准輸出或錯誤輸出緩沖區滿從而掛住主程序的問題,但是需要同時處理兩個流,有重復之嫌。如果能把標准輸出和錯誤輸出並為一個流,那只需要處理一個流即可。ProcessBuilder提供了這種能力。
創建Process有兩種方式,一種就是上述的通Runtime.exec來得到,還有一種可以通ProcessBuilder.start()來產生一個Process實例。
ProcessBuilder可以先設置必要的參數數據,如命令、環境變量、工作目錄、重定向錯誤流到標准輸出,然后start()根據這些參數來生成一個Process實例,啟動一個子進程來執行相應的命令。
代碼如下:
public final static void process(String[] cmdarray) throws Throwable {
ProcessBuilder pb = new ProcessBuilder(cmdarray);
pb.redirectErrorStream(true);
Process p = null;
BufferedReader br = null;
try {
p = pb.start();
br = new BufferedReader(new InputStreamReader(p.getInputStream()));
String line = null;
logger.info("Invoke shell: {}", StringUtils.join(cmdarray, " "));
while ((line = br.readLine()) != null) {
logger.info(line);
}
p.waitFor();
} finally {
if (br != null) {
br.close();
}
if (p != null) {
p.destroy();
}
}
}
public final static void process(String[] cmdarray) throws Throwable {
ProcessBuilder pb = new ProcessBuilder(cmdarray);
pb.redirectErrorStream(true);
Process p = null;
BufferedReader br = null;
try {
p = pb.start();
br = new BufferedReader(new InputStreamReader(p.getInputStream()));
String line = null;
logger.info("Invoke shell: {}", StringUtils.join(cmdarray, " "));
while ((line = br.readLine()) != null) {
logger.info(line);
}
p.waitFor();
} finally {
if (br != null) {
br.close();
}
if (p != null) {
p.destroy();
}
}
}
通過上述代碼可以看到,錯誤流被重定向到標准輸出流,那么程序只需要處理標准輸出就可以了。
而使用Runtime.getRuntime().exec() 調用外部程序, 獲取"標准輸出流", 老是阻塞. 在網上找了找, 覺得應該是"錯誤輸出流"的問題. 故為"錯誤輸出流"單開一個線程讀取之, "標准輸出流"就不再阻塞了。解決過程如下: