✨가상 메모리 시스템 초기화
paging_init() 함수는 PintOS의 커널 페이징 시스템을 초기화 하는 역할을 한다.
간단하게 paging_init() 함수에 대해서 알아보자.
- paging_init()
- 물리 메모리 영역 전체 (0 ~ mem_end)를 커널의 가상 메모리 주소 공간(KERN_BASE ~KERN_BASE + mem_end)에 1대1 매핑
- 커널 텍스트 영역 (start ~ _end_kernel_text) 까지를 읽기 전용으로 설정하여 보호
static void paging_init(uint64_t mem_end) {
uint64_t *pml4, *pte;
int perm;
pml4 = base_pml4 = palloc_get_page(PAL_ASSERT | PAL_ZERO);
extern char start, _end_kernel_text;
// Maps physical address [0 ~ mem_end] to
// [LOADER_KERN_BASE ~ LOADER_KERN_BASE + mem_end].
// 물리 주소 0 ~ mem_end를 커널 가상 주소 공간으로 매핑
for (uint64_t pa = 0; pa < mem_end; pa += PGSIZE) {
uint64_t va = (uint64_t)ptov(pa);
perm = PTE_P | PTE_W;
if ((uint64_t)&start <= va && va < (uint64_t)&_end_kernel_text)
// 커널 텍스트 영역은 읽기 전용 나머지는 읽기/쓰기 가능
perm &= ~PTE_W;
if ((pte = pml4e_walk(pml4, va, 1)) != NULL)
*pte = pa | perm;
}
// reload cr3
pml4_activate(0);
}
- 먼저 PML4 테이블을 위한 페이지 할당 (palloc_get_page)하고 페이지를 zero로 초기화
- 반복문을 통해 전체 물리 주소를 커널 가상 주소로 매핑 (페이지 크기 단위)
- 기본적으로 페이지 권한은 PTE_P (페이지 존재) 와 PTE_W(읽고,쓰기 가능)으로 설정
- 커널 텍스트 영역 (start ~ _end_kernel_text)은 쓰기 권한 제거하여 읽기 전용 설정
- pml4e_walk으로 VA에 해당하는 페이지 테이블 계층 생성 및 탐색
- PTE 생성되면 물리 주소 PA와 설정된 권한을 PTE에 저장
- pte는 포인터로 페이지 테이블 엔트리를 가르킴
- *pte는 실제 메모리 위치의 값
- pa = 0x1000 / perm = PTE_P | PTE_W ( PTE_P =0x1, PTE_W=0x2)
- pa | perm = 0x1000 | 0x3 = 0x 1003이 됨
- 위와 같은 예시의 경우 0x1003이 PTE에 저장
- 페이지 테이블 활성화 (CPU의 CR3 레지스터에 로드하여 페이징 시스템 활성화)
궁금점 ❗
여기에서 물리 메모리 영역 전체 (0 ~ mem_end)을
커널의 가상 메모리 주소 공간(KERN_BASE ~KERN_BASE + mem_end)에
1대1 매핑한다는게 너무나 이해가 되지 않았다.
KERN_BASE는 커널의 시작점인데, 여기에서 전체 물리 메모리 주소를 1대1 매핑한다고 ... ??
왜 1대1 매핑을 하고, 이게 의미가 있는건가 ..?
커널이 물리 메모리를 관리하기 위해 물리 메모리 전체 부분을 커널 가상 주소 공간에 매핑한다고 ...?
도대체 이게 뭔 소리인지를 한번 알아보자 .
주의 해야 할 것은 실제 load 하려는 elf 파일의 코드,데이터,bss와는 커널 영역과 크게 관련 없다는 것 !!
이 부분을 잘 못 생각해서 애먹음 (어찌 보면 당연한 소리)
아래와 같은 구조를 생각하면 좀 더 이해하기 쉬울거라 생각한다.
┌───────────────────────────────┐
│ 물리 메모리 매핑 │ ← `LOADER_KERN_BASE + 0x0`
│ (PA: 0x0 ~ mem_end) │
│ │
├───────────────────────────────┤
│ 커널 데이터/동적 메모리 │
├───────────────────────────────┤
│ 커널 코드 (읽기 전용) │ ← `start ~ _end_kernel_text`
└───────────────────────────────┘
커널 영역에 들어가는 것
- 커널 자체 (code,data,bss)
- 물리 메모리 전체 매핑
- 커널 스택 (각 스레드별 스택)
- 페이지 테이블 및 관리 데이터
- I/O 장치 메모리 매핑
커널 자체 (code,data,bss)에 code 영역에 우리가 pintos로 짠 함수들이 들어가는 것이다.
뭐 어렵게 생각할 필요 없다. 이게 전부인 것이다.
이렇게 하는 이유는 물리 메모리는 시스템의 자원이 제한된 자원으로, 이를 전체적으로 추적하고 관리해야 한다.
사용자 프로그램이 물리 메모리를 직접 접근하지 않고, 커널을 통해서만 요청하도록 설계 되어야한다.
ex) 메모리 할당하려면 malloc, 커널이 물리 메모리에서 필요한 페이지 할당
paging_init은 커널 영역을 초기화 해준 것이고 이후 사용자 가상 주소 공간 매핑은 별도로 이뤄진다.
load_sement나 setup_stack과 같은 함수에서 사용자 가상 주소와 물리 메모리 매핑을 설정한다.
아래의 간단한 예시로 좀 더 쉽게 이해해보자
페이지 테이블 설정
1. 프로세스 A의 페이지 테이블
사용자 공간 (0x00000000 ~ LOADER_KERN_BASE):
프로세스 A의 사용자 메모리를 매핑.
예: 0x00000000 → 물리 주소 0x100000 (사용자 코드).
예: 0x00100000 → 물리 주소 0x200000 (사용자 데이터).
커널 공간 (LOADER_KERN_BASE ~ LOADER_KERN_BASE + mem_end):
물리 메모리를 선형 매핑.
LOADER_KERN_BASE + 0x0 → 물리 주소 0x0.
LOADER_KERN_BASE + 0x1000 → 물리 주소 0x1000.
2. 프로세스 B의 페이지 테이블
사용자 공간 (0x00000000 ~ LOADER_KERN_BASE):
프로세스 B의 사용자 메모리를 매핑.
예: 0x00000000 → 물리 주소 0x300000 (사용자 코드).
예: 0x00100000 → 물리 주소 0x400000 (사용자 데이터).
커널 공간 (LOADER_KERN_BASE ~ LOADER_KERN_BASE + mem_end):
물리 메모리를 선형 매핑 (프로세스 A와 동일).
LOADER_KERN_BASE + 0x0 → 물리 주소 0x0.
LOADER_KERN_BASE + 0x1000 → 물리 주소 0x1000.
구체적인 매핑 결과
- 프로세스 A
가상 주소 물리 주소 설명
0x00000000 0x00100000 사용자 코드
0x00100000 0x00200000 사용자 데이터
LOADER_KERN_BASE + 0x0 0x00000000 커널 메모리 (공유)
LOADER_KERN_BASE + 0x1000 0x00001000 커널 메모리 (공유)
LOADER_KERN_BASE + 0x2000 0x00002000 커널 메모리 (공유)
- 프로세스 B
가상 주소 물리 주소 설명
0x00000000 0x00300000 사용자 코드
0x00100000 0x00400000 사용자 데이터
LOADER_KERN_BASE + 0x0 0x00000000 커널 메모리 (공유)
LOADER_KERN_BASE + 0x1000 0x00001000 커널 메모리 (공유)
LOADER_KERN_BASE + 0x2000 0x00002000 커널 메모리 (공유)
공유 매핑의 핵심
- 사용자 공간 (0x00000000 ~ LOADER_KERN_BASE):
- 사용자 코드와 데이터는 각 프로세스마다 고유한 물리 메모리를 매핑.
- 프로세스 A와 B는 서로 다른 물리 주소를 참조.
- 커널 공간 (LOADER_KERN_BASE ~ LOADER_KERN_BASE + mem_end):
- 모든 프로세스에서 동일한 가상 주소와 물리 주소를 매핑.
- 커널 코드와 데이터는 물리 메모리에서 공유.
결론
- 커널 영역 매핑:
- 모든 프로세스에서 동일하게 유지되며, 물리 메모리를 공유.
- 사용자 영역 매핑:
- 프로세스마다 독립적.
- 결과적으로:
- 커널은 모든 프로세스가 공유하며, 사용자 영역은 프로세스마다 고유한 메모리를 참조.
+ code, data, bss에 들어가는 내용 (추가 내용 )
(1) code 영역 (.text)
- 내용:
- 커널의 실행 코드(함수)
- 시스템 초기화 코드(예: paging_init)
- 시스템 콜 처리 코드, 스케줄러, 메모리 관리 로직 등
- 특징:
- 읽기 전용
- CPU가 직접 실행
(2) data 영역 (.data)
- 내용:
- 초기화된 전역 변수와 정적 변수
- 예: 커널 초기화 상태 플래그, 페이지 테이블 포인터 등
- 특징:
- 읽기/쓰기 가능
- 컴파일 시 값이 미리 정의
(3) bss 영역 (.bss)
- 내용:
- 초기화되지 않은 전역 변수와 정적 변수
- 예: 디스크 I/O 버퍼, 메모리 관리 구조체 초기 공간
- 특징:
- 런타임에 0으로 초기화
그래서 하고 싶은 말 !
Pintos 프로젝트에서 작성한 함수들은 모두 커널 영역의 code(텍스트 섹션)로 들어간다 !!!
✨가상 메모리 시스템 SET
초기 커널 페이지를 만들었고, 이제 사용자 가상 주소를 커널 물리 주소에 매핑을 해야한다.
말이 좀 헷갈리는데, 가상 주소를 물리 메모리에 연결하는 것이다.
굳이 왜 "커널 물리 주소" 라고 했을까 ?
→ 물리 메모리는 커널이 전체적으로 관리하기 때문이다. 사용자 가상 주소를 물리 메모리에 매핑할 때는, 커널이 미리 확보해 둔 물리 메모리를 사용한다. (사용자가 직접 물리 메모리 제어 할 수 없고, 커널이 중간에 조정 필요)
(위에 했던 내용들이다.)
bool pml4_set_page(uint64_t *pml4, void *upage, void *kpage, bool rw) {
ASSERT(pg_ofs(upage) == 0);
ASSERT(pg_ofs(kpage) == 0);
ASSERT(is_user_vaddr(upage));
ASSERT(pml4 != base_pml4);
uint64_t *pte = pml4e_walk(pml4, (uint64_t)upage, 1);
if (pte)
*pte = vtop(kpage) | PTE_P | (rw ? PTE_W : 0) | PTE_U;
return pte != NULL;
}
- 기본 조건 확인
- upage, kpage가 4KB 단위로 페이지 정렬 되어있는지
- upage가 사용자 가상 주소 범위에 있는지
- base_pml4를 수정하지 않도록 (base_pml4는 pml4 베이스로 fork 뜰 때 이거를 copy함)
- PTE 생성
- PML4 → PDPT → PDT → PT 를 순회
- 동적으로 페이지 테이블 생성
- pte가 반환되며 upage(사용자 가상 주소)에 해당하는 PTE 포인터 가르킴
- PTE 메모리의 값 설정
- 페이지 존재하고, 쓰기 가능하고, 사용자 모드에서도 접근 가능
이것도 예시로 이해해보자
- `upage = 0x00400000` (가상 주소, 사용자 공간)
- `kpage = 0x9000` (물리 주소를 가진 커널 페이지)
- 페이지 테이블 탐색 또는 생성
현재 PML4 구조:
┌───────────────┬───────────────┬───────────────┬─────┬───────────────┐
│ PML4 엔트리 0 │ PML4 엔트리 1 │ ... │ ... │ PML4 엔트리 511 │
└───────────────┴───────────────┴───────────────┴─────┴───────────────┘
↑
가상 주소의 PML4 인덱스 (PML4(upage) = 0)
- `pml4e_walk`로 PML4 → PDPT → PDT → PT를 따라가며 엔트리를 탐색.
- 필요한 경우 새 페이지 테이블을 생성:
- PDPT, PDT, PT 테이블이 없으면 `palloc_get_page`로 생성 후 연결.
- PTE 생성
최종적으로 PT(PAGE TABLE)에 해당 가상 주소 엔트리를 찾음:
┌───────────────┬───────────────┬───────────────┬───────────────┬───────────┐
│ PT 엔트리 0 │ PT 엔트리 1 │ ... │ upage 엔트리 │ ... │
└───────────────┴───────────────┴───────────────┴───────────────┴───────────┘
↑
upage = 0x00400000의 PT 엔트리
PTE 설정:
- 물리 주소: `vtop(kpage) = 0x9000` (kpage의 물리 주소).
- 플래그:
- `PTE_P` (Present): 페이지가 메모리에 존재함.
- `PTE_W` (Writable): 쓰기 가능 여부.
- `PTE_U` (User): 사용자 모드에서 접근 가능.
PTE 업데이트 결과:
┌───────────────┬───────────────┬───────────────┬────────────────────┬───────────┐
│ PT 엔트리 0 │ PT 엔트리 1 │ ... │ 0x9000 | PTE_P | PTE_W | PTE_U │ ...
└───────────────┴───────────────┴───────────────┴────────────────────┴───────────┘
kpage가 이해가 이해가 잘 안되면 아래 내용을 참고해보자.
가상 주소 공간 (커널 영역)
┌──────────────────────────────┐
│ _start │
│ .text │ 커널 코드
│ .data │ 커널 데이터
│ .bss │ 커널 BSS
│ _end │
├──────────────────────────────┤
│ _end ~ _end + mem_end │ 동적 메모리 (kpage 영역)
│ │
│ │ 동적 페이지 할당(palloc_get_page)
├──────────────────────────────┤
│ 커널 스택 (Kernel Stacks) │
│ 스레드 1: 스택 (4KB) │ 각 스레드별로 고정된 크기 할당
│ 스레드 2: 스택 (4KB) │
│ 스레드 3: 스택 (4KB) │
├──────────────────────────────┤
│ 커널 공간 여유 영역 │ 미사용 영역
└──────────────────────────────┘
이것도 위의 커널 구조와 똑같은데 표현만 조금 다르게 한 것이다.
kpage를 사용하는 이유는 물리 메모리에 직접 접근하지 않고, 커널을 통해 관리하고 요청하도록 설계하기 위함
(아까 했던 내용이랑 같음)
- 보안
- 보안적인 측면에서 커널을 통해서만 요청되어 안전
- 추상화
- 물리 메모리 제한된 자원인데 커널이 할당과 해제 책임지면 물리 메모리 복잡성 알 필요 없음
- 효율적 자원 관리
- 물리 메모리 직접 접근은 자원 관리 비효율적이거나 충돌 방생
- 중복 할당 방지
- 가상 메모리와의 연결
- kpage를 통해 물리 메모리를 간접적으로 접근
'크래프톤 정글' 카테고리의 다른 글
Pintos Project3 - Virture Memory Test case 트러블 슈팅 (read-boundary) (0) | 2024.12.05 |
---|---|
Pintos Project3 - Virture Memory 키워드 (1) | 2024.12.02 |
Pintos Project3 - Page Fault Handler (Page Fault 전반적인 과정 이해하기) (0) | 2024.12.02 |
Pintos Project3 - VA와 디스크 데이터 매핑 과정 이해하기 (0) | 2024.12.01 |
Pintos Project2 - User Program 흐름 잡기 (0) | 2024.12.01 |