利用Python+Java调用Shell脚本时的死锁陷阱详解
前言
最近有一项需求,要定时判断任务执行条件是否满足并触发Spark任务,平时编写Spark任务时都是封装为一个Jar包,然后采用Shell脚本形式传入所需参数执行,考虑到本次判断条件逻辑复杂,只用Shell脚本完成不利于开发测试,所以调研使用了Python和Java分别调用Spark脚本的方法。
使用版本为Python3.6.4及JDK8
Python
主要使用subprocess库。Python的API变动比较频繁,在3.5之后新增了run方法,这大大降低了使用难度和遇见Bug的概率。
subprocess.run(["ls","-l"]) subprocess.run(["sh","/path/to/your/script.sh","arg1","arg2"])
为什么说使用run方法可以降低遇见Bug的概率呢?
在没有run方法之前,我们一般调用其他的高级方法,即Olderhigh-levelAPI,比如call,check_all,或者直接创建Popen对象。因为默认的输出是console,这时如果对API不熟悉或者没有仔细看doc,想要等待子进程运行完毕并获取输出,使用了stdout=PIPE再加上wait的话,当输出内容很多时会导致Buffer写满,进程就一直等待读取,形成死锁。在一次将Spark的log输出到console时,就遇到了这种奇怪的现象,下边的脚本可以模拟:
#a.sh foriin{0..9999};do echo'***************************************************' done
p=subprocess.Popen(['sh','a.sh'],stdout=subprocess.PIPE) p.wait()
而call则在方法内部直接调用了wait产生相同的效果。
要避免死锁,则必须在wait方法调用之前自行处理掉输入输出,或者使用推荐的communicate方法。communicate方法是在内部生成了读取线程分别读取stdoutstderr,从而避免了Buffer写满。而之前提到的新的run方法,就是在内部调用了communicate。
stdout,stderr=process.communicate(input,timeout=timeout)
Java
说完了Python,Java就简单多了。
Java一般使用Runtime.getRuntime().exec()或者ProcessBuilder调用外部脚本:
Processp=Runtime.getRuntime().exec(newString[]{"ls","-al"}); Scannersc=newScanner(p.getInputStream()); while(sc.hasNextLine()){ System.out.println(sc.nextLine()); } //or Processp=newProcessBuilder("sh","a.sh").start(); p.waitFor();//deadlock
需要注意的是:这里stream的方向是相对于主程序的,所以getInputStream()就是子进程的输出,而getOutputStream()是子进程的输入。
基于同样的Buffer原因,假如调用了waitFor方法等待子进程执行完毕而没有及时处理输出的话,就会造成死锁。
由于JavaAPI很少变动,所以没有像Python那样提供新的run方法,但是开源社区也给出了自己的方案,如commonsexec,或http://www.baeldung.com/run-shell-command-in-java,或alvinalexander给出的方案(虽然不完整)。
//commonsexec,要想获取输出的话,相比python来说要复杂一些 CommandLinecommandLine=CommandLine.parse("sha.sh"); ByteArrayOutputStreamout=newByteArrayOutputStream(); PumpStreamHandlerstreamHandler=newPumpStreamHandler(out); Executorexecutor=newDefaultExecutor(); executor.setStreamHandler(streamHandler); executor.execute(commandLine); Stringoutput=newString(out.toByteArray());
但其中的思想和Python都是统一的,就是在后台开启新线程读取子进程的输出,防止Buffer写满。
另一个统一思想的地方就是,都推荐使用数组或list将输入的shell命令分隔成多段,这样的话就由系统来处理空格等特殊字符问题。
参考:
https://dcreager.net/2009/08/06/subprocess-communicate-drawbacks/https://alvinalexander.com/java/java-exec-processbuilder-process-1https://www.javaworld.com/article/2071275/core-java/when-runtime-exec—won-t.html
总结
以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对毛票票的支持。