7 minute read

지금까지 했던 빌드된 이미지를 실제 기기에 올리려는 노력은 실제 기기가 pixel폰이나 nexus 폰이어야 한다는 제약 조건 때문에 에뮬레이터에 올리는 것으로 대체하였다. 이제 실제로 AOSP 코드를 수정하고 빌드하여 적용시켜볼 것이다.


1. Zygote란?

Zygote라는 단어의 사전적 의미는 ‘분할 전의 세포나 수정란’이다. 즉, 안드로이드 시스템에서 Zygote란 새로운 애플리케이션이 실행될 때 실행에 필요한 요소들을 미리 준비해 둔 상태의 Zygote프로세스와 새로운 애플리케이션이 결합되어 실행될 수 있도록 미리 준비해놓은 프로세스라고 할 수 있다.

Zygote 프로세스는 실행되면서 달빅(Dalvik) 가상 머신을 초기화하고 구동시킨다. 안드로이드 애플리케이션은 자바로 작성돼 있어 리눅스 상에서 네이티브 프로세스로 실행될 수 없으며 달빅 가상 머신에서 동작한다. 각 안드로이드 애플리케이션은 독립적인 가상 머신 위에서 동작하는데, 실행될 때 자신이 동작할 가상 머신을 초기화하고 실행하는 과정에서 많은 시간이 소요되며 애플리케이션의 실행을 느리게 하는 요인이 된다. 때문에 안드로이드에서 Zygote 프로세스는 애플리케이션이 실행되기 전에 실행된 가상 머신의 코드 및 메모리 정보를 공유함으로써 애플리케이션이 실행되는 시간을 단축시킬 수 있다.

여기에 더해 안드로이드 프레임워크에서 동작하는 애플리케이션이 사용할 클래스와 자원을 미리 메모리에 로딩해 두고 이러한 자원에 대한 연결 정보를 구성하면 새로 실행되는 안드로이드와 애플리케이션은 필요한 자원들에 대한 연결 정보를 매번 새롭게 구성하지 않고도 그래도 사용할 수 있어서 빠르게 실행된다.

이번에는 안드로이드 애플리케이션을 실행하는 데 반드시 필요한 Zygote 프로세스가 실행되는 방법, 초기화 과정, 그리고 이를 통해 애플리케이션이 빠르게 시작되는 이유에 관해 좀 더 자세히 알아보겠다. 또한 안드로이드 플랫폼에서 Zygote 프로세스가 애플리케이션을 빠르게 시작하게 해주는 특징적인 방법을 이해하면 안드로이드 플랫폼에 추가한 자원이나 클래스를 로딩하는 과정을 효율적으로 만들 수 있다. 또한 현재 개발 중인 안드로이드 애플리케이션에서 새로운 프로세스를 실행할 때 발생하는 오버헤드를 어떻게 줄일지에 대한 아이디어도 얻을 수 있을 것이다.


2. Zygote를 통한 프로세스의 생성

시스템이 기동된 이후 Zygote 프로세스는 init 프로세스가 시스템 구동에 필요한 각종 데몬을 실행하고 난 뒤 실행된다. Zygote 프로세스가 실행된 이후에는 안드로이드 서비스 및 애플리케이션은 Zygote 프로세스를 통해 실행되는데, 실제 구동 중인 장치의 프로세스 목록을 통해 이를 확인할 수 있다.

안드로이드 SDK에 포함된 도구 가운데 adb(Android Debug Bridge) tool을 이용하면 AVD(Android Virtual Device)나 실제 동작 중인 안드로이드 장치에 셸로 연결할 수 있다.

1.PNG

먼저 위 그림처럼 에뮬레이터를 실행시키고 오른쪽 아래의 명령어

1
adb shell ps > ./Desktop/process_status

를 사용하여 현재 에뮬레이터에서 구동 중인 프로세스들은 어떤 것이 있는지 확인할 수 있다.

2.PNG

위 그림을 보면 먼저 init 프로세스의 PID는 1인 것을 확인할 수 있다.

3.PNG

그리고 좀 더 밑으로 내려가보면 pid 3788 프로세스의 이름이 zygote라고 써있는 것을 확인할 수 있다.

4.PNG

그리고 이런 zygote 프로세스를 ppid로 하는 프로세스들이 쭉 표시되는 것을 알 수 있다.

