100명 돌파 30% 할인 쿠폰 발행 
 100명 돌파(1,2부)를 기념하여 30% 할인 쿠폰(유데미)을 발행합니다.
감사합니다. 선착순 100명입니다.

  • 제목: 시스템 소프트웨어 개발을 위한 Arm 아키텍처의 구조와 원리 2부- 저자직강
  • 쿠폰 : 04B86316BE5DBE6FE937 (기한 ~3/2/2024)
  • 유데미 강의 링크 : 링크 

시스템 반도체, 자동차 분야 등
시스템 소프트웨어 개발자라면 
꼭 알아야 할 Arm 아키텍처.

 

스마트폰은 물론 AI용 SoC 시스템 반도체, 전기자동차 Automotive(자율주행, 인포테인먼트) 그리고 클라우드 서버 및 맥북에서 사용되는 Arm 프로세서는 모두 Armv8-A 기반 64비트 Cortex-A 프로세서 (Cortex-A53, Cortex-A57, Cortex-A72 등...) 입니다. 현재 시스템 소프트웨어 업계에서 가장 많이 필요한 기반 지식이 요구되는 내용이 Armv8-A 아키텍처라고 볼 수 있어요. 그런데, 최신 Arm 아키텍처인 Armv8-A와 Armv7-A 아키텍처가 너무 어렵게만 느껴지셨나요?

 

이 강의에서는 Armv8-A와 Armv7-A 아키텍처의 핵심 중 핵심인 아래와 같은 내용을 다룹니다.

 

  • 1부 복습
  • 익셉션을 알아야 하는 이유
  • Armv7-익셉션(Exception)
  • Armv8-익셉션(Exception)
  • GIC(Generic Interrupt Controller)
  • AAPCS(Arm Procedure Call Standard)를 알아야 하는 이유
  • Armv7-AAPCS
  • Armv8-AAPCS  

 

시스템 소프트웨어 개발자, 임베디드 시스템(BSP) 프로그래머 주목!

시스템 반도체, 전기 자동차 분야(자율주행, 인포테인먼트)를 포함한 시스템 소프트웨어 분야에서 역량을 키우고 싶은 주니어 개발자   

시스템 반도체, 전기 자동차 분야와 같은 시스템 소프트웨어 개발을 하고 싶은 취업 준비생 

시스템 소프트웨어 분야(메모리, 파일 시스템, 운영체제)의 대학원 진학을 목표로 하는 대학생

시스템 소프트웨어 분야로 커리어를 전환하려는 다른 분야의 개발자 

많은 리눅스 임베디드 개발자를 희망하는 분들이라면 컴퓨터 구조나 운영체제를 배웁니다. 그리고 CS 이론도 어느 정도 공부합니다. 최근에 떠오르고 있는 시스템 반도체와 전기 자동차 시스템 소프트웨어 개발자는 리눅스 디바이스 드라이버, RTOS 혹은 부트로더를 분석합니다. 하지만 그것만으로는 무엇인가 부족하다는 것을 스스로 느낍니다. 전문 시스템 소프트웨어 개발자가 되기 위해 반드시 Arm 아키텍처를 알아야 한다는 사실을 알고 있지만, 막상 Arm 스펙 문서를 펼치는 순간 쉽지 않은 길이며 독학은 어렵겠다는 판단을 하게 됩니다.  


Arm 아키텍처의 핵심을 
이해하기 쉽게 설명해드립니다!

  • 1️⃣ Arm 아키텍처의 레지스터와 어셈블리 명령어에 대한 이해와 동작 모드와 익셉션 레벨 등 핵심 이론을 이해할 수 있습니다.  
  • 2️⃣ Arm 아키텍처를 이루는 주요 내용을 코드 수준에서 다시 경험하며 Arm 아키텍처에 대한 이해 수준이 훨씬 더 높아집니다.
  • 3️⃣ Arm 아키텍처가 지원하는 다양한 기능을 적극 활용함으로써 높은 안정성과 성능을 보장받는 시스템 프로그램(드라이버, 부트로더)을 개발할 수 있습니다. 
  • 4️⃣ 실전 프로젝트 개발에 필요한 각종 이론 및 사례를 스스로 이해할 수 있습니다. 

시스템 소프트웨어 개발의 핵심은 CPU 및 컴퓨터 구조라 해도 과언이 아닙니다. IT 업계에서 가장 필요로 하는 CPU 프로세서인, 최신 Arm 아키텍처(Armv8-A, Armv7-A)에 관한 핵심 이론을 쉽게 설명했습니다!

