澳门正规赌博十大网站-澳门游戏网站
做最好的网站

Java调用Shell脚本时的死锁陷阱详解,调用外部命

前言

利用Python Java调用Shell脚本时的死锁陷阱详解,pythonshell

前言

最近有一项需求,要定时判断任务执行条件是否满足并触发 Spark 任务,平时编写 Spark 任务时都是封装为一个 Jar 包,然后采用 Shell 脚本形式传入所需参数执行,考虑到本次判断条件逻辑复杂,只用 Shell 脚本完成不利于开发测试,所以调研使用了 Python 和 Java 分别调用 Spark 脚本的方法。

使用版本为 Python 3.6.4 及 JDK 8

PythonJava调用Shell脚本时的死锁陷阱详解,调用外部命令。

主要使用 subprocess 库。Python 的 API 变动比较频繁,在 3.5 之后新增了 run 方法,这大大降低了使用难度和遇见 Bug 的概率。

subprocess.run(["ls", "-l"])
subprocess.run(["sh", "/path/to/your/script.sh", "arg1", "arg2"])

为什么说使用 run 方法可以降低遇见 Bug 的概率呢?

在没有 run 方法之前,我们一般调用其他的高级方法,即 Older high-level API,比如 call,check_all,或者直接创建 Popen 对象。因为默认的输出是 console,这时如果对 API 不熟悉或者没有仔细看 doc,想要等待子进程运行完毕并获取输出,使用了 stdout = PIPE 再加上 wait 的话,当输出内容很多时会导致 Buffer 写满,进程就一直等待读取,形成死锁。在一次将 Spark 的 log 输出到 console 时,就遇到了这种奇怪的现象,下边的脚本可以模拟:

# a.sh
for i in {0..9999}; do
 echo '***************************************************'
done 

p = subprocess.Popen(['sh', 'a.sh'], stdout=subprocess.PIPE)
p.wait()

而 call 则在方法内部直接调用了 wait 产生相同的效果。

要避免死锁,则必须在 wait 方法调用之前自行处理掉输入输出,或者使用推荐的 communicate 方法。 communicate 方法是在内部生成了读取线程分别读取 stdout stderr,从而避免了 Buffer 写满。而之前提到的新的 run 方法,就是在内部调用了 communicate。

stdout, stderr = process.communicate(input, timeout=timeout)

Java

说完了 Python,Java 就简单多了。

Java 一般使用 Runtime.getRuntime().exec() 或者 ProcessBuilder 调用外部脚本:

Process p = Runtime.getRuntime().exec(new String[]{"ls", "-al"});
Scanner sc = new Scanner(p.getInputStream());
while (sc.hasNextLine()) {
 System.out.println(sc.nextLine());
}
// or
Process p = new ProcessBuilder("sh", "a.sh").start(); 
p.waitFor(); // dead lock 

需要注意的是:这里 stream 的方向是相对于主程序的,所以 getInputStream()就是子进程的输出,而 getOutputStream()是子进程的输入。

Java调用Shell脚本时的死锁陷阱详解,调用外部命令。基于同样的 Buffer 原因,假如调用了 waitFor 方法等待子进程执行完毕而没有及时处理输出的话,就会造成死锁。
由于 Java API 很少变动,所以没有像 Python 那样提供新的 run 方法,但是开源社区也给出了自己的方案,如commons exec,或 alvin alexander 给出的方案(虽然不完整)。

// commons exec,要想获取输出的话,相比 python 来说要复杂一些
CommandLine commandLine = CommandLine.parse("sh a.sh");

ByteArrayOutputStream out = new ByteArrayOutputStream();
PumpStreamHandler streamHandler = new PumpStreamHandler(out);

Executor executor = new DefaultExecutor();
executor.setStreamHandler(streamHandler);
executor.execute(commandLine);

String output = new String(out.toByteArray());

但其中的思想和 Python 都是统一的,就是在后台开启新线程读取子进程的输出,防止 Buffer 写满。

另一个统一思想的地方就是,都推荐使用数组或 list 将输入的 shell 命令分隔成多段,这样的话就由系统来处理空格等特殊字符问题。

参考:

