Skip to content

fork与COW

追踪 Zygote preFork、native ForkCommon、页共享、SpecializeCommon 和写时复制边界。

基于android-17.0.0_r1
AndroidZygoteforkCOW源码阅读

fork与COW ​

Zygote 的内存收益来自 Linux fork 的 copy-on-write(COW),但“fork 后完全共享内存”是错误说法。fork 先复制进程的虚拟地址空间和页表关系,父子进程把可写页映射为 COW,共享同一物理页;任一方第一次写入时,内核才复制对应页。Zygote 预加载越稳定,子进程越能长期共享;specialization、Application 初始化、GC 和缓存写入会逐步制造私有页。

1. Java fork入口 ​

源码文件:frameworks/base/core/java/com/android/internal/os/Zygote.java

java
static int forkAndSpecialize(
        int uid, int gid, int[] gids,
        int runtimeFlags, int[][] rlimits,
        int mountExternal, String seInfo,
        String niceName, int[] fdsToClose,
        int[] fdsToIgnore, boolean startChildZygote,
        String instructionSet, String appDataDir,
        boolean isTopApp, ... ) {
    ZygoteHooks.preFork();

    int pid = nativeForkAndSpecialize(...);
    if (pid == 0 && gids != null && gids.length > 0) {
        NetworkUtilsInternal.setAllowNetworkingForProcess(
                containsInetGid(gids));
    }

    Thread.currentThread().setPriority(
            Thread.NORM_PRIORITY);
    ZygoteHooks.postForkCommon();
    return pid;
}

preFork() 停止 ART daemon、等待 native unregister;native fork 返回后,子进程更新网络允许状态,父子都恢复 Java 优先级并执行 postForkCommon()。COW 在 native fork 返回的一刻已经建立,post-fork hook 是运行时恢复,不是触发 COW 的开关。

2. ForkCommon准备 ​

源码文件:frameworks/base/core/jni/com_android_internal_os_Zygote.cpp

cpp
pid_t zygote::ForkCommon(
        JNIEnv* env, bool is_system_server,
        const std::vector<int>& fds_to_close,
        const std::vector<int>& fds_to_ignore,
        bool is_priority_fork,
        bool is_top_app,
        bool use_fifo_ui,
        bool purge) {
    SetSignalHandlers();
    BlockSignal(SIGCHLD, fail_fn);
    // 检查/关闭继承 fd,准备运行时和 profiling 状态。
    ...
    pid_t pid = fork();
    ...
}

ForkCommon 在 Zygote 单线程假设下阻塞 SIGCHLD,避免 fork 期间 handler 重开日志 fd;它还处理 fd allowlist、优先级和 runtime hooks。fork 前任何后台线程或不受控 fd 都可能导致父子状态不一致,因此这些准备是正确性条件,不只是性能优化。

3. COW的页级语义 ​

fork 后父子拥有独立页表,但页表项指向相同物理页并被标记为只读/COW。读操作继续共享;写操作触发 page fault,内核为写入方分配新页并复制原内容,然后更新写入方页表。复制粒度是内存页,不是 Java 对象,也不是整个堆。

4. 预加载为何有效 ​

SS006~SS008 的类、dex cache、resource ConstantState、shared library 和字体缓存都在父 Zygote 中建立。fork 后子进程读取这些对象时可共享物理页;如果每个应用都在 fork 后重复初始化,它们会分别分配/写入私有页。预加载是否划算取决于“多少子进程会读”和“对象是否稳定不写”。

5. 进程特化写入 ​

源码文件:frameworks/base/core/jni/com_android_internal_os_Zygote.cpp

cpp
static void SpecializeCommon(
        JNIEnv* env, uid_t uid, gid_t gid,
        jintArray gids, jint runtime_flags,
        ... ) {
    EnableKeepCapabilities(fail_fn);
    SetInheritable(permitted_capabilities, fail_fn);
    DropCapabilitiesBoundingSet(fail_fn,
            bounding_capabilities);
    MountEmulatedStorage(uid, mount_external,
            need_pre_initialize_native_bridge, fail_fn);
    ensureInAppMountNamespace(fail_fn);
    ...
    SetCapabilities(...);
    selinux_android_setcontext(
            uid, is_system_server,
            se_info_ptr, nice_name_ptr);
}

specialization 改变内核进程状态、mount namespace、capability、SELinux 和 runtime 配置。部分动作不复制 Java heap 页,但会写入 libc/ART/runtime bookkeeping,从而产生 COW。应用后续 ActivityThread、ClassLoader、Application 和缓存初始化会继续私有化更多页。

6. 哪些内存继续共享 ​

  • 只读 boot image、odex/vdex、共享库文件映射通常通过文件页共享,不依赖匿名 COW;
  • Zygote 预加载的匿名堆页在未写时 COW 共享;
  • Binder driver buffer、线程栈、应用堆新分配和 per-process native heap 是子进程私有;
  • MAP_SHARED 文件/ashmem 的写入可在进程间可见,与匿名 COW 不同;
  • GC 修改 mark bitmap、对象头或移动对象时可能造成额外私有页。

7. 父Zygote写入成本 ​

父 Zygote 在 fork 后若修改预加载对象所在页,会让自己获得私有副本;已有子进程仍指向原页,后续新子进程则继承父进程当前页。频繁修改全局缓存不仅增加父进程 PSS,还会让不同时刻 fork 的应用看到不同共享基线。因此 Zygote 常驻循环尽量避免写预加载对象。

8. 观测与误读 ​

PSS 会按共享者分摊共享页,USS 只统计进程私有页;一个应用 PSS 增长可能来自共享者退出导致分摊变化,而非实际新分配。分析 COW 应对比 private dirty、shared clean/dirty、匿名页和文件映射,并在相同进程集合下采样。

bash
# Java/native fork 与 post-fork hooks。
rg -n "forkAndSpecialize|preFork|nativeForkAndSpecialize|postForkCommon" \
  frameworks/base/core/java/com/android/internal/os/Zygote.java \
  libcore/dalvik/src/main/java/dalvik/system/ZygoteHooks.java

# native ForkCommon、fd、signal 和 specialization。
rg -n "ForkCommon|BlockSignal|fork\(|SpecializeCommon|MountEmulatedStorage|selinux_android_setcontext" \
  frameworks/base/core/jni/com_android_internal_os_Zygote.cpp

# 运行时采样:比较同一进程在初始化前后的 PSS/USS/private dirty。
adb shell dumpsys meminfo <pid>
adb shell cat /proc/<pid>/smaps_rollup

9. 失败与设计边界 ​

  • fork 前线程未停:可能复制不一致的用户态锁/运行时状态;
  • fd 未关闭:子进程继承 Zygote 控制面或敏感资源;
  • specialization 失败:子进程终止,不能回到未特化状态继续;
  • 过度预加载:所有子进程承担不常用页的页表/PSS 成本;
  • fork 后修改预加载缓存:增加 COW private dirty;
  • 把 COW 称为一次内存拷贝:忽略页级、按写触发和文件映射共享。

下一篇将进入 RuntimeInit.applicationInit() 与应用 Java main 的反射入口;不会重复 fork/COW 和 specialization。