接着上一篇 深入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 添加到socketFDs
,peers
添加了null。这里解释一下
- mZygoteSocket: 这是 Zygote Server 的主 LocalServerSocket。它的作用就像一个酒店的总机电话,专门用来接收新的连接请求,后面我们会看到它接到请求之后,会立马创建一个ZygoteConnection,并添加到peers,相当于创建了一个分机。
所以从设计上来说,socketFDs
和peers
保持一对一对应关系,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,会继续填充
mUsapPoolEventFD
和usapPipeFD
计算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
- 这里的Zygote,除了主socket server 之外,还有 AMS 这个大客户,如果开了USAP,还有若干个USAP的socket,不大可能超过100,所以性能没有问题
- 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中系统服务的学习~