Java调用Shell脚本时的死锁陷阱详解,调用外部命令。

Java调用Shell脚本时的死锁陷阱详解,调用外部命令。总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对帮客之家的支持。

Java调用Shell脚本时的死锁陷阱详解,调用外部命令。 前言 最近有一项需求,要定时判断任务执行条件是否满足并触发 Spark 任务,平时编...

Python中可以执行shell命令的相关模块和函数有:

Java调用Shell脚本时的死锁陷阱详解,调用外部命令。最近有一项需求,要定时判断任务执行条件是否满足并触发 Spark 任务,平时编写 Spark 任务时都是封装为一个 Jar 包,然后采用 Shell 脚本形式传入所需参数执行,考虑到本次判断条件逻辑复杂,只用 Shell 脚本完成不利于开发测试,所以调研使用了 Python 和 Java 分别调用 Spark 脚本的方法。

os.system
os.spawn*
os.popen*          --废弃
popen2.*           --废弃
commands.*      --废弃,3.x中被移除

使用版本为 Python 3.6.4 及 JDK 8

随着Python版本的更新,过多的模块引起代码的复杂与冗余,因此Python新引入了一个模块subprocess,将以上几个模块中的功能集中到它当中,以后我们只需import这一个即可。

Python

1、subprocess模块

subprocess的目的就是启动一个新的进程并且与之通信。

  • 运行python的时候,我们都是在创建并运行一个进程。像Linux进程那样,一个进程可以fork一个子进程,并让这个子进程exec另外一个程序。在Python中,我们通过标准库中的subprocess包来fork一个子进程,并运行一个外部的程序。
  • subprocess包中定义有数个创建子进程的函数,这些函数分别以不同的方式创建子进程,所以我们可以根据需要来从中选取一个使用。另外subprocess还提供了一些管理标准流(standard stream)和管道(pipe)的工具,从而在进程间使用文本通信。

主要使用 subprocess 库。Python 的 API 变动比较频繁,在 3.5 之后新增了 run 方法,这大大降低了使用难度和遇见 Bug 的概率。

1.1 call

父进程等待子进程执行命令,返回子进程执行命令的状态码(returncode,相当于Linux exit code),如果出现错误,不进行报错。

  • 这里说的返回执行命令的状态码的意思是:如果我们通过一个变量 res = subprocess.call(['dir',shell=True]) 获取的执行结果,我们能获取到的是子进程执行命令执行结果的状态码,即res=0/1 执行成功或者不成功,并不代表说看不到执行结果,在Python的console界面中我们是能够看到命令结果的,只是获取不到。想获取执行的返回结果,请看check_output。
  • 不进行报错解释:如果我们执行的命令在执行时,操作系统不识别,系统会返回一个错误,如:abc命令不存在,这个结果会在console界面中显示出来,但是我们的Python解释器不会提示任何信息,如果想让Python解释器也进行报错,请看check_call
    示例
#!/usr/bin/env python
# -*- coding:utf-8 -*-
import subprocess
print "1. ################## subprocess.call ###############"
print u"call方法调用系统命令进行执行,如果出错不报错"
subprocess.call(['dir'],shell=True)

:shell默认为False,在Linux下,shell=False时, Popen调用os.execvp()执行args指定的程序;shell=True时,如果args是字符串,Popen直接调用系统的Shell来执行args指定的程序,如果args是一个序列,则args的第一项是定义程序命令字符串,其它项是调用系统Shell时的附加参数。
  在Windows下,不论shell的值如何,Popen调用CreateProcess()执行args指定的外部程序。如果args是一个序列,则先用list2cmdline()转化为字符串,但需要注意的是,并不是MS Windows下所有的程序都可以用list2cmdline来转化为命令行字符串。在windows下,调用脚本时要写上shell=True。

subprocess.run(["ls", "-l"])
subprocess.run(["sh", "/path/to/your/script.sh", "arg1", "arg2"])

1.2 check_call

父进程等待子进程执行命令,返回执行命令的状态码,如果出现错误,进行报错。如果returncode不为0,则举出错误subprocess.CalledProcessError,该对象包含有returncode属性,可用try…except…来检查。
示例

#!/usr/bin/env python
# -*- coding:utf-8 -*-

