Notice
Recent Posts
Recent Comments
«   2025/02   »
1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28
Tags
more
Archives
Today
Total
관리 메뉴

seven05

[PintOS] 정글 WEEK08 WIL. PROJECT 2: USERPROGRAM 본문

SW JUNGLE 9기/Pintos

[PintOS] 정글 WEEK08 WIL. PROJECT 2: USERPROGRAM

박상비누 2024. 10. 7. 23:14

기간: 2024.10.01~2024.10.07

 

처음에 introduction에서 나오는 usrprog 쪽 코드들을 들여다보았을때는 alarm clock이나 thread와는 달리 진행 흐름이 전혀보이지 않았다. 우리가 하는 userprogram과제는 디스크에 저장되어있는 프로그램을 메모리로 가져와서 실행하고 그 유저프로그램이 하드웨어와 상호작용하는 행동을 하고싶으면 systemcall을 호출해서 커널이 대신 수행해주고 반환값을 주는 식으로 유저 영역과 커널영역을 구분해서 행동하도록 만들다보니 가상머신을 만들어서 동작해야하고 이러한 과정이 우리가 구현하는 부분에 전부 들어나있지않다보니 어디부터 만져야하는지 접근하기 힘들었다.

권영진교수님의 pintos lab2 ppt를 통해 코드 흐름을 보면서 감을 익힐수있었다.

pintos lab2 ppt의 일부

 

ppt를 보면서 파악한 userprogram 프로젝트에서 원하는 진행흐름

가상머신 부팅 (main thread가 실행됨) -> pintos --fs ... 명령어를 읽고 가상머신의 사양등 여러가지 설정을 하는 run_actions()를 수행함 -> process_create_initd()로 실제 우리가 가상머신 안에서 cmd_line으로 입력한다는 명령어가 들어간거처럼 process를 만드는데 그 역할을 앞서 계속 만들었던 thread를 생성함으로써 흉내내서 만듬 -> thread create를 통해 실행파일을 불러와서 동작시킬 프로세스를 만들고 -> load 함수에서 실행파일을 메모리에 복사해서 실행함 -> 실행된 파일에 있는 코드가 진행되고 system_call을 호출하게되면 syscall_handler가 제어권을 받아서 우리가 구현한 syscall 함수들을 동작하고 rax 레지스터에 반환값을 담아서 제어권을 다시 유저프로그램에 넘겨줌 -> 유저프로그램 실행이 끝나면 exit syscall 호출 -> 할당된 메모리 free, fd_table에 담겨있는 file들 file_close, 실행프로그램 thread_exit 호출해서 thread_dying 상태로변경후 do_schedule(), 메모리로 불러온 실행프로그램 자체를 file_close()까지 하고 process_wait 하고있는 main thread 에 여러가지 할당되었던것들을 모두 해제했다고 알리면서 대기 -> main thread가 종료처리 확인후 종료 확인 신호 보내면 -> 진짜로 실행프로그램 종료 -> 가상머신이였던 main thread도 종료하면서 테스트 코드 종료

 

구현하면서 만났던 난관들

1. fork()를 처음 구현했을때 어떻게 부모의 상태와 같은 자식 쓰레드를 생성하는지에 대해서 떠올리기가 쉽지않았는데 project1에서 thread가 전환될때 context_switching이 일어나는 매커니즘을 생각해서 레지스터의 값을 똑같이 가지고있으면 된다는걸 알게되었다. 하지만 막상 fork 함수를 구현한뒤에는 아래와 같은 버그를 만나게되었는데...

상당히 당황스러운 화면이였다. 보는것과 같이 rax,rdi값이 cccccccccc로 상당히 일관적으로 찍히는데 나머지 값은 변하거나 유지되거나 규칙성을 볼수없었고 fork_once함수를 호출했는데 여러개의 레지스터가 결과창에 계속 호출되는것이 뭔지 감도 못잡았었다. 그래서 gdb로 직접 찾아보았는데... idle 쓰레드라고 name이 찍히고 실제로 thread_create에 있는 idle 쓰레드를 만들때 넣는 값들이 들어가있는것을 확인할수있었다. 아니 분명 main 쓰레드가 실행되고 실행파일이 불러온뒤에 syscall을 통해 fork가 실행될터인데 어째서 idle 쓰레드가 실행되는지 도저히 알수가없었다. cpu 스케줄러가 도저히 idle 쓰레드를 갑자기 실행하는지 감이 잡히지가않았다. 그래서 우리는 fork가 실행될때 fork를 통해 실행해야하는 정보가 담긴 인터럽트 프레임 값이 잘못 넘겨지고 있다고 생각할수있었다. 그렇게 syscall.c 에 작성했던 fork 함수 부분을 봤는데 process_fork 함수로 넘겨주는 인자부분에 thread_current()->tf 값을 넘겨주고있었는데 우리가 thread 구조체안에 tf라는 인터럽트 프레임을 선언하긴했지만 생성할때를 빼고는 갱신해주지 않는다는것을 추적해볼수있었고 잘못된 인터럽트 프레임을 넘겨준다는것을 알수있었다. 결국 syscall 핸들러를 호출할때 넘겨져오는 인터럽트 프레임을 인자에 넘겨주면 잘 된다는것을 확인할수있었고 그렇게 디버깅을 완료 했다.

 

