深入Android 15 Zygote:ZygoteServer如何驾驭进程孵化

接着上一篇 深入Android 15 Zygote:从进程孵化器到系统基石

runSelectLoop

ini 复制代码
    Runnable runSelectLoop(String abiList) {
        ArrayList<FileDescriptor > socketFDs = new ArrayList<>();
        ArrayList<ZygoteConnection> peers = new ArrayList<>();

        socketFDs.add(mZygoteSocket.getFileDescriptor());
        peers.add(null);

        mUsapPoolRefillTriggerTimestamp = INVALID_TIMESTAMP;

首先创建了2个ArrayList, socketFDs中存放的是FileDescriptor,Linux一切皆文件,所以这个对象是对Linux 文件描述符的封装(Linux fd 是一个整数)。 peers存放的是已经配对好的zygote socket链接,用ZygoteConnection表示。

然后将mZygoteSocket的fd 添加到socketFDspeers添加了null。这里解释一下

  • mZygoteSocket: 这是 Zygote Server 的主 LocalServerSocket。它的作用就像一个酒店的总机电话,专门用来接收新的连接请求,后面我们会看到它接到请求之后,会立马创建一个ZygoteConnection,并添加到peers,相当于创建了一个分机。

所以从设计上来说,socketFDspeers保持一对一对应关系,index为0的是总机,专门用来接收新的连接请求。

mUsapPoolRefillTriggerTimestamp 记录了下一次应该检查并补充 USAP(Unspecialized App Process)池的时间点。

  • INVALID_TIMESTAMP: 常量-1,表示"当前没有计划内的补充任务"。

  • 作用: 在循环开始时,将此变量重置为一个无效值,意味着除非有事件(比如 USAP 被消耗)触发,否则 Zygote 不需要因为要补充 USAP 池而设置一个特定的 poll 超时。poll 可以无限期地等待下去,直到有真正的 I/O 事件发生。

USAP池

Android10开始,为了加速应用启动,Zygote 会提前 fork 一些"半成品"进程放在一个池子里,这就是 USAP 池。当需要启动新应用时,可以直接从池里拿一个来"特化",省去了 fork 的开销。网上有一些数据,应用冷启动可以加速20ms左右,可以当个参考,有个大致概念。

while 循环

ini 复制代码
        while (true) {
            fetchUsapPoolPolicyPropsWithMinInterval();
            mUsapPoolRefillAction = UsapPoolRefillAction.NONE;

            int[] usapPipeFDs = null;
            StructPollfd[] pollFDs;

fetchUsapPoolPolicyPropsWithMinInterval 会去读取系统设置的一些配置属性,为了避免过于频繁地读取属性(这有一定开销),内部肯定会有一些缓存策略,可以动态地调整这个值,便于USAP的调试和性能调优。

usapPipeFds- 声明一个整型数组,用于存放 USAP 报告管道的文件描述符 (FD),每个 USAP 进程在被创建时,都会和 Zygote 建立一个管道(Pipe)用于通信。Zygote 持有管道的读端,USAP 持有写端。当 USAP 被特化成应用后,它会通过管道的写端向 Zygote 发送报告。

StructPollfd[],存放所有需要被poll监听的fd。

ini 复制代码
            // Allocate enough space for the poll structs, taking into account
            // the state of the USAP pool for this Zygote (could be a
            // regular Zygote, a WebView Zygote, or an AppZygote).
            if (mUsapPoolEnabled) {
                usapPipeFDs = Zygote.getUsapPipeFDs();
                pollFDs = new StructPollfd[socketFDs.size() + 1 + usapPipeFDs.length];
            } else {
                pollFDs = new StructPollfd[socketFDs.size()];
            }

这段逻辑很简单,分别对开启了usap和未开启的情况做初始化。

+1 这个 1 代表的是 mUsapPoolEventFD,用于宏观管理 USAP 池的全局事件通知 FD。

初始化pollFDs

ini 复制代码
int pollIndex = 0;
for (FileDescriptor socketFD : socketFDs) {
    pollFDs[pollIndex] = new StructPollfd();
    pollFDs[pollIndex].fd = socketFD;
    pollFDs[pollIndex].events = (short) POLLIN;
    ++pollIndex;
}
  • Zygote 主 Server Socket 的 FD (在 socketFDs[0])

  • 所有已连接客户端的专用 Socket 的 FD (在 socketFDs[1] 及之后)

  • POLLIN 告诉内核,当这个 FD 有数据可读时,就会回调监听。对于 Server Socket,这意味着有新连接;对于客户端 Socket,这意味着客户端发送了命令。

  • 如果开启了usap,会继续填充mUsapPoolEventFDusapPipeFD

计算pollTimeoutMs

arduino 复制代码
            int pollTimeoutMs;

            if (mUsapPoolRefillTriggerTimestamp == INVALID_TIMESTAMP) {
                pollTimeoutMs = -1;
            } else {
                long elapsedTimeMs = System.currentTimeMillis() - mUsapPoolRefillTriggerTimestamp;

                if (elapsedTimeMs >= mUsapPoolRefillDelayMs) {
                    // The refill delay has elapsed during the period between poll invocations.
                    // We will now check for any currently ready file descriptors before refilling
                    // the USAP pool.
                    pollTimeoutMs = 0;
                    mUsapPoolRefillTriggerTimestamp = INVALID_TIMESTAMP;
                    mUsapPoolRefillAction = UsapPoolRefillAction.DELAYED;

                } else if (elapsedTimeMs <= 0) {
                    // This can occur if the clock used by currentTimeMillis is reset, which is
                    // possible because it is not guaranteed to be monotonic.  Because we can't tell
                    // how far back the clock was set the best way to recover is to simply re-start
                    // the respawn delay countdown.
                    pollTimeoutMs = mUsapPoolRefillDelayMs;

                } else {
                    pollTimeoutMs = (int) (mUsapPoolRefillDelayMs - elapsedTimeMs);
                }
            }

这段代码通过动态计算 poll 的超时时间,实现了一个混合事件驱动和定时任务的单一循环模型,非常高效:

  • 当没有定时任务时,它是一个纯粹的 I/O 事件服务器,无限期等待,不消耗 CPU。

  • 当有定时任务时,它将定时器的剩余时间作为 poll 的最大等待时间。这使得 Zygote 可以在等待 I/O 事件的同时,也在等待定时器到期,而无需使用额外的线程或复杂的定时器机制。

将多种等待源(I/O 事件、定时器)统一到 poll 的超时参数,跟Handler底层的epoll超时等待是类似的,都是为了一个目的

如何在单个线程的事件循环中,既能处理立即到来的事件,又能处理未来某个时间点才需要执行的延时任务,同时还要保证在没有事件时能高效地休眠?

开始poll

php 复制代码
            int pollReturnValue;
            try {
                pollReturnValue = Os.poll(pollFDs, pollTimeoutMs);
            } catch (ErrnoException ex) {
                throw new RuntimeException("poll failed", ex);
            }
  • Os.poll 是 Android Java 层对 Linux poll(2) 系统调用的一个封装。调用 Os.poll(),将 Zygote 进程挂起,等待 I/O 事件或定时任务到期。这是 Zygote 节省 CPU 资源的关键,在空闲时它完全不消耗 CPU。

  • 获取结果: 将 poll 的返回值存入 pollReturnValue,后续的代码将根据这个值来决定下一步的操作。

  • 处理核心错误: 对 poll 可能发生的致命错误进行捕获和处理,通过抛出运行时异常来表明问题的严重性,并触发系统的恢复机制。

为什么不用epoll

对Linux了解或者看过Handler等源码的同学一定会有疑问,为何不用epoll,epoll据说效率更高呀~

poll 和 epoll的区别

  • poll:适合监听少量文件描述符(FD),实现简单,跨平台兼容性好。每次调用都要把所有 FD 列表传给内核,内核遍历所有 FD,效率在 FD 数量大时会变低。

  • epoll:适合监听大量FD,内核维护一个事件表,用户只需注册一次,后续只需等待事件,效率高,尤其在 FD 数量大时优势明显。是高性能服务器(如 Nginx)常用的 I/O 多路复用机制。

理论推算

1. poll 的复杂度
  • poll 每次调用都要把所有 FD 的数组从用户态拷贝到内核态,然后内核线性遍历所有 FD,检查每个 FD 的状态。

  • 时间复杂度:O(N),N 是监听的 FD 数量。

2. epoll 的复杂度
  • epoll 只在注册/注销 FD时用红黑树(O(logN)),事件触发时是 O(1)(内核维护就绪队列)。

  • 事件循环时,epoll_wait 只返回有事件的 FD,不需要遍历所有 FD。

  • 实际事件分发复杂度:接近 O(1) per event。

实际工程经验
  • 几十个 FD:poll 和 epoll 性能几乎无差别。

  • 上百个 FD:poll 开始变慢,epoll 依然很快。

  • 几百到一千个 FD:poll 明显卡顿,epoll 依然流畅。

  • 上千个 FD:poll 可能导致进程卡死,epoll 依然高效。

社区和内核开发者的经验值
  • N ≈ 100 是一个常见的经验分界点。

  • N < 100:poll 足够用,性能差异不大。

  • N > 100:epoll 明显优于 poll,尤其是大部分 FD 长期无事件时。

为什么使用poll,而不是epoll

  1. 这里的Zygote,除了主socket server 之外,还有 AMS 这个大客户,如果开了USAP,还有若干个USAP的socket,不大可能超过100,所以性能没有问题
  2. Java的 Os.poll封装的很成熟,而epoll需要native开发,会增加复杂度

学框架,向框架学习==》我们日常做技术选型,架构设计的时候,也会有很多tradeoff,还是要casebycase的去审视~

poll超时返回

scss 复制代码
if (pollReturnValue == 0) {
    // The poll returned zero results either when the timeout value has been exceeded
    // or when a non-blocking poll is issued and no FDs are ready.  In either case it
    // is time to refill the pool.  This will result in a duplicate assignment when
    // the non-blocking poll returns zero results, but it avoids an additional
    // conditional in the else branch.
    mUsapPoolRefillTriggerTimestamp = INVALID_TIMESTAMP;
    mUsapPoolRefillAction = UsapPoolRefillAction.DELAYED;
}

该轮等待没有 I/O 事件发生, USAP 池补充的定时任务到点了,触发 USAP 池的实际补充操作。

有事件发生

ini 复制代码
                boolean usapPoolFDRead = false;

                while (--pollIndex >= 0) {
                    if ((pollFDs[pollIndex].revents & POLLIN) == 0) {
                        continue;
                    }

遍历FD,找到POLLIN事件的 FD,这里采用倒序遍历,这样在处理过程中如果需要移除某些 FD(比如关闭了某个连接),不会影响还未处理的 FD 的索引,避免遍历时出错。

Zygote Server 来任务了

ini 复制代码
if (pollIndex == 0) {
    // Zygote server socket
    ZygoteConnection newPeer = acceptCommandPeer(abiList);
    peers.add(newPeer);
    socketFDs.add(newPeer.getFileDescriptor());
}

我们知道pollIndex为0的是 Zygote Server socket,它接收的大部分都是AMS的fork新进程的请求。

  • 调用 acceptCommandPeer(abiList) 方法,接受新连接。
  • 这个方法内部会调用 mZygoteSocket.accept(),返回一个新的 LocalSocket,并用它创建一个 ZygoteConnection 对象。
  • ZygoteConnection 封装了与新客户端(如 AMS)之间的通信逻辑,包括 socket、输入输出流、命令处理等

这样设计的好处是,Zygote Server Socket负责接纳新连接,每有一个新客户端连接,Zygote 就把它的 FD 加入监听列表,后续可以处理它的命令

处理AMS请求

java 复制代码
else if (pollIndex < usapPoolEventFDIndex) {
                        // Session socket accepted from the Zygote server socket

                        try {
                            ZygoteConnection connection = peers.get(pollIndex);
                            boolean multipleForksOK = !isUsapPoolEnabled()
                                    && ZygoteHooks.isIndefiniteThreadSuspensionSafe();
                            final Runnable command =
                                    connection.processCommand(this, multipleForksOK);

重点是 final Runnable command = connection.processCommand(this, multipleForksOK); 读取 socket 上的命令(如 fork 新进程),并实际执行 fork 操作,

  • 在父进程(Zygote)中返回 null

  • 在子进程(fork 出来的新进程)中返回一个 Runnable,用于启动 Java 层主类(如 ActivityThread.main)

scss 复制代码
                            if (mIsForkChild) {
                                // We're in the child. We should always have a command to run at
                                // this stage if processCommand hasn't called "exec".
                                if (command == null) {
                                    throw new IllegalStateException("command == null");
                                }

                                return command;
                            } else {
                                // We're in the server - we should never have any commands to run.
                                if (command != null) {
                                    throw new IllegalStateException("command != null");
                                }

                                // We don't know whether the remote side of the socket was closed or
                                // not until we attempt to read from it from processCommand. This
                                // shows up as a regular POLLIN event in our regular processing
                                // loop.
                                if (connection.isClosedByPeer()) {
                                    connection.closeSocket();
                                    peers.remove(pollIndex);
                                    socketFDs.remove(pollIndex);
                                }
                            }

fork之后,父子进程各返回一次。

  • 子进程直接 return command,Zygote 事件循环会退出,转而执行新进程的主逻辑。
  • 父进程(Zygote)中,command 应该始终为 null, 并检查连接是否已被对端关闭,及时清理资源(关闭 socket,移除 FD 和连接对象),所以不用担心FD暴增的问题,使用完会释放。

处理USAP管道消息

arduino 复制代码
                        // Either the USAP pool event FD or a USAP reporting pipe.

                        // If this is the event FD the payload will be the number of USAPs removed.
                        // If this is a reporting pipe FD the payload will be the PID of the USAP
                        // that was just specialized.  The `continue` statements below ensure that
                        // the messagePayload will always be valid if we complete the try block
                        // without an exception.
                        long messagePayload;

                        try {
                            byte[] buffer = new byte[Zygote.USAP_MANAGEMENT_MESSAGE_BYTES];
                            int readBytes =
                                    Os.read(pollFDs[pollIndex].fd, buffer, 0, buffer.length);

                            if (readBytes == Zygote.USAP_MANAGEMENT_MESSAGE_BYTES) {
                                DataInputStream inputStream =
                                        new DataInputStream(new ByteArrayInputStream(buffer));

                                messagePayload = inputStream.readLong();
                            } else {
                                Log.e(TAG, "Incomplete read from USAP management FD of size "
                                        + readBytes);
                                continue;
                            }
                        } catch (Exception ex) {
                            if (pollIndex == usapPoolEventFDIndex) {
                                Log.e(TAG, "Failed to read from USAP pool event FD: "
                                        + ex.getMessage());
                            } else {
                                Log.e(TAG, "Failed to read from USAP reporting pipe: "
                                        + ex.getMessage());
                            }

                            continue;
                        }

                        if (pollIndex > usapPoolEventFDIndex) {
                            Zygote.removeUsapTableEntry((int) messagePayload);
                        }

                        usapPoolFDRead = true;

1. 检测到 USAP 池相关 FD 有事件(eventfd 或 pipe 可读)。

2. 读取并解析消息,获取 USAP 池状态变化信息。

3. 根据 FD 类型更新 USAP 池管理表,移除已被特化的 USAP。

4. 异常和完整性处理,保证主循环健壮。

USAP池补充机制

ini 复制代码
                if (usapPoolFDRead) {
                    int usapPoolCount = Zygote.getUsapPoolCount();

                    if (usapPoolCount < mUsapPoolSizeMin) {
                        // Immediate refill
                        mUsapPoolRefillAction = UsapPoolRefillAction.IMMEDIATE;
                    } else if (mUsapPoolSizeMax - usapPoolCount >= mUsapPoolRefillThreshold) {
                        // Delayed refill
                        mUsapPoolRefillTriggerTimestamp = System.currentTimeMillis();
                    }
                }
  • 如果当前池子数量小于最小池子规模(mUsapPoolSizeMin),说明池子已经"见底"了,要立即补充
  • 如果池子的"空位"数量(mUsapPoolSizeMax - usapPoolCount)大于等于补充阈值(mUsapPoolRefillThreshold),说明池子被消耗得比较多,但还没到最小值,设置 mUsapPoolRefillTriggerTimestamp 为当前时间,启动一个延迟补充的定时器
ini 复制代码
            if (mUsapPoolRefillAction != UsapPoolRefillAction.NONE) {
                int[] sessionSocketRawFDs =
                        socketFDs.subList(1, socketFDs.size())
                                .stream()
                                .mapToInt(FileDescriptor::getInt$)
                                .toArray();

                final boolean isPriorityRefill =
                        mUsapPoolRefillAction == UsapPoolRefillAction.IMMEDIATE;

                final Runnable command =
                        fillUsapPool(sessionSocketRawFDs, isPriorityRefill);

                if (command != null) {
                    return command;
                } else if (isPriorityRefill) {
                    // Schedule a delayed refill to finish refilling the pool.
                    mUsapPoolRefillTriggerTimestamp = System.currentTimeMillis();
                }
            }

USAP的补充逻辑,如果 fillUsapPool 返回了 command,说明当前线程已经 fork 成了新进程,需要立即返回,去执行新进程的主逻辑。如果没有返回 command,但当前是 IMMEDIATE 补充(优先补充),说明本轮补充还没补满池子,需要安排一个延迟补充,即设置 mUsapPoolRefillTriggerTimestamp,让下一轮循环再补充一次,直到池子补满。

  • 至此,ZygoteServer的任务就完成了,当有AMS的任务过来的时候,会走到ZygoteConnection.processCommand,会根据是否启用了USAP,走原始的fork或者将USAP池中的进程拿来初始化,这部分的逻辑大家可以自行研究。

关于Zygote的部分就告一段落了,后面我将开始system_server中系统服务的学习~

相关推荐
Code季风30 分钟前
SQL关键字三分钟入门:WITH —— 公用表表达式让复杂查询更清晰
java·数据库·sql
沿着缘溪奔向大海1 小时前
蓝牙数据通讯,实现内网电脑访问外网电脑
java·爬虫·python·socket·蓝牙
过期动态1 小时前
MySQL中的常见运算符
java·数据库·spring boot·mysql·spring cloud·kafka·tomcat
想用offer打牌1 小时前
一站式了解责任链模式
java·后端·设计模式·责任链模式
专注VB编程开发20年1 小时前
C# .NET多线程异步记录日声,队列LOG
java·开发语言·前端·数据库·c#
charlie1145141911 小时前
从C++编程入手设计模式——责任链模式
c++·设计模式·责任链模式
杰_happy1 小时前
责任链模式详解
设计模式·责任链模式
京东云开发者1 小时前
由 Mybatis 源码畅谈软件设计(八):从根上理解 Mybatis 二级缓存
java
YU_admin1 小时前
Java:常见算法
java·数据结构·算法
转码的小石2 小时前
Java面试复习指南:基础、多线程、JVM、Spring、算法精要
java·jvm·数据结构·算法·spring·面试·多线程