openjdk源码-java是如何执行shell命令的
一般我们在java中调用shell脚本的方式如下
public int executeLinuxCmd(String cmd) { LOGGER.info("cmd:{}", cmd); Runtime run = Runtime.getRuntime(); try { Process process = run.exec(cmd); InputStream in = process.getInputStream(); BufferedReader bs = new BufferedReader(new InputStreamReader(in)); StringBuffer out = new StringBuffer(); byte[] b = new byte[8192]; for (int n; (n = in.read(b)) != -1;) { out.append(new String(b, 0, n)); } LOGGER.info("job result:{}", out.toString()); in.close(); int exitcode = process.waitFor(); LOGGER.info("exitcode:{}", exitcode); process.destroy(); if(exitcode == 0) { return 1; } return -1; } catch (IOException e) { LOGGER.error("e",e); } catch (InterruptedException e) { LOGGER.error("e",e); } return -1; }
下面我们分析一下,其调用过程
1 执行线
首先我们创建了一个RunTime类
Runtime run = Runtime.getRuntime();
这是java.lang下的一个类,每个java进程都会有一个RuntTime实例,其中为我们提供了一些在java进程外执行系统命令的api。
然后,执行如下代码执行这个cmd命令
Process process = run.exec(cmd);
这个代码会创建一个新的进程,然后在新进程中执行这个cmd命令
下面的输入流作用是从执行的shell命令的输出中读取数据
InputStream in = process.getInputStream();
然后将输出写到输出流,进而打印出这个输出。
下面的代码作用是等待子进程执行完并获取子进程的退出码。
int exitcode = process.waitFor();
2 run.exec(cmd)做了啥
run.exec(cmd)调用的是RunTime下的方法,代码如下
public Process exec(String command) throws IOException { return exec(command, null, null); }
进而调用(我们只需看最后一行)
public Process exec(String command, String[] envp, File dir) throws IOException { if (command.length() == 0) throw new IllegalArgumentException("Empty command"); StringTokenizer st = new StringTokenizer(command);// 该类是字符串分割器,将会把xxx.sh aaa bbb分割成数组[xxx.sh,aaa,bbb] String[] cmdarray = new String[st.countTokens()]; for (int i = 0; st.hasMoreTokens(); i++) cmdarray[i] = st.nextToken(); return exec(cmdarray, envp, dir);// 第一个参数是数组[xxx.sh,aaa,bbb],后两个参数都是null }
进而执行,这里会创建一个ProcessBuilder对象,然后调用其start方法
public Process exec(String[] cmdarray, String[] envp, File dir) throws IOException { return new ProcessBuilder(cmdarray) .environment(envp) .directory(dir) .start(); }
我们看一下start方法
public Process start() throws IOException { String[] cmdarray = command.toArray(new String[command.size()]); cmdarray = cmdarray.clone(); String prog = cmdarray[0]; String dir = directory == null ? null : directory.toString(); try { return ProcessImpl.start(cmdarray, // 除了cmdarray(为[xxx.sh,aaa,bbb])和redirectErrorStream(为false)其他参数都是null。 environment, dir, redirects, redirectErrorStream); } catch (IOException | IllegalArgumentException e) { } }
然后执行ProcessImpl的如下方法
static Process start(String cmdarray[], java.util.Map<String,String> environment, String dir, ProcessBuilder.Redirect[] redirects, boolean redirectErrorStream) throws IOException { String envblock = ProcessEnvironment.toEnvironmentBlock(environment);//为null FileInputStream f0 = null;//标准输入 FileOutputStream f1 = null;//标准输出 FileOutputStream f2 = null;//标准错误输出 try { long[] stdHandles = new long[] { -1L, -1L, -1L };//此处有个if分支根据redirects参数处理标准输入输出重定向,这里删掉了。 return new ProcessImpl(cmdarray, envblock, dir, stdHandles, redirectErrorStream); //我们重点看这里,执行简单命令,只有cmdarray(为[xxx.sh,aaa,bbb])和stdHandles以及redirectErrorStream(为false)有值,其他都为null } finally { //处理f0,f1,f2的关闭工作,这里删除。 } }
我们接着往下看,下面代码将调用native方法创建进程并执行cmd
private ProcessImpl(String cmd[], final String envblock, final String path, final long[] stdHandles, final boolean redirectErrorStream) throws IOException { String cmdstr = createCommandLine( VERIFICATION_LEGACY, executablePath, cmd); //要执行的cmd字符串 handle = create(cmdstr, envblock, path, stdHandles, redirectErrorStream);//调用native函数创建新进程并执行cmd }
3 深入到openjdk的c程序
以最新的代码为例jdk-23为例说明,github地址:https://github.com/openjdk/jdk
linux相关代码在如下路径:
- jdk-master\src\java.base\unix\native\libjava\ProcessImpl_md.c
- jdk-master\src\java.base\unix\native\libjava\childproc.c
在ProcessImpl_md.c中,调用的是如下方法
static pid_t forkChild(ChildStuff *c) { //参数是一个结构体,包含了要执行的cmd命令 pid_t resultPid; resultPid = fork(); if (resultPid == 0) { //只有子进程才会进入if分支 childProcess(c); //子进程中执行该方法 } assert(resultPid != 0); /* childProcess never returns */ return resultPid; }
我们看看childProcess()方法
int childProcess(void *arg)//参数是一个结构体,包含了要执行的cmd命令 { const ChildStuff* p = (const ChildStuff*) arg; int fail_pipe_fd = p->fail[1]; JDK_execvpe(p->mode, p->argv[0], p->argv, p->envv); return 0; }
我们接着往下看JDK_execvpe函数
void JDK_execvpe(int mode, const char *file, const char *argv[], const char *const envp[]) { if (envp == NULL || (char **) envp == environ) { execvp(file, (char **) argv); return; } }
这里执行的linux内核系统调用execvp,用于在新创建的进程中执行一个新的程序。
execvp(file, (char **) argv);
至此,代码分析完毕
4 为什么java中不能执行source命令
source命令是bash程序的build-in命令,而java执行shell命令时并不是创建一个bash再执行这个shell,而是在新创建的进程中执行这个shell。也就是说,java执行shell不是在bash中执行的。因此java中不能执行source命令。