Arm 아키텍처가 어려운 이유는 Arm 아키텍처를 하드웨어 프로세서 관점으로만 배우기 때문입니다. 이 강의는 코드 분석과 구체적인 사례 소개로 Arm 아키텍처를 구성하는 내용이 실제 어떻게 사용되는지 설명합니다. 이 강의가 실력있는 시스템 소프트웨어 개발자가 될 수 있는 중추적인 역할을 해드릴 것입니다. 

 

⭐️ 강의의 차별화 포인트 

‘시스템 소프트웨어 개발을 위한 Arm 아키텍처의 구조와 원리’ 저자 직강 강의입니다.

 

먼저 시스템 반도체의 시스템 소프트웨어 개발 과정을 상세하게 설명하고, 시스템 소프트웨어 개발자(시스템 반도체 분야)가 되기 알아야 하는 내용을 소개합니다. 이와 더불어 Arm 아키텍처를 왜 배워야 하는지 상세히 설명합니다. 

 

시스템 반도체 개발 과정에서 Arm 아키텍처를 왜 잘 알아야 하는지 설명합니다. 또한 Arm 프로세서의 생태계와 비즈니스 모델에 대해서 설명합니다.



Arm 스펙 문서를 세세하게 분석하면서 Arm 아키텍처(Armv8-A, Armv7-A)에 대한 깊이 있는 내용을 설명합니다.



Arm 아키텍처의 스펙이 실제 소프트웨어로 어떻게 구현됐는지 어셈블리 명령어로 분석합니다. 큰 그림으로 전체 실행 흐름을 설명하고 실무에서 활용하면 좋은 다양한 디버깅 스킬도 함께 설명합니다. 



실무에서 많이 활용되는 TRACE32 디버거를 활용해 직접 디버깅을 하면서 Arm 아키텍처를 구성하는 '어셈블리 명령어', '레지스터', '동작 모드, '익셉션 레벨'의 동작 원리를 설명합니다.

 

 

책 집필/강의경력을 바탕으로
누구보다 깊고 상세하게!

국내 시스템 소프트웨어 분야에서 전무후무한! 'Arm 아키텍처(Armv8-A, Armv7-A)'와 '리눅스 커널' 책을 쓴 저자입니다. 또한 최신 시스템 소프트웨어 트렌드(전기자동차, 시스템 반도체- 시스템 소프트웨어)를 가장 잘 알고 있는 현업 개발자이자, 시스템 소프트웨어 분야에서 가장 지식 전파 활동을 활발하게 하는 교육자입니다. 

  • '시스템 소프트웨어 개발을 위한 Arm 아키텍처의 구조와 원리'(2023년) 저자
  • '디버깅을 통해 배우는 리눅스 커널의 구조와 원리' (2021년, 대한민국 학술원 우수도서상) 저자
  • '프로그래머스 데브 코스: 리눅스 시스템 및 커널 전문가' 메인 강사
  • 2022년 6월, 한국컴퓨터종합학술대회 (KCC2022) - 튜토리얼 발표 [ftrace를 이용해 리눅스 커널 정복하기]
  • LG전자 '리눅스 커널' 및 'Armv8 아키텍처' 사내 강사(국내 및 해외 개발자 포함) - (2020년~2024) 

국내에서 어느 누구보다 Arm 아키텍처(Armv8-A, Armv7-A)를 이루는 주요 기능을 잘 설명할 수 있는 교육자라고 자신있게 말씀드릴 수 있습니다. 

리눅스 커널에서의 인터럽트 처리 흐름
인터럽트가 발생했을 때 커널이 이를 처리하는 과정은 다음과 같이 3단계로 나눌 수 있습니다.



 1 단계: 인터럽트 발생
인터럽트가 발생하면 프로세스 실행을 중지하고 인터럽트 벡터로 이동합니다. 인터럽트 벡터에서 인터럽트 처리를 마무리한 후 다시 프로세스를 실행하기 위해 실행 중인 프로세스 레지스터 세트를 스택에 저장합니다. 이후 커널 내부 인터럽트 함수를 호출합니다. 


 2단계: 인터럽트 핸들러 호출
커널 내부에서는 발생한 인터럽트에 대응하는 인터럽트 디스크립터를 읽어서 인터럽트 핸들러를 호출합니다. 


 3단계: 인터럽트 핸들러 실행
인터럽트 핸들러에서 하드웨어를 직접 제어하고 유저 공간에 이 변화를 알립니다.

 


