Linux Kernel(4.19) Hacks

rousalome.egloos.com

포토로그 Kernel Crash


통계 위젯 (화이트)

230224
1178
109352


[리눅스커널] 메모리 관리: 가상 메모리 기법의 주요 개념 소개 14. Memory Management

가상 메모리의 주요 개념 소개

가상 메모리의 주요 개념을 다음 그림을 보면서 배워 보겠습니다. 


[그림 1] 가상주소와 물리주소 및 페이지와의 관계

위 그림과 함께 가상 주소를 물리 주소로 변환하는 실행 흐름을 배워봅시다.    

가장 왼쪽에 보이는 주소 맵이 가상 메모리입니다. 가장 윗 부분에 0x0000_0000 주소가 있는데 아랫 방향으로 주소가 커집니다. 가장 아랫 부분은 0FFF0_0000 주소입니다. 즉, 가상 주소 범위는 0x0~0xFFFF_FFFF 이며 전체 크기는 4GB입니다.

CPU에서 구동 중인 프로세스가 보는 주소는 가상 주소입니다. 우리가 분석한 모든 커널 함수는 이 가상 물리 메모리 공간에서 실행하는 것입니다.

가상 메모리 박스 오른쪽 화살표를 눈으로 따라가면 주소 변환이란 박스가 보입니다. 가상 주소를 물리 주소로 변환하는 기능입니다. 이 주소 변환 박스에는 페이지 테이블이 있습니다. 페이지 테이블은 다음 정보를 저장하고 있습니다.

"가상 주소를 물리주소로 변환할 수 있는 세부 정보"

이렇게 페이지 테이블 정보를 참고해 가상 주소를 물리 주소로 변환하는 것입니다. 이 과정으로 물리적으로 연속적이지 않은 주소도 페이지 테이블을 통해 연속적으로 처리할 수 있습니다.

위 그림에선 이해를 돕기 위해 페이지 테이블을 통해 가상 주소를 물리주소 변환한다고 설명을 드렸습니다. 사실 MMU(Memory Management Unit)이란 하드웨어가 주소 변환을 수행합니다. MMU 내 TLB(Translation Lookaside Buffer) 버퍼는 최근에 변환한 페이지 테이블 엔트리 정보를 갖고 있습니다. TLB는 페이지 테이블의 캐시와 같은 개념으로 보면 됩니다. 자주 쓰는 페이지 테이블 엔트리 정보를 TLB가 저장하는 것입니다.

가상 메모리 박스에서 시작한 화살표는 주소 변환 박스에서 물리 메모리 맵 방향으로 이어집니다. 가상 주소는 주소 변환을 거쳐 실제 물리 메모리 주소에 접근해 어떤 값을 읽거나 쓰는 것입니다.

가장 오른쪽에 있는 박스는 물리 메모리 맵인데 말 그대로 실제 시스템에 탑재한 메모리(RAM)입니다. 가상 위 부분에 0x0000 주소로 시작해 아래 방향으로 +0x1000 만큼 주소가 커집니다. 

이렇게 리눅스 커널은 물리 메모리를 0x1000(16진수) 바이트 즉 4K 단위로 짤라 관리합니다. 이렇게 0x1000 바이트 단위로 물리 메모리를 짤라 번호를 매기는데 이를 페이지 프레임 번호라고 합니다. 0x0~0x1000 범위 물리 주소는 0번째 프레임, 0x1000~0x2000 범위 주소는 1번째 프레임입니다. 커널 메모리 내부 시스템에서는 물리 주소를 프레임 번호로 자주 계산합니다. 

처음 리눅스 메모리 시스템을 공부하는 분은 위 그림을 보면 다음과 같은 의문이 들 것입니다.

"왜 물리 주소를 0x1000(4K) 단위로 짤라서 번호를 매길까?"

그 이유는 물리 주소를 4K 단위로 관리하는 페이지 디스크립터로 관리하기 위해서입니다.  

물리 주소를 페이지 프레임 번호로 변환하는 방법은 간단합니다.
페이지 프레임 번호 = 물리 주소 >> 12 

물리 주소를 오른쪽으로 12만큼 비트 쉬프트한 결과가 페이지 프레임 번호입니다.

