双生
近期翻了下 Google I/O 2019 中关于 Android Runtime 的演讲,其中一项优化内容对原有的知识体系发起了一道灵魂拷问,不得不对 Application 的启动流程重新审视。
应用进程真的直接由 Zygote 进程 Fork 而得吗?
回忆
将源码切换到 Android 9.0,看一看应用进程的启动流程。
从 Process.start()
出发,调用链将到达 ZygoteProcess.zygoteSendArgsAndGetResult()
中。
调用链:
Process.start()
–>ZygoteProcess.start()
–>ZygoteProcess.startViaZygote()
–>ZygoteProcess.zygoteSendArgsAndGetResult()
/**
* Sends an argument list to the zygote process, which starts a new child
* and returns the child's pid. Please note: the present implementation
* replaces newlines in the argument list with spaces.
*
* @throws ZygoteStartFailedEx if process start failed for any reason
*/
@GuardedBy("mLock")
private static Process.ProcessStartResult zygoteSendArgsAndGetResult(
ZygoteState zygoteState, ArrayList<String> args)
throws ZygoteStartFailedEx {
try {
// Throw early if any of the arguments are malformed. This means we can
// avoid writing a partial response to the zygote.
int sz = args.size();
for (int i = 0; i < sz; i++) {
if (args.get(i).indexOf('\n') >= 0) {
throw new ZygoteStartFailedEx("embedded newlines not allowed");
}
}
/**
* See com.android.internal.os.SystemZygoteInit.readArgumentList()
* Presently the wire format to the zygote process is:
* a) a count of arguments (argc, in essence)
* b) a number of newline-separated argument strings equal to count
*
* After the zygote process reads these it will write the pid of
* the child or -1 on failure, followed by boolean to
* indicate whether a wrapper process was used.
*/
final BufferedWriter writer = zygoteState.writer;
final DataInputStream inputStream = zygoteState.inputStream;
writer.write(Integer.toString(args.size()));
writer.newLine();
for (int i = 0; i < sz; i++) {
String arg = args.get(i);
writer.write(arg);
writer.newLine();
}
writer.flush();
...
} catch (IOException ex) {
...
}
}
可见,通过将解析完成的参数写入到 zygoteState
中的 Socket 通道,实现向 Zygote 进程发送创建用户进程请求的操作。
而 Zygote 进程中,runSelectLoop()
将收到应用程序创建的请求。当 Zygote 收到新的连接请求时,进程将被唤醒,并创建连接对应的 ZygoteConnection
对象,最终调用 ZygoteConnection.processOneCommand()
对请求进行解析处理。
/**
* Runs the zygote process's select loop. Accepts new connections as
* they happen, and reads commands from connections one spawn-request's
* worth at a time.
*/
Runnable runSelectLoop(String abiList) {
ArrayList<FileDescriptor> fds = new ArrayList<FileDescriptor>();
ArrayList<ZygoteConnection> peers = new ArrayList<ZygoteConnection>();
fds.add(mServerSocket.getFileDescriptor());
peers.add(null);
while (true) {
...
for (int i = pollFds.length - 1; i >= 0; --i) {
...
if (i == 0) {
ZygoteConnection newPeer = acceptCommandPeer(abiList);
peers.add(newPeer);
fds.add(newPeer.getFileDesciptor());
} else {
try {
ZygoteConnection connection = peers.get(i);
final Runnable command = connection.processOneCommand(this);
...
} catch (Exception e) {
...
} finally {
...
}
}
}
}
}
在 Android 8.0 及之前,
processOneCommand()
的函数名为runOnce()
。
ZygoteConnection.processOneCommand()
在对参数完成解析后,最终调用了静态工具类 Zygote
的 Zygote.forkAndSpecialize()
函数,以 Fork 出新的应用进程。而 Zygote.forkAndSpecialize()
最终调用 Zygote.nativeForkAndSpecialize()
在 Native 层完成进程的 Fork 与初始化操作。
/**
* Reads one start command from the command socket. If successful, a child is forked and a
* {@code Runnable} that calls the childs main method (or equivalent) is returned in the child
* process. {@code null} is always returned in the parent process (the zygote).
*
* If the client closes the socket, an {@code EOF} condition is set, which callers can test
* for by calling {@code ZygoteConnection.isClosedByPeer}.
*/
Runnable processOneCommand(ZygoteServer zygoteServer) {
...
pid = Zygote.forkAndSpecialize(parsedArgs.uid, parsedArgs.gid, parsedArgs.gids,
parsedArgs.runtimeFlags, rlimits, parsedArgs.mountExternal, parsedArgs.seInfo,
parsedArgs.niceName, fdsToClose, fdsToIgnore, parsedArgs.startChildZygote,
parsedArgs.instructionSet, parsedArgs.appDataDir);
...
}
双生
为了避免在启动应用程序时对 Zygote 进程的唤醒耗费系统资源,Android 10 在默认情况下不再从 Zygote 直接 Fork 应用进程,而是通过 Fork 多个监听同一 Socket 文件描述符的待用进程,称为 Unspecialized App Process(简称 USAP)。当接收到应用进程创建的信息时,其中的一个 USAP 将直接转化为应用程序进程,从而将 Zygote 移出整个启动流程。
新的流程同样从 Process.start()
开始,但此时在 ZygoteProcess.start()
中,startViaZygote()
调用前增加了一些处理逻辑。
函数 fetchUsapPoolEnabledPropWithMinInterval()
返回是否启用 Pre-fork 机制,而 informZygotesOfUsapPoolStatus()
将通知 Zygote 进程进行 USAP 进程池的相关操作。
/**
* Start a new process.
*
* <p>If processes are enabled, a new process is created and the
* static main() function of a <var>processClass</var> is executed there.
* The process will continue running after this function returns.
*
* <p>If processes are not enabled, a new thread in the caller's
* process is created and main() of <var>processclass</var> called there.
*
* <p>The niceName parameter, if not an empty string, is a custom name to
* give to the process instead of using processClass. This allows you to
* make easily identifyable processes even if you are using the same base
* <var>processClass</var> to start them.
*
* When invokeWith is not null, the process will be started as a fresh app
* and not a zygote fork. Note that this is only allowed for uid 0 or when
* runtimeFlags contains DEBUG_ENABLE_DEBUGGER.
*
* @param processClass The class to use as the process's main entry
* point.
* @param niceName A more readable name to use for the process.
* @param uid The user-id under which the process will run.
* @param gid The group-id under which the process will run.
* @param gids Additional group-ids associated with the process.
* @param runtimeFlags Additional flags.
* @param targetSdkVersion The target SDK version for the app.
* @param seInfo null-ok SELinux information for the new process.
* @param abi non-null the ABI this app should be started with.
* @param instructionSet null-ok the instruction set to use.
* @param appDataDir null-ok the data directory of the app.
* @param invokeWith null-ok the command to invoke with.
* @param packageName null-ok the name of the package this process belongs to.
* @param zygotePolicyFlags Flags used to determine how to launch the application.
* @param isTopApp Whether the process starts for high priority application.
* @param disabledCompatChanges null-ok list of disabled compat changes for the process being
* started.
* @param pkgDataInfoMap Map from related package names to private data directory
* volume UUID and inode number.
* @param whitelistedDataInfoMap Map from allowlisted package names to private data directory
* volume UUID and inode number.
* @param bindMountAppsData whether zygote needs to mount CE and DE data.
* @param bindMountAppStorageDirs whether zygote needs to mount Android/obb and Android/data.
*
* @param zygoteArgs Additional arguments to supply to the Zygote process.
* @return An object that describes the result of the attempt to start the process.
* @throws RuntimeException on fatal start failure
*/
public final Process.ProcessStartResult start(@NonNull final String processClass,
final String niceName,
int uid, int gid, @Nullable int[] gids,
int runtimeFlags, int mountExternal,
int targetSdkVersion,
@Nullable String seInfo,
@NonNull String abi,
@Nullable String instructionSet,
@Nullable String appDataDir,
@Nullable String invokeWith,
@Nullable String packageName,
int zygotePolicyFlags,
boolean isTopApp,
@Nullable long[] disabledCompatChanges,
@Nullable Map<String, Pair<String, Long>>
pkgDataInfoMap,
@Nullable Map<String, Pair<String, Long>>
whitelistedDataInfoMap,
boolean bindMountAppsData,
boolean bindMountAppStorageDirs,
@Nullable String[] zygoteArgs) {
// TODO (chriswailes): Is there a better place to check this value?
if (fetchUsapPoolEnabledPropWithMinInterval()) {
informZygotesOfUsapPoolStatus();
}
try {
return startViaZygote(processClass, niceName, uid, gid, gids,
runtimeFlags, mountExternal, targetSdkVersion, seInfo,
abi, instructionSet, appDataDir, invokeWith, /*startChildZygote=*/ false,
packageName, zygotePolicyFlags, isTopApp, disabledCompatChanges,
pkgDataInfoMap, whitelistedDataInfoMap, bindMountAppsData,
bindMountAppStorageDirs, zygoteArgs);
} catch (ZygoteStartFailedEx ex) {
Log.e(LOG_TAG,
"Starting VM process through Zygote failed");
throw new RuntimeException(
"Starting VM process through Zygote failed", ex);
}
}
而 zygoteSendArgsAndGetResult()
中将不再直接向 Zygote 的 Socket 通道写入请求,原有的逻辑被封装到 attemptZygoteSendArgsAndGetResult()
中,而启用 Pre-fork 机制后则会调用另一个分支,attemptUsapSendArgsAndGetResult()
。
/**
* Sends an argument list to the zygote process, which starts a new child
* and returns the child's pid. Please note: the present implementation
* replaces newlines in the argument list with spaces.
*
* @throws ZygoteStartFailedEx if process start failed for any reason
*/
@GuardedBy("mLock")
private Process.ProcessStartResult zygoteSendArgsAndGetResult(
ZygoteState zygoteState, int zygotePolicyFlags, @NonNull ArrayList<String> args)
throws ZygoteStartFailedEx {
...
if (shouldAttemptUsapLaunch(zygotePolicyFlags, args)) {
try {
return attemptUsapSendArgsAndGetResult(zygoteState, msgStr);
} catch (IOException ex) {
// If there was an IOException using the USAP pool we will log the error and
// attempt to start the process through the Zygote.
Log.e(LOG_TAG, "IO Exception while communicating with USAP pool - "
+ ex.getMessage());
}
}
return attemptZygoteSendArgsAndGetResult(zygoteState, msgStr);
}
attemptUsapSendArgsAndGetResult()
中同样执行了向 Socket 通道写入请求的操作,只是这个 Socket 不再是与 Zygote 通信的,而是前文提及的 USAP 共用的 Socket。
private Process.ProcessStartResult attemptUsapSendArgsAndGetResult(
ZygoteState zygoteState, String msgStr)
throws ZygoteStartFailedEx, IOException {
try (LocalSocket usapSessionSocket = zygoteState.getUsapSessionSocket()) {
final BufferedWriter usapWriter =
new BufferedWriter(
new OutputStreamWriter(usapSessionSocket.getOutputStream()),
Zygote.SOCKET_BUFFER_SIZE);
final DataInputStream usapReader =
new DataInputStream(usapSessionSocket.getInputStream());
usapWriter.write(msgStr);
usapWriter.flush();
...
}
}
转到 Zygote 进程,runSelectLoop()
当接收到由 informZygotesOfUsapPoolStatus()
发送的信息时,将调用 fillUsapPool()
初始化 USAP 进程池。
重点在于,fillUsapPool()
内部进行了 Fork 操作以创建 USAP,在 Fork 之后 Zygote 进程获取的返回的 Runnable
为 null
,而 USAP 在未转变为应用进程前将进入休眠,转变后与原逻辑中 processOneCommand()
函数相似,返回需执行的 Runnable
。
/**
* Runs the zygote process's select loop. Accepts new connections as
* they happen, and reads commands from connections one spawn-request's
* worth at a time.
*/
Runnable runSelectLoop(String abiList) {
ArrayList<FileDescriptor> socketFDs = new ArrayList<>();
ArrayList<ZygoteConnection> peers = new ArrayList<>();
socketFDs.add(mZygoteSocket.getFileDescriptor());
peers.add(null);
mUsapPoolRefillTriggerTimestamp = INVALID_TIMESTAMP;
while (true) {
fetchUsapPoolPolicyPropsWithMinInterval();
...
if (pollReturnValue == 0) {
...
} else {
boolean usapPoolFDRead = false;
while (--pollIndex >= 0) {
if ((pollFDs[pollIndex].revents & POLLIN) == 0) {
continue;
}
if (pollIndex == 0) {
... *
} else if (pollIndex < usapPoolEventFDIndex) {
... *
} else {
...
usapPoolFDRead = true;
}
}
if (usapPoolFDRead) {
int usapPoolCount = Zygote.getUsapPoolCount();
if (usapPoolCount < mUsapPoolSizeMin) {
// Immediate refill
mUsapPoolRefillAction = UsapPoolRefillAction.IMMEDIATE;
} else if (mUsapPoolSizeMax - usapPoolCount >= mUsapPoolRefillThreshold) {
// Delayed refill
mUsapPoolRefillTriggerTimestamp = System.currentTimeMillis();
}
}
}
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();
}
}
}
}
fillUsapPool()
内部实现预处理、Fork、初始化等操作,Fork 的操作将调用 Zygote.forkUsap()
。
/**
* Refill the USAP Pool to the appropriate level, determined by whether this is a priority
* refill event or not.
*
* @param sessionSocketRawFDs Anonymous session sockets that are currently open
* @return In the Zygote process this function will always return null; in unspecialized app
* processes this function will return a Runnable object representing the new
* application that is passed up from usapMain.
*/
Runnable fillUsapPool(int[] sessionSocketRawFDs, boolean isPriorityRefill) {
Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "Zygote:FillUsapPool");
// Ensure that the pool properties have been fetched.
fetchUsapPoolPolicyPropsIfUnfetched();
int usapPoolCount = Zygote.getUsapPoolCount();
int numUsapsToSpawn;
if (isPriorityRefill) {
// Refill to min
numUsapsToSpawn = mUsapPoolSizeMin - usapPoolCount;
...
} else {
// Refill up to max
numUsapsToSpawn = mUsapPoolSizeMax - usapPoolCount;
...
}
// Disable some VM functionality and reset some system values
// before forking.
ZygoteHooks.preFork();
while (--numUsapsToSpawn >= 0) {
Runnable caller =
Zygote.forkUsap(mUsapPoolSocket, sessionSocketRawFDs, isPriorityRefill);
if (caller != null) {
return caller;
}
}
// Re-enable runtime services for the Zygote. Services for unspecialized app process
// are re-enabled in specializeAppProcess.
ZygoteHooks.postForkCommon();
resetUsapRefillState();
Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
return null;
}
最终的 Fork 通过 Zygote.nativeForkUsap()
在 Native 层中进行。
/**
* Fork a new unspecialized app process from the zygote
*
* @param usapPoolSocket The server socket the USAP will call accept on
* @param sessionSocketRawFDs Anonymous session sockets that are currently open
* @param isPriorityFork Value controlling the process priority level until accept is called
* @return In the Zygote process this function will always return null; in unspecialized app
* processes this function will return a Runnable object representing the new
* application that is passed up from usapMain.
*/
static Runnable forkUsap(LocalServerSocket usapPoolSocket,
int[] sessionSocketRawFDs,
boolean isPriorityFork) {
...
int pid =
nativeForkUsap(pipeFDs[0].getInt$(), pipeFDs[1].getInt$(),
sessionSocketRawFDs, isPriorityFork);
if (pid == 0) {
IoUtils.closeQuietly(pipeFDs[0]);
return usapMain(usapPoolSocket, pipeFDs[1]);
} else {
...
}
}
在 Fork 完成时,获取到返回值为 0 的子进程 USAP 将调用 usapMain()
,并在内部通过调用 LocalServerSocket.accept()
(这是一个阻塞操作)监听 USAP 共用的 Socket,等待应用进程创建的信息。在接收到应用进程创建信息后,USAP 将通过 specializeAppProcess()
将进程转化为应用进程,并生成返回需执行的 Runnable
。
由于 Socket 的特性,当多个 USAP 监听同一个 Socket 时,只有一个 USAP 会被唤醒并执行处理,其它进程保持休眠状态。
偏个题。
如果多个进程或者线程在等待同一个事件,当事件发生时,所有线程和进程都会被内核唤醒,唤醒后通常只有一个进程获得了该事件并进行处理,其他进程发现获取事件失败后又继续进入了等待状态,在一定程度上降低了系统性能,这称为 惊群效应。
这里多个 USAP 共同监听了同一个 Socket,而在 Linux Kernel 2.6 后 Socket 的
accept()
通过维护一个等待队列来解决这一问题,因此这段代码中避免了惊群效应。
/**
* This function is used by unspecialized app processes to wait for specialization requests from
* the system server.
*
* @param writePipe The write end of the reporting pipe used to communicate with the poll loop
* of the ZygoteServer.
* @return A runnable oject representing the new application.
*/
private static Runnable usapMain(LocalServerSocket usapPoolSocket,
FileDescriptor writePipe) {
...
while (true) {
try {
sessionSocket = usapPoolSocket.accept();
...
if (argStrings != null) {
args = new ZygoteArguments(argStrings);
// TODO (chriswailes): Should this only be run for debug builds?
validateUsapCommand(args);
break;
} else {
...
}
} catch (Exception ex) {
...
}
}
try {
...
specializeAppProcess(args.mUid, args.mGid, args.mGids,
args.mRuntimeFlags, rlimits, args.mMountExternal,
args.mSeInfo, args.mNiceName, args.mStartChildZygote,
args.mInstructionSet, args.mAppDataDir, args.mIsTopApp,
args.mPkgDataInfoList, args.mWhitelistedDataInfoList,
args.mBindMountAppDataDirs, args.mBindMountAppStorageDirs);
...
return ZygoteInit.zygoteInit(args.mTargetSdkVersion,
args.mDisabledCompatChanges,
args.mRemainingArgs,
null /* classLoader */);
} finally {
...
}
}