Copy on Write (CoW)
Project 3의 Extra 과제인 Copy on Write에 관해 알아보겠습니다.
먼저, Git book에서는 copy on write에 대해 다음과 같이 정의하고 있습니다.
그래서 fork를 할 때에는 새로운 물리 페이지를 할당하지 않고 부모와 같은 물리 페이지를 매핑만 해주었다가 추후에 write를 시도할 때에만 새로운 물리 페이지를 할당해 주면 되는 것입니다.
우리는 filebacked Page의 경우에 이미 같은 물리 메모리를 공유하도록 구현했기 때문에 여기서 힌트를 얻을 수 있었습니다.
기존에 우리 프로그램은 이 페이지가 작성가능한지만을 검사하고 있었습니다. 그런데 이 페이지가 원래 부모 프로세스가 만들어 놓은 Anonymous Page이고 fork된 프로세스에서 write 하려고 한다면 이것을 fault를 내서 막아야한다고 생각했습니다. 따라서 fork할 때 만드는 모든 Anon_page에 대해 writable을 false로 복제하여 page fault를 발생시킬 수 있을 것 같다는 아이디어가 떠올랐습니다.
그런데, 이 것이 부모와 같은 page에 write하려고 하는 것인지 아니면 진짜로 작성하면 안되는 페이지를 요청한 것인지 구분할 필요가 있었습니다. 따라서 새로운 accessible 이라는 토큰을 추가했습니다.
기존에는 anonymous page를 fork할 때 새로운 페이지를 만들고 kva 또한 할당하여 물리 메모리를 그대로 복제하는 방식으로 구현했습니다.
이것을 맵핑만 해놓고 kva는 생성하지 않고 기존의 kva를 받아오는 copy_claim_page라는 함수를 새로 만들었습니다.
함수에서 frame을 새로 만드는 것까지는 동일하지만, 기존의 kva를 가리키게 만들고 accessible을 설정해 준 후 pml4를 writable을 false로 swap_in을 수행합니다.
이제 fork된 페이지에 대해서 writable가 무조건 false이기 때문에 해당 페이지에 write를 시도하면 page_fault가 발생합니다.
이에 따라서 try_handle_fault 에서 이를 구분할 수 있어야 합니다. 그 동안은 writable이 금지된 페이지에 write를 시도했을 때 무조건 false를 반환했었는데 이를 accessible token을 검사하는 함수를 호출하도록 수정했습니다.
먼저 페이지에 대해서 검사하는데 access가 금지되어 있다면 fork된 페이지가 아니라 실제로 접근해서는 안되는 페이지이기 때문에 바로 false를 리턴 해줍니다. 만약 access 가능하다면 이는 fork된 페이지기 떄문에 먼저 기존 kva을 백업해주고 kva에 user_pool에서 새로운 페이지를 할당합니다. 이 때 용량이 꽉 차서 할당할 수 없다면 evict_frame을 수행하여 만들어줍니다. 그 후 원본 kva를 현재 frame에 복원하고 pml4에 할당해 줍니다.
이로써 우리는 자식 페이지에서 write를 할 때만 kva를 할당하는 copy on write를 구현할 수 있었습니다.
그러나 다른 곳에서 문제가 발생했습니다.
자식 프로세스가 할 일 모두 끝내고 종료할 때 Process_cleanup을 수행하게 되는데 이 때 spt를 지우는 것 뿐만 아니라 pml4도 같이 파괴시킵니다. 그런데 우리의 프로세스가 부모의 Anonymous_Page를 가져와서 write를 시도하지 않은 페이지들은 아직 부모의 kva를 참조하고 있다면, 그래서 자식이 exit 할 때 해당 kva까지 파괴해버립니다. 그래서 추후에 부모가 접근하면 fault가 뜨는 문제가 발생했습니다.
메모리 접근이 안되어 처음에는 모든 pml4삭제 자체를 막고 OS가 shutdown할 때만 pml4를 삭제하는 방법으로 해결했습니다. 그러나 이는 fork 속도를 올리고자 만든 프로그램이 OS가 종료되기 전에는 메모리 누수가 발생하기 때문이 이는 근본적인 해결책이 아니라고 생각했습니다.
첫번째 방법으로 페이지를 destroy할 때 부모로 부터 fork된 페이지라면 삭제를 막는 방법이었습니다. Page의 SPT Kill을 뒤로 미루고 pml4를 검사해서 fork 되지 않은 페이지만 삭제해봤습니다.
해당 방법은 페이지가 깔끔하게 삭제되지않고 잔여물이 남아서 kernel offset이 뒤틀리는 현상이 발생해 실패했습니다.
두번째는 fork 할 때 마다 각 페이지를 marking하여 남겨놨다가 나중에 destroy할 경우 marking된 페이지가 남아있다면 부모가 exit할 때 까지 그 페이지의 destroy를 막는 방법이었습니다.
해당 방법은 너무 복잡하고 수정해야 하는 범위가 너무 광범위하여 포기하게 되었습니다.
세번째는 그냥 pml4_destroy를 막고 pml4_clear만 수행하여 kva의 연결만 끊은 상태로 만들어 재가용하게 돌려줬습니다. 그리고 나중에 OS가 종료될 때만 kva를 destory 시키도록 하는 것이었습니다.
해당 방법으로 모든 메모리 누수를 잡아낼 수 있었고 프로젝트를 완료했습니다.