그러면 물리 주소를 0x1000 단위로 페이지 프레임 번호를 매겨 관리하는 이유는 무엇일까요? 가장 큰 이유는 페이지 프레임 번호로 페이지 디스크립터인 struct page 주소를 알 수 있기 때문입니다. 다음 그림을 보면서 페이지 디스크립터에 대해 배워봅시다.

[그림 2] 페이지 프레임과 물리 주소와의 관계

페이지 디스크립터는 페이지 프레임을 관리하는 자료구조이며 struct page 구조체입니다.
페이지 프레임 번호를 알면 페이지 디스크립터를 쉽게 얻을 수 있습니다.

페이지 디스크립터는 페이지 프레임을 관리하는 자료구조이며 mem_map 이란 포인터형 배열에서 관리합니다. 페이지 프레임 번호를 PFN(Page Frame Number)라고 한다면 mem_map[PFN] 에 페이지 프레임을 관리하는 페이지 디스크립터가 있습니다.

전체 가상 메모리 시스템 그림으로 되돌아 가면 6 번째 페이지 프레임은 mem_map[6] 에서 확인할 수 있습니다. 위 그림에서 만약 페이지 프레임 번호가 7이면 mem_map[7]에 해당 페이지 디스크립터가 있습니다.

페이지 디스크립터는 struct page 구조체로 물리 메모리에 대한 속성 정보를 담고 있습니다.

우리는 리눅스가 메모리를 페이지 단위로 관리한다는 말을 많이 듣습니다. 이를 달리 표현하면 다음 문장과 같습니다.
4K(0x1000) 단위 페이지를 struct page 구조체 자료구조로 관리한다.

프로세스에 대한 자료구조가 struct task_struct 이듯 페이지를 표현하는 자료구조는 struct page 구조체인 것입니다.
리눅스 메모리 시스템에서는 다음과 같이 가상주소로 페이지 디스크립터로 변환하는 계산을 매우 자주합니다.
가상 주소 -> 물리 주소 -> 페이지 디스크립터
페이지 디스크립터 -> 페이지 프레임 번호 -> 물리 주소 -> 가상 주소

가상 주소를 알면 페이지 디스크립터 주소를 계산할 수 있습니다. 반대로 페이지 디스크립터 주소로 가상 주소를 알 수 있습니다. 이 과정에서 몇 번의 비트 시프트 연산으로 주소 변환을 할 수 있습니다. 

여기까지 가상 메모리 시스템의 기본 개념에 대해 소개를 드렸습니다. 가상 메모리 시스템을 처음 접하는 분에겐 좀 낯설고 어려울 수 있습니다. 하지만 다음 절을 천천히 읽으면 서서히 이해가 갈 것이라 믿습니다.
다음 소절에서 각각 세부 개념을 조금 더 자세히 살펴보겠습니다. 

가상 메모리와 가상 주소란

우리가 분석한 모든 커널 함수는 가상 주소 공간에서 실행합니다. 그림 가상 메모리 맵을 보면서 가상 주소에 대해 배워 보겠습니다.
 
[그림 3] 가상 메모리에서 유저 공간과 커널 공간 주소 공간

커널 공간을 결정짓는 컨피그는 CONFIG_PAGE_OFFSET입니다. 대부분 리눅스 커널 시스템에서 PAGE_OFFSET은 0xC000_0000으로 지정합니다. 그런데 라즈비안은 0x8000_0000으로 지정돼 있습니다. CONFIG_PAGE_OFFSET은 커널 컴파일 시 결정됩니다. 

0x0000_0000 번지부터 0x8000_0000 번지 구간은 유저영역 메모리 공간입니다. 이어서 0x8000_0000~0xFFFF_0000 구간은 커널 영역 메모리 공간입니다.

위 그림을 보면 유저 프로세스는 0x0000_0000~0x8000_0000 주소 구간에서 실행하고, 커널 프로세스는 커널 주소 공간인 0x8000_0000~0xFFFF_FFFF 주소 구간에서 실행합니다.

그런데 커널 공간과 유저 공간 별 메모리 접근 권한은 다릅니다.
커널 코드를 실행 중인 커널 모드에서는 0x0000_0000~0xFFFF_FFFF 전체 주소 구간에 접근할 수 있습니다. 유저 프로세스는 커널 주소 공간인 0x8000_0000~0xFFFF_FFFF 구간은 접근할 수 없습니다. 가상 주소 공간을 유저와 커널 공간으로 나눈 다음 유저 프로세스 커널 주소 공간에 접근할 수 없도록 제약을 둡니다. 

