sw 사관학교 정글

PintOS - Project2 - UserProgram

stmyrecord 2022. 11. 29. 13:27

열흘동안 PintOS project2를 구현었다. 이 과정에서 특별히 어려웠거나 중요하다고 생각하는 부분을 간단히 소개하겠다. 


1. System Call이 호출되는 과정

우리는 userprog/syscall.c에서 system call handler와 여러 system call함수들을 구현하여 시스템콜이 돌아가도록 만들었다. 처음에는 system call 함수가 호출되면 곧바로 내가 만든 함수가 호출된다고 생각했지만 그게 아니었다. 가장 처음은 lib/user/syscall.c에 있는 함수가 호출된다. 

 

lib/user/syscall.c의 fork()함수

예를들어 fork()를 호출하면 우리가 만든 userprog/syscall.c에 있는 fork()함수가 곧바로 호출되는 것이 아니라 lib/user/syscall에 있는 fork()함수가 호출된다. 이 함수는 syscall1()함수를 호출한다. 

 

lib/user/syscall.c의 syscall1()함수

syscall1(), syscall2() 등 끝에 숫자가 붙은 이 함수들은 syscall을 호출하는데, 단순히 들어온 인자의 개수에 따라 0, 1, 2, 3, 4, …로 나눈 것이고 syscall()함수를 호출하여 인자를 넘겨주는 역할만 한다. 

 

lib/user/syscall.c의 syscall()함수

이후,  syscall()함수에 들어간 인자들은 각각 레지스터에 들어가서 저장된다. 그리고, __asm __volatile()함수가 실행되면 레지스터에 저장된 인자들이 syscall 명령어를 통해서 전달되고, syscall 명령어가 실행되기 위해 user모드에서 kernel모드로 전환된다. 

 

syscall-entry.S

syscall 명령은 인자를 syscall_handler로 전달하는데, 여기서 드디어 우리가 구현한 userprog/syscall.c의 syscall handler함수가 호출된다. 


 

2. fork() System Call에 대한 이해

userprog/syscall.c의 syscall_handler()함수

fork() 시스템콜은 부모 스레드와 똑같은 자식 스레드를 생성하는 시스템콜이다. 일반적으로 다른 시스템콜들은 인터럽트프레임 안에서 하나의 레지스터 값만 받아서 하나의 인자로 넣는다. 그런데 우리가 구현한 fork 시스템콜은 특이하게도 두번째 인자로 intr_frame 전부인 f를 그대로 받는다. 이는 fork 시스템콜이 process_fork()함수에 들어가서 자식 스레드에게 인터럽트 프레임을 전달하여 자식이 부모의 레지스터 정보를 전부 복붙하도록 하기 위함이다. 

userprog/syscall.c의 fork 시스템콜

userprog/process.c의 process_fork()함수

사실 처음 process_fork()함수를 구현할 때에는 현재 스레드 'curr'의 구조체 멤버로 있는 'tf'멤버를 받아와서 복붙을 했었는데, 이렇게 했더니 작동이 안되었다. 'tf'멤버도 인터럽트프레임을 저장하는데 왜 안될까? 아래의 그림을 보며 살펴보자. 

tf와 if의 차이

위의 그림은 메모리를 나타낸 모식도이고 커널영역과 유저영역으로 나뉘어져있다.

커널영역에는 Struct thread가 있으며 이 구조체 멤버로 'tf'가 있다. 이 멤버는 다른 스레드와 context switching을 할 때 레지스터 정보를 저장한다. 이 때의 레지스터는 유저가 사용하던 레지스터일 수도 있고 커널이 사용하던 레지스터일 수도 있다.

 

하지만 시스템콜이 일어나는 상황은 하나의 스레드 안에서 유저모드->커널모드 context switching이 일어나는 상황이지, 스레드간 context switching 상황이 아니다. 따라서 fork 시스템콜에서 자식에게 'tf'를 참조하여 전달하면 잘못된 값을 주게 되는 것이다.

 

반대로 커널 스택에 있는 'if'는 하나의 스레드 안에서 유저모드에서 커널모드로 context switching이 일어날 때, 유저의 레지스터 정보를 저장하는 곳이다. 따라서 우리가 필요한 if는 이것이다. fork 시스템콜은 이 if를 자식 스레드에게 전달하기 위해 인자로 interrupt frame을 받는 것이다. 


3. rox-multichild 테스트케이스 디버깅 과정 (Timeout 원인)