이해를 돕기 위해 한 가지 예를 들어보겠습니다. 안드로이드 휴대폰에서 화면을 손을 만지는 동작에서 여러분이 손으로 휴대폰 화면을 터치하면 내부 동작은 다음과 같은 단계로 나눌 수 있습니다.

 1단계: 터치 인터럽트 발생
하드웨어적인 터치 모듈이 변화를 감지하고 터치 모듈에 대한 인터럽트를 발생시킵니다. 이때 인터럽트 벡터가 실행됩니다.


 2단계: 터치 인터럽트 핸들러 호출
커널은 터치 인터럽트 번호로 해당 인터럽트 디스크립터를 읽습니다. 다음 인터럽트 디스크립터에 저장된 인터럽트 핸들러 주소를 찾아 인터럽트 핸들러를 호출합니다.


 3단계: 터치 인터럽트 핸들러 실행
결국 터치 인터럽트 핸들러는 해당 터치 인터럽트를 받아 정해진 처리를 합니다. 화면을 업데이트하거나 하드웨어 터치 디바이스에 인터럽트를 잘 받았다는 사실을 알립니다. 


“인터럽트 디스크립터”, “인터럽트 벡터” 같은 낯선 용어로 설명했는데, 이러한 용어의 공학적 의미는 하나하나 각 장에서 다룰 예정입니다. 


인터럽트가 발생하면 이를 커널이 처리하는 과정을 터치 드라이버를 예로 들어 살펴봤습니다. 인터럽트 발생을 처리하는 단계를 함수 흐름과 실행 주체별로 분류하면 다음 그림과 같습니다. 

 


그림 5.5 ARM 프로세서/리눅스 커널/디바이스 드라이버별 인터럽트 처리 흐름

전체 실행 흐름은 다음의 3단계로 분류할 수 있습니다.

1. ARM 프로세스
인터럽트가 발생하면 실행 중인 코드를 멈춘 후 인터럽트 벡터로 실행 흐름을 이동합니다. ARM 프로세스와 연관된 동작입니다. 

2. 리눅스 커널
인터럽트 벡터로 프로그램 카운터를 브랜치합니다. 커널 인터럽트 내부 함수에서 인터럽트를 관리하는 자료구조인 인터럽트 디스크립터를 읽습니다. 인터럽트 디스크립터에 저장된 인터럽트 핸들러를 호출합니다.

3. 디바이스 드라이버
각 디바이스 드라이버에서 등록한 인터럽트 핸들러를 실행해 인터럽트 발생에 대한 처리를 수행합니다.

정리하면 “인터럽트로 하드웨어적인 변화가 발생하면 리눅스 커널에서 어떻게 처리하는가"입니다. 이를 위해 이번 절에서는 인터럽트에 대해 소개했으니 이어지는 절에서 인터럽트 컨텍스트에 대해 살펴보겠습니다.

공간 지역성과 시간 지역성을 활용해 캐시 알고리즘을 구성할 수 있다고 소개했습니다. 그런데 데이터 구조나 알고리즘을 사용할 때 메모리에 접근하는 패턴을 관찰하면 시간 지역성이나 공간 지역성을 확인하기 어려운 경우가 있습니다. 예를 들어, 링크드 리스트로 데이터를 관리할 때 메모리에 접근하는 패턴을 관찰하면 특정 위치에 있는 데이터에 다시 접근할 확률이 높지 않아 시간 지역성과 같은 특징을 지니지 않습니다. 

 

또한 링크드 리스트는 배열과 달리 인접한 메모리 주소 공간에 접근하지 않아 공간 지역성에도 맞지 않습니다. 그런데 코드를 유심히 분석하면 프로그램은 함수의 호출과 자료구조로 구성돼 있다는 사실을 알 수 있습니다. 자주 사용하는 자료 구조나 알고리즘은 어느 정도 정해져 있습니다. 예를 들어, 링크드 리스트나 스택과 같은 데이터 구조나 알고리즘을 사용해 데이터를 관리하는 패턴을 볼 수 있습니다. 

 

알고리즘의 특징을 활용해 프로그램이 메모리에 접근하는 패턴을 예측할 수 있는데, 이를 알고리즘 지역성이라고 합니다.
이번에는 간단한 예시 코드를 보면서 알고리즘 지역성에 대해 알아봅시다.