이어서 가상 메모리 맵에서 유저 공간 메모리와 커널 공간 메모리를 구간을 나눠서 메모리 관리를 하는 이유가 무엇인지 생각해 봅시다.

만약 어떤 시스템 메모리 공간을 유저 주소와 커널 주소 공간 구분없이 0~4GB(0x0000_0000~0xFFFF_FFFF) 가상 주소 공간을 연속으로 쓰고 있다고 가정합시다. 

만약 메모리 관리 기법을 잘 알고 있는 리눅스 시스템 개발자 라즈베리파이에서 볼 수 있는 Geany와 같은 어플리케이션까지을 구현하면 별 문제가 발생하지 않을 것입니다. 유저와 커널 모드로 실행 흐름을 두 개로 나눠서 설계할 필요가 없습니다.

그런데 리눅스 시스템 개발자가 아닌 리눅스 커널 메모리 관리 세부 기법을 잘 모르는 응용 프로그램 개발자가 어플리케이션을 구현한다고 가정합니다. 물론 유저 모드와 커널 모드가 없는 운영체제 환경입니다. 그런데 응용 프로그램에서 커널 자료구조나 함수가 위치한 메모리 공간을 오염시키면 어떤 문제가 발생할까요? 커널 패닉으로 리눅스 시스템이 리부팅하거나 다양한 시스템 오류를 유발할 것입니다.

응용 어플리케이션을 개발하고 설치할 수 있는 라즈베리파이 같은 범용 운영체제에서는 0~2G 가상 메모리 공간까지는 유저 모드만 접근할 수 있고 커널 모드에서는 0~4G 메모리까지 접근할 수 있도록 제한을 걸어 둡니다. 

그러면 유저 공간에서 커널 공간 메모리를 접근할 수 없으면 여러 가지로 불편할 것입니다.
여러 플렛폼에서 유저 공간에서 하드웨어 디바이스 드라이버를 제어하는 HAL(Hardware Adaptation Layer)이란 소프트웨어 계층을 볼 수 있습니다. 만약 유저 공간에서 커널 공간 메모리를 제어하려면 저수준 함수를 호출해 시스템 콜로 커널에 서비스를 요청해야 합니다. 
 
[그림 4] 유저 공간에서 커널 공간으로 시스템 콜을 실행하는 흐름도

시스템 콜을 실행하면 유저 모드에서 커널 모드로 스위칭을 한 다음 유저 공간에서 요청한 서비스를 커널 공간에서 실행합니다.

정리하면 유저 공간에서 커널 공간 메모리를 직접 접근해 메모리를 오염시키는 것을 방지하기 위해 가상 메모리 구간을 나누고 따로 권한을 부여하는 것입니다.

페이징에서 메모리 주소를 계산하는 방법 소개

페이징은 가상 메모리를 페이지 테이블을 통해 물리 주소로 변환하는 과정이자 기법입니다. 페이징(Paging)은 페이지를 뜻하는 명사와 행위를 의미하는 ~ing를 붙혀 만든 단어입니다. 즉, 페이지에 접근해 메모리를 변환해 관리하는 전반적인 동작을 의미합니다.

리눅스 커널에서 메모리를 관리하는 단위가 4K입니다. 여기서 4K는 16진수로 0x1000입니다.
그런데 리눅스 메모리 관리에 대한 내용을 읽으면 페이지란 '함정'에 빠져서 더 이상 진행이 되지 않습니다. 페이지 과정이 어렵기도 하지만 페이지 과정에서 주소를 변환하고 계산하는 방식이 낯설기 때문입니다. 이번에는 페이지 단위로 주소를 변환하는 방식을 소개합니다.

이해를 돕기 위해 먼저 쉬운 예를 들겠습니다. 0x900000 쪽으로 구성된 책이 있다고 가정하겠습니다. 그러면 0x403012 쪽을 어떻게 찾아갈까요? 0x403012 쪽이 있는 위치를 찾기 위해 많은 시간을 허비할 것입니다. 갑자기 바이너리 트리나 서칭 알고리즘이 떠오릅니다. 그런데 만약 0x1000 쪽이 하나의 챕터로 구성돼 있고 챕터 기준으로 페이지를 찾아가면 어떨까요? 
1 0x0000~0x1000: 0장
2 0x1000~0x1fff: 1장
3 0x2000~0x2fff: 2장
4 0x3000~0x3fff: 3장