골머리를 썩혔던 여러 테스트 케이스들 중에 rox-multichild를 소개하겠다. 

rox-multichild 테스트케이스, CHILD_CNT가 5로 설정되었다.

rox-multichild 테스트 케이스의 과정은 위와 같다. 'child-rox'파일을 열고, 읽고, 쓰기를 한 뒤에 실행을 한다. 이때, 부모는 5명의 자식을 fork를 통해 만들고 곧바로 exec()를 통해 실행시킨다. 이후, 부모는 wait()를 통해 자식들이 실행되기를 기다린다. 

테스트케이스를 자세히 분석하기에 앞서서 fork-wait-exit의 한가지 사례의 과정에 대해 살펴보자. 

fork-wait-exit 사례 예시

fork()가 일어나면 parent는 child_create()함수를 실행시켜 새로운 자식을 만들며, 이때 3개의 sema가 initialize(sema_init)된다. 이후 fork()에서는 fork_sema를 sema_down하여 자식이 fork_sema를 sema_up해주기를 기다린다. 자식은 create된 이후에 readylist에 들어가서 차례를 기다리다가 차례가 되면 먼저 do_fork()함수를 실행한다.

 

__do_fork()에서는 부모의 정보를 자기자신에게 복붙을 하며 마지막에 fork_sema를 sema_up해준다. 이와 동시에 부모의 fork()는 종료된다. 이후, 부모의 wait()가 호출되는데, 여기서는 exit_sema를 sema_down하여 자식이 exit()를 수행하기를 기다린다. 이후 자식이 exit()를 수행하다가 exit_sema를 sema_up해준다. 그 후에는 자식이 free_sema를 sema_up한다. 이는 부모가 자식이 cleanup되어 정보가 모두 사라지기 전에 exit_status를 받아서 저장하기 위함이다. 부모는 wait()에서 자식의 exit_status를 저장하고 나면 free_sema를 sema_up해주고 자식의 exit()는 종료된다. 

 

rox-multichild.output - fail : timeout

위의 그림은 rox-multichild 테스트케이스를 실패한 결과이다. printf를 통해 process_wait함수와 process_exit함수의 sema up&down 전후로 자식의 tid를 찍어보았다.

 

16라인을 보면 메인함수가 tid가 3인 부모스레드를 만든 것을 알 수 있다. 이후 일련의 과정을 거친 뒤에 25라인을 보면 부모가 process_wait()에서 exit_sema를 down하여 tid가 4인 자식스레드를 기다리고 있다는 것을 알 수 있다. 그런데 26라인에서 알지 못하는 이유로 tid 4인 자식 스레드가 exit(-1)로 종료되었다. 이후 tid가 5인 자식 스레드가 process_exit()에서 exit_sema를 up하지만 부모는 tid 4인 자식을 기다리고 있기 때문에 아무 일도 일어나지 않는다. 이후 tid가 5인 자식 스레드는 free_sema를 down하여 부모가 free_sema를 up해주기를 기다리지만 부모 스레드는 tid 4인 자식 스레드를 기다리고 있다. 즉, tid 4인 자식 스레드의 비정상 종료 때문에  timeout이 발생한 것이다. 

 

__do_fork()함수에서 file을 복제하는 코드

printf문을 찍어가며 문제가 나는 부분을 찾아보니 __do_fork()함수 내부의 file을 복제하는 코드에서 자식이 종료된다는 것을 찾게 되었다. 알고보니 file이 NULL인 경우에도 file_duplicate()를 진행하여 비정상 종료가 발생한 것이었다. 246라인에서 NULL인 경우를 예외처리 해주었더니 다행이도 정상적으로 동작하게 되었다. 

rox-multichild.output - pass
위의 그림은 성공한 테스트케이스의 output이다. tid=3인 부모가 5명의 자식(tid = 4~8)을 만들고 모두 정상적으로 동작하는 것을 알 수 있다. 디버깅을 위해 찍어둔 printf문을 지우면 pass한다. 

 

PintOS project1과 project2를 구현한 모든 코드는 아래의 github 링크의 master branch에 올라와있다.

https://github.com/PintOS-firsthalf-tangerine/W08_5_PintOS

 

GitHub - PintOS-firsthalf-tangerine/W08_5_PintOS

Contribute to PintOS-firsthalf-tangerine/W08_5_PintOS development by creating an account on GitHub.

github.com

 

'sw 사관학교 정글' 카테고리의 다른 글

PintOS - Project3 - Virtual Memory  (0) 2022.12.12