struct node {
02 struct node *next; // 다음 노드 주소를 저장
03 int data; // 데이터 저장 필드
04 };
05
06 struct node *curr = head->next; // 링크드 리스트 포인터에 첫 번째 노드의 주소를 저장
07 while (curr != NULL) // 노드의 포인터가 NULL이 아닐 때까지 실행
08 {
09 curr = curr->next; // 포인터에 다음 노드의 주소를 저장
10 process_data(curr); // process_data 함수를 호출해 노드를 처리
11 }

 

위 코드는 링크드 리스트를 구성하는 노드에 접근해 데이터를 처리하는 루틴입니다. 07 ~ 11번째 줄은 노드를 순회하면서 노드의 포인터가 NULL이 아닐 때까지 while 루프를 실행합니다. 그런데 링크드 리스트에 존재하는 자료 구조는 연속적인 메모리 공간에 위치하지 않습니다. 만약 링크드 리스트라는 자료 구조의 패턴을 미리 파악한다면 다음과 같이 처리할 수 있습니다.


06 struct node *curr = head->next; // 링크드 리스트 포인터에 첫 번째 노드의 주소를 저장
07 // 링크드 리스트를 순회하면서 노드 주소를 미리 로딩
08 while (curr != NULL) // 노드의 포인터가 NULL이 아닐 때까지 실행
09 {
10 curr = curr->next; // 포인터에 다음 노드의 주소를 저장
11 process_data(curr); // process_data 함수를 호출해 노드를 처리
12 }


이전 코드와 비교했을 때 달라진 점은 07번째 줄입니다. 07번째 줄은 미리 링크드 리스트를 순회해 노드포인터를 임시로 로딩하는 동작을 표현한 주석입니다. 07번째 줄과 같이 링크드 리스트를 미리 순회해 노드 포인터를 로딩하면 링크드 리스트를 구성하는 노드에 접근하는 08 ~ 12번째 줄의 실행 시간이 더 빨라질 것입니다.


링크드 리스트로 구성된 데이터를 어떤 패턴으로 접근할지는 예측이 가능한데, 이를 위해서는 링크드 리스트라는 자료구조와 알고리즘의 패턴을 이해해야 합니다. 알고리즘의 특징을 활용해 프로그램이 메모리에 접근하는 패턴을 예측하는 과정을 알고리즘 지역성이라고 합니다.


누군가 평소에 자주 들르는 카페에 다시 갈 가능성이 있을까요? 한 번도 안 간 사람보다 다시 갈 확률이 높을 것입니다. 이와 유사하게 프로그램이 특정 명령어나 특정 변수를 반복적으로 사용하는 패턴을 시간 지역성(temporal locality)이라고 합니다. 프로그램이 실행될 때 특정 명령어나 변수에 자주 접근하면 가까운 미래에 사용할 것을 대비해 사용된 명령어나 데이터를 캐시에 미리 로딩합니다.

 

앞에서 예시로 든 예제 코드를 보면서 시간 지역성에 대해 알아봅시다.

01 static int arr[10];
02 int sum = 0;
03 int weight = 10;
04
05 for (i = 0; i < 10; i++) {
06 sum += arr[i] + weight;
07 }


06번째 줄은 arr[] 배열의 &a[0] ~ &a[9] 주소에 있는 배열의 데이터를 읽어서 sum에 더하는 루틴입니다. 데이터에 접근하는 패턴을 자세히 관찰하면 다음과 같은 사실을 확인할 수 있습니다.


weight 지역 변수가 위치한 주소에 반복적으로 접근할 확률이 높다.

 


이런 특징을 활용해 &a[1] ~ &a[9] 주소에 있는 데이터를 로딩할 때 &weight 주소에 있는 데이터를 미리 로딩하면 속도 개선 효과를 얻을 수 있습니다. 이처럼 다시 접근할 것으로 예상되는 데이터를 미리 로딩해 캐시를 설계할 수 있는데, 이를 시간 지역성이라고 합니다.


시간 지역성을 활용한 다양한 알고리즘이 제안됐는데, 그중 프리페칭이 가장 널리 알려진 알고리즘입니다. 프리페칭은 최근에 CPU 코어에서 접근한 주소 접근 이력을 참고해 더 많은 양의 데이터를 미리 페치하는 기법입니다.

 

 

