Jungle

[TIL] pintos : 가상 메모리 구현 (2) - fork , mmap , swap

손가든 2023. 12. 29. 23:38

이전 포스팅을 통해 uninit 유형의 페이지를 생성하여 spt에 넣어 놓은 뒤 page_fault가 발생하여 demanding 될 경우 page를 할당해주는 lazy loading 의 진행 과정을 살펴보았다.

 

오늘은 lazy loading에서 추가되어 필요한 fork의 추가 작업

 

mmap 메모리 매핑 페이지 시스템콜을 구현한 것과

 

마지막으로 swap disk을 활용하여 물리 메모리로부터 퇴거와 복귀를 하는 작업에 대해 살펴보겠다.

 


 

Fork 추가 작업

이전엔 물리 메모리가 가상화되어 있지 않았으므로 그저 pml4를 전체 탐색하여 모든 페이지를 자식에게 복사해주기만 하면 되었지만 이제는 spt 테이블을 통해 메모리를 가상화하였으므로 spt의 내용을 복제하고 유형별로 물리 메모리도 할당해주어야 한다.

 

그전에 페이지들의 유형이 어떻게 저장되어있는지 알 필요가 있다.

 

나는 페이지별로 어디에 적재되어 있는지에 대한 위치가 다르므로 해당 유형에 marker를 추가했다.

 

 

이 enum vm_type를 잘 살펴보면 비트로 마킹해놓을 수 있게 해 놓았다.

 

0~3은 실제 유형을 나타내고 이는 2진수로 생각하면 2개의 비트로 판별할 수 있다.

 

따라서 이후의 비트는 마커로 사용할 수 있게 된다.

 

나는 마커를 총 4개 사용하였는데,

 

VM_MARKER_0은 stack 페이지를 구분하기 위해 표시했고,

 

VM_MARKER_1은 mmap segment를 다루기 위해 시작 페이지에 표시했다.

 

또한 VM_SWAP과 VM_DISK는 각각 anon페이지와 file페이지가 물리 메모리가 아닌 스왑과 디스크에 있게 될 경우 표시해주었다.

 

 

이 유형을 이용하여 spt 테이블의 각 요소들을 자식에게 복사해주는 supplemental_page_table_copy 함수를 구현했다.

 

이를 살펴 보자.

 

supplemental_page_table_copy() 함수

 

매우 길고 더럽게 작성된 결과물은 각 유형별로 수행해야 되는 요구사항이 달라서 어쩔수 없었다고 생각했는데,

 

이는 추후에 리팩토링이 가능한 부분이 곳곳에 보이는 것 같다. (중복된 내용이 좀 있는 듯 함. 추후에 수정 예정)

 

부모의 페이지를 순회하면서 각 유형별로 수행해야 하는 내용이 달랐다.

 

mmap에 기억하는 요소들의 방법과 anon의 내용이 달랐고,

 

저장되어 있는 위치를 표시하는 마커도 달랐기에 함수가 조금씩 다를 수 밖에 없었다.

 

아무튼 이 함수에서는 이전 포스팅에서 소개한 함수들을 최대한 활용했고, 페이지 유형별로 해줘야 하는 연결과 초기화를 진행했다.

 

이 함수의 구동 원리에 대한 자세한 포스팅은 이전에 작성했으므로 넘어가겠다.

 

 


 

MMAP & MUNMAP 시스템 콜

 

VM_FILE 유형의 페이지를 생성하고 삭제하는 시스템콜인 mmap과 munmap을 구현한 내용에 대해 이야기해보겠다.

 

요구사항도 많은 우리 깃북

 

깃북을 살펴보니 mmap이 어떤 것인지 이해할 수 있었다.

 

파일을 페이지로 가져와서 읽고 쓸수 있도록 지원하라는 것 같다.

 

이때 파일의 크기가 page의 배수일 수 있다는 것 보니 load_segment에서 해줬던 것 처럼 while문으로 여러개의 페이지를 할당해주면 된다는 것 같았다.

 

그리고 munmap에서는 file이 변경되었다면 disk에 파일을 변경시키라고 적혀 있었다.

 

 

그래서 먼저 시스템콜을 구현했다.

 

처음에는 깃북에 적힌 예외상황만 고려해줬었는데 테스트가 막 실패하면서 모든 테스트의 경우를 다 고려해주고 나니

 

예외 상황이 이렇게나 많게 변경됬다.

 

이 함수를 통해 각각 do_mmap()과 do_munmap()으로 이동한다.

 

 

do_mmap()

 

먼저 do_mmap() 함수를 보자.

 

프로세스를 로드할때 사용했던 방식이 file을 읽어 페이지에 저장한 것인데, 이는 do_mmap()이 원하는 방식과 동일하기에 거의 비슷하게 함수를 가져와 수행했다.

 

 