이 프로세스들이 Zygote를 통해 생성되고 실행된 안드로이드 애플리케이션들이다.

이번에는 일반적인 리눅스 시스템에서 새로운 프로세스를 생성해서 실행하는 것과 안드로이드 플랫폼에서 Zygote를 이용해 애플리케이션 프로세스를 생성해서 실행되는 과정에 어떤 차이점이 있는지 알아볼 것이다.

우선 일반적인 리눅스 시스템에서 새로운 애플리케이션을 실행하는 과정은 다음과 같다.

6.png

그리고 안드로이드에서 새 애플리케이션 프로세스를 생성해서 실행하는 경우를 살펴보자

7.png

두 과정은 언뜻보면 별로 다른 것이 없어보인다.

실제로도 굉장히 비슷하다. 하지만 한가지 차이점이 있는데 리눅스에서는 새로운 프로세스를 실행할 때 Process A로부터 Process A’를 fork() 명령어를 통해 생성하고 exec(‘B’) 명령어를 사용해 B의 코드를 메모리에 로딩한다. 이때 Process A의 메모리 정보는 지워지고 로딩된 B를 실행하는데 필요한 메모리를 새롭게 구성한 후 프로세스 B가 사용할 공유 라이브러리에 대한 링크 정보를 새로 구성하게 된다. 만약 ProcessB가 사용할 공유 라이브러리가 메모리에 이미 로딩돼있는 상태라면 이에 대한 링크 정보만 새롭게 구성하지만 그렇지 않은 경우에는 스토리지에서 해당 라이브러리를 메모리로 로딩하는 과정이 추가로 필요하다. 이러한 과정이 새로운 프로새스를 실행할 때마다 일어나게 된다.

반면 안드로이드에서는 Zygote 프로세스를 fork() 시스템 콜을 호출하여 자식 프로세스인 Zygote’ 프로세스를 생성한다. 생성된 Zygote’ 프로세스는 부모인 Zygote 프로세스의 코드 영역과 링크 정보를 공유한다. 따라서 새로운 안드로이드 애플리케이션 A는 fork()를 통해 생성된 프로세스의 코드 영역을 새롭게 로딩하는 것이 아니라</mark> 이미 공유 라이브러리들이 갖춰진 복제된 달빅 가상 머신 위에서 동적으로 로딩된다. 여기서 안드로이드 애플리케이션 A는 실제로는 실행할 애플리케이션의 클래스이고 안드로이드 애플리케이션은 달빅 가상머신 위에서 동작하는 클래스이다.

안드로이드의 이러한 점 때문에 애플리케이션 A는 Zygote 프로세스가 구성해놓은 라이브러리 및 리소스에 대한 링크 정보를 새로 로딩하는 과정없이 바로 쓸 수 있고 따라서 빠르게 실행될 수 있는 것이다.

5.png

위 그림은 zygote 프로세스가 실행되고 실제 fork()된 뒤 애플리케이션 A가 실행되는 과정을 그림으로 나타낸 것이다. Zygote프로세스는 실행되면 Dalvik VM을 실행 및 초기화를 진행하고 애플리케이션들이 사용할 resource들과 class들을 미리 해당 가상머신에 올려놓는다.

그리고 어떤 애플리케이션이 실행될 때 fork() 명령어를 통해 자기 자신을 복사하고 복사된 zygote’ 프로세스에 애플리케이션 A만 로딩하여 바로 실행한다.


TIP

또한 구글은 안드로이드를 발표할 때 주요한 특징으로 Zygote를 설명하면서 COW(Copy on Write)를 통해 기존에 이미 메모리 상에서 동작중인 프로세스의 재사용성을 극대화하고 공유 라이브러리를 통해 메모리 사용량을 최소화한다고 설명한 바 있다.

프로세스를 생성할 때 새로 생성된 프로세스는 부모 프로세스와 메모리 공간을 공유한다. 즉, 자식 프로세스는 부모 프로세스가 생성한 메모리 공간에 관한 정보를 모두 복사하여 그대로 사용하는데, COW는 이러한 메모리 공간을 복사하는 시점에 대한 기법이다.

