2011年1月27日木曜日

Zygoteは何をしているのか?

脱線コーナーを抜けて一休み後、とりあえず、続きです。

さて、/system/bin/app_processがZygoteの本体で、JavaのZygoteInit.main()を呼び出すという話を以前の勉強会で出しました。で、前回はそのまま、system_serverプロセスをforkするお話に突入してしまったわけですが、ここからは、その続きの始まりです。ここからは、コードを拾い出して書いて行きますが、かなり大幅に省略していたりしますのでご了承下さい。

public static void main(String argv[]) {
        :
        :
    registerZygoteSocket();
        :
        :
    if (argv[1].equals("true")) {
        startSystemServer();  //前回はここまで
    } else if (!argv[1].equals("false")) {
        throw new RuntimeException(argv[0] + USAGE_STRING);
    }

    Log.i(TAG, "Accepting command socket connections");

    if (ZYGOTE_FORK_MODE) {
        runForkMode();
    } else {
        runSelectLoopMode();
    }

    closeServerSocket();
}
まず先に、registerZygoteSocket()を見ておきます。名前からしてZygoteの必要なソケットの登録とわかります。
        :
        :
private static void registerZygoteSocket() {
    String env = System.getenv(ANDROID_SOCKET_ENV);
    fileDesc = Integer.parseInt(env);
    sServerSocket = new LocalServerSocket(
    createFileDescriptor(fileDesc));

とりあえず、環境変数を取得してLocalServerSocketを生成しています。

続いては前回入り込んだstartSystemServer()では、子プロセスをforkしていましたが、system_serverの生成が終わると、親のプロセスは普通にここまで戻ってきます。続きを見てみると、if文に囲まれた個所が目に入ります。このif文ですが
    private static final boolean ZYGOTE_FORK_MODE = false;
ということで、Gingerbreadさんのコードだと、runSelectLoopMode()に入ります。というわけで処理を追いかけて見ます。
private static void runSelectLoopMode() throws MethodAndArgsCaller {
        :
        :
   while (true) {
       int index;
       /*
       * Call gc() before we block in select().
       * It's work that has to be done anyway, and it's better
       * to avoid making every child do it. It will also
       * madvise() any free memory as a side-effect.
       *
       * Don't call it every time, because walking the entire
       * heap is a lot of overhead to free a few hundred bytes.
       */
       if (loopCount <= 0) {
            gc();
            loopCount = GC_LOOP_COUNT;
        } else {
            loopCount--;
        }
        try {
            fdArray = fds.toArray(fdArray);
            index = selectReadable(fdArray);
        } catch (IOException ex) {
            throw new RuntimeException("Error in select()", ex);
        }
        if (index < 0) {
            throw new RuntimeException("Error in select()");
        } else if (index == 0) {
            ZygoteConnection newPeer = acceptCommandPeer();
            peers.add(newPeer);
            fds.add(newPeer.getFileDesciptor());
        } else {
            boolean done; done = peers.get(index).runOnce();
            if (done) {
                peers.remove(index);
                fds.remove(index);
            }
        }
    }
}

「先生!!なんだか、Zygoteちゃんがgc()呼んでます!」的なコードが見える気がしますが、とりあえず今は無視します。「え〜」って声が聞こえる気はしますが、今回はそこはメインじゃないですし。誰か、ここを追いかけてPF部で発表してくれる人はいないものかな・・・・。

とりあえず進みます。ここでは保持しているfdのリストを元にselectReadable()を呼び出します。selectReadable()は、Nativeコードで、実体は frameworks/base/core/jni/com_android_internal_os_ZygoteInit.cpp にあります。
Jniのためコードがちょっと面倒なので簡単に何をやってるかだけ抜き出すと、引数で渡したfdのArrayをfdsetにFD_SETした後、

do {
    err = select (nfds, &fdset, NULL, NULL, NULL);
} while (err < 0 && errno == EINTR);

とまぁ、select待ちしたうえで、selectを抜けると、Arrayの何番目がFD_ISSETかを判定して返します。

index==0の場合、つまり、最初に待ち受けしているFDに接続要求が来ると
private static ZygoteConnection acceptCommandPeer() {
    try {
        return new ZygoteConnection(sServerSocket.accept());
    } catch (IOException ex) {
        throw new RuntimeException("IOException during accept()", ex);
    }
}

ということで、acceptしたfdを持ったZygoteConnectionを生成しており、それをarrayに追加した後、acceptしたfdをfdsに追加しているわけです。

で、もしも0以外のfd(つまりsServerSocket.accept()でacceptしたfd)がreadableになると、対象となるZygoteConnection#runOnce()を呼び出します。

boolean runOnce() throws ZygoteInit.MethodAndArgsCaller {
        :
        :
    args = readArgumentList();
        :
        :
   parsedArgs = new Arguments(args);

    applyUidSecurityPolicy(parsedArgs, peer);
    applyDebuggerSecurityPolicy(parsedArgs);
    applyRlimitSecurityPolicy(parsedArgs, peer);
    applyCapabilitiesSecurityPolicy(parsedArgs, peer);

    int[][] rlimits = null;

    if (parsedArgs.rlimits != null) {
        rlimits = parsedArgs.rlimits.toArray(intArray2d);
    }

    pid = Zygote.forkAndSpecialize(parsedArgs.uid, parsedArgs.gid,
                                                        parsedArgs.gids, parsedArgs.debugFlags, rlimits);
        :
        :
    if (pid == 0) {
        // in child
        handleChildProc(parsedArgs, descriptors, newStderr);
        // should never happen
        return true;
    } else { /* pid != 0 */
        // in parent...pid of < 0 means failure
        return handleParentProc(pid, descriptors, parsedArgs);
    }

readArgumentList()は、acceptした時のsocketから引数のリストを読み出しています。何をどう読み込んでパースしているのか、その後のチェック等は後日に回すものとして先に進みます。

取得した引数に従って、まずは子プロセスをforkしています。何となく想像がつくかと思いますが、uid,gid等の設定に従って子プロセスをSpecializeしているわけです。

その後、親プロセスであるZygoteの本体は
private boolean handleParentProc(int pid,FileDescriptor[] descriptors, Arguments parsedArgs) {
        :
        :
    ZygoteInit.setpgid(pid, ZygoteInit.getpgid(peer.getPid()));
        :
        :
    for (FileDescriptor fd: descriptors) {
        ZygoteInit.closeDescriptor(fd);
    }
        :
        :
    mSocketOutStream.writeInt(pid);
        :
        :
    if (parsedArgs.peerWait) {
        mSocket.close();
        return true;
    }
    return false;
}
のように、子プロセスをpeerのプロセスグループへの登録を行った後、descriptorを閉じ、要求をしてきた相手にpidを返した後、peerを維持する必要がなければ、socketを閉じて処理をLoopに戻し、ZygoteConnectionを削除します。

起こされた子プロセスは・・・というと
private void handleChildProc(Arguments parsedArgs, FileDescriptor[] descriptors,
                                               PrintStream newStderr) {
    if (parsedArgs.peerWait) {
        ZygoteInit.setCloseOnExec(mSocket.getFileDescriptor(), true);
        sPeerWaitSocket = mSocket;
    } else {
        closeSocket();
        ZygoteInit.closeServerSocket();
    }

    if (descriptors != null) {
        ZygoteInit.reopenStdio(descriptors[0],
        descriptors[1], descriptors[2]);

        for (FileDescriptor fd: descriptors) {
            ZygoteInit.closeDescriptor(fd);
        }
        newStderr = System.err;
    }
サーバー側のFDをcloseした後、
    if (parsedArgs.runtimeInit) {
        RuntimeInit.zygoteInit(parsedArgs.remainingArgs);
    } else {
        ClassLoader cloader;

        if (parsedArgs.classpath != null) {
            cloader
                = new PathClassLoader(parsedArgs.classpath,
                                                        ClassLoader.getSystemClassLoader());
        } else {
            cloader = ClassLoader.getSystemClassLoader();
        }
        String className;
        className = parsedArgs.remainingArgs[0];
        String[] mainArgs = new String[parsedArgs.remainingArgs.length - 1];

        System.arraycopy(parsedArgs.remainingArgs, 1,mainArgs, 0, mainArgs.length);
        ZygoteInit.invokeStaticMain(cloader, className, mainArgs);
    }
}
runtimeInitがどのような場合にtrueになっているのか調べ切れていませんが、後者の処理になった場合、引数で指定されたクラスをロードし、Static Mainを呼び出しているようです。

大ざっぱにまとめると、Zygoteは、socketを生成して待ち受け、要求が来ると、自身のコピーである子プロセスで送信されてきた引数を元にJavaのアプリケーションを起動していると考えて良いかと思います。

とりあえず、今夜の調査はここまでとしておきましょう。

2 件のコメント:

  1. Zygoteの親プロセスのヒープは動作するたびに少しずつですがゴミがたまっていきます。forkした子プロセスはその時のメモリの状態から始まることになります。子プロセスをなるべく「きれいな」状態のヒープで開始させてあげたいので、定期的に明示的にgcを行っているのだろうと考えられます。

    返信削除
  2. なるほど。だから、そこにgc()があったんですね。

    返信削除