2. multi-oom 해결하기 (메모리 누수 잡기)

syn-read, syn-write와 같은 테스트들을 통과할수있게 syscall_lock을 만들고 나서야 마지막 테스트인 multi-oom이 동작이되었다. 동작조차안되고 바로 Fail이 뜨던 이 테스트가 동작하는것만으로 기뻐하기에 무색하게 바로 fail이 떳다. 실패 메세지를 분석하니 처음에는 191개의 자식 프로세스가 생성되었지만 이번에는 160개가 생성되어 최소치에 미치지 못했다는 메세지였다. 이게 무슨 테스트인지 알아보기위해 테스트 코드를 살펴보았고 10번동안 더이상 생성하지못할때까지 fork를 하고난뒤에 모두를 종료하고 다시 생성하지못할때까지 생성한뒤에 같은 갯수의 fork를 실행하지못하면 메모리 누수가 있다고 판단하는 테스트였다. 우리의 코드는 무려 30개치의 메모리가 누수되고있다는 뜻임을 알수있었다. 

처음에는 palloc으로 메모리를 할당받은 모든 부분이 free가 제대로 되지않는다고 생각되어 전부 찾아서 혹시나 의심이 되는 조건문 분기지점에 모두 집어넣어봤지만 효과는없었고, 다음으로는 문자열 parsing을 하는 부분을 찾아서 지역변수로 선언된 문자열 배열이 뭔가 남나 싶어서 모두 palloc으로 할당받게 바꾸어보았지만 이것또한 아니였다. 우리의 코드는 모든 해제를 exit 함수에서 진행하고 exit 함수에서 close를 통해 free를 하고있던 구조였는데 아무리 봐도 free를 안하는 부분은 없어서 더더욱 미궁속으로 빨려들어가고있었다.

그렇게 한참을 헤메고 multi-oom의 특성상 gdb를 찍기에는 전체 동작과정자체가 대략 3000번이 넘는 코드였기에 엄두를 못내고있었지만 이대로는 답이없다 싶어서 돌려보았는데, fd_table 올바르게 관리되지 못하고있다는 것을 발견할수있었고(물론 이 과정이 상당히 고되고 힘들었다 ㅠㅠ), next_fd라는 int값으로 fd_table을 빠르게 접근할수있게 했었는데 이 next_fd값을 올바르게 관리하지못해서 file_close가 제대로 안되는 상황이라는것을 알수있었고 이것을 고쳤더니 multi-oom 테스트가 10번을 돌리면 5번은 통과하는 이상한 상황에 놓였다.

191개를 만들면 5~9번째 반복사이에서 190개만 만들어지면 실패하거나 10번째까지 191개가 자연스럽게 만들어져서 성공하는 상황이였다. 아마 큰 메모리 누수는 잡혔지만 아주 사소한 메모리 누수가 발생해서 쓰레드 1개치 만큼의 메모리 누수가 쌓이면 실패하고 아니면 통과되는 상황이라고 예상되었다. 그럼에도 어떠한 해결책도 도저히 생각해낼수없었는데, 아직도 의문이지만 코드 리펙토링을 마치고나서 갑자기 multi-oom을 돌렸더니 190개로 생성되고 테스트 끝까지 190개로 계속 성공하도록 바뀌었다. 진짜 왜인지 모른다 반복횟수가 모자라서 메모리 누수를 감지하지못하나 싶어서 테스트코드에 있는 반복횟수를 100번으로 10배나 올려서 테스트 했지만 통과했다. 어떻게 마지막 메모리 누수가 잡히게됐는지 아직도 모른다. 리펙토링을 맡은 친구가 변경한 작업은 띄어쓰기 조정, 주석제거, 변수명 직관적으로 바꾸기와 같은 작업들이 대부분이였고 유일하게 로직을 건드린 부분은 do_fork에서 FD_MAX 값보다 큰경우 error를 내보내는 부분을 없앤것인데... 코드의 흐름대로라면 아예 접근조차 안되는 값이기에 영향이 없어야 하는거같은데 테스트가 통과되니 많이 혼란스럽다. 한참이 지나고 성장한 내가 이글을 다시 봤을때 이유를 알수있으면 좋을거같다..