이번 포스팅에서는 개념에 관한 정리보다는 프로젝트를 진행하면서 궁금했던 점을 위주로 정리해보려고 한다!
프로젝트 3을 진행하다 보면, anon page나 file page에서 swap in 함수로 load_segment 함수를 사용할 일이 생긴다. 이때 load를 할 주소 값을 user virtual address로 넘겨주는 경우와 kernel virtual address로 넘겨주는 경우의 차이가 발생한다.
이 차이가 왜 발생하는지, 또 user virtual address의 경우 kernel virtual address와의 mapping 정보를 page table에 추가해 주었기 때문에 참조가 가능한데, kernel virtual address는 어떻게 바로 참조가 가능한지에 대해 알아보았다.
User Virtual Address & Kernel Virtual Address
저번 포스팅에서도 언급했었지만, 다시 한번 간략하게 정리하고 넘어가려고 한다. 우선 user virtual address는 유저가 사용하는 가상 주소 공간을 말한다. 이 주소 공간은 모든 프로세스가 독립적으로 사용한다. 즉, 프로세스 A가 사용하는 0x1000 주소는 프로세스 B가 사용하는 0x1000 주소와 실제로 매핑되어 있는 물리 프레임이 다르다는 것이다! 이를 통해 프로세스 A에서 프로세스 B가 사용하는 주소인 0x1000에 접근한다고 해서 B가 사용하고 있는 데이터를 얻어오는 것이 불가능해진다. 커널에서는 이러한 방식으로 프로세스 간의 memory protection 기능을 제공하고 있다.
반면에 kernel virtual address는 실제 물리 프레임과 1대 1 매핑이 되어있는 가상 주소 공간을 말한다. 핀토스에서는 KERN_BASE를 기준으로 유저 가상 주소 공간과 커널 가상 주소 공간을 나누고 있으며, KERN_BASE 위의 주소들은 실제 물리 프레임과 1대 1로 매핑이 되어있다.
여기에서 1대 1로 매핑이 되어있다는 것이 무슨 의미일까? 핀토스가 실행될 때의 코드를 한번 살펴보자.
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].
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);
}
위의 코드를 보면, 주석에도 적혀있는 것처럼 커널의 가상 주소와 실제 물리 프레임 주소 간의 매핑을 진행한다. 이때 매핑 정보는 커널의 page table인 base_pml4에 추가되는 것을 볼 수 있다. 이 코드를 통해 우리는 커널 또한 page table을 가지고 있으며, 그 page table에는 kernel virtual address와 physical frame 간의 1대 1 매핑 정보가 담겨있다는 것을 알 수 있다.
그다음에는 유저 프로세스를 생성할 때, page table을 생성하는 부분을 살펴보자.
우리가 fork 또는 exec 시스템 콜을 호출해서 __do_fork 함수나 process_exec 내의 load 함수에서 pml4_create 함수가 호출된다. 이 함수는 새로운 page table을 생성하는 함수이다. 위의 코드에서 빨간색 네모 박스 부분을 살펴보면, base_pml4에서 값을 복사해 오는 부분을 확인할 수 있다. 이 말은 커널의 page table 정보를 유저 프로세스가 똑같이 복사해 온다는 것을 의미한다. 즉, 유저 프로세스 또한 처음 시작할 때부터 실제 물리 프레임과 1대 1로 매핑되어 있는 kernel virtual address의 정보가 page table에 담겨있다는 것을 의미한다. 이를 통해 위에서 언급했던 의문들이 해소될 수 있다.
먼저 CPU가 특정 주소값으로 접근할 때는 항상 page table을 거쳐간다. 이는 CPU가 받은 주소값이 실제 물리 프레임과 매핑되어 있는 주소인지, 아니면 page table을 통해 kernel virtual address를 얻어와야 하는 주소인지 알 수 없기 때문이다. 따라서 모든 주소에 접근할 때 항상 page table을 거쳐가고, 이에 따라 모든 프로세스들은 kernel virtual address가 물리 프레임과 매핑되어 있는 정보를 page table에 담을 필요가 있다. 이는 kernel mode에서 프로세스가 kernel virtual address에 접근해야 하는 상황이 발생할 수 있기 때문이다.
따라서 우리가 user virtual address에 write 작업을 하면, user virtual address에 해당하는 page table entry의 dirty bit이 활성화되고, kernel virtual address에 write 작업을 하면 kernel virtual address에 해당하는 page table entry의 dirty bit이 활성화되는 것이다. 이를 통해 user virtual address에 해당하는 page table entry의 dirty bit을 활성화시키지 않으면서, 페이지에 값을 쓰는 것이 가능해진다. 이러한 상황은 페이지에 초기 내용을 load 하는 과정에서 발생한다. load 과정에서는 파일을 실행하기 위한 정보가 쓰이는 것이므로, 엄밀히 따지면 user의 write 작업이라고 보기 힘들다. 따라서 load 과정에서 dirty bit이 활성화되는 것은 적절하지 않으며, 우리는 kernel virtual address를 통해 page에 값을 써주어 user page entry의 dirty bit이 활성화되는 것을 방지하였다.
다음 내용을 이어가기에 앞서, virtual address의 구조를 조금 살펴보고 가자. 상위 비트 값들은 페이지 테이블에 접근하기 위한 offset이나 pointer인데, 이와 관련한 내용들은 docs의 introduction에도 잘 나와있고, 잘 정리해 둔 글들도 많기 때문에 이번 포스팅에서 주로 다루는 내용인 physical offset에 관해서만 언급하고 넘어가겠다.
우리가 virtual address를 통해 얻고자 하는 것은 physical frame의 address이다. 하지만 위의 virtual address structure를 보면 상위 48 비트는 실제 frame의 address와는 직접적인 연관이 없음을 알 수 있다. 반면 하위 12 비트의 physical offset은 우리가 return 값으로 받기 원하는 frame의 address에 직접적으로 쓰이는 값이다. 이를 통해 알 수 있는 것이 하나 있는데, 바로 page table entry에 저장되는 physical frame address의 하위 12 비트는 아무런 쓸모가 없다는 것이다! 왜냐하면 우리가 page table entry에 접근하여 상위 48 비트만큼을 실제 물리 주소로 사용하고, 하위 12 비트는 버리고 나서 인자로 들어온 virtual address의 하위 12 비트를 사용하기 때문이다.
하지만 이렇게 쓸모없는 12 비트조차도 쓸모 있게 사용하기 위해, 우리는 page table entry의 하위 12 비트를 각종 flag를 나타내기 위해 사용한다. 이 정보는 include/threads/pte.h 파일에 나와있다.
위의 그림은 인텔의 공식 문서에 나와있는 내용이다. pte.h 보다 조금 더 보기 편하게 정리되어 있어 가져왔다. 우리가 프로젝트 3을 하면서 신경 써야 할 flag는 다음과 같다.
- 0번 bit : 현재 페이지가 page table에 존재하는지를 나타내는 flag이다. 1이면 page table에 존재하는 것.
- 1번 bit : 현재 페이지가 read only page인지를 나타내는 flag이다. 값이 0이면 read only임을 나타내고, 만약 앞의 present bit이 0이면 이 값은 아무 의미도 가지지 않는다.
- 2번 bit : 현재 페이지가 kernel만 접근 가능한 페이지인지, 아니면 user/kernel 모두 접근 가능한 페이지인지를 나타내는 flag이다. 만약 값이 0이면 kernel만 접근 가능한 페이지이다.
- 5번 bit : 현재 페이지에 접근한 적이 있는지를 나타내는 flag이다. 우리가 가상 주소를 통해 어떤 데이터에 접근하면, CPU가 자동으로 page table entry에서 access bit을 1로 활성화시켜 준다.
- 6번 bit : 현재 페이지에 write 작업이 수행된 적이 있는지를 나타내는 flag이다. 유저 프로그램에서 가상 주소를 통해 write 작업을 하면, access bit과 마찬가지로 CPU가 자동으로 dirty bit을 1로 활성화시켜 준다.
실제로 kernel virtual address에 대한 매핑 정보가 유저 프로세스의 page table에도 존재하는지 확인해 보기 위해 printf로 pte 값을 출력해 보았다. pml4_get_page 함수를 통해 값을 얻어오면, 실제 물리 프레임과 매핑되어 있는 virtual address를 return 하기 때문에 flag를 확인할 수 없다. 그래서 pml4_get_page 내부에서 printf를 통해 pte 값을 출력해 보았다. 먼저 user virtual address 영역에 있는 buffer를 통해 kva 값을 얻어오고, 다시 kva 값을 통해 pgae table에서 kva에 해당하는 page table entry를 찾아주었다. 그 결과 두 개의 pte 값을 얻을 수 있었고, 각 pte 값들을 이진수로 변환한 결과 위의 그림과 같이 나왔다. 이때 buffer로 찾은 page는 3번 비트가 1로 활성화되어 있어 user page임을 알 수 있었고, 반면에 kva 값을 통해 찾은 page는 3번 비트가 0으로 꺼져있어 kernel page 임을 알 수 있었다.
위와 같은 과정을 통해 결론적으로 user virtual address와 kernel virtual address 모두 둘을 구분하지 않고 CPU에서 주소에 접근할 때는 항상 page table에 접근하여 주소값을 얻어온다는 것을 알게 되었다. 프로젝트 3의 경우 시간이 조금 여유롭게 남았기 때문에 이전 프로젝트들보다 좀 더 근본적인 궁금증을 해결하는데 집중했던 것 같다. 덕분에 가상 메모리와 페이지 테이블에 대해 조금 더 깊은 이해를 할 수 있게 되었던 것 같다!
'Operating System' 카테고리의 다른 글
[Pintos] Project 4 - File System(FAT) (0) | 2023.05.30 |
---|---|
[Pintos] Project 2 - System Call(fork, wait, exec, exit) (0) | 2023.05.20 |
[Pintos] Project 3 - Memory Management (0) | 2023.05.16 |
[Pintos] System Call - File I/O (1) | 2023.05.09 |
[Pintos] Multi-Level Feedback Queue Scheduler(mlfqs) (1) | 2023.05.08 |