책이 위와 같은 챕터가 구성돼 있다고 미리 알고 있는 상황입니다. 그러면 챕터를 먼저 찾아간 다음에 챕터 내 페이지를 검색하는 것이 더 빠르지 않을까요?

만약 0x3012 쪽이 있다고 가정합시다. 먼저 챕터를 찾아가 보겠습니다. 
 
[그림 5] 책에서 페이징 매기는 방법 소개

이미 우리는 한 개 챕터는 0x1000 크기임을 알 수 있습니다. 그러면 0x3012 쪽은 어느 챕터일까요?

3장은 0x3000~0x3fff 쪽 범위니 0x3012 쪽은 3장임을 알 수 있습니다.
그러면 이를 수식으로 표현해볼까요? 먼저 0x3012를 2진수로 나열해봅시다.
11000000010010 >> 12 
---------------------
              11

페이지를 12만큼 오른쪽 비트 쉬프트 연산을 한 결과는 이진수로 11입니다. 이진수 11은 10진수로는 3입니다. 이 수식으로 챕터를 바로 계산할 수 있습니다.

0x3012 쪽은 3챕터인데 0x3012에서 0x3000으로 챕터의 위치를 확인할 수 있습니다.
다음 수식으로 볼까요?
0x3012 = 0x3000(챕터 위치) + 0x12(챕터 내에서의 쪽) 

그러면 0x3012를 이렇게 표현할 수 있습니다.
3번째 챕터 내 0x12 번째 쪽 
이번에는 0x403014 쪽을 위 기준으로 어떻게 찾을까요?
 
[그림 6] 페이징 매기는 방법을 활용해 0x403014 쪽을 찾는 과정

이번에도 0x403012 쪽을 12 만큼 쉬프트 연산을 하면 챕터를 알 수 있습니다.
0x403012(16진수)
10000000011000000010010 >> 12 
-------------------------------
             010000000011(0x403) 
0x403014: 0x403 챕터 내 + 0x14 번째 쪽 

위에서 들었던 예시로 어떤 책의 특정 쪽을 알려고 하면 먼저 챕터를 찾은 다음 챕터 내 쪽 수를 찾습니다. 이렇게 책의 챕터를 0x1000 쪽 단위로 분류하면 빨리 챕터를 찾을 수 있습니다.

리눅스에서 메모리를 관리할 때 이와 같은 방식으로 페이지 프레임 번호를 찾습니다. 물리 주소를 입력으로 받아 페이지 프레임 번호를 계산하는 것입니다.

이렇게 리눅스 커널은 페이징 과정에서 페이지 번호를 찾은 다음 페이지 내 오프셋을 찾는 계산을 합니다. 이 방식을 쓰면 조금 더 빨리 주소 위치를 검색할 수 있습니다. 
 
[그림 7] 리눅스 메모리 시스템에서 페이지 프레임을 찾는 방법  

위 그림은 이전에 다룬 Chapter에서 페이지 프레임으로 변경한 것입니다. 리눅스에서는 물리 메모리를 0x1000 단위로 짤라 번호를 붙힙니다. 0x1000~0x2000 범위 메모리 주소를 1 번째 페이지 프레임, 0x2000~0x3000 구간은 2 번째 페이지 프레임이라고 합니다.

물리 메모리 주소를 입력으로 받아 책의 쪽 정보에서 챕터를 찾듯이 페이지 프레임 번호를 찾는
계산을 매우 자주 합니다.

만약 물리 메모리 주소가 0x82012이면 몇 번째 페이지 프레임 번호일까요?
물리 메모리 주소를 오른쪽으로 12만큼 오른쪽으로 쉬프트하면 됩니다.
0x82012 >> 12
-------------
   0x82 

결과 0x82 번째 페이지 프레임 번호란 것을 알 수 있습니다.

그러면 페이지 프레임 번호는 왜 계산할까요?
여러 가지 이유가 있지만 대표적으로 페이지 프레임 번호를 알면 자연히 페이지 디스크립터 주소를 알 수 있습니다.