이 do_mmap() 함수는 유저가 요청한 addr에서 시작하여 page를 segment 단위로 할당해주기 때문에

 

addr의 페이지만 비어있는지 확인해서는 안됬다.

 

따라서 중간에 저렇게 do-while문을 사용하여 모든 페이지가 비었는지 체크하도록 했다.

 

그리고 read_bytes와 zero_bytes는 생각하는데 매우 골머리를 앓았는데

 

사용자가 요청하는 page와 file의 경우별로 다른 예외 상황이 있어서 그것을 모두 처리해줘야 해서 삼항 연산자를 사용해준 모습이다.

(이걸 직접 설명하려면 그림과 설명이 꽤 긴 용량을 차지할 것 같으므로 각자 코드를 보고 내 의도를 이해해주길 바랍니다..)

 

이후 이전 uninit 페이지를 만들었던 load_segment() 함수와 매우 비슷한 형태인 load_file_page를 구현했다.

 

 

거의 비슷한 이 함수의 유일한 차이점은 if-else문이 생긴 점이다.

 

이 구현은 munmap() 함수에서 mmap 된 페이지 세그먼트의 시작 페이지를 식별하기 위해 이전에 말했던 마커를 추가해주기 위해 저렇게 구현했다.

 

이후의 함수 flow는 이전에 uninit때 load되었던 방식과 동일하다.

 

이후 lazy_loading되는 mmap 세그먼트는 다음 함수에 의해 메모리가 채워진다.

 

 

이 함수가 lazy_load_segment()와 차이를 가진다면 이전에는 aux의 내용을 free해주었지만

 

file은 퇴거될 경우 다시 disk로 불러오기 위해 load되어야 하는 정보들을 기억해야 한다.

 

따라서 file_page 구조체 안에 aux의 값을 저장하는 코드가 추가되었고,

 

추가적으로 DISK에 있는지 확인하는 마커가 표시되어 있었다면 지워주도록 했다.

 

 

do_munmap()

이번엔 해당 세그먼트를 반환하는 munmap()함수를 살펴보자.

 

munmap 시스템 콜은 mmap으로 요청했던 메모리 매핑 페이지 세그먼트의 시작주소로 요청받는다.

 

요청 받을 경우 해당 시작주소의 mmap 세그먼트 페이지들을 전부 삭제해주도록 했다.

 

세그먼트 단위이므로 이는 do-while문을 통해 매 페이지를 삭제해주었고,

 

나는 만약 uninit인 file 유형의 페이지거나, file 유형의 헤드(VM_MARKER_01)가 아닌 페이지라면 계속 삭제해주도록 하여

 

다른 mmap 요청의 세그먼트와 구분하도록 했다.

 

여기서 사용한 세마포어는 퇴거 정책을 위한 frame 리스트의 삽입과 삭제가 발생할 때, race 상황을 회피하기 위해 사용했다.

 

do-while문 안의 spt_remove_page는 vm_dealloc_page()로 가고,

 

이는 file 유형의 페이지인 경우 file_backed_destroy() 함수로 이동한다.

 

 

여기서 dirty 비트를 참조하여 만약 수정되었다면 파일을 동기화하는 코드를 추가하였고, 그 이후 pml4와 관련된 내용의 삭제와 frame 테이블에서의 삭제, file 닫기, frame 구조체 반환 작업을 수행해주었다.

(이 모든 작업을 빠지지 않고 생각해 내어 해줘야 한다는 것이 정말 머리 터지는 일임)

 


SWAP

 

진짜 마지막으로 스왑을 구현한 것에 대하여 설명하겠다.

 

스왑에 대한 깃북의 내용을 읽어보니 anon 페이지는 swap 디스크에 내용을 복사해 둔 뒤 퇴거하고,

 

mmap으로 매핑한 file 페이지는 그냥 디스크에 동기화한 후 퇴거하면 되었다.

 

 

file 페이지는 그냥 디스크에 동기화만 하면 되기 때문에 다른 데이터 공간이 필요없지만 anon의 경우는 swap만을 위한

 

데이터 공간을 마련해줘야 하는데 이를 device/disk.c 소스파일에 있는 disk_write , disk_read 함수를 사용하여 적용했다.

 

 

또한 스왑 디스크의 특정 섹터가 빈 공간인지 아닌지를 확인하기 위해서 또 추가적으로 자료구조가 필요했는데,

 

이는 백승현 코치님이 말씀해주신 "비트맵이 필요하다면 구현해야할 수도 있다."는 말이 떠올라 비트맵을 사용했다.

 

 

이렇게 구현해야 할 내용을 필기하여 정리한 뒤 disk.c 코드를 확인하며 disk_get()함수로 disk를 선언해줘야 함을 알게됬다.

