안드로이드란 #5. zygote

zygote는 안드로이드의 실체다. zygote를 이해하지 못하고서는 결코 안드로이드의 작동원리에 대해 안다고 이야기할 수 없다. 그만큼 중요한 부분이고, 안드로이드라는 OS의 특수성이 모두 이 안에 담겨있다.

DVM 또한 zygote의 프로세스 생성 과정에 포함되어 있기 때문에, 안드로이드 가상머신 과정의 모든 것이 바로 zygote 라고 하여도 무방하다.

앞서 JNI와 NDK에 대해 먼저 알아보고 C언어와 JAVA를 넘나드는 방법을 알아본 이유 또한 zygote와 service framework에 대한 이해를 높이기 위함이다.

zygote의 내부를 필수적으로 들여다보아야 할 이유는 여러가지가 있지만, 순차적으로 그 기능들을 분류해 보면 다음과 같다.

1. init프로세스에서 init.rc를 파싱하는 과정에서 zygote의 app_process 를 실행
2. app_process에서 Dalvik VM의 생성과 ZygoteInit 진입
3. ZygoteInit 진입 후 새로운 어플리케이션 실행요청을 받기 위한 소켓 바인딩
4. preload되는 class들과 resource들을 로드
5. system server 실행 : audio / surface flinger, MediaPlayerServer, CameraServer, 각종 매니저 및 service frameworks 등등등등
6. 어플리케이션 실행요청에 대한 처리 : forkAndSpecialize 로 자기복제 및 어플리케이션 클래스 로드

zygote의 총체적 기능이라 함은, 가상머신의 생성과 새로운 프로세스의 시작을 fork()로 처리하기 위해서이다. fork()는 자기 자신의 알고리즘을 복제하는 함수다.(= 포킹) 즉 가상머신이 어플리케이션이 실행되기 위한 모든 준비를 미리 마쳐 놓고, 어플 실행시에 준비된 가상머신을 복제하여 어플리케이션을 실행한다면 미리 준비된 클래스와 리소스의 참조를 활용할 수 있다. 안드로이드의 어플리케이션이 자바로 작성되어 있음에도 불구하고 이런 방식을 통해 빠른 구동이 가능해지기 때문에 zygote가 하는 일들은 성능을 위해 매우 중요하다.

이제 언급할 내용들은 우리가 어떤 방식으로 소스를 접해 나가야 하는지에 대한 가이드라인이다.

1. init프로세스에서 init.rc를 파싱하는 과정에서 zygote의 app_process 를 실행

init 프로세스의 소스들은 이미 우리가 한차례 분석한 경험이 있다. 이 과정에서 init.rc를 파싱하여 생성할 프로세스들을 루프에서 처리한다. init.rc를 보면 다음과 같은 부분이 있다.

service zygote /system/bin/app_process -Xzygote /system/bin --zygote --start-system-server 
    class main 
    socket zygote stream 660 root system 
    onrestart write /sys/android_power/request_state wake 
    onrestart write /sys/power/state on 
    onrestart restart media 
    onrestart restart netd

(넥서스7의 젤리빈 init.rc에서..)

zygote 프로세스를 시작하기 위해 app_process를 실행하고 뒤에는 실행 인자들을 적어 두었다.
첫줄을 분석해보면
app_process -자바옵션 프로세스생성디렉토리 --가상머신에서로딩할클래스 --클래스에전달할옵션
이 내용을 서비스 리스트에 추가하고 init.c에 있던 루프에서 처리를 하면서 app_process를 실행하게 된다.
첫줄 아래에 적힌 내용들은 main클래스를 실행하고, UDS소켓의 종류(스트림)와 퍼미션을 부여한다.

2. app_process에서 Dalvik VM의 생성과 ZygoteInit 진입

app_process의 메인 소스(frameworkd/base/cmds/app_process/app_main.c)의 메인함수를 들여다보자.

