openjdk源码-java是如何执行shell命令的

发布时间 2023-12-26 19:50:29作者: zhenjingcool

一般我们在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命令。