Who is pid 1? Android's boot sequence, measured
Search for “Android boot sequence” and you get the same picture every time:
four boxes, left to right, bootloader → kernel → init → zygote, with an arrow
labelled “system_server” trailing off the right edge. It’s not wrong. It’s
just not load-bearing — you cannot answer a single practical question with it.
Can my daemon read /data yet? Which of these boxes is /system mounted in?
If boot takes eighteen seconds, which box has the seconds in it?
The useful framing isn’t “which program is running.” It’s what exists now that didn’t a moment ago. Boot is capability accretion, and there are four capabilities that matter: a root filesystem, a loaded SELinux policy, the property service, and the framework. Every boundary worth drawing in a boot diagram is one of those four arriving.
The good news is that you don’t have to take anyone’s word for the timings,
including mine. Init instruments its own boot into system properties, so any
device will tell you where its seconds went. Everything below is
android-15.0.0_r36 on Cuttlefish (aosp_cf_x86_64_auto-userdebug), and every
number comes off that device.
running per second — note that it is not a ramp but two bursts with a five-second void between them, and that the void has a cause you can point at.Init is one binary that execs itself three times
This is the part no diagram shows, and it’s the first thing that made the rest
make sense. There is no separate “first stage init” executable. There’s one
/system/bin/init, and it dispatches on how it was invoked —
system/core/init/main.cpp:53:
int main(int argc, char** argv) {
if (!strcmp(basename(argv[0]), "ueventd")) {
return ueventd_main(argc, argv);
}
if (argc > 1) {
if (!strcmp(argv[1], "subcontext")) return SubcontextMain(argc, argv, &function_map);
if (!strcmp(argv[1], "selinux_setup")) return SetupSelinux(argv);
if (!strcmp(argv[1], "second_stage")) return SecondStageMain(argc, argv);
}
return FirstStageMain(argc, argv);
}
Five modes in one binary. The kernel starts it with no arguments, so it falls
through to FirstStageMain, whose job is to get enough of a filesystem
mounted that the rest of Android is reachable. Then, at
first_stage_init.cpp:559:
const char* path = "/system/bin/init";
const char* args[] = {path, "selinux_setup", nullptr};
execv(path, const_cast<char**>(args));
It execs itself. The selinux_setup stage loads the policy, and then does
the move that explains the whole design — selinux.cpp:735:
if (selinux_android_restorecon("/system/bin/init", 0) == -1) {
PLOG(FATAL) << "restorecon failed of /system/bin/init failed";
}
const char* path = "/system/bin/init";
const char* args[] = {path, "second_stage", nullptr};
execv(path, const_cast<char**>(args));
Pid 1 relabels its own executable and re-execs. Why? Because an SELinux
domain transition only happens on exec. You cannot change a running
process’s security context; the kernel assigns it when the process is
exec’d. Init starts life in the kernel domain, and the only way for it to
become u:r:init:s0 — the context it needs in order to spawn everything else
into the right domains — is to load the policy, fix its own file label, and
run itself again. Three stages exist because the SELinux model demands three
execs, not because someone liked the number.
On my device that middle stage costs 24 ms, and the two execv calls are
the reason init: init second stage started! is a thing you see in the log
rather than an implementation detail.
The other two modes are worth knowing. ueventd — the device-node and
firmware manager — is the same binary, invoked through a different
argv[0]. And subcontext is how vendor_init works: init forks a copy of
itself into u:r:vendor_init:s0 so that commands from vendor .rc files run
with vendor privileges rather than init’s. You can watch it happen:
init: Forked subcontext for 'u:r:vendor_init:s0' with pid 91
The ordering is a 30-line block, not an emergent property
Second-stage init doesn’t have a hardcoded boot sequence in C++. It has an
action queue, and it primes it with exactly three built-in triggers —
init.cpp:1061, 1080, 1087:
am.QueueEventTrigger("early-init");
...
am.QueueEventTrigger("init");
...
am.QueueEventTrigger("late-init");
That’s it. Everything else you think of as a boot stage is queued from
init.rc itself. And late-init turns out not to be a stage at all — it’s a
sequencer. Its entire body, at system/core/rootdir/init.rc:503, is
trigger commands:
on late-init
trigger early-fs
trigger fs
trigger post-fs
trigger late-fs
trigger post-fs-data
trigger load-bpf-programs
trigger bpf-progs-loaded
trigger zygote-start
trigger firmware_mounts_complete
trigger early-boot
trigger boot
I found this genuinely clarifying. The stage ordering that gets drawn as
architecture is a list, in a file, that you can read in thirty seconds and
edit. And because trigger enqueues rather than calls, the stages are
sequential by queue position — which becomes important in a minute.
Measured on my device, that list plays out as:
| trigger | at |
|---|---|
early-init |
2.030 s |
init |
2.223 s |
late-init |
2.283 s |
early-fs |
2.295 s |
fs |
2.302 s |
post-fs |
2.306 s |
late-fs |
2.443 s |
post-fs-data |
7.641 s |
zygote-start |
8.294 s |
early-boot |
8.314 s |
boot |
8.329 s |
Six of the eleven stages complete inside 150 ms. Then something takes five seconds. We’ll get there.
Two kinds of trigger, and who fires them
Init has event triggers, which are the ones above, and property triggers,
which look almost identical in an .rc file:
on boot
...
on property:sys.boot_completed=1
...
Both run their commands in the same single-threaded queue, executed by init.
The difference is who decides when. on boot fires because init worked
its way down that list in init.rc. on property:sys.boot_completed=1 fires
because someone set a property — and that someone is Java, in system_server:
// frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java:5223
SystemProperties.set("sys.boot_completed", "1");
That’s inside finishBooting(). So on boot means “init has finished its own
bring-up,” and sys.boot_completed means “the framework believes it has
finished booting” — two claims about different things, made by different
processes, 8.5 seconds apart on my device (8.329 s vs 16.831 s). A
crash-looping system_server never fires the second one; nothing about init
would notice.
This distinction is the one I’d most want a beginner to take away, because
on boot reads like it means “the device has booted” and it does not. It
means init is done with the parts init controls. Zygote’s service had already been started 18 ms earlier.
The device tells you its own timings
Every service init starts gets a property — service.cpp:183:
if (new_state == "running") {
uint64_t start_ns = time_started_.time_since_epoch().count();
std::string boottime_property = "ro.boottime." + name_;
if (GetProperty(boottime_property, "").empty()) {
SetProperty(boottime_property, std::to_string(start_ns));
}
}
So adb shell getprop | grep ro.boottime is a free boot profile. On my
device that’s 140 entries: ten of them are init’s own stage instrumentation,
the other 130 are one per service. The if (...empty()) matters — only the
first start is recorded, so a service that gets restarted keeps its
original timestamp.
One absence is more informative than any of the values: ro.boottime.system_server is empty. Init never started system_server — zygote forked
it. Init instruments its own children, so the gap in the data marks exactly
where init’s authority ends and the framework’s begins. Same reason there’s
no ro.boottime for any app process.
What “booted” means, precisely
So, four capability arrivals, with numbers:
- A root filesystem — first stage, 0.96 s to 1.97 s. Before this, nothing outside the ramdisk exists.
- SELinux policy — 1.97 s, and pid 1 re-execs to pick up its own domain. Before this there is no meaningful notion of who may do what.
- The property service — 2.03 s, when second-stage init creates
/dev/socket/property_service. Before this,setprophas nowhere to go andon property:triggers cannot fire. /data— 7.64 s on this boot. This is the big one for anything stateful, and the one people underestimate.persist.properties don’t exist before it either, because they’re read off/data/property.- The framework — zygote at 8.29 s,
sys.boot_completedat 16.83 s.
“Booted” is five different moments, and which one you mean determines whether your code works. That’s the diagram I actually wanted when I started: not which program is running, but what you’re allowed to assume.
Reading list
system/core/init/main.cpp— 40 lines, and the five modes are the whole architecture.system/core/init/selinux.cpp:735— the restorecon-and-re-exec, with a comment explaining it better than I have.system/core/rootdir/init.rc:503—on late-init, the boot order as a list you can read.system/core/init/service.cpp:183— wherero.boottime.*andinit.svc.*come from.- Android init language reference
— the authoritative
.rcsyntax doc, including every commandwait,wait_for_propand friends.
The previous post, on Treble enforcement,
covers the other half of what init is doing when it parses those .rc files
out of four different partitions — and why which partition a file came from
decides what it’s permitted to do.