오늘은 안드로이드의 커널(Kernel) 내용을 정리하려 한다.
인사이드 안드로이드 OS(Inside Android OS) 책의 내용 중 커널에 대한 내용을 정리하였다. 전체를 보고 느낀점은 한정적인 자원들(배터리, 메모리)을 위해 최대한 OS를 컴펙트하게 설계했다는 느낌을 가장 크게 받았다.
안드로이드 시스템은 리눅스 커널을 사용한다. 이러한 리눅스 커널은 거대하고 매우 복잡한 소프트웨어 패키지라서 새로운 디바이스를 대상으로 커널을 브링업하는 일은 상황에 따라서 상당한 시간을 투자해야만 할 수도 있다.
리눅스 커널은 완전히 내부가 차단된 형태의 필수 바이너리 코드로 안드로이드에 통합된다. 안드로이드 빌드 시스템 관점에서 리눅스 커널은 단순히 의존성이 있는 파일 중 하나다. 안드로이드 시스템에는 커널 모듈이라고 하는 매우 특별한 다수의 수정 항목이 있다.
개요
01. 리눅스 커널
커널은 사용자 애플리케이션이 활동할 수 있는 가상 환경을 생성한다.
리눅스는 사용자 애플리케이션에게 가상 실행 공간과 파일 시스템 두 가지 환경적 역할의 기능을 제공하는 커널 패밀리의 한 종류다. 추가로 커널은 안드로이드의 포팅 계층 역할도 수행한다. 안드로이드를 새로운 디바이스에 포팅하는 절차는 대부분의 일이 리눅스를 디바이스에 포팅하고, 그 위에 안드로이드 스택을 다운로드하는 것이다.
02. 리눅스 커널 프로세스 관리
한 대의 컴퓨터 하드웨어는 보유한 프로세서들은 여러 프로세서를 동시에 실행할 수 있다. 즉 각 코어(프로세서)는 칩에 전원이 공급되면 일련의 명령어 스트림을 실행하기 시작하고 전원이 꺼지면 실행을 중단한다. 이상적이고 이론적인 컴퓨터는 각 코어가 동일한 성능을 보유하고 있으며, 메모리에 동일한 접근 성능을 보여주지만, 일부 투자 대비 효과를 극대화하기 위해 차별화된 능력치를 부여하고 자주 사용되는 작업과 그렇지 않은 작업을 차등하여 코어를 할당하는 아키텍처도 존재한다. 이러한 아키텍처가 안드로이드용으로 널리 사용되는 디바이스로 ARM의 big.LITTLE 아키텍처가 있다.
커널의 첫 번째 임무는 하드웨어 상에 가상 실행 환경을 생성하는 일이다.
가상 환경은 위의 하드웨어 환경과 매우 다르다. 가상 환경 안에서는 거의 무한 개의 프로세스가 동시에 실행되는 것처럼 보인다. 각각의 프로세스는 하나 혹은 그 이상의 가상 CPU를 통해 명령어들을 실행하고 다른 프로세스들이 접근할 수 없는 자기만의 독자적인 메모리 주소 공간을 갖는다.
💡 스레드와 프로세스, 그리고 애플리케이션
프로세스와 함께 빼놓지 않고 설명되는 것이 스레드다.
스레드는 하나의 가상 CPU 혹은 프로세서에 의해 한 번에 하나씩 순서대로 실행되는 명령어 시퀀스를 의미한다. 프로세스는 다른 어떤 프로세스와 공유하지 않는 독자적인 개인 주소 공간을 갖는 하나 혹은 여러 개의 스레드를 의미한다. 애플리케이션은 특정 목적을 가진 코드의 집합을 의미한다.
03. 리눅스 커널 메모리 관리
커널의 두 번째 주요 임무는 메모리 관리다.
하드웨어 메모리는 임의 접근 방식(ex. RAM)과 블록 구조 방식(ex. SSD, HDD)으로 크게 두 가지 종류로 구분할 수 있다. 임의 접근 방식인 RAM의 경우는 단순히 특정 메모리 셀의 주소를 지정함으로써 저장된 정보를 읽거나 기록할 수 있다. 모든 메모리가 임의 접근 방식 메모리라면 이상적이지만 비싸다. 그래서 블록 구조 메모리를 같이 사용한다. 블록 구조 메모리를 통해 값을 갖고오는 일은 다음과 같은 일련의 과정을 거친다. (편의상 임의 접근 방식을 RAM, 블록 구조 메모리를 SSD로 작성했다.)
💡 임의 접근 방식과 블록 구조 방식의 차이
두 가지 모두 블록 구조이며 주소 지정이 가능하긴 하다. 유일한 진짜 차이는 크게 두 가지다. 블록의 크기 와 데이터 버스를 거쳐 이동할 때의 걸리는 시간이다. 메모리의 한 블록의 비트 크기가 디바이스 워드 크기보다 작거나 같다면 그 메모리는 임의 접근 방식이라고 간주할 수 있다. 반대로 워드 크기보다 크다면 블록 구조 방식으로 간주한다.
▸ 메모리 관리 첫 번째 트릭 - Swap w/ Virtual Memory
이제 가장 중요한 Swap에 대한 부분이다.
Swap은 어떤 유휴 상태 프로세스가 메모리 일부를 당장 사용하지 않고 있다면(사용자 입력 대기 혹은 일정 시간이 지나기를 대기하는 경우) 커널은 다른 프로세스가 메모리를 사용할 수 있도록 사용하지 않는 메모리를 해제할 수 있다. 단순 해제해서 데이터를 삭제하는 것이 아니라, 위의 데이터 전송에 대한 일련의 과정을 반대로 실행하여 임의 접근 방식 메모리에서 블록 구조 방식 메모리(RAM에서 SSD)로 데이터를 전송하는 과정 수행 후 다른 프로세스가 메모리를 사용할 수 있도록 메모리를 해제한다. 이런 과정이 Swap이다. 이후 반대로 유휴 상태 프로세스가 다시 동작하게 되면 위의 데이터 전송에 대한 일련의 과정을 다시 실행하여 마지막 상태를 그대로 복구한다. 이 Swap하는 곳이 가상 메모리(Virtual Memory)다. 이는 사용되는 메모리 크기의 합이 디바이스에 설치된 하드웨어 RAM의 총량보다 훨씬 커질 수 있게 하는 주요 기술 중 하나다.
하지만 안드로이드 4.4 (KitKat) 버전 미만의 경우는 해당 Swap 기능이 치명적이었다.
당시 모바일 디바이스에는 SSD가 사용되었는데, 2002년 즈음의 SSD는 쓰기 동작할 때마다 수명이 심각하게 단축되기 때문에 RAM의 일부를 SSD로 Swap하는 동작은 치명적이었다. 그래서 나온 것이 zram이다. zram은 RAM > SSD로 Swap하는 것이 아니라 RAM > RAM으로 Swap하는 것이다. RAM 안에서 프로세스의 메모리 일부를 Swap해서 저장하기 위한 압축 메모리 공간을 제공한다.
▸ 메모리 관리 두 번째 트릭 - Catalog tree
다음 메모리 관리 내용은 파일 시스템이다.
블록 구조 메모리는 트리 구조가 아니지만, 커널이 블록 구조 메모리를 트리 형태의 파일 시스템으로 만들 수 있다면 어떤 것이라도 쉽게 탐색할 수 있을 것이다. 커널은 하나 혹은 그 이상의 파일 시스템이라는 컴포넌트를 보유하고 있으며, 각 파일 시스템은 다양한 종류의 블록 구조 메모리 하드웨어를 관리하며 파일 트리 추상화를 생성한다.
04. 안드로이드 커널
안드로이드 시스템이 리눅스 커널에 기반을 둔 것은 사실이지만, 안드로이드 시스템이 요구하는 매우 특별한 커널 기능을 필요로 하기 때문에 모든 리눅스 커널이 안드로이드를 지원하지 않는다. 이는 안드로이드를 새로운 디바이스에 포팅하는 경우 주요 장애물이 될 수 있다.
대부분 커널처럼 안드로이드는 다양한 하드웨어와 소프트웨어 요구 사항을 위해 커스터마이징이 가능한 플로그인 시스템을 구현하고 있다. 이 시스템이 커널 모듈(Kernel moudles)이며, 마우스, 키보드, 스피커 등의 디바이스 드라이버가 여기에 해당한다.
디바이스 드라이버의 역할은 두 가지다.
안드로이드는 안드로이드 프레임워크와 하드웨어 추상 계층(HAL, Hardware Abstraction Layer)이라고 하는 또 하나의 계층을 추가했다. 이 계층으로 안드로이드는 유사한 종류의 디바이스에 접근하는 서로 다른 커널 드라이버에 대해 단일한 API를 제공할 수 있게 되었다. 이러한 커널의 일부로 실행되는 디바이스 드라이버는 커널이 할 수 있는 일은 무엇이든 할 수 있다보니, 오류를 발생할 때 종종 커널 패닉(Kernel panic)이 발생하여 시스템 전체를 정지시킬 수 있다. 이러한 이유로 아래의 내용과 같이 디바이스 드라이버는 가능한 작고 단순하게 만드는 것이 관례다.
📖 Corbet, Rubini, Kroah-Hartman, 2005
… 디바이스 드라이버의 역할은 매커니즘을 제공하는 것이지 정책을 제공하는 것이 아님을 강조하고 있다. … "어떤 기능을 제공할 것인가"(매커니즘)와 "이 기능을 어떻게 사용할 수 있을까"(정책)가 그 그것이다. …
05. 안드로이드 커널의 주요 기능
안드로이드가 기준으로 삼고 있는 바닐라 커널로부터 안드로이드 커널을 생성하기 위한 패치 세트를 생성하는 것은 상대적으로 간단하다. 이번 절에서는 안드로이드 커널에 특화된 몇 가지 중요한 기능을 정리한다.
▸ 웨이크락(Wakelock) 혹은 서스펜드-블로커(Suspend-blockers)
기본적으로 대부분 리눅스 디바이스는 계속 실행 중이다. 모바일 디바이스에서 현재까지 가장 중요한 리소스는 배터리인데, 이러한 리눅스 특성은 안드로이드에 치명적이었다. 그래서 안드로이드팀은 리눅스의 활성화 살태를 반대로 뒤집는 것이었다.
절전 상태에 머무르게 되면 배터리 수명 입장에서는 최선이지만 필요한 작업을 수행하는 데는 좋지 못한 환경이다. 그래서 웨이크락에 진입해야 한다.
웨이크락은 단순 이진플래그다. 각 애플리케이션은 본인의 상황에 따라 웨이크락을 지정하고, 커널은 웨이크락을 설정하고 있는 다른 애플리케이션이 없으면 디바이스를 절전 상태로 전환한다.
서스펜드 블로커라는 이름이 사용된 이유는 다음과 같다.
최초 웨이크락은 커널이 디바이스를 절전 상태로 전환시키는 도중에 어떤 애플리케이션이 웨이크락을 설정하는 경우 이를 대응할 수 없는 이슈가 있었다. 이를 해결하고 서스펜드 블로커라는 이름으로 변경 후 다시 패치되었다.
안드로이드 6.0 (Marshmallow) 버전에서는 Doze와 App Standby라는 두 가지 추가적인 전원 관리 툴이 추가되었다. 디바이스가 Doze 모드로 진입하면 네티워크 접근, Wi-Fi 스캐닝 등 여러 제한이 생기고 이 모드에서는 애플리케이션이 웨이크락을 잡고 있을 이유가 없다고 가정하고 웨이크락을 완전히 무시한다.
▸ 바인더(Binder)
바인더는 안드로이드의 IPC 방식이며 안드로이드의 심장과 같은 역할이다. 앞에서 정리한 웨이크락인 한정된 리소스를 사용자 애플리케이션에게 그대로 허용하기에는 아래와 같은 위험 문제가 있었다.
위 사례와 같은 이와 같이 사용자 애플리케이션이 한정된 리소스를 사용함에 따라 발생하는 위험을 해결하기 위해 바인더가 사용되는데, 사용자 애플리케이션은 한정된 리소스를 직접 사용할 수 없다. 그래서 사용자 애플리케이션은 한정된 리소스를 사용하는 시스템 서비스를 통해 사용할 수 있다. 여기서 사용자 애플리케이션이 시스템 서비스에게 요청하고 응답 받을 때 사용되는 것이 바인더이다. 다음과 같은 일련의 과정을 통해 소통하게 된다.
바인더가 프로세스 간의 중재자 역할을 할 수 있는 것은 바인더가 커널 레벨에 포함되어 있기 때문이다. 커널은 프로세스를 생성하는 주체이기 때문에 곧, 바인더는 각 프로세스의 상태를 알 수 있다.
▸ 로우 메모리 킬러(Low Memory Killer)
대부분의 모바일 운영체제는 사용자가 실행할 수 있는 애플리케이션의 개수를 제한하는 방식을 채택한다. 하지만 안드로이드는 거의 초기부터 사용자가 원하는 수만큼 애플리케이션을 시작하도록 허용했다. 이러한 허용은 사용자가 애플리케이션을 생성할 때마다 메모리 사용량이 증가한다는 이야기이고, 이는 곧 위의 Swap을 야기하는 것이고, 당시 SSD와 같은 플래시 메모리는 수명 이슈 때문에 Swap이나 Virtual Memory를 지원하지 못하는 딜레마가 있었다.
이러한 이슈를 해결하기 위해 모든 리눅스 커널에 포함되어 있는 OOMK(Out-Of-Memory-Killer)를 그대로 확장했다. OOMK는 커널이 위험한 수준의 메모리 부족 상황을 감지하면 우선순위 테이블에 따라 어떤 애플리케이션의 실행을 위해 다른 애플리케이션을 강제 종료한다. 이러한 OOMK로 인해 메모리 이슈로 인해 랙(Rack)에 설치된 서버를 직접 방문해서 리부팅하지 않을 수 있었다. 안드로이드는 이를 확장한 LMK(Low-Memory-Killer)를 사용한다.
LMK는 두 개의 테이블에 의해 동적으로 동작한다. 첫 번째 테이블은 안드로이드 시작 시 구성되는 정적 테이블이며, 여러 개의 애플리케이션 우선순위 그룹을 생성하고 각 그룹에 따라 메모리 임계값을 설정하고 -16에서 15까지 31개의 우선순위를 명시한다. 두 번째 테이블은 애플리케이션이 특정 순간 사용자에게 얼마나 중요한가를 기준으로 프로세스에게 동적으로 우선순위를 지정한다. 안드로이드의 액티비티 매니저(Activity Manager) 서비스가 이 두 번째 테이블을 관리한다.
▸ Ashmem과 Pmem 그리고 Ion
바인더는 물리적인 메모리의 한 블록을 하나 혹은 여러 개 애플리케이션의 가상 메모리 공간에 매핑하는 방식으로 동작한다. 애플리케이션 중 하나가 그 공유 메모리 블록에 기록하면, 다른 모든 애플리케이션들은 그 기록을 볼 수 있다. 이러한 기능을 Ashmem 담당한다. 이는 리눅스 시스템에 존재하는 공유 메모리 시스템인 shmem 코드 기반으로 만들어졌다. shmem보다 Ashmem은 훨씬 단순한 파일 기반의 API 형식이고, 메모리 압박이 발생하는 경우 메모리를 해제할 수 있는 능력이 있다. Ashmem에 의해 할당된 메모리는 참조 카운터가 적용되어 바인더에 의해 프로세스 간 참조 정보가 공유된다.
Ashmem이 가상 메모리 블록을 공유하는 것이라면 Pmem은 물리 메모리를 할당할 수 있도록 공유하는 것이다. 초기의 안드로이드에서 Pmem이 사용되었고 최신 안드로이드는 공유 물리 메모리 풀을 관리하기 위해 Pmem 대신 Ion을 사용한다.
▸ 로거(Logger)
위에 정리한 안드로이드의 영구 메모리에 쓰기 동작에 따라 수명이 줄어드는 이슈로 인해서 안드로이드는 시스템 로그를 파일에 기록할 수 없었다. 그래서 안드로이드는 이 문제를 해결하기 위해 시스템 로그를 커널의 원형 버퍼에 기록한다. 그렇기 때문에 안드로이드 시스템이 재부팅되면 메모리가 초기화되고 모든 로그는 사라진다. 아래의 path를 보면 로그 버퍼들이 파일 시스템 내의 목록으로 보이지만 이 버퍼들은 파일이 아니고 커널이 메모리 내의 어떤 것이든 파일처럼 보이게 만들 수 있기 때문에 다음과 같이 표기된다.
▸ 알람(Alarm)
안드로이드는 미리 정해진 시간에 깨어날 수 있는 기능이 필요하다. 이러한 기능과 웨이크락 기능의 협력을 통해 애플리케이션은 수면 상태에 계속 빠지지 않고 특정 시간에 맞춰 스스로 스케줄링할 수 있다.
▸ 파라노이드 네트워킹(Paranoid Networking)
대부분 운영체제에서 네트워킹은 모든 애플리케이션에 제공하는 일반적인 서비스다. 이러한 네트워크 서비스는 경우에 따라 비용이 발생하기 때문에 사용자들이 이 네트워크 서비스를 직접 통제할 수 있어야 한다.
안드로이드 시스템은 각각의 새로운 애플리케이션이 설치할 때 고유한 사용자 ID를 할당한다. 이 애플리케이션은 하나 혹은 여러 개의 프로세스 그룹에 지정될 수 있는데 AID_INET 프로세스 그룹에 속하지 않으면 네트워크 요청을 거부된다. 이러한 매커니즘으로 사용자는 네티워크 서비스를 직접 통제할 수 있다.