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.