近期翻了下 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() 在对参数完成解析后,最终调用了静态工具类 ZygoteZygote.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 移出整个启动流程。

Pre-forking Application Processes

Google I/O 2019 中对 Pre-fork 流程说明的幻灯片

新的流程同样从 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 进程获取的返回的 Runnablenull,而 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 {
        ...
    }
}

参考链接