메모리를 복사하는 것은 오버헤드가 매우 크기 때문에 생성된 자식 프로세스가 부모의 메모리 공간을 참조만 할 경우에는 이를 복사하지 않고 부모의 메모리 공간을 공유하게 된다. 공유하는 메모리 정보를 자식 프로세스가 수정하는 시점에서 부모 프로세스의 메모리 정보를 자신의 메모리 공간으로 복사하는 기법이 바로 COW 기법이다.

그리고 만약 fork()를 호출한 이후에 exec()가 바로 실행되면 새로 생성되는 프로세스의 메모리 공간은 부모 프로세스의 메모리 공간과 내용이 달라진다. 이런 경우 COW기법은 무의미해지며 오히려 새로운 프로세스를 실행하는데 있어 큰 오버헤드를 초래할 뿐이므로 COW기법이 적용된 운영체제에서는 가급적 fork()가 호출된 이후 바로 exec()를 호출하는 것은 삼가해야 한다.



3. 실제 코드를 통해 알아보는 Zygote 프로세스가 실행되는 과정

Zygote는 자바로 작성돼 있으므로 다른 네이티브 서비스나 데몬과 같이 init 프로세스에서 바로 실행할 수 없다. 자바로 작성돼 있는 Zygote 클래스가 동작하려면 달빅 가상 머신이 생성돼야 하고, 생성된 가상 머신 위에서 ZygoteInit 클래스를 로딩하고 실행해야 한다. 이러한 작업을 수행하는 프로세스가 바로 app_process이다.

8.png

위 그림이 zygote 프로세스가 시작되게 하는 app_process의 실행 과정을 그림으로 표현한 것이다.

이제 실제 코드를 살펴보겠다.

9.PNG

먼저 위 코드로 app_main.cpp의 위치를 찾아준다.

3.1. Appruntime 객체 생성

10.PNG

그리고 main함수로 가보면 위 그림과 같이 AppRuntime 객체를 생성한다. 내 실행환경에서는 185번째 줄에 있었다. AppRuntime 객체는 AndroidRuntime 클래스를 상속하는데, AndroidRuntime은 안드로이드 애플리케이션이 동작하기 위한 달빅 가상 머신을 초기화하고 실행하는 클래스이다. AppRuntime 객체를 생성할 때 computeArgBlockSize 함수를 사용하여 argc와 argv의 블록 사이즈만큼을 초기화하는 것을 알 수 있다.

11.PNG

그리고 위 코드를 통해 생성된 AppRuntime 객체를 통해 달빅 가상 머신을 실행하기에 앞서 환경변수와 실행 시 전달된 인자를 파싱하여 가상 머신으로 전달할 옵션을 runtime 객체에 addOption 메소드를 이용하여 추가한다.


app_process에는 다음과 같은 형식으로 인자가 전달된다.

12.PNG

그리고 원래는 init.rc파일에 init 프로세스에서 app_process를 실행할 때 전달되는 명령문이 존재한다. 인사이드 안드로이드 책에서는 자세히 나와있지만 필자는 아직 찾지 못하였다. 해당 인자를 건든다면 Zygote를 실행시키지 않고도 init process를 진행할 수 있을 텐데 참 아쉽다.


3.2. AppRuntime 객체 실행

가상 머신으로 전달할 인자를 분석해서 AppRuntime 클래스의 인스턴스에 저장되면 객체를 로딩하고 main() 메서드를 실행한다.

13.PNG

14.PNG

위 코드에서 266번째 줄에서 실행할 클래스의 이름이 –zygote인지 확인한다. –zygote가 인자로 전달됐다고 가정하고 실행 흐름을 계속 따라가 보자. 그러면 336번째 줄에서 start() 함수를 호출하면서 비로소 가상 머신이 생성되고 초기화된다. 생성된 가상 머신에 ZygoteInit 클래스를 로딩하고 main() 메서드로 실행 흐름이 바뀐다.

runtime.start()의 첫 번째 인자로 전달되는 com.android.internal.os.ZygoteInit을 Fully Qualified Name, 줄여서 FQN이라고 한다. FQN은 클래스의 패키지 이름과 클래스 이름을 완전히 명시한 것인데, 이 이름이 클래스 로더에 전달되면 클래스 로더는 패키지 이름을 경로명으로 해석하고 해당 경로에서 ZygoteInit 클래스를 찾아 로딩한다. app_process에 ‘–start-system-server’가 마지막인자로 전달됐으므로 AppRuntime의 start() 멤버 함수의 두 번째 인자에는 ‘true’가 전달된다.