자주 가는 카페가 있다면 카페 옆에 있는 다른 가게에 갈 가능성도 있습니다. 이처럼 프로그램이 어떤 데
이터를 사용하면 그 데이터와 인접한 (주소에 있는) 데이터에 접근할 확률이 높습니다. 이 같은 패턴을 공
간 지역성(spatial locality)이라고 합니다.
【정보】 섹션 정보를 활용한 공간 지역성
프로그램을 컴파일하면 컴파일러는 연관된 객체(자료구조)를 특정 메모리 공간(섹션)에 모아두는 경향이 있습니다.
이번에는 다음 예제 코드를 보면서 공간 지역성에 대해 더 자세히 알아봅시다.
01 static int arr[10];
02 int sum = 0;


03
04 for (i = 0; i < 10; i++) {
05 sum += arr[i];
06 }
05번째 줄은 arr[] 배열의 &a[0] 주소에서 &a[9] 주소에 있는 배열의 데이터를 읽어서 sum에 더하는 루틴입
니다. 데이터의 위치와 데이터에 접근하는 패턴을 자세히 관찰하면 다음과 같은 사실을 확인할 수 있습
니다.
■ &a[0] ~ &a[9] 주소 구간에 위치한 데이터는 연속적인 메모리 공간에 위치한다.
■ &a[0] 주소에 접근하면 &a[1] ~ &a[9] 주소에 접근할 확률이 높다.
이런 특징을 활용해 &a[0] ~ &a[9]가 존재하는 주소에 미리 접근해 데이터를 캐시에 올려 놓으면 for 루프
가 더 빨리 동작할 것입니다. 이처럼 이미 사용된 데이터 주소 근처에 있는 데이터를 미리 로딩하는 방식
을 활용해 캐시를 설계합니다.
이처럼 공간 지역성은 최근 접근한 데이터의 주변 공간에 다시 접근하는 소프트웨어의 패턴을 뜻합니다.
이런 특징을 잘 활용해 연속적인 공간에 존재하는 메모리 데이터를 미리 로딩하면 메모리에 접근하는 횟
수를 줄일 수 있습니다. 이 같은 구조로 설계된 시스템에서는 같은 코드를 실행해도 더 빨리 실행될 수 있
습니다.


【정보】 디맨드 페치란?
공간 지역성을 활용하는 기법 중 하나는 디맨드 페치(demand fetch)입니다. 디맨드 페치란 프로그램이 메인 메모리에
접근해 명령어나 데이터를 캐시에 로딩한 다음에 일정 시간 동안 유지하는 방식을 의미합니다.



이전 절에서는 한 개의 캐시를 기준으로 캐시의 기본 개념을 설명했습니다. Arm 프로세서를 포함한 대부분의 프로세서는 멀티 레벨로 캐시가 구성돼 있습니다. 다음 그림을 보면서 멀티 레벨 캐시에 대해 알아봅시다. 
 


그림 18.2 메모리 계층 구조에서 캐시의 역할 

그림의 왼쪽과 오른쪽에 있는 Core는 말 그대로 CPU 코어를 뜻합니다. Core의 아랫부분을 보면 L1I$와 L1D$ 캐시가 보입니다. 여기서 L1은 레벨 1 캐시(레벨 원으로 발음)라고 하며 L1I$는 L1 명령어(Instruction) 캐시, L1D$는 L1 데이터(Data) 캐시를 뜻합니다.

위와 같이 Arm 프로세서를 비롯한 대부분 프로세서는 캐시가 여러 계층으로 구성돼 있습니다. L1(Level 1: 레벨 1 캐시), L2(Level 2: 레벨 2 캐시)는 대부분 Cortex 프로세서에서 볼 수 있고, L3 캐시까지 있는 프로세서도 확인할 수 있습니다. 그림에서 L3$ 로 명시된 부분은 L3 캐시를 나타냅니다. 

 


[중요] LLC(Last Level Cache) 란?
보통 마지막 레벨의 칩 내에 위치한 캐시를 특별히 LLC(Last Level Cache)라고 부릅니다. LLC 이후는 시간이 매우 오래 걸리는 칩 밖의 메모리 계층으로 이동하므로 특별히 구분합니다.

만약 Arm 코어에서 접근하는 데이터나 명령어가 L1, L2 캐시, 그리고 LLC에 없으면 프로세서 밖에 존재하는 메모리에 접근해 데이터나 명령어를 로딩해야 합니다. 이 과정에서 버스와 메모리 컨트롤러와 같은 하드웨어의 도움을 받아야 하므로 실행 시간이 더 오래 걸립니다. 따라서 LLC에서 가장 중요한 캐시 성능 지표는 캐시 미스로 선정하는 경우가 많습니다.

 