import subprocess

print "2. ################## subprocess.check_call ##########"
print u"check_call与call命令相同,区别是如果出错会报错"
subprocess.check_call(['dir'],shell=True)
subprocess.check_call(['abc'],shell=True)
print u"call方法与check_call方法都知识执行并打印命令到输出终端,但是 
 获取不到,如果想获取到结果使用check_output"

为什么说使用 run 方法可以降低遇见 Bug 的概率呢?

1.3 check_output

父进程等待子进程执行命令,返回子进程向标准输出发送输出运行结果,检查退出信息,如果returncode不为0,则举出错误subprocess.CalledProcessError,该对象包含有returncode属性和output属性,output属性为标准输出的输出结果,可用try…except…来检查。
示例

#!/usr/bin/env python
# -*- coding:utf-8 -*-

import subprocess

print "3. ################## subprocess.check_output ##############"
res1 = subprocess.call(['dir'],shell=True)
res2 = subprocess.check_call(['dir'],shell=True)
res3 = subprocess.check_output(['dir'],shell=True)
print u"call结果:",res1
print u"check_call结果:",res2
print u"check_output结果:n",res3

可见,call/check_call 返回值均是命令的执行状态码,而check_output返回值是命令的执行结果。
如果在执行相关命令时,命令后带有参数,将程序名(即命令)和所带的参数一起放在一个列表中传递给相关方法即可,例如:

import subprocess

retcode = subprocess.call(["ls", "-l"])
print retcode

在没有 run 方法之前,我们一般调用其他的高级方法,即 Older high-level API,比如 call,check_all,或者直接创建 Popen 对象。因为默认的输出是 console,这时如果对 API 不熟悉或者没有仔细看 doc,想要等待子进程运行完毕并获取输出,使用了 stdout = PIPE 再加上 wait 的话,当输出内容很多时会导致 Buffer 写满,进程就一直等待读取,形成死锁。在一次将 Spark 的 log 输出到 console 时,就遇到了这种奇怪的现象,下边的脚本可以模拟:

1.4 Popen

实际上,subprocess模块中只定义了一个类: Popen。上面的几个函数都是基于Popen()的封装(wrapper)。从Python2.4开始使用Popen来创建进程,用于连接到子进程的标准输入/输出/错误中去,还可以得到子进程的返回值。这些封装的目的在于让我们容易使用子进程。当我们想要更个性化我们的需求的时候,就要转向Popen类,该类生成的对象用来代表子进程。
构造函数如下:

subprocess.Popen(args, bufsize=0, executable=None, stdin=None, 
 stdout=None, stderr=None, preexec_fn=None, close_fds=False,  
 shell=False, cwd=None, env=None, universal_newlines=False,  
 startupinfo=None, creationflags=0)
  • 参数args可以是字符串或者序列类型(如:list,元组),用于指定进程的可执行文件及其参数。如果是序列类型,第一个元素通常是可执行文件的路径。我们也可以显式的使用executeable参数来指定可执行文件的路径。
  • 参数stdin, stdout, stderr分别表示程序的标准输入、输出、错误句柄。他们可以是PIPE,文件描述符或文件对象,也可以设置为None,表示从父进程继承。
  • 如果参数shell设为true,程序将通过shell来执行。
  • 参数env是字典类型,用于指定子进程的环境变量。如果env = None,子进程的环境变量将从父进程中继承。

与上面的封装不同,Popen对象创建后,主程序不会自动等待子进程完成。我们必须调用对象的wait()方法,父进程才会等待 (也就是阻塞block)。
示例

#!/usr/bin/env python
import subprocess

child = subprocess.Popen(['ping','-c','4','www.baidu.com'])  #创建一个子进 程,进程名为child,执行操作ping -c 4 www.baidu.com
child.wait()  #子进程等待
print 'hello'

此外也可以在父进程中对子进程进行其它操作。

child.poll() # 用于检查子进程是否已经结束。设置并返回returncode属性。
child.wait() # 等待子进程结束。设置并返回returncode属性。

