Fábio Maia

Fábio Maia

Zygote Fork in Android: Process Creation Without execve()

The odd thing about launching an Android app is the missing execve(). A normal Unix program launch is fork-then-exec: copy the caller, then replace the child with a new executable. Android keeps the copy step, but app processes usually continue from a pre-warmed process instead: Zygote.

# Classic Unix shape
fork()                                  = 12345
[pid 12345] execve("/usr/bin/yes", ...) = 0

# Android app launch shape, tracing zygote
clone(...)                              = 23456
# no execve() in the child app process

Preload once

Zygote is useful because it has already done expensive work before any app is launched. In AOSP, ZygoteInit.preload() spells out the idea: load framework classes, resources, HALs, the graphics driver, shared libraries, and text resources up front (ZygoteInit.java:128).

static void preload(TimingsTraceLog bootTimingsTraceLog) {
    Log.d(TAG, "begin preload");
    bootTimingsTraceLog.traceBegin("BeginPreload");
    beginPreload();
    bootTimingsTraceLog.traceEnd(); // BeginPreload

    bootTimingsTraceLog.traceBegin("PreloadClasses");
    preloadClasses();
    bootTimingsTraceLog.traceEnd(); // PreloadClasses

    bootTimingsTraceLog.traceBegin("PreloadResources");
    Resources.preloadResources();
    bootTimingsTraceLog.traceEnd(); // PreloadResources

    Trace.traceBegin(Trace.TRACE_TAG_DALVIK, "PreloadAppProcessHALs");
    nativePreloadAppProcessHALs();
    Trace.traceEnd(Trace.TRACE_TAG_DALVIK);

    Trace.traceBegin(Trace.TRACE_TAG_DALVIK, "PreloadGraphicsDriver");
    maybePreloadGraphicsDriver();
    Trace.traceEnd(Trace.TRACE_TAG_DALVIK);

    preloadSharedLibraries();
    preloadTextResources();
}

The class preload is literally driven from the preloaded-classes file. That is why child app processes start with so much common framework state already mapped (ZygoteInit.java:282).

private static void preloadClasses() {
    final VMRuntime runtime = VMRuntime.getRuntime();

    InputStream is;
    try {
        is = new FileInputStream(PRELOADED_CLASSES);
    } catch (FileNotFoundException e) {
        Log.e(TAG, "Couldn't find " + PRELOADED_CLASSES + ".");
        return;
    }

    Log.i(TAG, "Preloading classes...");
    long startTime = SystemClock.uptimeMillis();
    ...
}

Fork and specialize

When system server asks for an app process, Zygote does not exec an APK. The decisive line is Zygote.forkAndSpecialize(...): fork the warmed process, then apply the UID, GID, SELinux label, mount mode, process name, app data directory, and other per-app settings (ZygoteConnection.java:248).

pid = Zygote.forkAndSpecialize(parsedArgs.mUid, parsedArgs.mGid,
        parsedArgs.mGids, parsedArgs.mRuntimeFlags, rlimits,
        parsedArgs.mMountExternal, parsedArgs.mSeInfo, parsedArgs.mNiceName,
        fdsToClose, fdsToIgnore, parsedArgs.mStartChildZygote,
        parsedArgs.mInstructionSet, parsedArgs.mAppDataDir,
        parsedArgs.mIsTopApp, parsedArgs.mPkgDataInfoList,
        parsedArgs.mAllowlistedDataInfoList, parsedArgs.mBindMountAppDataDirs,
        parsedArgs.mBindMountAppStorageDirs,
        parsedArgs.mBindMountSyspropOverrides);

try {
    if (pid == 0) {
        // in child
        zygoteServer.setForkChild();
        zygoteServer.closeServerSocket();
        IoUtils.closeQuietly(serverPipeFd);
        serverPipeFd = null;

        return handleChildProc(parsedArgs, childPipeFd,
                parsedArgs.mStartChildZygote);
    } else {
        // In the parent. A pid < 0 indicates a failure and will be handled in
        // handleParentProc.
        IoUtils.closeQuietly(childPipeFd);
        childPipeFd = null;
        handleParentProc(pid, serverPipeFd);
        return null;
    }
} finally {
    IoUtils.closeQuietly(childPipeFd);
    IoUtils.closeQuietly(serverPipeFd);
}

Enter the app

In the child, handleChildProc() closes parent-only state and calls ZygoteInit.zygoteInit(...) for a normal app process (ZygoteConnection.java:528). That path initializes common runtime state, starts native Binder support, and returns the Java entry point selected by RuntimeInit.applicationInit(...) (ZygoteInit.java:976).

// ZygoteConnection.handleChildProc(...)
if (!isZygote) {
    return ZygoteInit.zygoteInit(parsedArgs.mTargetSdkVersion,
            parsedArgs.mDisabledCompatChanges,
            parsedArgs.mRemainingArgs, null /* classLoader */);
} else {
    return ZygoteInit.childZygoteInit(
            parsedArgs.mRemainingArgs  /* classLoader */);
}

// ZygoteInit.zygoteInit(...)
public static Runnable zygoteInit(int targetSdkVersion, long[] disabledCompatChanges,
        String[] argv, ClassLoader classLoader) {
    if (RuntimeInit.DEBUG) {
        Slog.d(RuntimeInit.TAG, "RuntimeInit: Starting application from zygote");
    }

    Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "ZygoteInit");
    RuntimeInit.redirectLogStreams();

    RuntimeInit.commonInit();
    ZygoteInit.nativeZygoteInit();
    return RuntimeInit.applicationInit(targetSdkVersion, disabledCompatChanges, argv,
            classLoader);
}

For a regular Android app process, that reflective entry eventually lands in android.app.ActivityThread.main() (ActivityThread.java:8907).

public static void main(String[] args) {
    Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "ActivityThreadMain");

    // Install selective syscall interception
    AndroidOs.install();

    // CloseGuard defaults to true and can be quite spammy.  We
    // disable it here, but selectively enable it later (via
    // StrictMode) on debug builds, but using DropBox, not logs.
    CloseGuard.setEnabled(false);

    Environment.initForCurrentUser();
    ...
}

The payoff is copy-on-write sharing: boot classes, resources, native libraries, and ART structures can remain shared physical pages until a process mutates them. The cost is that every app inherits a large common origin. Android then has to turn that shared origin into isolation with UIDs, SELinux, Binder policy, namespaces, seccomp, and per-app data setup.