android进程是怎么来的

文章目录

    • 前言
    • [一、fork() 系统调用基础](#一、fork() 系统调用基础)
      • [1.1 基本语义](#1.1 基本语义)
      • [1.2 写时复制(Copy-On-Write)](#1.2 写时复制(Copy-On-Write))
      • [1.3 fork / vfork / clone 的关系](#1.3 fork / vfork / clone 的关系)
    • [二、Android 中的 Fork:Zygote 进程模型](#二、Android 中的 Fork:Zygote 进程模型)
      • [2.1 整体架构](#2.1 整体架构)
      • [2.2 为什么用 fork 而不是 exec?](#2.2 为什么用 fork 而不是 exec?)
      • [2.3 完整启动流程(关键函数链)](#2.3 完整启动流程(关键函数链))
      • [2.4 从 app_process 到 Java 世界:启动接力细节](#2.4 从 app_process 到 Java 世界:启动接力细节)
      • [2.5 preload() 到底预加载了什么](#2.5 preload() 到底预加载了什么)
    • 三、关键函数源码解析
      • [3.1 ZygoteInit.main() --- Zygote 入口](#3.1 ZygoteInit.main() — Zygote 入口)
      • [3.2 Zygote.forkSystemServer() --- fork system_server](#3.2 Zygote.forkSystemServer() — fork system_server)
      • [3.3 Zygote.forkAndSpecialize() --- fork 普通应用进程(最核心)](#3.3 Zygote.forkAndSpecialize() — fork 普通应用进程(最核心))
      • [3.4 Native 层 ForkCommon() --- 真正调用 fork() 的地方](#3.4 Native 层 ForkCommon() — 真正调用 fork() 的地方)
      • [3.5 SpecializeCommon() --- 子进程"特殊化"](#3.5 SpecializeCommon() — 子进程"特殊化")
      • [3.6 handleChildProc() --- 子进程进入 Java 世界](#3.6 handleChildProc() — 子进程进入 Java 世界)
      • [3.7 父进程如何收到请求:AMS → Zygote socket](#3.7 父进程如何收到请求:AMS → Zygote socket)
      • [3.8 USAP 优化(Android 10+)](#3.8 USAP 优化(Android 10+))
      • [3.9 Zygote 自己的生命周期:runSelectLoop 与"自我净化"](#3.9 Zygote 自己的生命周期:runSelectLoop 与"自我净化")
    • 四、动手实践:两个可编译运行的示例
      • [4.1 示例 1:基础版](#4.1 示例 1:基础版)
      • [4.2 示例 2:Zygote 风格(fork 循环孵化)](#4.2 示例 2:Zygote 风格(fork 循环孵化))
      • [4.3 如何编译运行](#4.3 如何编译运行)
    • 五、踩坑记录与注意事项
      • [5.1 实战踩坑:-Werror,-Wunused-parameter](#5.1 实战踩坑:-Werror,-Wunused-parameter)
      • [5.2 为什么 fork 前必须停掉所有线程?](#5.2 为什么 fork 前必须停掉所有线程?)
      • [5.3 文件描述符的继承](#5.3 文件描述符的继承)
      • [5.4 fork 与 JNI/ART 的交互](#5.4 fork 与 JNI/ART 的交互)
      • [5.5 调试角度](#5.5 调试角度)
    • 六、总结调用链


P.S. 推荐一个大神的教程给想要了解或者学习人工智能知识的读者,这个教程里内容讲解通俗易懂且风趣幽默,对我帮助很大。我想与大家分享这个宝藏教程,请点击下方链接查看, 传送门https://blog.csdn.net/qq_74013365

前言

先说个结论:你手机上每一个 App,都是"复制"出来的。不是复制粘贴代码那种复制,是进程级别的复制------Linux 调一次 fork(),系统当场给你克隆一个一模一样的进程。微信是这么来的,抖音是这么来的,连系统自己都是这么来的。整个 Android,本质上就是一台大型复印机。

我第一次听说 Zygote 的时候专门去查了翻译:受精卵。我当时就愣住了,合着系统工程师给进程起名是照着生物课本起的。但你别说,这名字起得是真准------一个受精卵,分裂出你手机里所有 App,科学,太科学了。

这篇文章就把这条"分裂"的主线从头捋一遍:fork 是什么 → Android 拿它干了什么 → 源码里怎么写的 → 我自己踩过的坑。看完你会明白,Android 的"秒开"不是玄学,是有人提前把饭做好了,你只管端。

一、fork() 系统调用基础

1.1 基本语义

fork() 的语义一句话:创建一个和当前进程几乎完全一样的子进程。注意是"几乎",因为它会故意留一点不一样的地方,不然父子俩没法相认。

最经典的坑在返回值:这玩意儿调用一次,返回两次。父进程拿到子进程的 PID,子进程拿到 0,失败返回 -1。我第一次看到这句话以为是文档写错了,后来发现是自己肤浅了------一次函数调用,代码走两条分支,你还走不了回头路。这比人生选择还狠,选完就回不去了。

c 复制代码
#include <unistd.h>
pid_t pid = fork();
if (pid < 0) {
    // 失败
} else if (pid == 0) {
    // 子进程:这里执行的代码只在子进程中运行
    execl("/system/bin/sh", "sh", NULL);
} else {
    // 父进程:pid 是子进程的 PID
    int status;
    waitpid(pid, &status, 0);
}

继承什么、不继承什么,看这张表:

特性 说明
返回值 调用一次、返回两次:父进程返回子进程 PID,子进程返回 0,失败返回 -1
地址空间 子进程获得父进程地址空间的副本(虚拟地址相同,物理页通过 COW 共享)
继承的内容 文件描述符(共享文件偏移量)、信号处理函数、环境变量、堆/栈/代码段副本
不继承的内容 锁所在的线程没了(只复制调用线程)、PID 不同、挂起信号被清空

翻译成人话:除了户口本上名字不一样,其他能复印的全复印了。

1.2 写时复制(Copy-On-Write)

按理说,复制一个进程得把内存整个拷贝一份,几个 G 起步。内核觉得这样太败家,于是搞了个 COW(Copy-On-Write,写时复制):fork 的时候不复制物理内存,只把页表标成只读,父子俩共用同一批物理页。谁想写,谁才触发缺页异常,当场复制那一页。

复制代码
fork 后:
父进程页表 ──┐
            ├──> 同一批物理页 (只读)
子进程页表 ──┘
父/子任一方写某页:
写方 ──> 新分配的物理页 (可写) ← COW 缺页处理
另一方 ──> 原物理页

你把它理解成大学宿舍合看教材就行:全班一本教材,谁要做笔记,谁自己再掏钱买一本。内核抠门到什么程度?你不动它,它一辈子不给你买。

这就是 Zygote 模式高效的核心:所有 App 进程从 Zygote fork 出来,共享 Zygote 里预加载的类和资源。只要没人写这些页,物理内存就只有一份。程序员省内存,内核也省内存,大家都有光明的未来。

再从硬件视角看成本:fork 要做的只是复制页表、给物理页记个引用计数、把两个进程的 TLB 弄失效,完事。微秒级操作。真正贵的类验证、资源解析,Zygote 早就提前干完了。所以"秒开"的本质是什么?是有人提前把课备好了,上课铃一响,你直接进教室抄板书。

1.3 fork / vfork / clone 的关系

Linux 内核里其实没有独立的 fork 实现,所有路径最后都汇到 clone()(内核函数叫 kernel_clone(),老版本叫 _do_fork)。区别只在参数:

  • fork() = clone(SIGCHLD, 0) ------ 全复制
  • vfork() = clone(CLONE_VFORK | CLONE_VM | SIGCHLD, 0) ------ 共享地址空间且挂起父进程(为 exec 优化,现代已被淘汰)
  • pthread_create() = clone(CLONE_VM | CLONE_FILES | CLONE_THREAD | ...) ------ 共享地址空间

一句话总结这三个的关系:fork 是复制全家,vfork 是借房子住,pthread 是同居。Android 的 bionic 里 fork()SYS_clone 系统调用实现,bionic/libc/bionic/fork.cpp 里还得处理 pthread_atfork 注册的回调。别问为什么要绕一圈,问就是历史包袱,写系统的人也有自己的难处。

二、Android 中的 Fork:Zygote 进程模型

2.1 整体架构

Android 里 fork 最大的用户就是 Zygote。所有应用进程和 system_server 都是从它 fork 出来的,架构图长这样:

复制代码
init (PID 1)
  └─ app_process (Zygote, 通过 init.rc 启动)
      ├─ fork → system_server ← Zygote.forkSystemServer()
      ├─ fork → com.android.settings ← Zygote.forkAndSpecialize()
      ├─ fork → com.tencent.mm ← 同上
      └─ fork → ... 所有 app 进程

你看这张图,满屏的 fork。system_server 是亲儿子,第一个生;后面每个 App 都是克隆体。也就是说,你手机里几百个应用,全是同一个妈生的。这么一想,微信和抖音居然是亲兄弟------你们还天天让它俩打架。

2.2 为什么用 fork 而不是 exec?

传统 Linux 启动程序是 fork + exec:先复制,再把整个地址空间换掉。Android 一看这流程直摇头:我好不容易把类加载好了,你 exec 一下全没了,那我这班不是白加了?于是它决定只 fork 不 exec。理由有三条:

1. 免重复加载 :Zygote 启动时 preload() 了数千个核心类和系统资源,fork 出的子进程天然继承这些加载好的类,ART 运行时直接就绪,不需要每个 App 重新启动一个虚拟机。

2. 启动速度:App 冷启动只需几毫秒的 fork 加少量初始化,而不是几百毫秒的 JVM 启动加类加载。

3. 内存共享:预加载的类通过 COW 被所有 App 共享同一份物理内存,大幅省 RAM。

翻译一下:传统模式是你借了模板复制简历,然后一份份把内容全改了;Android 模式是你把简历做好,打印几百份,谁要谁拿。区别就是,后者省下来的时间够你多摸两轮鱼。

2.3 完整启动流程(关键函数链)

这条链子就是 Android 的开机主线:init 启动 app_process,C++ 层把 ART 虚拟机拉起来,接力棒交给 Java 的 ZygoteInit.main(),preload 完东西后 fork 出 system_server,再进入循环等 AMS 的请求。请求一到,解析参数、现场克隆一个 App。

复制代码
init (init.rc: service zygote /system/bin/app_process)
  └─ AppRuntime/AppMain.main() // frameworks/base/cmds/app_process/app_main.cpp
      └─ AndroidRuntime.start("com.android.internal.os.ZygoteInit")
          └─ ZygoteInit.main() // Java 世界入口
              ├─ preload() // 预加载类和资源
              ├─ ZygoteServer() // 创建 /dev/socket/zygote (LocalServerSocket)
              ├─ forkSystemServer() // ① fork 出 system_server
              └─ runServerLoop() // ② 循环等待 AMS 请求
                  └─ ZygoteConnection.processOneCommand()
                      ├─ readArgumentList() // 读取启动参数 (uid, gid, seinfo, niceName...)
                      ├─ Zygote.forkAndSpecialize() // ★ 核心 fork
                      │   ├─ nativeForkAndSpecialize (JNI)
                      │   │   └─ ForkCommon() → fork() // 真正的系统调用
                      │   │   └─ SpecializeCommon() // uid/gid 设置、SELinux、capabilities
                      │   └─ 返回后父子进程分道扬镳
                      ├─ 子进程: handleChildProc() → RuntimeInit.applicationInit()
                      │   → 找到 ActivityThread.main() → app 开始运行
                      └─ 父进程: handleParentProc() → 继续监听 socket

反正从头到尾就一个原则:我自己先把最贵的准备工作做完,你们复制我就行。

2.4 从 app_process 到 Java 世界:启动接力细节

Zygote 本质是磁盘上的一个可执行文件:/system/bin/app_process。入口在 C++ 层 app_main.cpp,由它完成 "C++ → Java" 的接力:

cpp 复制代码
// app_main.cpp(简化)
int main(int argc, char* const argv[]) {
    AppRuntime runtime(argv[0], computeArgBlockSize);
    // 解析 --zygote / --start-system-server 等参数
    if (zygote) {
        runtime.start("com.android.internal.os.ZygoteInit", args, zygote);
    }
    ...
}

AppRuntime.start() 只干两件事:用 JNI_CreateJavaVM() 把 ART 虚拟机拉起来,然后 FindClass + CallStaticVoidMethod 调用 ZygoteInit.main()。从这以后,控制权就进了 Java 世界。C++ 工程师退居二线,深藏功与名。

另外 main() 入口还会调用 ZygoteHooks.startZygoteNoChildCreation() 注册 Zygote 阶段的运行时钩子,老版本 Dalvik 时代还会挂一个 SIGUSR1 信号处理来触发 GC。翻译:老祖宗留下的手艺,新版本也得接着用。

2.5 preload() 到底预加载了什么

ZygoteInit.preload() 一次性完成四件事,全部成为后续所有 App 的"遗产":

方法 预加载内容 收益
preloadClasses() 数千个常用类(由 /system/etc/preloaded-classes 列表定义:HashMap、ActivityThread、ContextImpl......) 所有 app 免去类加载/验证耗时
preloadResources() framework-res.apk 的常用 drawable、字符串、布局 资源解析结果常驻内存直接共享
preloadGraphics() OpenGL/EGL 上下文 初始化 GPU 驱动/着色器,app 首帧不用冷启动图形栈
preloadTextResources() 文本整形/断字典等资源(HarfBuzz 相关) 预热文本渲染路径

你品品,一个进程,出生前就已经把书读完了。人家这叫赢在起跑线,而且是物理意义上的------内存里就一份,谁用都是那一份。

三、关键函数源码解析

以下代码基于 AOSP 主线,做了删减注释。

3.1 ZygoteInit.main() --- Zygote 入口

java 复制代码
// frameworks/base/core/java/com/android/internal/os/ZygoteInit.java
public static void main(String[] argv) {
    ZygoteServer zygoteServer = null;
    ...
    // 预加载:类 + 资源 + OpenGL/图形驱动,这是所有 app 共享的"遗产"
    preload(bootTimingsTraceLog);
    // fork 出 system_server(进程名 system_server,uid=1000)
    if (startSystemServer) {
        Runnable r = forkSystemServer(...);
        if (r != null) {
            r.run(); // 注意:只有子进程才会拿到非 null 返回并执行
            return;
        }
    }
    // 父进程继续:监听 zygote socket
    zygoteServer = new ZygoteServer(isPrimaryZygote);
    Runnable caller = zygoteServer.runServerLoop(); // 死循环
    ...
}

注意这段代码的优雅之处:fork 的"一次调用、两次返回",在 Java 层被包装成了"子进程返回一个待执行的 Runnable,父进程返回 null"。用 r != null 区分父子,不用写 C 风格的 if (pid == 0)

C 程序员表示:我用 if (pid == 0) 判断了十年,你现在告诉我还能更优雅?行吧,Java 确实有点东西。

3.2 Zygote.forkSystemServer() --- fork system_server

java 复制代码
// frameworks/base/core/java/com/android/internal/os/Zygote.java
static Runnable forkSystemServer(int uid, int gid, int[] gids, int runtimeFlags, ...) {
    ...
    int pid = nativeForkSystemServer(uid, gid, gids, runtimeFlags, rlimits,
            permittedCapabilities, effectiveCapabilities);
    if (pid == 0) {
        // ---- 子进程 ----
        // 嵌套 try:如果 system_server 出问题,直接杀掉整个 zygote,
        // 让 init 重启整个框架
        if (hasSecondZygote()) {
            waitForSecondaryZygote(socketName);
        }
        zygoteServer.closeServerSocket(); // 子进程不需要监听 socket,必须关闭!
        return handleSystemServerProcess(parsedArgs); // 执行 SystemServer.main
    } else {
        // ---- 父进程 (Zygote) ----
        return null;
    }
}

system_server 是 Zygote 的第一个孩子,而且特别金贵:它出问题,整个 Zygote 都得跟着陪葬,让 init 重启整个框架。代码里那层嵌套 try,翻译过来就是:系统服务器要是挂了,全家重启,别救,救不了。

还有个细节:子进程里必须 closeServerSocket(),把从 Zygote 继承来的 socket 关掉。不然 system_server 还攥着 Zygote 的门钥匙,想再造进程就造进程,那不乱套了。

3.3 Zygote.forkAndSpecialize() --- fork 普通应用进程(最核心)

java 复制代码
// frameworks/base/core/java/com/android/internal/os/Zygote.java
static Runnable 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) {
    VM_HOOKS.preFork(); // ① 停止守护线程、触发 GC(保证单线程状态下 fork)
    int pid = nativeForkAndSpecialize(uid, gid, gids, runtimeFlags, rlimits,
            mountExternal, seInfo, niceName, fdsToClose, fdsToIgnore,
            startChildZygote, instructionSet, appDataDir);
    if (pid == 0) {
        // ---- 子进程 ----
        VM_HOOKS.postForkChild(...); // 恢复 JDWP 等
        return ...; // 返回子进程真正入口
    } else {
        // ---- 父进程 ----
        VM_HOOKS.postForkCommon(); // 恢复 Zygote 自己的守护线程
        return null;
    }
}

这是整个 Zygote 最核心的函数,普通 App 都是在这条路上被克隆出来的。三个 VM Hook(由 ART 的 Daemons 实现)非常关键:preFork() 停掉所有后台守护线程(HeapTaskDaemon、FinalizerDaemon 等)并做一次 GC,保证在单线程状态下 fork;postForkChild() 在子进程里重新拉起守护线程;postForkCommon() 在父进程里恢复。

为什么要停线程?第五部分细说,先剧透一句:fork 只复制调用线程,其他线程直接消失,但它们的锁不会消失------这问题大了去了,比"离职了工牌还挂在门禁系统里"还麻烦。

3.4 Native 层 ForkCommon() --- 真正调用 fork() 的地方

cpp 复制代码
// frameworks/base/core/jni/com_android_internal_os_Zygote.cpp
static pid_t ForkCommon(JNIEnv* env, bool is_system_server,
                        const std::vector<int>& fds_to_close) {
    // 设置 SIGCHLD 处理:让子进程退出后自动 reap,避免僵尸进程占用 PID
    SetSignalHandlers();
    // 关闭传入的 fd,防止子进程持有不该持有的描述符
    for (int fd : fds_to_close) close(fd);
    pid_t pid = fork(); // ★★ Linux 系统调用,一切发生在这里
    if (pid == 0) {
        // ---- 子进程 ----
        // 屏蔽所有信号直到 specialize 完成,
        // 防止中途被杀导致权限状态不一致
        gIsChild = true;
    } else if (pid > 0) {
        gIsChild = false;
    }
    return pid;
}

Native 层,真正的系统调用发生在这里。SetSignalHandlers() 注册 SIGCHLD 处理,让子进程退出后自动被 reap,避免僵尸进程占着 PID 不放;然后关掉不该继承的 fd;最后一行 fork(),命运的分岔路口。

子进程这边还要屏蔽所有信号直到 specialize 完成,防止中途被杀导致权限状态不一致。你可以理解成:刚克隆完还没换衣服,先别出门,外面乱。

3.5 SpecializeCommon() --- 子进程"特殊化"

cpp 复制代码
// frameworks/base/core/jni/com_android_internal_os_Zygote.cpp
static void SpecializeCommon(JNIEnv* env, uid_t uid, gid_t gid, int* gids, ...) {
    // 1. 挂载命名空间:app 的私有存储 (sdcard/external)
    MountEmulatedStorage(uid, mount_external, use_native_bridge, &fail_fn);
    // 2. 丢弃所有 privileges(capabilities),只保留参数中显式允许的
    DropCapabilitiesBoundingSet(fail_fn);
    // 3. SELinux 上下文:从 "zygote" 域切换到 "app domain"
    //    SELinux 规则只允许 zygote 域 fork,且一次 fork 内必须完成切换
    ...
    // 4. 设置进程名(nice name,如 "com.android.settings")
    SetProcessName(...);
    // 5. 恢复 SIGCHLD 为默认、解除信号屏蔽
    UnsetChldSignalHandlers();
    // 6. 切换真实/有效 uid、gid 和补充组
    //    setresgid / setresuid ------ 一旦生效,进程永久失去 root
    ...
}

fork 出来的子进程一开始是什么身份?uid 0、root 权限、system 的 SELinux 域。说白了,刚出生的孩子是个裸 root 号。必须立刻降级成目标 App 身份,这个过程叫 specialize。

重点来了:setresuid() 是不可逆的。一旦生效,这个进程永久失去 root,再也回不去。降权这种事,一旦做了就回不了头,比辞职还干脆。所以 specialize 必须在 fork 后一气呵成,中间用信号屏蔽保护------别问,问就是怕你手抖。

3.6 handleChildProc() --- 子进程进入 Java 世界

java 复制代码
// frameworks/base/core/java/com/android/internal/os/ZygoteConnection.java
private Runnable handleChildProc(Arguments parsedArgs, ...) {
    // 关闭从 zygote 继承来的 socket fd(子进程不该监听)
    closeSocket();
    // 最终入口:RuntimeInit.applicationInit()
    // -> 通过反射找到 android.app.ActivityThread 的 main() 并包装成 Runnable 返回
    return ZygoteInit.zygoteInit(parsedArgs.mTargetSdkVersion,
            parsedArgs.mRemainingArgs, cl, null);
}

子进程的收尾工作:关掉从 Zygote 继承来的 socket,然后返回一个 Runnable。这个 Runnable 一路 return 回 runServerLoop() 的调用方再 run(),此时调用栈已经干干净净,等价于一个全新启动的进程。

你注意这个设计:不是中间直接调用,而是层层 return 回去执行。相当于交接棒不中途递,直接递回起点重新跑,保证路上不带任何历史包袱。

3.7 父进程如何收到请求:AMS → Zygote socket

复制代码
ActivityManagerService.startProcess()
  └─ Process.start()
      └─ ZygoteProcess.startViaZygote()
          └─ zygoteSendArgsAndGetResult()
              └─ 写入 LocalSocket → /dev/socket/zygote
Zygote 端
  runServerLoop() 中 accept() → processOneCommand() → forkAndSpecialize()

AMS 传给 Zygote 的参数包括:--uid--gid--gids--runtime-flags--seinfo--nice-name--target-sdk-version,还有入口类名 android.app.ActivityThread

通信协议朴素到感人:zygote 的监听 socket 由 init 解析 init.rcsocket zygote stream 660 root system 创建于 /dev/socket/zygote(LocalSocket 构建在 Linux AF_UNIX socket 之上),AMS 连接后按行写入参数,Zygote 端解析、fork,再把新进程的 PID 写回 socket。一问一答,无锁、无共享内存。

对比一下现在动不动就上微服务的架构,Zygote 这个 AF_UNIX socket 简直像菜市场里的大爷大妈------不整虚的,直接喊。

3.8 USAP 优化(Android 10+)

fork + specialize 本身是有延迟的(页表复制、SELinux 切换)。Android 10 引入 USAP(Unspecialized App Process Pool,未特殊化的应用进程池):Zygote 空闲时预先 fork 出一批"半成品"进程(fork 完但不 specialize),AMS 请求来了直接取一个现成的做 specialize,把 fork 的耗时从关键路径上拿掉了。

翻译:以前是客人点菜才现杀现做,现在是提前切好配好,下单五分钟上齐。用户感知不到,但确实快了。这就是程序员说的"缓存",只不过缓存的是进程,不是数据。

3.9 Zygote 自己的生命周期:runSelectLoop 与"自我净化"

Zygote 启动完就进入 runSelectLoop()(新版本外层包装成 runServerLoop())的死循环,永不退出,除非整个系统重启。它本身就是一个标准的 poll 多路复用服务器:维护一个 fd 列表(监听 fd + 已接受连接 + USAP 状态管道),OS.poll() 等任意 fd 就绪,新连接就 accept,旧连接交给 processCommand(),单次请求异常只记日志继续循环。

一个永远不退休的劳模,还是 7×24 小时那种。你手机上所有 App 都靠它活着,它自己连个休息日都没有。

还有个容易被忽略的细节:Zygote 会给自己做 GC。每次孵化都会让堆里的共享对象被"污染"一点(比如触发过 finalize 的预加载对象),所以它周期性执行 gcIfNecessary()(内部走 gcAndFinalize()System.gc() + runFinalization()),把"脏"对象清走,保持堆纯净。堆越纯净,下一次 fork 的 COW 共享比例就越高。

你可以这么理解:它每天睡前都要把自己收拾干净,就为了明天复制出来的孩子能继承一套干净的房子。母爱的伟大,进程也不例外。

四、动手实践:两个可编译运行的示例

fork() 是 POSIX 接口,Windows 上没有,要在 Linux / WSL / Mac / Android 设备上跑。

4.1 示例 1:基础版

纸上谈兵没意思,来点能跑的。第一个例子演示 fork 最核心的四个点:一次调用两次返回、父子分支、COW 内存隔离、waitpid 回收子进程。

c 复制代码
// fork_demo.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
int g_count = 100; // 全局变量,用来演示写时复制(COW)
void do_fork_demo(void) {
    pid_t pid = fork(); // ★ 一次调用,两次返回
    if (pid < 0) {
        // fork 失败(比如超过进程数上限)
        perror("fork failed");
        exit(1);
    } else if (pid == 0) {
        // ===== 子进程分支:fork 返回 0 =====
        printf("[child ] pid=%d, ppid=%d, g_count=%d\n",
               getpid(), getppid(), g_count);
        g_count = 999; // 写内存 -> 触发 COW,父进程看不到
        printf("[child ] after write g_count=%d\n", g_count);
        sleep(1); // 模拟子进程干活
        exit(42); // 子进程退出码 42
    } else {
        // ===== 父进程分支:fork 返回子进程 PID =====
        printf("[parent] created child pid=%d, g_count=%d\n", pid, g_count);
        int status = 0;
        waitpid(pid, &status, 0); // 等待并回收子进程(否则变僵尸进程)
        if (WIFEXITED(status)) {
            printf("[parent] child exited with code=%d\n", WEXITSTATUS(status));
        }
        printf("[parent] my g_count is still %d (父子内存已隔离)\n", g_count);
    }
}
int main(void) {
    printf("before fork, pid=%d\n", getpid());
    do_fork_demo();
    return 0;
}

运行结果(顺序可能交错):

复制代码
before fork, pid=12345
[parent] created child pid=12346, g_count=100
[child ] pid=12346, ppid=12345, g_count=100
[child ] after write g_count=999
[parent] child exited with code=42
[parent] my g_count is still 100 (父子内存已隔离)

运行结果里最扎心的是最后两行:子进程把 g_count 改成 999,父进程那边还是 100。内存隔离,物理意义上的"我写我的,你看不见"。比某些人的隐私保护意识强多了。

4.2 示例 2:Zygote 风格(fork 循环孵化)

第二个例子模拟 Zygote 模式:父进程像 Zygote 一样常驻循环,不断 fork 出子进程,子进程 fork 后立刻"specialize"(改进程名)再执行任务。

c 复制代码
// mini_zygote.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
#include <signal.h>
#include <errno.h>
#include <sys/prctl.h>
static void sigchld_handler(int sig) {
    (void)sig; /* 参数是信号处理函数原型的要求,此处不使用 */
    // 异步回收所有已退出的子进程,避免僵尸进程占用 PID
    while (waitpid(-1, NULL, WNOHANG) > 0) { }
}
/* 模拟 ZygoteConnection.forkAndSpecialize:fork 出一个子进程并"特殊化" */
static pid_t fork_and_specialize(const char *nice_name, int exit_code) {
    pid_t pid = fork();
    if (pid == 0) {
        /* ---- 子进程:fork 后立刻 specialize ---- */
        // 1. 改进程名(对应 Zygote 设置 nice_name,如 "com.android.settings")
        char comm[16];
        snprintf(comm, sizeof(comm), "%s", nice_name);
        prctl(PR_SET_NAME, comm, 0, 0, 0);
        // 2. 降权:真实 Android 里这里是 setresuid/setresgid + SELinux 切域
        printf(" [%s] spawned, pid=%d, doing work...\n", nice_name, getpid());
        sleep(2);
        exit(exit_code);
    } else if (pid > 0) {
        printf("[zygote] forked '%s' -> pid %d\n", nice_name, pid);
        return pid;
    }
    perror("fork");
    return -1;
}
int main(void) {
    // Zygote 也做了同样的事:注册 SIGCHLD 处理器
    //(见 ForkCommon -> SetSignalHandlers)
    struct sigaction sa;
    memset(&sa, 0, sizeof(sa));
    sa.sa_handler = sigchld_handler;
    sa.sa_flags = SA_RESTART | SA_NOCLDSTOP;
    sigaction(SIGCHLD, &sa, NULL);
    const char *apps[] = { "app:browser", "app:music", "app:camera" };
    printf("[zygote] running, pid=%d\n", getpid());
    for (int i = 0; i < 3; i++) {
        fork_and_specialize(apps[i], i);
        sleep(1); // 模拟 AMS 陆续发来的启动请求
    }
    printf("[zygote] all children spawned, keep listening...\n");
    sleep(4); // 父进程常驻,就像 Zygote 永远监听 socket
    printf("[zygote] exit\n");
    return 0;
}

运行时另开一个终端执行 ps,会看到父进程旁边挂着三个改名后的子进程------这正是 Zygote 和所有 App 进程在设备上呈现的样子。

复制代码
console:/ # ./mini_zygote
[zygote] running, pid=19465
[zygote] forked 'app:browser' -> pid 19466
  [app:browser] spawned, pid=19466, doing work...
[zygote] forked 'app:music' -> pid 19472
  [app:music] spawned, pid=19472, doing work...
[zygote] forked 'app:camera' -> pid 19482
  [app:camera] spawned, pid=19482, doing work...
[zygote] all children spawned, keep listening...
[zygote] exit
console:/ # ps -A | grep epoll_
root      19465 14172 8730796  3292 hrtimer_nanosleep 0 S epoll_server
root      19472 19465 8730796   536 hrtimer_nanosleep 0 S epoll_server
root      19482 19465 8730796   536 hrtimer_nanosleep 0 S epoll_server

想想看,你手机里几百个进程全是这套路出生的,跟俄罗斯套娃似的。

4.3 如何编译运行

Linux / WSL 上:

bash 复制代码
gcc fork_demo.c -o fork_demo && ./fork_demo
gcc mini_zygote.c -o mini_zygote && ./mini_zygote

真实 Android 设备上(NDK 交叉编译,以 arm64 为例):

bash 复制代码
# 用 NDK 自带的 clang
$NDK/toolchains/llvm/prebuilt/windows-x86_64/bin/aarch64-linux-android33-clang \
    fork_demo.c -o fork_demo
adb push fork_demo /data/local/tmp/
adb shell chmod +x /data/local/tmp/fork_demo
adb shell /data/local/tmp/fork_demo

五、踩坑记录与注意事项

5.1 实战踩坑:-Werror,-Wunused-parameter

在 AOSP/NDK 环境编译上面的信号处理函数时,你大概率会遇到这个报错:

复制代码
epoll_server.c:26:33: error: unused parameter 'sig' [-Werror,-Wunused-parameter]
static void sigchld_handler(int sig)
                                ^
1 error generated.
06:29:34 ninja failed with: exit status 1

原因特别喜剧:信号处理函数的签名必须带 int sig 参数(POSIX 规定的原型),但函数体里真的用不到它。而编译环境开了 -Werror -Wunused-parameter,"未使用的参数"直接升级成编译错误。

这就好比公司团建要求必须戴工牌:你戴了,全程没人查;你不戴,直接扣分。标准解法是在函数体第一行写 (void)sig;,相当于举着工牌在门口晃一下:看,我带了,用不用是我的事。

c 复制代码
static void sigchld_handler(int sig) {
    (void)sig; /* 参数是信号处理函数原型的要求,此处不使用 */
    while (waitpid(-1, NULL, WNOHANG) > 0) { }
}

备选方案:只声明类型不命名(static void sigchld_handler(int),clang 接受但不完全规范),或者 Android.bpcflags-Wno-unused-parameter(不推荐,会掩盖其他真正的问题)。另外建议 sa_flags 写上 SA_RESTART | SA_NOCLDSTOP:前者让被信号打断的阻塞调用自动重启(避免 epoll_wait/read 返回 EINTR),后者避免子进程停止/继续时也触发 handler。

5.2 为什么 fork 前必须停掉所有线程?

这是全文最值得记的坑:fork() 只复制调用线程,其他线程直接消失,但它们持有的锁不会释放。想象一下:一个人锁了门然后原地消失,钥匙也跟着没了,后来的人全堵在门口。子进程里再调 malloc,直接死锁。

POSIX 的解法是 pthread_atfork() 注册回调;Android/ART 的解法是 VM_HOOKS.preFork() 先停掉守护线程,保证 fork 发生在单线程上下文。先清场,再谈事,比某些会议室还讲究。

5.3 文件描述符的继承

子进程会继承 Zygote 打开的所有 fd(带 O_CLOEXEC 标记的除外)。这就是 fdsToClose / fdsToIgnore 参数的由来:每个子进程必须关闭 zygote 的监听 socket 等敏感 fd,否则 App 就能通过这个 socket 指挥 Zygote 再造进程。

翻译:相当于你拿到了整栋楼的备用钥匙。不交出来?那谁都能随时给整栋楼开门,这是严重安全漏洞。所以每个子进程出生第一件事:把家门钥匙交出来。

5.4 fork 与 JNI/ART 的交互

COW 意味着子进程刚 fork 完时和 Zygote 内存几乎零成本,但任何写入(类去偏置、对象重映射)都会造成私有页拷贝------这是 App 首次启动内存上涨的原因之一。

Zygote 在 fork 前还会对"带 finalize 的预加载对象"做特殊处理,避免所有子进程被共享可变对象的写放大拖累。你可以理解为:发家前先把家里的贵重物品分好,省得孩子们分了家产还打架。

5.5 调试角度

想知道一个进程从哪来?/proc/<pid>/statps -o PPID,所有 App 的父进程都是 zygote。小时候问"我是从哪来的",Android 的回答永远只有一句话:你是被 fork 出来的。

Zygote 的 fork 计数和失败,看 logcat 里的 Zygote tag。想量化 fork/specialize 耗时,am start -W 看 WaitTime,细节在 system_server 的 atrace am_proc_start 里。

六、总结调用链

复制代码
[Java]   ZygoteInit.main
           └ Zygote.forkAndSpecialize ── VM_HOOKS.preFork()(停线程+GC)
[JNI]      └ nativeForkAndSpecialize
              ├ ForkCommon() ────────── fork() ←------ Linux syscall(COW)
              └ SpecializeCommon() ─── setresuid/setresgid、SELinux 切域、
                                       drop capabilities、进程名
[Java]   子进程: handleChildProc → RuntimeInit.applicationInit
                  └ ActivityThread.main() ← app 诞生
          父进程: VM_HOOKS.postForkCommon() → 继续监听 socket

一句话总结:Android 的 fork 机制 = Linux fork() 的 COW 语义 + Zygote 预加载的类/资源遗产 + fork 后立即 specialize 降权------用极低的成本批量孵化安全隔离的应用进程。

复制、继承、换身份重新做人------这就是你手机里每个 App 的一生。下次面试官问"Android 进程是怎么来的",你可以优雅地回答:都是复制出来的,连"妈"都是同一个。

源码都在 AOSP 里:frameworks/base/core/java/com/android/internal/os/Zygote.javaZygoteConnection.javaframeworks/base/core/jni/com_android_internal_os_Zygote.cppbionic/libc/bionic/fork.cpp。想深入的自取。

P.S. 推荐一个大神的教程给想要了解或者学习人工智能知识的读者,这个教程里内容讲解通俗易懂且风趣幽默,对我帮助很大。我想与大家分享这个宝藏教程,请点击下方链接查看,传送门https://blog.csdn.net/qq_74013365

相关推荐
Είναι η κοπέλα1 小时前
小目标检测实战:YOLO26 的四板斧与避坑清单
人工智能
陈童学哦1 小时前
Codex配置瘦身实践:给Agent提示词做一次深度大扫除
人工智能
shark-chili1 小时前
关于AI辅助编程的认知
数据库·人工智能·redis·macos·缓存
机核研创社1 小时前
一次性内裤无人生产线详解:从裁片到成品 12–35 秒一件的整线自动化架构
人工智能·自动化
有Li1 小时前
【AI答疑】MR数据预处理步骤
人工智能·笔记·算法·语言模型·医学生
衡石科技1 小时前
让 BI 能力可编排:CLI、Headless API 与可治理执行
人工智能·chatbi
蓝速科技1 小时前
国产化信创终端等保合规核心防护与落地方案丨蓝速科技
大数据·运维·数据库·人工智能·科技
GEO实战经验分享2 小时前
王涛:GEO 服务商能力评估清单——基础、结构、战略、风控四层怎么核验
大数据·人工智能·chatgpt
干就完事了2 小时前
机器学习-音乐艺人流行趋势预测
人工智能·机器学习·阿里云