Popen.communicate(input=None) 
# 与子进程进行交互。向stdin发送数 据,或从stdout和stderr中读取数  
据。可选参数input指定发送到子进程的参数。Communicate()返回一个元 
组:(stdoutdata, stderrdata)。注意:如果希望通过进程的stdin向其发送  
数据,在创建Popen对象的时候,参数 stdin必须被设置为PIPE。同样, 
如果希望从stdout和stderr获取数据,必 须将stdout和stderr设置为PIPE。

child.kill() # 终止子进程
child.send_signal() # 向子进程发送信号
child.terminate() # 终止子进程
child.pid # 获取子进程的进程ID。
child.returncode # 获取进程的返回值。如果进程还没有结束,返回None。

子进程文本流控制
子进程的标准输入、标准输出和标准错误如下属性分别表示:
child.stdin | child.stdout | child.stderr
我们还可以在Popen()建立子进程的时候改变标准输入、标准输出和标准错误,并可以利用subprocess.PIPE将多个子进程的输入和输出连接在一起,构成管道(pipe),如下2个例子:
例1

#!/usr/bin/env python
import subprocess

child = subprocess.Popen(['ls','-l'],stdout=subprocess.PIPE)  #将标准输出定向输出到subprocess.PIPE
print child.stdout.read()   #使用 child.communicate()  也可

例2

#!/usr/bin/env python
import subprocess

child1 = subprocess.Popen(['cat','/etc/passwd'],stdout=subprocess.PIPE)
child2 = subprocess.Popen(['grep','root'],stdin=child1.stdout,stdout=subprocess.PIPE)

print child2.communicate()

subprocess.PIPE实际上为文本流提供一个缓存区。child1的stdout将文本输出到缓存区,随后child2的stdin从该PIPE中将文本读取走。child2的输出文本也被存放在PIPE中,直到communicate()方法从PIPE中读取出PIPE中的文本。
注意:communicate()是Popen对象的一个方法,该方法会阻塞父进程,直到子进程完成

# a.sh
for i in {0..9999}; do
 echo '***************************************************'
done 


p = subprocess.Popen(['sh', 'a.sh'], stdout=subprocess.PIPE)
p.wait()

1.5 进程通信

如果想得到进程的输出,管道是个很方便的方法。

  • 简单情况

例1

p=subprocess.Popen("dir", shell=True)  
p.wait()  

subprocess.call(["ls", "-l"])

subprocess.call("exit 1", shell=True)

shell参数根据你要执行的命令的情况来决定,上面是dir命令,就一定要shell=True了,p.wait()可以得到命令的返回值。
如果上面写成a=p.wait(),a就是returncode。那么输出a的话,有可能就是0【表示执行成功】。

  • 分开输出

例2

p=subprocess.Popen("dir", shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)  
(stdoutput,erroutput) = p.communicate()  

p.communicate会一直等到进程退出,并将标准输出和标准错误输出返回,这样就可以得到子进程的输出了。

  • 合并输出

例3

p=subprocess.Popen("dir", shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)  
(stdoutput,erroutput) = p.communicate()  

标准输出和标准错误输出是分开的,也可以合并起来,只需要将stderr参数设置为subprocess.STDOUT就可以了。

  • 逐行输出

例4

p=subprocess.Popen("dir", shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)  
while True:  
    buff = p.stdout.readline()  
    if buff == '' and p.poll() != None:  
        break  
  • 死锁

如果使用了管道,而又不去处理管道的输出,如果子进程输出数据过多,死锁就会发生了。
例5

p=subprocess.Popen("longprint", shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)  
p.wait() 

longprint是一个假想的有大量输出的进程,假设在xp, Python2.5的环境下,当输出达到4096时,死锁就发生了。当然,如果我们用p.stdout.readline或者p.communicate去清理输出,那么无论输出多少,死锁都是不会发生的。或者我们不使用管道,比如不做重定向,或者重定向到文件,也都是可以避免死锁的。

  • 多命令执行

在shell中我们知道,想要连接多个命令可以使用管道。
在subprocess中,可以使用上一个命令执行的输出结果作为下一次执行的输入。

本文由澳门正规赌博十大网站发布于澳门游戏网站,转载请注明出处:Java调用Shell脚本时的死锁陷阱详解,调用外部命

相关阅读