文章目录
-
- 前言
- [一、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.rc 中 socket 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.bp 的 cflags 加 -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>/stat 或 ps -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.java、ZygoteConnection.java、frameworks/base/core/jni/com_android_internal_os_Zygote.cpp、bionic/libc/bionic/fork.cpp。想深入的自取。
P.S. 推荐一个大神的教程给想要了解或者学习人工智能知识的读者,这个教程里内容讲解通俗易懂且风趣幽默,对我帮助很大。我想与大家分享这个宝藏教程,请点击下方链接查看,传送门https://blog.csdn.net/qq_74013365