다음 소절에 이어서 페이지 프레임과 페이지 디스크립터에 대해 살펴보겠습니다.

페이지 프레임 번호와 페이지 디스크립터란

리눅스에서는 물리 메모리를 0x1000(4KB) 바이트 단위로 짤라 관리합니다. 다음 그림을 보면서 페이지 프레임 번호와 페이지 디스크립터에 대해 살펴보겠습니다.
 
[그림 8] 페이지 프레임 번호와 mem_map 과의 관계


리눅스는 물리 메모리를 0x1000(4KB) 바이트 단위로 짤라서 관리합니다.
위 그림 오른쪽 물리 메모리 맵을 보면 각 주소 구간별 페이지 프레임 번호를 확인할 수 있습니다.

왼쪽 박스는 struct page 타입 mem_map 배열이며 각 인덱스는 오른쪽 물리 메모리의 페이지 프레임 번호에 매핑됩니다. 왼쪽 mem_map 박스를 보면 각 인덱스 마다 struct page가 보입니다. mem_map은 모든 물리 메모리 페이지 프레임에 대한 정보를 담아둔 struct page 구조체 배열입니다. 페이지 프레임을 관리하는 자료구조를 페이지 디스크립터라고 하며 구조체는 struct page입니다.


struct page 구조체는 다음과 같은 필드로 구성돼 있습니다.
(struct page *) (struct page*)0xEC778540  
  (long unsigned int) flags = 128 = 0x80,   
  (struct address_space *) mapping = 0x0,
  (void *) s_mem = 0x0,
  (long unsigned int) index = 0xC518EC00,
  (void *) freelist = 0xC518EC00,   
  (bool) pfmemalloc = FALSE,
  (unsigned int) counters = 2149580809 = 0x80200009,
  (atomic_t) _mapcount = ((int) counter = -2145386487 = 0x80200009),
  (unsigned int:16) inuse = 9 = 0x9,      
  (unsigned int:15) objects = 32 = 0x20,   
  (unsigned int:1) frozen = 1 = 0x1,
(int) units = -2145386487 = 0x80200009,

위 페이지 디스크립터는 슬랩 페이지를 표현합니다.  

페이지 테이블이란

페이지 테이블은 가상 주소를 물리 주소로 변환할 수 있는 매핑 정보를 갖고 있습니다. 다음 그림을 보면서 페이지 테이블을 알아봅시다.
 
[그림 9] 페이지 테이블 실행 흐름도

가상 메모리 공간 내 가상 주소는 페이지 테이블을 통해 물리 메모리 내 물리 주소에 접근합니다. 페이지 테이블을 통해서 물리적으로 연속적이지 않은 주소 공간도 연속적으로 쓸 수 있습니다.

페이지 테이블 별 데이터나 레코드를 페이지 테이블 엔트리라고 부르며 이를 Page Table Entry(PTE)라고 합니다. 페이지 테이블 엔트리는 하단 [1:0] 비트 값에 따라 다음과 같이 분류할 수 있습니다.
섹션 엔트리
라지 페이지 테이블 엔트리
스몰 페이지 테이블 엔트리

대부분 커널 공간에 있는 가상 주소는 섹션 엔트리를 통해 한번 페이지 테이블 변환을 통해 물리 주소에 접근합니다. 

페이지 테이블 베이스 주소는 TTBR(Translation Table Base Register)에서 확인할 수 있으며 리눅스 커널에서 swapper_pg_dir 전역 변수로 확인할 수 있습니다.

이번 절까지 가상 메모리 시스템의 기본 개념에 대해 소개했습니다. 이어서 가상 메모리 시스템에서 가상 주소를 물리 주소로 변환하는 과정을 알아봅시다.

"혹시 궁금한 점이 있으면 댓글로 질문 남겨주세요. 아는 한 성실히 답변 올려드리겠습니다!" 

Thanks,
Austin Kim(austindh.kim@gmail.com)

Reference(커널 메모리 소개) 
가상 주소를 물리 주소로 어떻게 변환할까?   
메모리 존(Zone)에 대해서   
커널 메모리 할당은 어떻게 할까   
슬랩 메모리 할당자와 kmalloc 슬랩 캐시 분석   
커널 메모리 디버깅







핑백

덧글

댓글 입력 영역