주석에 보면 채널 1, 디바이스 1이 스왑이라고 적혀있다.

 

그리고 비트맵 소스파일을 보니 비트맵을 생성하는 함수도 구현되어있었다.

 

이 두 함수를 통해 swap disk와 swap_disk속 저장 값의 존재 여부를 파악하는 bitmap을 생성했다.

 

 

이때 비트맵 생성 함수의 인자를 보면 'disk_size(swap_disk)/8' 로 되어있는데

 

이는 스왑디스크의 1개의 sector가 512byte를 저장할 수 있고,

 

page사이즈인 4096바이트는 8개의 섹터에 걸쳐 저장할 수 있다.

 

 

따라서 스왑디스크는 8개의 섹터가 한 단위로 채워지고 지워지고를 수행하므로

 

섹터의 총 갯수에서 8을 나눈 값을 bitmap의 총 길이로 설정했다.

 

 

그리고 이후 anon_swap_in 함수를 구현했다.

 

 

anon 페이지는 swap_idx라는 멤버를 가지는데 이는 해당 페이지가 페이지단위로 몇번째 섹터에 데이터가 위치하는지 저장된다.

 

이는 swap_out 함수에서 저장될때 idx를 기억하고, swap_in될때 invalid한 수인 -1로 초기화시켰다.

 

이때 처리해줘야 하는 일이 하나 존재하는데,

 

바로 fork시에는 어떻게 처리하는가 이다.

 

메인 메모리에 적재되어있다면 그냥 memcpy를 통해 자식 프로세스 가상 메모리에 적재해주면 되지만,

 

이 경우 디스크의 내용을 디스크로 복제하는 것이 매우 불필요하고 복잡하다.

 

따라서 fork시에는 해당 디스크의 내용이 복제됬다는 의미로 fork_cnt를 +1하는데

 

이는 swap_in될때 비트맵을 false로 바꿔주지 않고 만약 fork_cnt가 0보다 크면 fork_cnt만 -1하고 비트맵 마킹은 그대로 두도록 해서 복제한 것처럼 로직을 처리했다.

 

 

그리고 swap_out에서 false의 비트 인덱스를 순회하며 찾는 bitmap_scan_and_flip()함수를 통해 인덱스를 찾아낸 뒤

 

512바이트씩 disk_write를 통해 스왑 디스크에 저장하도록 했다.

 

 

파일 페이지도 이것과 매우 유사하다.

 

하지만 파일 페이지는 비트맵이 필요없다.

 

 

왜냐하면 이전 lazy_load_file_segment() 함수를 통해 파일의 주소를 기억하고 있기 때문이다.

 

따라서 'file_backed_swap_out'은 'file_backed_destroy'에서 구현한 것과 매우 비슷하게 작성했고,

 

'file_backed_swap_in'은 lazy_load_file_segment()를 호출하는 것으로 대응했다.

 

 

 

file 유형의 페이지나 anon 유형의 페이지나 모두의 공통점은 clock 퇴거 정책을 위해 현재 swap_in된 page를 가장 최근에 접근한 페이지라는 것을 기억하기 위해 buffer로 전역 변수에 저장해놓는 것이다.

 

이는 내가 구현한 clock 퇴거 정책때 순회하는 시작점으로 사용했다. 

 

 


 

핀토스를 마치며..

 

첫번째 스레드의 구동방식, 스케줄링을 통한 시분할 시스템의 구현부터

 

두번째 유저 프로세스의 접근 제어와 허가방식을 시스템콜로 처리해주는 코드를 구현했고

 

마지막으로 매 틱별로 스케줄링되는 상황에서의 여러 프로세스의 메모리 가상화까지 구현하였다.

 

 

짧다면 짧고 길었다면 긴 5주동안 핀토스 운영체제를 직접 구현해보며 내가 얻은 가장 큰 수확은 개발에 대한 자신감이다.

 

내가 절대 해낼수 없어 보일 만큼 어렵고 좌절스러운 순간도 물론 있었지만 포기하지 않고 끝까지 물고 늘어져 달려왔다.

 

그렇게 끌고온 마지막에는 내가 이 프로젝트 코드에 대해 정확히 알고 있기에,

 

내가 생각한 방식이 잘못될 수 없다는 자신감을 가질 수 있었다.

 

 

 

아무튼 정말 나에게는 소중한 프로젝트였고, 5주간 몰입하며 정말 재미있었다.

 

 

0주차 회고에서 얘기했던 미치도록 노력했는가에 대해 그렇다고 답할 수 있는 순간이었다.

 

 

 

이제 컴퓨터 구동 방식 정확히 파악 완료

 

- 핀토스 완