L1 캐시, L2 캐시를 처음 봤을 때 "캐시를 L1으로만 잘 설계하면 되지, 여러 계층의 캐시를 설계하는 이유는 무엇일까?"라는 의문이 생깁니다. 여러 계층의 캐시를 구성하면 프로세서의 성능을 최대한 끌어올릴 수 있기 때문입니다. 캐시의 접근 속도와 용량 사이에 트레이드오프가 있기 마련인데, 멀티 레벨로 캐시를 구성하면 트레이드오프를 최대한 극복할 수 있습니다.

프로세서가 데이터를 요청하면 다음과 같은 순서로 처리합니다. 

1. 먼저 L1 캐시에서 찾는다.
2. 만약 L1 캐시에 데이터가 없으면 L2 캐시에서 찾는다.
3. 그래도 데이터가 없으면 LLC에서 찾는다.
4. 만약 LLC에 도달해도 데이터가 없으면 메인 메모리에 접근해 데이터를 가져 온다.

CPU 코어에 가장 인접한 L1 캐시 내부는 명령어 캐시와 데이터 캐시가 따로 존재합니다. 그다음 레벨인 L2 캐시 이상의 캐시는(L2 캐시, L3 캐시) 하나의 캐시에서 명령어와 데이터를 함께 처리하는 방식으로 설계합니다. 

대부분의 Arm 프로세서는 캐시를 멀티 레벨로 구성하며, 성능을 극대화하는 방향으로 시스템을 설계합니다. 멀티 캐시에 대한 더 자세한 내용은 18.3절 ‘멀티 레벨 캐시’를 참고합시다.

이렇게 해서 캐시를 구성하는 주요 개념을 설명했습니다. 다음 절에서는 캐시와 관련된 알고리즘을 소개합니다.

 

 

 

MMU에서 가상 주소를 물리 주소로 변환하는 구조를 알아봤으니 변환 테이블의 내부 구조와 가상 주소가 물리 주소로 변환되는 실행 흐름을 알아봅시다.


페이지 테이블 엔트리


변환 테이블을 통해 가상 주소를 물리 주소로 변환하려면 먼저 가상 주소의 형식을 알아야 합니다. 가상 주소 영역을 일정한 크기의 블록으로 나눌 수 있는데, 여기서 한 개의 테이블 엔트리는 한 개의 블록에 해당됩니다.

 


그림 19.7 가상 주소가 변환 테이블을 통해 물리 주소로 변환되는 흐름

각각의 엔트리는 물리 메모리에서 해당되는 블록 주소와 물리 주소에 접근할 때 사용할 속성으로 구성돼 있습니다.

변환 테이블은 일종의 엔트리 블록으로 구성돼 있습니다. 변환 테이블에 있는 엔트리 0은 블록 0, 엔트리 1는 블록 1에 대한 매핑 정보를 제공합니다. 또한 각각 엔트리는 물리 주소의 블록에 해당되는 주소와 속성 정보를 담고 있습니다.


페이지 테이블 룩업

테이블 룩업은 변환 작업이 진행될 때 실행됩니다. 운영체제의 프로세스가 주소에 접근하면 주소 변환이 실행됩니다. 이때 가상 주소는 다음 다이어그램과 같이 두 가지 부분으로 나뉩니다.

 


그림 19.8 가상 주소의 구조와 페이지 룩업이 진행되는 흐름

위 다이어그램은 싱글 페이지 룩업 동작을 나타냅니다. ①에서 '엔트리'로 명시된 상위 비트는 찾을 블록 엔트리를 알려주는데, 테이블에 접근하는 인덱스로 활용됩니다. 변환 테이블을 구성하는 각각 엔트리 블록은 가상 주소에 대한 물리 주소의 베이스 주소를 담고 있습니다. 

그림을 통해 전체 구조를 확인했으니 주소가 변환되는 과정을 알아봅시다. 위 그림에서 ①로 표시된 부분은 CPU가 가상 주소를 실행하는 동작을 나타냅니다. CPU에서 실행되는 프로세스 입장에서 바라보는 주소는 가상 주소입니다. 이번에는 화살표와 함께 ②로 표시된 부분은 가상 주소의 베이스 주소를 바탕으로 주소 변환 정보를 담고 있는 페이지 테이블에 접근하는 동작입니다. 페이지 테이블의 시작 주소를 나타내는 TTBR1_EL1 레지스터를 통해 페이지 테이블의 베이스 주소를 참고해 변환 정보가 담긴 페이지 테이블 엔트리의 주소를 찾습니다. 그림의 ③은 페이지 테이블 엔트리 레코드를 읽어서 물리 주소로 변환하는 동작을 나타냅니다. 물리 주소에서 보이는 베이스는 페이지 테이블 엔트리에 있는 주소 정보입니다. ①에서 보이는 '오프셋'으로 명시된 하위 비트는 블록 내부의 오프셋을 나타내며, 변환 과정에서 변경되지 않습니다.


멀티 레벨 페이지 변환


이제까지 한 개의 주소 변환 테이블을 통해 주소가 변환되는 과정을 알아봤습니다. 한 개의 변환 테이블을 통해 한 번에 주소가 변환되므로 이를 싱글 룩업이라고 합니다. 그런데 Aarch64에서는 다수의 변환 페이지 테이블을 사용해 주소 변환 작업이 수행될 수 있는데, 이를 멀티 레벨 페이지 룩업을 통해 가상 주소가 물리 주소로 변환된다고 할 수 있습니다. 

이어서 다음 그림을 보면서 멀티 레벨 페이지 룩업의 실행 흐름을 알아봅시다.
 
출처: Learn the architecture - AArch64 memory management https://developer.arm.com/documentation/101811/latest/

그림 19.9 가상 주소가 멀티 레벨 변환 페이지를 통해 변환되는 과정

ARMv8-A 아키텍처에서 지원하는 최대 페이지 변환 레벨은 4개이며, 실제 레벨의 범위는 0 ~ 3 사이입니다. 멀티 레벨로 구성된 페이지 테이블을 구성하면 큰 사이즈의 블록과 작은 사이즈의 블록을 모두 활용할 수 있습니다. 멀티 레벨 변환 테이블을 구성하는 블록의 특징은 다음과 같습니다.

 큰 블록은 작은 블록보다 페이지 룩업에 걸리는 연산이 더 적습니다.(추가로 큰 블록들은 TLB에 더 효율적으로 캐싱됩니다.)
 작은 블록들은 TLB에서 캐싱할 때 비효율적입니다. 캐싱이 비효율적인 이유는 작은 블록들이 주소를 변환하는 과정에서 메모리에 더 자주 액세스해 데이터 읽기를 시도하기 때문입니다.


[정보] 큰 크기 블록과 작은 크기 블록을 사용할 때의 트레이드오프
큰 사이즈의 블록과 작은 사이즈의 블록으로 구성된 멀티 변환 테이블을 사용해 가상 주소를 변환할 때 트레이드오프가 있습니다. 이는 멀티 레벨 캐시와 비슷한 개념으로 볼 수 있습니다. L1 캐시의 가장 중요한 성능 지표는 캐시 라인을 읽어 오는 속도이고 L2 캐시의 가장 중요한 성능 지표는 캐시 히트 비율입니다. 따라서 큰 사이즈 블록과 작은 사이즈의 블록을 적절히 활용해 멀티 레벨 테이블을 구성할 필요가 있습니다.

 

 



 

Documentation – Arm Developer

 

developer.arm.com

 

 

MMU의 주된 역할은 가상 주소를 물리 주소로 변환하는 일입니다. 그렇다면 MMU는 어떻게 구성돼 있고 어떤 과정으로 가상 주소를 물리 주소로 변환할까요? 다음 그림을 보면서 MMU의 실행 흐름에 대해 알아봅시다.

 
출처: Learn the architecture - AArch64 memory management https://developer.arm.com/documentation/101811/latest/

그림 19.6 MMU를 통해 주소가 변환되는 흐름

프로세스가 가상 주소에 접근하면 먼저 MMU에게 주소가 전달됩니다. MMU는 먼저 TLB(Translation Lookaside Buffer)에 해당 주소에 대한 변환 정보가 있는지 확인합니다. 여기서 TLB는 최근에 사용된 주소 변환 정보를 담고 있는 일종의 캐시입니다. 

만약 가상 주소에 대한 변환 정보가 TLB에 있으면 바로 물리 주소로 변환합니다. 만약 MMU가 TLB 내에 최근에 변환된 테이블 정보를 읽지 못하면 ‘Table Walk Unit’을 통해 메모리로부터 적절한 테이블 엔트리를 읽습니다.

[중요] 캐시에 접근하기 전에 물리 주소로 변환하는 이유

Arm 코어 옆에 캐시가 있는데, 캐시에 접근하려면 먼저 MMU를 통해 가상 주소를 물리 주소로 변환하는 과정이 완료돼야 합니다. 메인 메모리 주소에 접근하기 전에 캐시에 접근해 메모리 주소가 있는 지 체크하기 때문입니다. 이 같은 과정을 캐시 룩업이라고 합니다.


이번에는 다음 그림을 통해 가상 주소와 물리 주소 공간이 어떻게 구성됐는지 알아봅시다. 그리고 가상 주소를 물리 주소로 변환하는 변환 테이블에 대해서도 배워 봅시다.

 

 

 

그림 19.5 소프트웨어 관점에서 가상 주소 공간과 물리 주소 공간

 

그림의 왼쪽 부분부터 봅시다. 왼쪽 부분은 소프트웨어 관점에서 가상 주소 공간을 나타냅니다. 가상 주소에는 각각 영역별로 특정 속성을 지닌 코드와 데이터가 존재합니다. 소프트웨어를 실행하는 주인공인 프로세스는 가상 주소 공간에서 실행됩니다.

 

프로세스가 가상 주소 공간에서 어떤 명령어를 실행하면 메모리에 위치한 변환 테이블에 접근합니다. 이는 그림의 가운데 부분에 해당됩니다. 변환 테이블에 의해 가상 주소는 물리 주소 공간에 접근합니다.

 

여기서 가운데 존재하는 '변환 테이블' 동적으로 업데이트되며, 운영체제에서 실행되는 프로세스마다 각자의 '변환 테이블' 갖게 됩니다. 프로세스마다 가상 주소를 물리 주소로 변환할 있는 '변환 테이블' 있다는 것은 각각 프로세스마다 독립적인 가상 주소 공간에서 실행될 있다고 있습니다.

 

이렇게 해서 전체 그림을 보면서 운영체제와 메모리 구조 관점에서 메모리 매니지먼트가 무엇인지 알아봤습니다. 가상 주소와 물리 주소 공간이 존재할 있게 Arm 아키텍처에서는 MMU 제공하며, 이를 활용해 가상 메모리를 관리합니다. 이어지는 절에서 MMU 대해 자세히 알아봅시다.

리눅스를 비롯한 대부분의 운영체제에서는 가상 메모리를 메모리 관리 기법으로 활용합니다. 이번에는 가상 메모리와 물리 주소에 대해 알아보고 가상 메모리 기법이 적용된 이유를 알아봅시다.

대부분의 운영체제는 다양한 메모리(DRAM) 상에서 실행될 수 있는데, 다음 그림은 가상 메모리 기법을 적용하기 전의 시스템 구조도입니다.
 

그림 19.2 물리 메모리와 메모리 시스템의 관계

물리 메모리가 '물리 메모리 A' ~ '물리 메모리 D'까지 있습니다. 만약 소프트웨어 개발자가 물리 메모리 타입에 따라 주소 오프셋을 변경하는 설정을 하거나 추가로 물리 메모리와 관련된 설정을 하면 소프트웨어의 복잡도가 많이 늘어날 수 있습니다. 물리 메모리에 대한 예외 상황을 점검해야 하니 골치가 아플 것입니다.

하드웨어 측면에서는 다양한 메모리 공급사가 있으며, 대표적인 업체로 삼성전자, 하이닉스, 도시바를 예로 들 수 있습니다. 또한 SoC 업체(브로드컴, 인텔, 퀄컴)별로 서로 다른 물리 메모리 맵을 구성하며, 리눅스 커널와 같은 운영체제 커널이 실행하는 물리 메모리의 주소가 다릅니다. 다음 그림에서 Soc A는 0x8000_0000 물리 주소에 '리눅스 커널' 이미지를 실행하고, SoC B는 0x4000_0000 물리 주소에서 '리눅스 커널' 이미지를 실행합니다.   

 

그림 19.3 SoC 벤더별 메모리 맵

그런데 만약 프로세스 입장에서 물리 메모리별로 서로 다른 주소에 접근한다면 시스템 복잡도가 높아질 것입니다. 물리 메모리별로 무엇인가 따로 설정을 해야 하기 때문입니다. 그런데 가상 메모리 기법을 적용하면 시스템의 전체 구조를 다음과 같이 그릴 수 있습니다. 

 

그림 19.4 가상 메모리 기법을 적용한 메모리 시스템의 전체 구조

시스템 소프트웨어 개발자 입장에서는 가상 메모리 범위 내의 가상 주소 처리에만 신경 쓰면 됩니다. 시스템에 어떤 물리 메모리를 탑재했는지 걱정할 필요가 없습니다.

+ Recent posts