ProcessBuilderの中を覗いてみる

UNIXカーネルの設計を読んでいて以下のような記述を見つけたとき、ふと疑問が浮かんだ。

UNIXカーネルの設計 p164 7.1 プロセスの生成
UNIXオペレーティングシステム利用者が新しいプロセスを作る唯一の方法は, forkシステムコールを呼び出すことである。

そういえばjavaの外部コマンドを実行しているjava.lang.ProcessBuilder(古くはRuntime.exec)は、内部的にfork()しているのだろうか。さっそくJDKのコードを見てみる。

Linux環境ではvfork()を実行

ProcessBuilderのコードをネイティブコードに向かって掘り下げていくと、jdk/src/solaris/native/java/lang/UNIXProcess_md.cにおいて実際に子プロセスをvfork()して、子プロセスにおいてexecvpなど実行してProcessBuilderで指定されたコマンドや実行ファイルを起動している。呼び出しの流れは以下のようである。

java.lang.ProcessBuilder.start(…)  // OS共通
 → java.lang.ProcessImpl.start(…) // UNIX系OS用のクラス(solarisフォルダ配下)
  → java.lang.UnixProcess.<init> // UNIXProcess.java.linux
-------------------- ここからネイティブコード (C言語) ---------------------
    → Java_java_lang_UNIXProcess_forkAndExec(...)
       (この関数の先でvfork()が行われる)

UNIXProcess.java.linuxのコードを見ると、プロセスの生成にvfork()ではなく、fork()を使うようにシステムプロパティで明示的に設定できるオプションがある。

java -Djdk.lang.Process.launchMechanism=fork test/Main

試しに上記のオプションを設定して、以下のコードように大量に子プロセスの生成と実行を試みたが、設定していても、していなくてもあまり結果は変わらなかった。手元のCentOS6.5仮想マシンでは、いずれも8000ミリ秒前後で、vfork()/fork()の差よりも、実行毎の値の揺れの方が大きかった。

java -versionのタスクを128回実行するコード。

public class Main {

    public static void main(String ... args) {
        new Main().process(args);
    }

    public void process(String ... args) {
        ExecutorService executor = Executors.newCachedThreadPool();
        List<Callable<Integer>> tasks = genTask(128);
        try {
            long start = System.currentTimeMillis();
            executor.invokeAll(tasks);
            long end = System.currentTimeMillis();
            long time = end - start;
            System.out.println("time: " + time);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            executor.shutdownNow();
        }
    }
	
    private List<Callable<Integer>> genTask(int amount) {
        List<Callable<Integer>> tasks = new ArrayList<>();
            for (int i = 0; i < amount; i++) {
                tasks.add(new JDKRunner());
            }
        return tasks;
    }
}

java -versionを実行するタスク。

public class JDKRunner implements Callable<Integer> {
    @Override
    public Integer call() {
        ProcessBuilder processBuilder = new ProcessBuilder("java", "-version");
        try {
            Process p = processBuilder.start();
            return p.waitFor();
        } catch (IOException | InterruptedException e) {
            e.printStackTrace();
            return 1;
        }	
    }
}

vforkのmanページにある通り、Linuxのfork()は古いUNIXのように親プロセスのコピーをfork()時点では行わず、変更時に行うcopy-on-writeで実装されている影響だろうか。java.lang.ProcessBuilderから実行する分にはfork()とvfork()の性能差ははっきりしなかった。

まとめ

ProcessBuilderも内部的にはvfork()を実行して子プロセスを生成している。

Javaの売りの一つである『OSに依存しない』ことは、OSに依存する部分をJava SE APIやJavaVMとして実装してくれている開発者がいるからこそ成り立っていると思いました。