3.3. 달빅 가상 머신의 생성

AppRuntime의 start() 멤버 함수에서는 달빅 가상 머신을 실행하기에 앞서 app_process로부터 전달받은 가상 머신의 옵션 외에 가상 머신 실행에 관련된 각종 시스템 프로퍼티와 환경 변수를 가져온다. 이에 따라 가상 머신의 실행 옵션도 바뀐다.

1
int property_get(const char *key, char *value, const char *default_value)

가상 머신의 실행 옵션을 설정하기 위해 property_get() 함수를 호출해서 시스템에 설정된 값을 조회한다. 위 코드는 시스템의 프로퍼티 영역을 조회하는데 사용되는 property_get() 함수의 원형이다. 시스템 프로퍼티 영역은 init.rc의 setprop 구문을 통해 init 프로세스나 그 밖의 다른 프로세스에 의해 설정된다.

달빅 가상 머신의 실행 옵션을 변경할 필요가 있다면 AppRuntime에서 가상 머신을 실행하기 전에 property_get()함수를 호출하는 코드를 참조해서 init.rc에 해당하는 프로퍼티를 설정하거나 app_process의 인자로 가상 머신의 실행 옵션을 전달하면 된다.(달빅 가상 머신의 옵션에 관한 세부 사항은 “source\dalvik\docs\embedded-vm-controm.htm”파일을 참조하면 된다.)

15.PNG

16.PNG

JNI_CreateJavaVM() 함수를 통해 달빅 가상 머신을 생성하고 실행하며, 함수로 전달되는 인자는 다음과 같다.

1
jint JNI_CreateJavaVM(JavaVM** p_vm, JNIEnv** p_env, void* vm_args)
  • JavaVm** pVM: 생성된 JavaVm 클래스의 인스턴스에 대한 포인터

  • JNIEnv** p_env : 가상 머신에 접근하기 위한 JNIEnv 클래스의 인스턴스에 대한 포인터

  • void* vm_args : 지금까지 설정한 가상 머신의 옵션

다음으로 생성된 가상 머신에서 사용할 JNI 함수를 등록한다.

17.PNG

18.PNG

20.PNG

19.PNG

위 코드에서 호출되는 startReg() 함수는 static const RegJNIRec gRegJNI[] 배열에 저장돼 있는 함수를 호출한다. 가상 머신에서 사용할 JNI 함수를 등록하고 나면, 가상 머신 상에서 동작하는 자바 클래스에서 이 네이티브 함수들을 호출할 수 있게 된다. 이와 관련된 함수는 frameworks/base/core/jni 디렉터리의 소스 코드에 정의돼 있다. JNI에 대한 자세한 사항은 JNI와 NDK part에서 공부할 것이다.

3.4. ZygoteInit 클래스의 실행

이제 생성된 VM에서 동작할 클래스를 로딩하는 부분을 살펴보겠다. 앞서 설명한 것처럼 app_process는 전달된 인자에 따라 Zygote 외의 다른 클래스도 호출할 수 있다. 아래 코드는 AndroidRuntime 클래스의 start() 함수에서 해당 클래스를 찾아 main() 메서드를 호출하는 부분이다.

21.PNG

지금은 Zygote와 관련된 실행 흐름을 살펴 보고 있으므로 인자로 전달된 className은 com.android.internal.os.ZygoteInit을 가리킨다.

1210번 줄에서 FindClass() 함수로 실행할 클래스를 로딩하고 나면 1214번째 줄에서 GetStaticMethodID() 함수를 통해 해당 클래스에서 매개변수가 String 배열이고 반환 값이 Void이며, 정적 메서드인 main()을 찾는다. 1220번 줄에서 main 메서드를 호출하면 이제 실행 흐름은 가상 머신 위에서 동작하는 자바 애플리케이션(여기서는 ZygoteInit 클래스)으로 바뀌며, 이후 네이티브 영역에서 진행돼온 C++ 코드의 실행흐름은 가상 머신이 종료될 때까지 진행되지 않는다.


4. ZygoteInit 클래스의 기능

지금가지 가상 머신을 생성하고 ZygoteInit 클래스를 로딩했다. ZygoteInit 클래스의 기능을 요약하면 다음 그림과 같다.

22.png

Categories:

Updated: