CS:APP 9장 9.7 사례 연구 : 인텔 코어 i7 / 리눅스 메모리 시스템 인텔 Core i7 프로세서는 다음과 같은 메모리 계층 구조를 가지고 있다. -L1 캐시 : 각 코어에 32kb의 데이터 캐시와 32kb의 명령어 캐시가 있으며, 8-way 세트 연관 방식이다. - 8 way 세트 연관 방식이란, 캐시가 여러 개의 세트로 나뉘어 있고, 각 세트마다 8개의 슬롯(라인)을 가질 수 있는 구조를 의미한다. 즉, 하나의 메모리 블록은 여러 캐시 라인 중 8개 중 어느 곳에나 저장될 수 있어 충돌 가능성을 줄이고, 캐시 히트율을 높인다. - L2 캐시 : 각 코어에 256kb의 통합 캐시가 있으면, 8-way 세트 연관 방식이다. - 마찬가지로, 동일한 메모리 주소가 L2 캐시에 저장될 수 있는 위치가 8개 있다는 의미이다. - L3 캐시 : 모든 코어가 공유하는 8MB 캐시로, 16-way 세트 연관 방식이다. - 16개의 라인 중 하나에 데이터를 배치할 수 있어, 더 많은 유연성과 높은 캐시 효율을 제공한다. -TLB(Translation Lookaside Buffer): 주소 변환을 빠르게 하기 위한 캐시로, L1과 L2 TLB가 존재한다. - 메모리 관리 장치(MMU) : 가상 주소를 물리 주소로 변환하는 하드웨어이다. --------------------------- Core i7은 4단계의 페이지 테이블을 사용하여 가상 주소를 물리 주소로 변환한다. -------------------- haswell 아키텍처는 64비트 주소 공간 지원이 가능함 CPU 내부 설계상 가상 주소 64비트, 물리 주소 64비트 모두 가능하지만 실제로 그렇게까지 사용하지는 않는다. 현실에서는 가상 주소는 48비트, 물리 주소는 52비트만 사용 - 가상 주소 48비트 -> 프로세스당 2^48 = 256TB의 가상 메모리 공간 제공 - 물리 주소 52비트 ->2^52 = 4PB(페타바이트)까지 실제 RAM 주서 지정가능 -> 현실에는 RAM이 그정도는 아니지만 확장을 대비한 것 * 상위 비트는 사용하지 않고 예약(reserved)만 해둔 상태 32비트 시스템도 여전히 지원 - 호환성을 위해 32비트 가상 주소, 물리 주소도 함께 지원한다. - 예전 소프트웨어, 드라이버, 운영체제 등을 위해 반드시 필요한 기능 *64비트를 다 안쓰는 이유 - 주소 공간이 너무 넓어지면 -> 페이지 테이블이 너무 커짐 - 성능 / 메모리 효율 문제로 필요할 때만 확장하는 게 현실적 - 현재 OS, 하드웨어는 대부분 48비트/52비트 수준이면 충분하다. ---- 프로세서 패키지 - CPU 칩 자체 안에 포함된 모든 구성요소들을 말한다. - 보통 하나의 CPU 소켓 안에 들어있는 모든 하드웨어 유닛을 뜻한다. 여기서 패키지는 CPU 본체 하나 안에 들어있는 모든것. 네 개의 코어 - 각각의 코어는 독립적인 명령어 실행 유닛이다. - 동시에 여러 스레드를 처리할 수 있어서 병렬성이 크게 향상된다. 모든 코어가 공유하는 크기가 큰 L3 캐시 - L1, L2 캐시는 각 코어 전용 - L3 캐시는 4개의 코어가 함께 공유 DDR3 메모리 컨트롤러 - DRAM(메인 메모리)는 CPU 외부에 연결되어 있다. - 이와 통신하기 위해 메모리 컨트롤러가 필요 - 예전에는 메인보드에 있었는데, 요즘은 CPU 패키지 안에 탑재되어 있다. *이로 인해 CPU <-> 메모리 간 지연을 줄이고 메모리 접근 속도 향상 +-------------------------------------+ | Core i7 Processor Package | | | | [Core 0] [Core 1] [Core 2] [Core 3] ◀ 각각 L1, L2 캐시 내장 | |__________________________| (독립적인 연산 유닛) | ↓ | [L3 캐시] ◀ 모든 코어가 공유 (8MB 등) | ↓ | [Memory Controller (DDR3)] ◀ DRAM과 직접 통신 +-------------------------------------+ ↓ [Main Memory (DRAM)] --------- TLB의 계층 구조 - TLB는 가상주소 -> 물리 주소 매핑을 빠르게 하기 위한 주소 변환 캐시 L1 TLB 빠르지만 작음(보통 수십 개 항목) L2 TLB 느리지만 큼 (수백 개 항목) -L1 -> L2 계층 구조로 속도와 용량을 동시에 확보 CPU가 메모리 주소를 찾으려고 할 때, 먼저 L1 TLB에서 매핑을 찾고, 없으면 L2 TLB를 확인 데이터와 인스트럭션 캐시의 계층 구조 L1 데이터 캐시 변수나 배열 등 데이터 값 저장 L1 인스트럭션 캐시 실행할 명령어 코드 저장 L2 캐시 L1을 보조하는 중간 캐시 L3 캐시 모든 코어가 공유하는 대형 캐시 각 코어는 L1 캐시 (Data + Instruction) 와 L2 캐시를 따로 가지고 있고, 모든 코어가 L3 캐시는 공유 QuickPath 다른 코어들과 빠르게 통신할 수 있는 전용 고속 링크 - 예전에는 FBS(Front Side Bus) 하나를 CPU, 메모리, I/O가 공유했지만 - QuickPath는 각 코어와 다른 컴포넌트(독립적인 단위 모듈)들이 직접 연결된다. 특징 - 포인트-투-포인트 -> 하나하나 직접 연결 - 더 빠른 데이터 전송 - 병목 현상 방지 외부 I/O 브리지 -CPU 바깥 세상(USB,SSD,GPU 등)과 통신하게 해주는 인터페이스(서로 다른 두 개의 시스템, 장치 사이에서 정보나 신호를 주고받는 경우의 접점이나 경계면) - 대표적 I/O 브리지 : PCIe(PCU Express) * SSD, 그래픽카드, 마우스, 네트워크 모두 여기 통해 연결 전체 구조 [ Core N ] ├─ L1 Instr Cache ├─ L1 Data Cache ├─ L2 Cache ├─ L1/L2 TLB ├─ QuickPath Link ───► [ 다른 코어 / 메모리 컨트롤러 ] └─ I/O Bridge ───────► [ SSD, GPU, USB 등 외부 장치 ] --------- TLB들은 가상 주소를 사용 - TLB는 CPU가 사용하는 가상 주소에 대해 빠르게 물리 주소를 찾기 위해 만는 변환 캐시이다. * TLB가 없다면 매번 페이지 테이블 4단계를 따라가야 하니 시간이 오래 걸린다. TLB는 4중 집합 결합성 (4-way) - TLB는 여러 개의 세트로 나뉘고, 각 세트마다 4개의 엔트리(줄)를 가질 수 있다. * 하나의 가상 주소가 매핑될 수 있는 위치가 4곳 중 하나 * 충돌 방지가 쉬워지고, 히트율이 높아짐 블록 크기는 64바이트 - 블록은 한번에 캐시에 올라가는 데이터 단위이다. - 메모리에서 데이터를 읽어올 떄는 64바이트씩 덩어리로 가져온다. L1 / L2 8-way, 한 세트에 8줄이 있어 저장 위치가 8곳 L3 16-way, 더 많은 위치에 데이터 저장 가능 (유연성↑, 히트율↑) 페이지 크기는 4kb 또는 4mb - 가상 메모리 시스템의 기본 단위인 페이지는 시스템 부팅 시 두 가지 중 하나로 선택된다. 4kb, 기본 설정(일반적인 경우) 4mb (Huge page 사용)성능 중시 환경(예: DB, 게임엔진)에서 사용 Huge Page - 일반적으로 가상 메모리는 4kb 크기의 페이지 단위로 나눈다. - 시스템이 많은 메모리를 쓸수록 - 페이지 수가 많이지고 - 페이지 테이블도 거대해지고 - TLB 캐시도 자주 밀려나면서 성능 저하가 생긴다. *한 페이지를 4MB 같이 크게 만들어서 관리할 페이지 수를 줄이는 개념이다. ✅ TLB 효율 ↑ 한 번의 TLB 엔트리로 더 많은 메모리 영역을 커버할 수 있음 → TLB miss 줄어듦 ✅ 페이지 테이블 크기 ↓ 전체 페이지 수가 줄어들기 때문에 페이지 테이블 공간 절약 ✅ 성능 향상 TLB miss 줄고 페이지 테이블 접근 줄어들어 → 메모리 접근 성능 개선 ✅ CPU 부하 감소 페이지 폴트나 주소 변환 관련 오버헤드 줄어듦 ❌ 메모리 낭비 가능 ↑ 프로그램이 4MB보다 적은 메모리만 써도, 4MB 단위로 잡혀버림 → 내부 단편화 생김 ❌ 관리 유연성 ↓ 더 큰 단위로 메모리를 할당하니 세밀한 메모리 제어가 어려움 ❌ 일부 시스템에서 설정 복잡 리눅스에서는 HugeTLBfs, transparent huge page 설정 필요 ❌ 프로세스가 많은 경우 효과 제한 공유 메모리 상황 등에서는 기대만큼 성능 향상이 안 나타날 수도 있음 쉬운 비유 4KB 페이지: 📚 얇은 노트를 수천 권으로 정리 4MB Huge Page: 📕 두꺼운 책 몇 권만으로 정리 ➡️ 페이지 수를 줄이면 관리도 쉬워지고, 찾기도 빠르지만 ➡️ 낭비될 수 있는 공간도 많아질 수 있다 대용량 메모리를 쓰는 DB 서버, 머신러닝 시스템, 게임 엔진에서는 Huge Page 설정 시 성능이 확 올라간다. ----- Core i 7 프로세서에서의 주소 변환 과정 요약 1. 가상 주소 생성 : CPU는 프로그램 실행 중에 가상 주소를 생성한다. ???? CPU의 가상 주소 생성이 이해가 안가서 찾아봄 - CPU가 프로그램의 변수나 데이터에 접근할 때 그 위치를 가상 주소로 표현한다 - 컴파일된 실행 파일 안에는 가상 주소에 대한 힌트는 있지만 실제로 언제 어디에 로딩될지는 실행 시점에 운영체제가 정한다. - 즉 컴파일 시에는 이 함수는 어디 위치할지 계획만 있을 뿐 실제 그 주소가 사용될지는 OS가 결정한다. 가상 주소 공간 구성 ✅ 운영체제(OS) 코드, 힙, 스택, 라이브러리 등 각각 어디에 배치할지 결정 메모리 매핑 (페이지 테이블 생성) ✅ 운영체제(OS) 가상 주소 ↔ 물리 주소 매핑 정의 가상 주소 사용 ✅ 프로그램(유저 코드) malloc, 변수 접근 등으로 가상 주소를 사용 주소 변환 실행 ✅ CPU(MMU + TLB) OS가 만든 매핑을 기반으로 실제 주소로 변환하고 접근 OS: 가상 주소를 구성하고 매핑 테이블(page table)을 만든다. CPU: 그 가상 주소를 받아서, TLB/페이지 테이블을 통해 물리 주소로 변환하고 메모리에 접근한다. 2. TLB 조회 : TLB는 최근의 가상 주소와 물리 주소의 매핑을 저장하는 캐시이다. CPU는 먼저 TLB를 조회해 해당 가상 주소의 물리 주소 매핑이 있는지 확인. 3. TLB 미스 시 페이지 테이블 조회 : TLB에 해당 패핑이 없으면, CPU는 페이지 테이블을 순차적으로 조회하여 가상 주소를 물리주소로 변환한다. 이 과정은 일반적으로 4단계 페이지 테이블을 커진다. 4. 물리 주소 획득 및 메모리 접근 : 페이지 테이블을 통해 물리 주소를 획득한 후, CPU는 해당 물리 주소를 사용해 실제 메모리에 접근한다. 5. 캐시 시스템 활용 : L1, L2, L3 캐시를 사용해 메모리 접근 속도를 향상시킨다. 이 캐시들은 각각 8-way, 8-way, 16- way 집합 결합성을 가지며, 블로그이 크기는 64바이트이다. ------ 이론적으로는 코어 i7 아키텍처가 페이지 테이블들을 스왑 가능하게 할지라도 실제로 페이지 테이블 자체는 항상 메모리에 상주 시켜둔다. 리눅스는 페이지 테이블은 절대 디스크로 내보내지 않으며, 실행 중인 프로세스의 페이지 테이블은 항상 RAM에 상주한다. +++[ RAM ] (RAM의 종류에 두가지가 있다) ├── DRAM ← 메인 메모리 (리눅스에서 free -h 치면 나오는 그거) └── SRAM ← CPU 캐시 (속도 빠름, 용량 작음) ------ CR3 레지스터는, 현재 활성화된 프로세스의 '최상위 페이지 테이블'(PML4)의 시작 물리 주소를 저장하고 있는 곳이다. - PML4는 64비트 시스템의 4단계 주소 변환 체계에서 "최상위 레벨(레벨 4)"에 해당하는 페이지 테이블이다. 전체 페이지 테이블 구조 요약 - 64비트 주소를 사용하지만, 실제로는 상위 48비트만 사용되고, 이걸 4단계로 나눠서 주소를 물리 주소로 변환한다. Level 4 PML4 상위 9비트 CR3가 가리키는 테이블 Level 3 PDPT 다음 9비트 PML4 엔트리 하나가 가리킴 Level 2 PD 다음 9비트 Level 1 PT 다음 9비트 - Page offset 마지막 12비트 페이지 내 실제 위치 ➡️ 총 9 + 9 + 9 + 9 + 12 = 48비트 주소 구조! PML4 → PDPT → PD → PT → 페이지 프레임 ✔ 이건 트리 구조처럼 동작한다. ✔ 가상 주소는 이 구조를 한 단계씩 따라가며 최종 물리 주소에 도달한다. 어떤 가상주소 0x7f1234567890 이 있다고 가정 1. 이 주소의 상위 9비트는 PML4 테이블의 인덱스이다. - 이 인덱스를 이용해 PML4 테이블의 한 엔트리를 선택 2. 그 엔트리는 PDPT의 물리 주소를 가리킨다. 3. 그 PDPT 테이블의 또 다른 9인덱스를 사용해서 PDPT의 엔트리 중 하나를 고른다. 4. 이 과정을 반복 - PDPT → PD → PT (각각 인덱스를 통해 하위 테이블 선택) 5. 마지막으로 PT 테이블의 엔트리는 실제 물리 페이지 프레임의 시작 주소를 준다 6. 가상 주소의 하위 12비트(page offset)는 그 물리 페이지 프레임 내에서 몇 번째 바이트인지를 알려준다. 가상 주소 (48비트) ├─ [PML4 index] ← 상위 9비트 → PML4 테이블에서 1개 선택 ├─ [PDPT index] ← 다음 9비트 → PDPT 테이블에서 1개 선택 ├─ [PD index] ← 다음 9비트 → PD 테이블에서 1개 선택 ├─ [PT index] ← 다음 9비트 → PT 테이블에서 1개 선택 └─ [Page offset] ← 하위 12비트 → 페이지 내 위치 각 가상 주소에서 사용되는 9비트는 그 단계의 테이블에서 몇 번째 엔트리를 선택할지를 지정하는 인덱스이다. ✔️ 즉, 테이블 전체가 9비트로 만들어지는 게 아니라, 가상 주소에서 9비트를 잘라서 테이블의 엔트리를 선택한다는 뜻 9비트 인덱스 → 2⁹ = 512개 ✔️ 그러니까 각 테이블(PML4, PDPT, PD, PT) 은 512개의 엔트리를 갖는 배열이라고 보면 됩니다. [ 가상 주소 ] ├─ PML4 인덱스 → [PML4 테이블] ─▶ PDPT 주소 ├─ PDPT 인덱스 → [PDPT 테이블] ─▶ PD 주소 ├─ PD 인덱스 → [PD 테이블] ─▶ PT 주소 ├─ PT 인덱스 → [PT 테이블] ─▶ 페이지 프레임 주소 └─ Page offset → 물리 페이지 내 바이트 위치 ▶ 최종 물리 주소 = 페이지 프레임 시작 주소 + offset 페이지 테이블의 내용을 알고있다면 가상 주소가 매핑되는 실제 물리 주소를 계산할 수 있다. 이 부분이 가상 메모리 시스템의 핵심 원리이자, 운영체제, 커널 디버깅, 메모리 포렌식의 기반이다. 가상 주소 -> 물리 주소 변환은 순전히 테이블에 저장된 값들을 따라가는 과정이다. ✔ 가상 주소 = (PML4 인덱스, PDPT 인덱스, PD 인덱스, PT 인덱스, 오프셋) ✔ 페이지 테이블은 각각 다음 단계의 “물리 주소”를 담고 있음 --- CR3는 페이지 테이블의 주소를 담고 있다. - 즉 , 이 프로세스의 가상 주소를 물리 주소로 바꾸려면 어떤 테이블을 참조해야하는 가 - 그 출발점을 담고 있는 게 바로 CR3 레지스터 문맥 전환이란 - 운영체제가 현재 CPU에서 실행되는 프로세스를 다른 프로세스로 바꾸는 것 ➡️ 이때 운영체제는 무엇을 바꿔야 할까요? ✔️ CPU 레지스터 상태 ✔️ 스택 포인터, 프로그램 카운터 ✔️ CR3 레지스터 (페이지 테이블 위치) ← 바로 여기! 🔄 문맥 전환 시 실제로 일어나는 일 1. 현재 프로세스의 CR3 값을 저장 2. 새 프로세스의 CR3 값을 로드 3. 그에 따라 가상 주소 → 물리 주소 매핑도 변경 📘 왜 중요한가? 각 프로세스는 자기만의 가상 주소 공간을 가짐 CR3를 바꾸는 순간, CPU는 전혀 다른 가상 주소 맵을 갖게 됨 이게 바로 프로세스 간 메모리 보호와 격리의 핵심! --- 핵심 키워드 정리 P (Present bit) 이 페이지 또는 테이블이 실제로 메모리에 존재함을 나타냄 (1이면 메모리에 있음) PPN (Physical Page Number) 물리 페이지 프레임의 번호 (실제 RAM 내 위치) 40비트 x86-64 시스템에서 보통 물리 주소는 상위 40~52비트만 사용됨 페이지 테이블 엔트리 구조 (64비트 기준 예) 비트 역할 [0] P (Present) ← 1이면 이 엔트리는 유효하고, RAM에 있음 [1~11] 권한 정보 (RW, UX, accessed, dirty 등) [12~51] PPN (물리 페이지 번호) ← 바로 이게 RAM 위치! [52~63] 사용 안 함 또는 예약 📘 리눅스에서 “P=1은 항상 성립한다”는 건? 리눅스는 실행 중에 페이지 테이블을 스왑 아웃하지 않기 때문에, 페이지 테이블을 구성하는 모든 엔트리들은 항상 P=1, 즉 메모리에 상주합니다. 📌 그래서 CPU는 페이지 테이블 구조를 신뢰하고, 엔트리를 따라가면서 주소 변환을 진행할 수 있어요. ✔ 모든 단계(PML4, PDPT, PD, PT) 의 페이지 테이블은 ✔ 각각 512개 엔트리를 가지며, ✔ 각 엔트리는 64비트 크기고, ✔ 그 구조는 그림 9.23에서 말한 PPN 포맷 그대로예요. ✔ 각 테이블은 바로 다음 단계 페이지 테이블의 시작 물리 주소를 담고 있어요. ✔ 그리고 그 주소는 64비트 중 40비트가 PPN (페이지 프레임 번호) 로 저장되어 있고, ✔ 하위 12비트는 페이지 정렬 때문에 항상 0이라서 저장 안 해도 됨. ----- 이것은 페이지 테이블에 4KB 정렬 요구사항을 강요하고 있다는 점 !!!!!왜 페이지 테이블은 4kb인가? ✅ 1. 메모리 단편화를 줄이기 위해 (낭비 최소화) 페이지 크기가 너무 작으면 → 페이지 테이블이 너무 커짐 (엔트리 수 ↑) 페이지 크기가 너무 크면 → 메모리 낭비 많아짐 (내부 단편화 ↑) 📌 4KB는 이 둘 사이에서 아주 절묘한 밸런스예요. ✅ 3. 페이지 테이블 크기를 적당하게 유지 가능 64비트 주소 공간에서 4KB 페이지를 쓰면 한 프로세스는 최대 256TB의 가상 주소 공간을 사용 각 페이지 테이블에는 512개 엔트리 (2⁹) 다단계 구조로 효율적 관리 가능 ➡️ 페이지 크기를 키우면 페이지 수는 줄지만, ➡️ 너무 크면 작은 데이터도 큰 페이지에 담겨서 낭비 심해짐 [ 가상 주소 (48비트) ] ├─ PML4 index (9비트) ─▶ PML4 테이블 (512칸) → PDPT 주소 ├─ PDPT index (9비트) ─▶ PDPT 테이블 (512칸) → PD 주소 ├─ PD index (9비트) ─▶ PD 테이블 (512칸) → PT 주소 ├─ PT index (9비트) ─▶ PT 테이블 (512칸) → 페이지 시작 주소 └─ Page offset (12비트) → 실제 바이트 위치 📘 그럼 이 구조는 어떤 느낌인가요? 마치 “4층짜리 주소 지도”를 따라가는 탐험 같아요! PML4: 제일 상단 지도 → 대륙 수준 PDPT: 국가 지도 PD: 도시 지도 PT: 거리 지도 Page offset: 정확한 집 주소 ➡️ 하나하나 좁혀가면서 최종적으로 RAM 상의 정확한 위치에 도달합니다. --- ✅ 3. XD 비트 (Execute Disable) XD = 1 → 실행 금지 (eXecute Disable) XD = 0 → 실행 허용 🧠 이것은 64비트 시스템에서 새로 추가된 보안 기능이에요! 📌 목적: 메모리에 존재하지만 코드가 아닌 영역에서는 인스트럭션 실행을 막기 위해 사용 예: 스택, 힙 → 실행하면 안 되니까 XD=1 🧨 이를 통해 버퍼 오버플로우 공격 방지 가능! 공격자가 스택에 악성 코드를 삽입해 실행하려 해도 → 실행 ❌ ------ ✅ x86-64 주소 변환 계층 구조: CR3 → PML4 → PDPT → PD → PT → 데이터 페이지 ✔ 이 모든 테이블이 페이지 테이블 엔트리(PTE) 라고 부르는 64비트 엔트리들의 집합입니다. ✔ 각 단계의 엔트리에도 R/W, U/S, XD 등의 접근 권한 비트가 존재합니다! 🧠 핵심 포인트 접근 권한 비트는 각 단계마다 있으므로, 굳이 최종 단계(PT)까지 안 내려가도, 중간 단계에서 접근 불가를 판단하고 탐색을 중단할 수 있어요! 🎯 예를 들어볼게요 가상 주소 접근 시… CPU가 PML4 테이블의 엔트리를 봤는데, U/S = 0 (Supervisor-only) 현재는 User 모드 👉 ❌ 접근 차단 → 바로 페이지 폴트! 📌 더 아래 (PDPT, PD, PT, 페이지...) 내려갈 필요도 없음 → 비용 절약! --- ✅1. Accessed Bit (참조 비트) MMU는 어떤 가상 주소가 읽히거나 쓰일 때, 해당 페이지의 Accessed 비트를 1로 설정합니다. 운영체제는 이걸 보고: 최근에 자주 쓰인 페이지인가? 안 쓰인 페이지는 교체해도 되겠네! 📌 이 비트는 페이지 교체 알고리즘 (예: LRU, Clock) 에 사용됩니다. ✅ 2. Dirty Bit (D비트) 페이지가 쓰기(write) 되었을 때 MMU가 이 비트를 1로 설정합니다. 즉, 이 페이지는 메모리에서 바뀌었으므로, 나중에 교체될 때 → 디스크에 다시 써줘야 한다(write-back) 📌 쓰기 안 된 페이지는 그대로 버려도 되지만, 📌 Dirty 페이지는 버리면 안 됨! → 디스크에 복사 후 삭제 📂 실제 동작 흐름 예시 어떤 페이지가 메모리에 로딩됨 (Accessed = 0, Dirty = 0) CPU가 해당 페이지를 읽음 → Accessed = 1 CPU가 해당 페이지에 씀 → Dirty = 1 나중에 메모리 부족으로 페이지 교체 필요 커널이 Dirty = 1인 걸 확인 → 디스크로 write-back → 교체 ---- 가상 주소 → 물리 주소 → 캐시 접근의 흐름에서, 실제 하드웨어(MMU + 캐시) 가 얼마나 효율적으로 병렬로 처리하는지 설명 순차적인 접근 전통적인 방식(느림) 가상 주소를 페이지 테이블 통해 물리 주소로 변환 변환된 물리 주소로 L1 캐시 접근 거기서 데이터 로드 ➡️ 이건 완전 "한 단계씩 처리" 방식 → 느려요 ✅ 실제 하드웨어는? 가상 주소의 일부는 캐시 인덱스에 바로 사용 가능하니까, 주소 번역이 끝나기 전에도 L1 캐시 탐색을 시작할 수 있어요! 💡 이걸 **“가상 주소의 부분적 사용 (주소 중첩 접근)”**이라고 합니다. 가상 주소 = [페이지 번호 | 페이지 오프셋] (예: 48비트 주소에서 아래 12비트는 오프셋) 페이지 오프셋 = 물리 주소의 오프셋과 동일 ➡️ 이 오프셋 부분을 L1 캐시의 인덱싱에 바로 사용 가능! 가상 주소 ├── [상위 비트: 페이지 번호] → 페이지 테이블 조회 (물리 주소 계산) └── [하위 비트: 페이지 오프셋] → L1 캐시 인덱싱 시작 ⏱ 이 둘을 "동시에" 시작하니까 전체 속도 향상! ⏱ ---- [ 가상 주소 (48비트) ] ┌──────────────┬────────────┬────────────┬────────────┬──────────────┐ │ PML4 index │ PDPT index │ PD index │ PT index │ Page offset │ │ (9비트) │ (9비트) │ (9비트) │ (9비트) │ (12비트) │ └──────────────┴────────────┴────────────┴────────────┴──────────────┘ ▲ ▲ ▲ ▲ ▲ │ │ │ │ └→ 페이지 내부 위치 │ │ │ └→ PT 테이블에서 인덱스 │ │ └→ PD 테이블에서 인덱스 │ └→ PDPT 테이블에서 인덱스 └→ PML4 테이블에서 인덱스 [ 가상 주소 (VA) - 48비트 ] ├── PML4 index (9비트) ├── PDPT index (9비트) ├── PD index (9비트) ├── PT index (9비트) └── Page offset (12비트) ↓↓ 페이지 테이블을 타고 ↓↓ [ PTE 안의 40비트 물리 페이지 번호 (PPN) ] → 실제 DRAM에서 해당 페이지가 위치한 주소 ✔ “물리 페이지 번호(PPN)”는 실제 메모리에 존재하는 페이지의 번호이고, 그걸 페이지 테이블 엔트리(PTE)가 가리키는 거다.” VPN은 프로세스가 쓰는 논리적인 주소 공간의 페이지 번호이고, PPN은 실제 RAM 내의 물리적 페이지 번호입니다. 이 둘을 연결해주는 게 바로 페이지 테이블입니다. 페이지 테이블 엔트리(PTE)는 "가상 페이지 번호(VPN)는 이 PPN에 매핑된다" 라고 말해주는 역할을 해요. 페이지 테이블 = 여러 개의 페이지 테이블 엔트리를 가진 배열(또는 리스트) 📘 쉽게 비유하자면 "가상 주소(VPN)는 주소 책의 페이지 번호" → 주소 책을 따라가면 (PML4 → PDPT → ...) → 실제 집 주소(PPN)를 알려주는 카드(PTE)가 나옴 → 그리고 거기 있는 offset만큼 들어가면 정확한 집 위치 도착! -------- 9.7.2 리눅스 가상 메모리 시스템 1. 프로세스별 가상 주소 공간 리눅스는 각 프로세스마다 독립적인 가상 주소 공간을 제공함 즉, 프로세스 A와 B는 같은 가상 주소(0x400000)를 쓸 수 있지만, → 서로 다른 물리 주소에 매핑됨 2. 주소 공간 구역 나누기 리눅스는 프로세스의 가상 주소 공간을 다음과 같이 구분해요: 텍스트 영역 실행 중인 프로그램의 코드 (읽기 + 실행) 데이터 영역 전역변수, static 변수 (읽기 + 쓰기) 힙 영역 malloc 등 동적 메모리 (위로 확장) 스택 영역 함수 호출, 지역 변수 (아래로 확장) 공유 라이브러리 영역 .so 파일들이 매핑되는 공간 커널 영역 시스템 전용 공간 (사용자 접근 불가) 3. 페이지 테이블 구조 리눅스는 x86-64 구조에 따라 4단계 페이지 테이블 (PML4 → PDPT → PD → PT) 를 사용 가상 주소의 상위 36비트를 9비트씩 나누어 탐색 4. TLB와 캐시 활용 TLB (Translation Lookaside Buffer) 는 최근의 주소 변환 정보를 캐시함 주소 변환이 빠르게 이루어지고, 성능을 높임 5. 페이지 폴트 처리 가상 주소에 접근 시 해당 페이지가 메모리에 없다면 → 페이지 폴트 발생 커널은: 주소가 유효한지 확인 권한 체크 (읽기/쓰기/실행) 디스크에서 페이지 로드 페이지 테이블 갱신 명령 재실행 6. 프로세스 상태 정보 관리 (task_struct) 리눅스는 각 프로세스마다 task_struct라는 구조체를 유지함 여기에 가상 메모리 맵, 스택 포인터, 파일 디스크립터 등 상태 정보가 들어있다. ----- task_struct는 리눅스 커널이 프로세스 하나하나를 표현하기 위해 사용하는 “프로세스 제어 블록(PCB)”입니다. 운영체제가 프로세스를 관리하려면 각 프로세스의 다음과 같은 정보가 필요하겠죠? 현재 상태 (Running? Sleep?) 프로세스 ID 우선순위 CPU 사용 시간 메모리 맵 파일 핸들 부모/자식 관계 ✔️ 이런 모든 정보를 하나에 묶어놓은 구조체가 바로 task_struct 입니다! ---- 📘 mmap() — 메모리 매핑 시스템 호출 ✅ mmap이란? mmap()은 사용자 공간에서 파일이나 디바이스, 메모리 영역을 가상 주소 공간에 매핑하는 시스템 호출이에요. 📌 주요 기능 파일을 메모리에 직접 매핑해서 읽기/쓰기 익명 메모리(파일 아닌 공간) 할당 → malloc 없이 힙처럼 사용 가능 공유 메모리 생성 및 프로세스 간 공유 동적 라이브러리(.so 파일) 로딩에도 사용됨 📘 vm_area_struct — 가상 메모리 구역 구조체 리눅스 커널이 프로세스의 가상 주소 공간을 관리하기 위해 사용하는 자료구조입니다. 즉, **"이 주소 구간은 어떤 용도이고, 어떤 권한을 갖는가"**를 담고 있어요. struct vm_area_struct { unsigned long vm_start; // 시작 가상 주소 unsigned long vm_end; // 끝 가상 주소 unsigned long vm_flags; // 접근 권한 (읽기, 쓰기, 실행) struct file *vm_file; // 매핑된 파일 정보 (NULL이면 익명) struct mm_struct *vm_mm; // 이 영역이 속한 프로세스의 메모리 맵 struct vm_area_struct *vm_next; // 다음 영역 (연결 리스트) ... }; mmap과 vm_area_struct의 관계 프로세스가 mmap()을 호출하면 커널은 vm_area_struct를 하나 새로 만들어서 가상 주소 공간에 적절한 위치를 정해 매핑하고 해당 영역을 프로세스의 메모리 맵에 추가합니다 📌 이 메모리 맵은 프로세스마다 하나씩 존재하며, → task_struct → mm_struct → vm_area_struct 연결 리스트로 연결됨