Baseline: AOSP android-14.0.0_r1. Zygote is a long-lived process that prepares an ART environment and forks app processes. system_server is one specially created child that hosts many core Java framework services. ART executes managed DEX code. These are related parts of startup, not synonyms.
Before you start#
Read init and Android services, Java, ART, and Native Interfaces, The Android Security Model, and Programs, Memory & System Calls. You need to recognize Java method calls, process vs thread, virtual memory, and Linux process identity. No Android app coding is required.
Learning goals#
You can narrate the pinned Android 14 process-creation route from a framework request to a Zygote child; explain preload and copy-on-write; distinguish system_server startup from ordinary app specialization; and state what a process-list snapshot does and does not prove.
ART and Zygote#
Read the text version
- Parent process: Mapping of this page → Original page P: Initially shared for reads (unchanged)
- Child process: Mapping of this page → Original page P: Initially shared for reads (before write)
- Child process: Mapping of this page → Private copy P′: Created for the child’s write (after write)
ART (Android Runtime) loads DEX bytecode and provides class loading, garbage collection, and execution/compilation services (ART overview). ART may interpret or compile code using JIT/AOT mechanisms depending on Android version, device policy, and app use (ART JIT compiler). ART is not the source-language compiler and does not decide whether an app is allowed to use a protected feature; framework and kernel security checks participate in that decision.
During boot, init starts Zygote using an init service declaration (init.zygote64.rc, Android 14 tag, which runs app_process64 ... --zygote --start-system-server and declares the zygote socket). Zygote initializes runtime state and preloads selected classes/resources, then waits for process-creation commands on its control socket. Preloading avoids repeating some setup in each new process. When Zygote calls fork(), parent and child initially map many of the same memory pages as copy-on-write (COW): physical pages can be shared until one process writes, at which point the operating system gives the writer a private copy (fork(2): on Linux, fork() is implemented using copy-on-write pages). This is not shared mutable Java object access between independent apps. Zygote is not a container; process identity and other isolation setup still happen for children.
Parent/child timeline
- initZygote
start app_process / ART and preload
- Zygotesystem_server
fork system_server at startup
- system_serverZygote
later: framework requests app process
- ZygoteApp child
fork and specialize UID / runtime
- App childsystem_server
app may make later Binder requests
Time runs downward. Dashed arrows show returns.
Read the text version
- init → Zygote: start app_process / ART and preload
- Zygote → system_server: fork system_server at startup
- system_server → Zygote: later: framework requests app process
- Zygote → App child: fork and specialize UID / runtime
- App child → system_server: app may make later Binder requests
init starts zygote
│
▼
zygote parent: initializes ART/preloads; waits for commands
│ request for app process │ request for system_server
▼ ▼
fork child fork child
specialize app UID/GID, groups, apply system-server identity and
SELinux context, limits, runtime launch arguments/security setup
│ │
▼ ▼
app entry path / ActivityThread.main() SystemServer.main() → run()
This is a conceptual timeline; exact specializations and branches vary by process type and release. Native specialization for identity and limits is in com_android_internal_os_Zygote.cpp, Android 14 tag (for example SetGids, SetRLimits, and selinux_android_setcontext). The important distinction is that the parent remains Zygote and serves future requests; the child changes identity/configuration before app code runs. A child forked by a privileged parent does not simply inherit Zygote's authority.
Source trail in AOSP 14
In pinned ZygoteInit.java, main(String[] argv) prepares runtime state, performs preload unless configured lazily, constructs a ZygoteServer, and enters its select loop. ZygoteServer.runSelectLoop receives commands; the zygote command-processing path eventually forks and returns a child-side runnable/entry path. The system-server path calls forkSystemServer; when the call returns a child-side Runnable, the caller runs it. In the child, the destination is SystemServer.main, which enters run() and starts framework services in dependency-aware phases.
For an ordinary app, Activity Manager requests process startup through the zygote protocol (ZygoteProcess.startViaZygote). The child is specialized with app identity and runtime configuration, then the app process entry path reaches ActivityThread.main() and the component lifecycle. This is not one method call directly from app code: process launch is a framework/runtime control path, separate from the later Binder calls an app makes to system services. Native daemons may instead be started by init; specialized/isolated processes also have distinct paths.
When reading the source, write down definition → caller → child-only path. A method named fork might describe either a request or an actual fork call; verify the implementation and branch. Do not treat a short excerpt with omitted statements as a compilable program. Exact arguments and security handling should be read at the pinned call sites, not guessed from this simplified narrative.
system_server starts core Java services such as activity/task management and package management (SystemServer.startBootstrapServices). These are objects hosted in one process, not one Linux process per service. Its elevated platform role is not immunity: UID, capabilities, SELinux domain, permission checks, and service-specific rules still constrain operations. Startup phases express ordering/instrumentation; they are not separate processes.
Practice#
The process timeline does not yet explain how a DEX method is resolved or compiled, what keeps an object alive, or why a native worker cannot reuse another thread's JNI environment. Inside ART and JNI follows those mechanisms next, including class-loader identity, profile-guided compilation, GC roots, and native-linker failures.
Prerequisites: browser/Gitiles access to pinned AOSP 14 sources. Optional runtime observation needs workstation ADB Platform-Tools and a booted disposable Cuttlefish/emulator or authorized device. This exercise is read-only; no debugger attachment to protected processes.
Source-only chain
Open pinned ZygoteInit.java, ZygoteServer.java, ZygoteProcess.java, ActivityThread.java, and SystemServer.java. Find main, runSelectLoop, the system-server fork path, and child entry points. Record one incoming call and one outgoing call for each relevant method. Note which operations occur in the parent and which in child. Do not copy the process identity details out of context; use the source to explain them.
Expected: two branches (ordinary app and system_server) from a shared Zygote parent. A failed search may mean symbol moved within the same tag; inspect nearby callers and search the pinned project, rather than inventing a call edge.
Optional process observation
# HOST terminal; each adb shell command executes on the selected device.
adb devices -l
adb shell ps -A -o USER,PID,PPID,NAME
adb shell dumpsys activity processes
Expected: a Zygote process (often ABI-suffixed), system_server, and app processes; names, columns, visibility, and dumpsys output vary. unauthorized means stop; no device means do not substitute your daily phone. If ps rejects -o, use adb shell ps -A and record the limitation. This is a snapshot, not proof of the historical fork sequence, COW page sharing, process SELinux domain, or a complete identity transition. Do not attach a debugger or try to change credentials.
Knowledge check#
- Why can a Zygote child initially share physical pages without sharing mutable Java state?
- What is the difference between a Zygote parent and its app child after fork?
- Is Activity Manager normally a separate process from
system_serverin this AOSP 14 model? - Why does a process listing fail to prove the exact launch call chain?
Answers#
- COW shares pages until a write; modified pages become private. Process address spaces and runtime semantics do not make arbitrary mutable objects jointly shared.
- Parent stays available to process future requests; child is specialized and proceeds to the requested app/system-server entry path.
- No. It is a framework service object hosted within
system_serverin the usual design;SystemServerstarts the activity manager service throughActivityManagerService.Lifecycle(SystemServer.java). - It is only a current snapshot; it does not show prior calls, fork details, specialization, or security policy.
Checkpoint#
Submit a source-backed two-branch timeline. Every arrow must cite a method/file and say parent, child, or later client call. Pass if the app identity is assigned in the child path before app code and SystemServer.main is distinguished from app ActivityThread.main. Targeted remediation: if you cannot place a method in the parent or child, reread ZygoteInit.main, ZygoteServer.runSelectLoop, and the child-side runnable in the pinned source; mark the branch at the actual fork boundary and follow callers instead of inferring from method names. If the identity step is unclear, revisit the earlier Android Security Model lesson and then locate the child specialization call. Redraw the timeline before proceeding.
Sources#
- Pinned Android 14
ZygoteInit.java,ZygoteServer.java,ZygoteProcess.java,ActivityThread.java, andSystemServer.java. - ART overview.
init.zygote64.rc, Android 14 tag — thezygoteservice declaration and socket that init uses.com_android_internal_os_Zygote.cpp, Android 14 tag — native specialization (groups, limits, SELinux context).- ART JIT compiler — interpreter, JIT, and AOT execution; supports the ART interpret/compile sentence.
- fork(2), Linux man-pages — copy-on-write note for
fork()on Linux. - Processes and threads and Android security.