AppRuntime runtime; // 런타임 객체 생성. AndroidRuntime의 생성자 호출.
....
if (zygote) { // init.rc에서 호출된 클래스네임이 --zygote 일 경우 runtime.start
        runtime.start("com.android.internal.os.ZygoteInit", 
                startSystemServer ? "start-system-server" : "");

AppRuntime이라는 클래스의 생성과정을 따라가게 되면 frameworks/base/core/jni/AndroidRuntime.cpp의 내용을 이용하게 된다는 것을 알 수 있었다. 

    /* start the virtual machine */
    JNIEnv* env;
    if (startVm(&mJavaVM, &env) != 0) {
        return;
    }
    onVmCreated(env);

가장 먼저 볼 내용은 바로 start()함수가 호출하는 startVM()이다. 각종 DVM의 초기 설정 및 실행을 하는 코드가 들어있다. 또한 개발자나 사용자가 설정한 프로퍼티(예를들어 build.prop이라던가) 에서도 초기값을 가져온다는 점을 알 수 있다. VM의 생성은 JNI_CreateJavaVM()의 호출로 이루어진다.

int AndroidRuntime::startVm(JavaVM** pJavaVM, JNIEnv** pEnv)
{
    int result = -1;
    JavaVMInitArgs initArgs;
    JavaVMOption opt;
    char propBuf[PROPERTY_VALUE_MAX]; // 각종 프로퍼티들을 가져옴
    char stackTraceFileBuf[PROPERTY_VALUE_MAX];
    char dexoptFlagsBuf[PROPERTY_VALUE_MAX];
    char enableAssertBuf[sizeof("-ea:")-1 + PROPERTY_VALUE_MAX];
    char jniOptsBuf[sizeof("-Xjniopts:")-1 + PROPERTY_VALUE_MAX];
    char heapstartsizeOptsBuf[sizeof("-Xms")-1 + PROPERTY_VALUE_MAX];
    char heapsizeOptsBuf[sizeof("-Xmx")-1 + PROPERTY_VALUE_MAX];
    char heapgrowthlimitOptsBuf[sizeof("-XX:HeapGrowthLimit=")-1 + PROPERTY_VALUE_MAX];
    char heapminfreeOptsBuf[sizeof("-XX:HeapMinFree=")-1 + PROPERTY_VALUE_MAX];
    char heapmaxfreeOptsBuf[sizeof("-XX:HeapMaxFree=")-1 + PROPERTY_VALUE_MAX];

........(중략)

    if (JNI_CreateJavaVM(pJavaVM, pEnv, &initArgs) < 0) {
        ALOGE("JNI_CreateJavaVM failed\n");
        goto bail;
    }

    result = 0;

bail:
    free(stackTraceFile);
    return result;
}

JNI_CreateJavaVM()은 dalvik/vm/Jni.c에 존재하며, JavaVM인스턴스 포인터와 JNIEnv인스턴스 포인터, 그리고 가상머신 옵션을 인자로 받는다. 이 함수의 내부 구조에 대해서까지는 굉장히 복잡하므로 지금 굳이 다루지 않겠다.

가상머신이 활용하는 각종 네이티브 함수는 배열로 저장된다.

static const RegJNIRec gRegJNI[] = {
    REG_JNI(register_android_debug_JNITest),
    REG_JNI(register_com_android_internal_os_RuntimeInit),
    REG_JNI(register_android_os_SystemClock),
    REG_JNI(register_android_util_EventLog),
    REG_JNI(register_android_util_Log),
    REG_JNI(register_android_util_FloatMath),
......

startVM()과정을 마친 start()함수가 위 배열을 등록하는 startReg()함수를 호출한다. 이후로는 가상머신의 자바 클래스에서 네이티브 함수 호출이 가능해진다. 이 네이티브함수들은 frameworks/base/core/jni 디렉토리에 정의되어 있다.

    if (startReg(env) < 0) {
        ALOGE("Unable to register all android natives\n");
        return;
    }

init.rc에 의해 넘겨진 --zygote 클래스네임은 app_main.c의 메인함수의 if문에 따라 com.android.internal.os.ZygoetInit 을 가리킨다. 그리고 이 클래스 네임은 start()함수가 호출하는 toSlashClassName() 함수에 의해 . 을 / 로 치환하여 FindClass()함수가 해당 경로를 찾을 수 있도록 스트링을 조작한다.

char* slashClassName = toSlashClassName(className);
    jclass startClass = env->FindClass(slashClassName);
    if (startClass == NULL) {
        ALOGE("JavaVM unable to locate class '%s'\n", slashClassName);
        /* keep going */
    } else {
        jmethodID startMeth = env->GetStaticMethodID(startClass, "main",
            "([Ljava/lang/String;)V");
        if (startMeth == NULL) {
            ALOGE("JavaVM unable to find main() in '%s'\n", className);
            /* keep going */
        } else {
            env->CallStaticVoidMethod(startClass, startMeth, strArray);

#if 0
            if (env->ExceptionCheck())
                threadExitUncaughtException(env);
#endif
        }
    }

그리고 env->CallStaticVoidMethod() 함수에 의해 ZygoteInit클래스가 로딩된다.

3. ZygoteInit 진입 후 새로운 어플리케이션 실행요청을 받기 위한 소켓 바인딩

    public static void main(String argv[]) {
        try {
            // Start profiling the zygote initialization.
            SamplingProfilerIntegration.start();

            registerZygoteSocket(); // 바인딩 소켓 생성
            EventLog.writeEvent(LOG_BOOT_PROGRESS_PRELOAD_START,
                SystemClock.uptimeMillis());
            preload(); // 프리로드
            EventLog.writeEvent(LOG_BOOT_PROGRESS_PRELOAD_END,
                SystemClock.uptimeMillis());

            // Finish profiling the zygote initialization.
            SamplingProfilerIntegration.writeZygoteSnapshot();

            // Do an initial gc to clean up after startup
            gc();

            // If requested, start system server directly from Zygote
            if (argv.length != 2) {
                throw new RuntimeException(argv[0] + USAGE_STRING);
            }

            if (argv[1].equals("start-system-server")) {
                startSystemServer(); // 시스템서버 시작
            } else if (!argv[1].equals("")) {
                throw new RuntimeException(argv[0] + USAGE_STRING);
            }

            Log.i(TAG, "Accepting command socket connections");

            if (ZYGOTE_FORK_MODE) { 
                runForkMode(); // 새로운 어플리케이션 실행으로 zygote포킹시
            } else {
                runSelectLoopMode(); // 최초 zygote생성시
            }

            closeServerSocket();
        } catch (MethodAndArgsCaller caller) {
            caller.run();
        } catch (RuntimeException ex) {
            Log.e(TAG, "Zygote died with exception", ex);
            closeServerSocket();
            throw ex;
        }
    }

frameworks/base/core/java/com/android/internal/os/ZygoteInit.java의 main() 메소드 진입 후 registerZygoteSocket()이 호출된다. 여기서 사용되는 UDS(유닉스도메인소켓)은 init프로세스가 이미 /dev/socket/zygote 에 이미 생성해 두었으며, 이후 실행될 ActivityManaver로부터 새로운 어플리케이션의 실행 요청을 수신하는 역할을 한다.

private static void registerZygoteSocket() {
        if (sServerSocket == null) {
            int fileDesc;
            try {
                String env = System.getenv(ANDROID_SOCKET_ENV); 
                fileDesc = Integer.parseInt(env); // 환경변수 등록된 소켓의 file descripter를 스트링으로 받아 integer로 변환하여 저장
            } catch (RuntimeException ex) {
                throw new RuntimeException(
                        ANDROID_SOCKET_ENV + " unset or invalid", ex);
            }

            try {
                sServerSocket = new LocalServerSocket(
                        createFileDescriptor(fileDesc)); // 정수화된 파일디스크립터로부터 인스턴스 생성
            } catch (IOException ex) {
                throw new RuntimeException(
                        "Error binding to local socket '" + fileDesc + "'", ex);
            }
        }
    }

여기서 생성된 LocalServerSocket 인스턴스는 이후 루프에서 처리된다.

4. preload되는 class들과 resource들을 로드

preload()에 의해 호출되는 preloadClasses()와 preloadResources()는 가상머신 생성 후 미리 어플리케이션의 구동에 필요한 클래스들과 리소스들을 가져오는 메소드다.

private static final String PRELOADED_CLASSES = "preloaded-classes";

framework/base/preloaded-classes 파일은 이렇게 스트링 상수로 정의되어있다.
파일의 내용은 대략 이러한 형식이다.

# Classes which are preloaded by com.android.internal.os.ZygoteInit. 
# Automatically generated by frameworks/base/tools/preload/WritePreloadedClassFile.java. 
# MIN_LOAD_TIME_MICROS=1250 
# MIN_PROCESSES=10 
android.R$styleable 
android.accounts.Account 
android.accounts.Account$1 
android.accounts.AccountManager 
android.accounts.AccountManager$13 
android.accounts.AccountManager$6 
android.accounts.AccountManager$AmsTask 
android.accounts.AccountManager$AmsTask$1 
android.accounts.AccountManager$AmsTask$Response
........

그러면 preloadClasses()의 내용을 한번 살펴보도록 하자.

    private static void preloadClasses() {
        final VMRuntime runtime = VMRuntime.getRuntime();

        InputStream is = ClassLoader.getSystemClassLoader().getResourceAsStream(
                PRELOADED_CLASSES); // 입력 스트림 생성

......(중략)

            try {
                BufferedReader br
                    = new BufferedReader(new InputStreamReader(is), 256); // 한줄씩 읽어옴

                int count = 0;
                String line;
                while ((line = br.readLine()) != null) {
                    // Skip comments and blank lines.
                    line = line.trim(); // 트리밍
                    if (line.startsWith("#")  line.equals("")) { // 주석 및 빈줄 무시
                        continue;
                    }

......(중략)

                        Class.forName(line); // 메모리로 동적 로딩 (클래스정보 및 static변수 초기화)

......(중략)

        }
    }

이러한 과정으로 클래스들을 가져오게 된다. 로딩시간은 adb의 logcat을 이용해 확인 가능하다.

그러면 이번엔 리소스를 가져오는 부분을 살펴보자.

    private static void preloadResources() {

......(중략)

            mResources = Resources.getSystem(); // 시스템리소스 리턴
            mResources.startPreloading(); // preloading
            if (PRELOAD_RESOURCES) {

......(중략)

                TypedArray ar = mResources.obtainTypedArray(
                        com.android.internal.R.array.preloaded_drawables);
                int N = preloadDrawables(runtime, ar);

......(중략)

                ar = mResources.obtainTypedArray(
                        com.android.internal.R.array.preloaded_color_state_lists);
                N = preloadColorStateLists(runtime, ar);

......(중략)

            }
            mResources.finishPreloading();

......(중략)

    }

drawable과 color state 리소스를 로딩한다.
이들은 frameworks/base/core/res/res/values/array.xml에 기술되어 있다.

5. system server 실행 : audio / surface flinger, MediaPlayerServer, CameraServer, 각종 매니저 및 service frameworks 등등등등

preload()가 끝난 후에는 이런저런 작업 후 startSystemServer()를 시작한다.

    private static boolean startSystemServer()
            throws MethodAndArgsCaller, RuntimeException {
        /* Hardcoded command line to start the system server */
        String args[] = {
            "--setuid=1000",
            "--setgid=1000",
            "--setgroups=1001,1002,1003,1004,1005,1006,1007,1008,1009,1010,1018,3001,3002,3003,3006,3007",
            "--capabilities=130104352,130104352",
            "--runtime-init",
            "--nice-name=system_server",
            "com.android.server.SystemServer", // 시스템서버 클래스
        };

.....(중략)

            /* Request to fork the system server process */
            pid = Zygote.forkSystemServer( // 포킹 후 시스템서버 프로세스 생성 및 실행
                    parsedArgs.uid, parsedArgs.gid,
                    parsedArgs.gids,
                    parsedArgs.debugFlags,

.....(중략)

        /* For child process */
        if (pid == 0) {
            handleSystemServerProcess(parsedArgs); // 시스템서버 클래스의 메인메소드를 실행한다.
        }

        return true;
    }

이런 과정을 통해 포킹된 zygote가 시스템서버 클래스를 얹은 채로 메인메소드가 동작하게 된다.
frameworks/base/services/java/com/android/server/SystemServer.java를 살펴보도록 하자.

    public static void main(String[] args) {

.....(중략)

        System.loadLibrary("android_servers");
        init1(args);
    }

loadLibrary 메소드는 JNI 네이티브 라이브러리 호출시 사용되는 만큼 이 부분에서는 네이티브서버를 실행하려 한다는 것을 알 수 있다. 여기서 호출하는 네이티브 라이브러리는 init1()함수를 가지고 있고, init1()은 다시 system_init()함수를 호출하는데 해당되는 코드는 frameworks/base/cmds/system_server/library/system_init.cpp 이다.

extern "C" status_t system_init()
{
    ALOGI("Entered system_init()");

    sp proc(ProcessState::self());

    sp sm = defaultServiceManager(); // 서비스매니저
    ALOGI("ServiceManager: %p\n", sm.get());

    sp grim = new GrimReaper();
    sm->asBinder()->linkToDeath(grim, grim.get(), 0);

    char propBuf[PROPERTY_VALUE_MAX];
    property_get("system_init.startsurfaceflinger", propBuf, "1");
    if (strcmp(propBuf, "1") == 0) {
        // Start the SurfaceFlinger
        SurfaceFlinger::instantiate(); // SurfaceFlinger의 실행
    }

    property_get("system_init.startsensorservice", propBuf, "1");
    if (strcmp(propBuf, "1") == 0) {
        // Start the sensor service
        SensorService::instantiate(); // 센서 서비스 실행
    }

    ALOGI("System server: starting Android runtime.\n");
    AndroidRuntime* runtime = AndroidRuntime::getRuntime();

    ALOGI("System server: starting Android services.\n");
    JNIEnv* env = runtime->getJNIEnv();
    if (env == NULL) {
        return UNKNOWN_ERROR;
    }
    jclass clazz = env->FindClass("com/android/server/SystemServer");
    if (clazz == NULL) {
        return UNKNOWN_ERROR;
    }
    jmethodID methodId = env->GetStaticMethodID(clazz, "init2", "()V"); // init2()메소드 호출
    if (methodId == NULL) {
        return UNKNOWN_ERROR;
    }
    env->CallStaticVoidMethod(clazz, methodId);

    ALOGI("System server: entering thread pool.\n");
    ProcessState::self()->startThreadPool();
    IPCThreadState::self()->joinThreadPool();
    ALOGI("System server: exiting thread pool.\n");

    return NO_ERROR;
}

이렇게 SurfaceFlinger와 SensorService를 실행하고 난 뒤 다시 SystemServer.java의 init2()메소드를 호출하게 된다.

    public static final void init2() {
        Slog.i(TAG, "Entered the Android system server!");
        Thread thr = new ServerThread(); // 서버스레드 생성
        thr.setName("android.server.ServerThread");
        thr.start(); // 스레드 시작
    }

모든 서비스가 무사히 시작되면 Zygote는 zygote와 system_server의 두 가지 프로세스의 준비만으로, 새로운 어플리케이션을 실행할 준비가 완료된다.

6. 어플리케이션 실행요청에 대한 처리 : forkAndSpecialize 로 자기복제 및 어플리케이션 클래스 로드

ZygoteInit의 메인클래스에서 보았듯 ZYGOTE_FORK_MODE가 false로 되어 있으므로 runSelectLoopMode()메소드가 호출된다.

    private static void runSelectLoopMode() throws MethodAndArgsCaller {

.....(중략)

        fds.add(sServerSocket.getFileDescriptor()); // /dev/socket/zygote에 바인딩한 소켓 디스크립터를 배열에 추가
        peers.add(null);

        int loopCount = GC_LOOP_COUNT;
        while (true) {

.....(중략)

                index = selectReadable(fdArray); // 이벤트가 발생하는 인덱스를 반환

.....(중략)

            if (index < 0) {
                throw new RuntimeException("Error in select()");
            } else if (index == 0) {
                ZygoteConnection newPeer = acceptCommandPeer(); // 이벤트 발생시 ZygoteConnection 객체 생성
                peers.add(newPeer);
                fds.add(newPeer.getFileDesciptor()); // 생성된 인스턴스의 입출력을 위해 파일디스크립터 배열에 추가
            } else {
                boolean done;
                done = peers.get(index).runOnce(); // 포킹 및 새로운 어플리케이션 실행

                if (done) {
                    peers.remove(index);
                    fds.remove(index);
                }
            }
        }
    }

runSelectLoopMode()는 다시 ZygoteConnection의 객체를 생성하고 runOnce()메소드를 호출한다. ZygoteConnection.java 소스는 ZygoteInit.java과 같은 경로에 존재한다.

    boolean runOnce() throws ZygoteInit.MethodAndArgsCaller {

.....(중략)

        try {
            args = readArgumentList(); // 새로 생성할 프로세스에 대한 옵션을 읽어들인다
            descriptors = mSocket.getAncillaryFileDescriptors();
        } 

.....(중략)

        int pid = -1;
        FileDescriptor childPipeFd = null;
        FileDescriptor serverPipeFd = null;

        try {
            parsedArgs = new Arguments(args); // 읽어들인 옵션으로 각종 설정

            applyUidSecurityPolicy(parsedArgs, peer, peerSecurityContext);
            applyRlimitSecurityPolicy(parsedArgs, peer, peerSecurityContext);
            applyCapabilitiesSecurityPolicy(parsedArgs, peer, peerSecurityContext);
            applyInvokeWithSecurityPolicy(parsedArgs, peer, peerSecurityContext);
            applyseInfoSecurityPolicy(parsedArgs, peer, peerSecurityContext);

            applyDebuggerSystemProperty(parsedArgs);
            applyInvokeWithSystemProperty(parsedArgs);

            int[][] rlimits = null;

            if (parsedArgs.rlimits != null) {
                rlimits = parsedArgs.rlimits.toArray(intArray2d);
            }

            if (parsedArgs.runtimeInit && parsedArgs.invokeWith != null) {
                FileDescriptor[] pipeFds = Libcore.os.pipe();
                childPipeFd = pipeFds[1];
                serverPipeFd = pipeFds[0];
                ZygoteInit.setCloseOnExec(serverPipeFd, true);
            }

            // zygote를 포킹하여 새로운 프로세스를 생성한다.
            pid = Zygote.forkAndSpecialize(parsedArgs.uid, parsedArgs.gid, parsedArgs.gids,
                    parsedArgs.debugFlags, rlimits, parsedArgs.mountExternal, parsedArgs.seInfo,
                    parsedArgs.niceName);
        } 

.....(중략)

        try {
            if (pid == 0) {
                // in child
                IoUtils.closeQuietly(serverPipeFd);
                serverPipeFd = null;
                handleChildProc(parsedArgs, descriptors, childPipeFd, newStderr);
                // 새로 생성된 자식 프로세스의 필요한 클래스 로딩 및 main()메소드 호출
                // should never get here, the child is expected to either
                // throw ZygoteInit.MethodAndArgsCaller or exec().
                return true;
            } else {
                // in parent...pid of < 0 means failure
                IoUtils.closeQuietly(childPipeFd);
                childPipeFd = null;
                return handleParentProc(pid, descriptors, serverPipeFd, parsedArgs); // 새로운 프로세스 생성 후 요청을 종료하고 소켓을 닫는 작업 수행
            }
        } finally {
            IoUtils.closeQuietly(childPipeFd); // 반복처리되지 않도록 하는 부분
            IoUtils.closeQuietly(serverPipeFd);
        }
    }

이 과정을 통해 zygote는 스스로를 포킹하여 다른 프로세스를 만들어내고 자식프로세스에 어플리케이션의 클래스를 얹어서 main()을 로딩하며, 원본 프로세스는 절대 죽지 않고 그대로 남아 어플리케이션 실행요청을 종료하는 작업을 수행한다.

이러한 점을 확인하기 위한 일련의 프로세스 id는 adb shell에서 ps명령어를 통해 확인 가능하며, ppid가 0도 1도 2도 아닌 것들은 대부분 zygote가 포킹한 프로세스인 것을 확인할 수 있을 것이다.

logcat을 보면 이러한 동적 로딩 구조가 가상머신에 프리로딩된 프로그램과 자원을 그대로 활용하여 얼마나 빠른 속도로 어플리케이션을 실행할 수 있도록 하는지 알 수 있다.

이번 설명은 이 정도로 마치고, 좀더 디테일한 설명은 팀원들의 숙제로 남긴다.

Leave a Reply

Your email address will not be published. Required fields are marked *