95개의 테스트 케이스 중 15개를 실패한 코드입니다. 주의해서 참고하세요.
pass tests/userprog/args-none
pass tests/userprog/args-single
pass tests/userprog/args-multiple
pass tests/userprog/args-many
pass tests/userprog/args-dbl-space
pass tests/userprog/halt
pass tests/userprog/exit
pass tests/userprog/create-normal
pass tests/userprog/create-empty
pass tests/userprog/create-null
pass tests/userprog/create-bad-ptr
pass tests/userprog/create-long
pass tests/userprog/create-exists
pass tests/userprog/create-bound
pass tests/userprog/open-normal
pass tests/userprog/open-missing
pass tests/userprog/open-boundary
pass tests/userprog/open-empty
pass tests/userprog/open-null
pass tests/userprog/open-bad-ptr
pass tests/userprog/open-twice
pass tests/userprog/close-normal
pass tests/userprog/close-twice
pass tests/userprog/close-bad-fd
pass tests/userprog/read-normal
pass tests/userprog/read-bad-ptr
pass tests/userprog/read-boundary
pass tests/userprog/read-zero
pass tests/userprog/read-stdout
pass tests/userprog/read-bad-fd
FAIL tests/userprog/write-normal
pass tests/userprog/write-bad-ptr
FAIL tests/userprog/write-boundary
pass tests/userprog/write-zero
pass tests/userprog/write-stdin
pass tests/userprog/write-bad-fd
pass tests/userprog/fork-once
pass tests/userprog/fork-multiple
pass tests/userprog/fork-recursive
pass tests/userprog/fork-read
pass tests/userprog/fork-close
pass tests/userprog/fork-boundary
pass tests/userprog/exec-once
pass tests/userprog/exec-arg
pass tests/userprog/exec-boundary
pass tests/userprog/exec-missing
pass tests/userprog/exec-bad-ptr
pass tests/userprog/exec-read
pass tests/userprog/wait-simple
pass tests/userprog/wait-twice
pass tests/userprog/wait-killed
pass tests/userprog/wait-bad-pid
pass tests/userprog/multi-recurse
pass tests/userprog/multi-child-fd
pass tests/userprog/rox-simple
FAIL tests/userprog/rox-child
FAIL tests/userprog/rox-multichild
pass tests/userprog/bad-read
pass tests/userprog/bad-write
pass tests/userprog/bad-read2
pass tests/userprog/bad-write2
pass tests/userprog/bad-jump
pass tests/userprog/bad-jump2
pass tests/filesys/base/lg-create
FAIL tests/filesys/base/lg-full
FAIL tests/filesys/base/lg-random
FAIL tests/filesys/base/lg-seq-block
FAIL tests/filesys/base/lg-seq-random
pass tests/filesys/base/sm-create
FAIL tests/filesys/base/sm-full
FAIL tests/filesys/base/sm-random
FAIL tests/filesys/base/sm-seq-block
FAIL tests/filesys/base/sm-seq-random
FAIL tests/filesys/base/syn-read
FAIL tests/filesys/base/syn-remove
FAIL tests/filesys/base/syn-write
pass tests/userprog/no-vm/multi-oom
pass tests/threads/alarm-single
pass tests/threads/alarm-multiple
pass tests/threads/alarm-simultaneous
pass tests/threads/alarm-priority
pass tests/threads/alarm-zero
pass tests/threads/alarm-negative
pass tests/threads/priority-change
pass tests/threads/priority-donate-one
pass tests/threads/priority-donate-multiple
pass tests/threads/priority-donate-multiple2
pass tests/threads/priority-donate-nest
pass tests/threads/priority-donate-sema
pass tests/threads/priority-donate-lower
pass tests/threads/priority-fifo
pass tests/threads/priority-preempt
pass tests/threads/priority-sema
pass tests/threads/priority-condvar
pass tests/threads/priority-donate-chain
15 of 95 tests failed.
https://yskisking.tistory.com/244
PintOS 프로젝트 2주차 [User Programs / System Call] (1)
95개의 테스트 케이스 중 15개를 실패한 코드입니다. 주의해서 참고하세요.pass tests/userprog/args-nonepass tests/userprog/args-singlepass tests/userprog/args-multiplepass tests/userprog/args-manypass tests/userprog/args-dbl-sp
yskisking.tistory.com
저번 글과 이어지는 글이다. 이번 글에선 fork()부터 구현해 보도록 하겠다.
fork() 시스템 콜은, 현재 프로세스를 복제하여 새로운 프로세스를 생성한다.
아래는 GitBook의 fork에 대한 설명이다.
%RBX, %RSP, %RBP, %R12 - %R15(얘네들은 callee-saved register(피호출자 함수에 저장하는 레지스터)) 를 제외한 나머지 레지스터값은 복제할 필요가 없다. 반드시 자식 프로세스의 process id를 반환해라. 그렇지 않으면 유효한 pid가 아니다. 자식 프로세스에서, 리턴값은 반드시 0이어야 한다. 자식 프로세스는 반드시 부모 프로세스로부터 파일 디스크립터, 가상 메모리 공간 등을 포함한 자원을 복사해와야 한다. 부모 프로세스는 자식 프로세스가 성공적으로 복제된 것을 알고 나서 fork로부터 리턴해야 한다. 즉, 만약 자식 프로세스가 자원을 복제하는데 실패하면 부모 프로세스의 fork() 호출은 반드시 TID_ERROR를 반환해야 한다.
이 템플릿은 threads/mmu.c 내에 있는 pml4_for_each() 를 사용해 전체 user memory 공간을 복제하는데, 대응하는 페이지 테이블 구조체를 포함한다. 하지만 빈 부분(pte_for_each_func에서)을 채워야 한다.
fork()가 이루어지기 위해선 레지스터, 파일 디스크립터, 메모리 값 등등 다양한 데이터를 부모 프로세스로부터 복제해야 한다.
또한, 부모 프로세스는 자식 프로세스의 복제 과정이 끝나기를 기다려야 하는데, 이유는 다음과 같다.
1. 데이터 일관성과 동기화
- 부모 프로세스의 메모리와 자원을 복제하는 과정에서 부모 프로세스의 상태가 변하지 않아야 한다.
- 복제가 완전히 완료된 후 자식 프로세스가 실행되어야 한다.
- 복제 과정 중에 부모 프로세스가 계속 실행된다면, 복제되어야 하는 데이터가 변경될 수 있으므로 데이터 일관성을 해칠 수 있다
2. 자식 프로세스 상태 확인
- process_fork 함수를 통해 부모를 복제하여 자식을 만들게 되는데, 복제 성공 여부에 따라서 적절한 리턴값을 확인하여 필요할 경우 오류 처리를 해야 한다. ( 성공 시 부모에게 자식의 pid 리턴, 실패 시 ERROR 리턴 )
3. POSIX 표준 준수
- POSIX 표준에 따르면 fork 시스템 콜은 성공 시 부모에게 자식의 pid를, 자식에게 0을 반환해야 한다.
- 이를 위해서는 부모가 자식의 복제 과정(thread_create + __do_fork)이 끝나기를 기다렸다가 자식의 pid를 받아가야 한다.
syscall.c
fork 시스템 콜이다.
우리는 process_fork를 파고 들어가며 다양한 함수들을 수정해야 한다.
thread_name은 자식 프로세스의 이름이 되고, 인터럽트 프레임 f에는 복제해야 할 부모 프로세스가 실행중이던 다양한 정보들이 담겨있다.
process.c
실질적으로 fork를 진행하는 process_fork 함수이다.
if 정보를 직접 넘기면, 부모 프로세스가 계속 실행되면서 값이 변할 수 있기 때문에 부모 스레드의 parent_if 필드에 if 값을 복사하여 부모 스레드 자체를 __do_fork의 인자로 넘긴다. __do_fork 에서는 parent->parent_if 형식으로 if를 가져와 복제할 것이다.
thread_create를 통해 자식 프로세스가 생성되면 ready queue에 들어가게 되고 곧 실행될 것이다. 부모 프로세스는 sema_down을 통해서 자식 프로세스가 running된 후 __do_fork 작업을 마칠 때 까지 기다리게 된다.
__do_fork가 완료되면 자식 프로세스가 sema_up을 해 주고, process_fork 함수는 자식 프로세스의 pid를 리턴한다. 만약 복제가 실패했다면 TID_ERROR를 리턴하게 된다.
thread.c
init_thread 함수이다. fork에서 쓸 semaphore를 미리 초기화해 두어야 한다.
나중에 쓸 wait_sema와 free_sema도 함께 초기화해 주자.
thread.c
thread_create 함수이다.
프로세스를 생성할 때 현재 프로세스의 child list에 자식 프로세스를 넣어주는 코드를 추가해 주자.
process.c
process_fork에서 사용하는 get_child 함수이다.
pid를 받아서, 현재 스레드의 child list에서 pid에 해당하는 자식 스레드를 리턴한다.
process.c
__do_fork 함수이다. 자식 프로세스가 running되면 가장 먼저 이 함수를 실행하게 되며, 여기서 부모 프로세스 정보를 실제로 복제하는 작업을 진행한다.
부모 프로세스 if에 있는 메모리 값, 레지스터 정보들을 복제한 후 자식 프로세스에게 페이지 테이블을 할당한다. 이후 process_activate를 통해 복제한 값들을 실제로 CPU에 올리고 자식 프로세스가 실행될 수 있는 준비를 한다.
마지막으로 파일 디스크립터 테이블까지 복제하며, fd_idx 값도 복제한다.
복제 작업이 완료되면, sema_up을 통해 부모 프로세스가 진행하던 process_fork 함수가 이어서 진행되도록 한다.
주석을 상세히 달았으니 이해하기 어렵지 않을 거라고 생각한다.
process.c
duplicate_pte 함수이다. __do_fork 페이지 복제 부분의 pml4_for_each()의 3번째 인자로 들어가는데, 실제로 페이지를 복제하는 것은 여기서 이루어진다. 부모 프로세스의 페이지 테이블 엔트리(PTE)마다 duplicate_pte가 실행되어 부모의 PTE를 자식 프로세스로 복제한다.
커널 영역은 모든 프로세스가 공유하기 때문에 커널 영역일 경우 즉시 true를 리턴한다.
부모 페이지 테이블에서 va와 매핑되는 실제 물리 메모리 주소를 찾되, 없으면 false를 리턴한다.
이후 부모 PTE를 복사할 newpage 변수에 페이지를 할당받은 후 복사한다. writable 여부(쓰기가능여부)도 확인하여 저장해 둔다.
마지막으로 pml4_set_page 함수를 이용해 자식 프로세스의 페이지 테이블(pml4)에 PTE를 추가한다.
__do_fork의 이 부분에서 부모의 PTE를 전부 순회하며 duplicate_pte 함수로 PTE를 전부 복제하는 것이다.
이렇게 fork는 끝났다.
syscall.c
exec 시스템 콜이다. command line으로 받은 명령어를 인자로 받아서 파일을 실행하는 역할을 한다.
입력값으로 들어오는 file_name은 "/bin/ls -l foo bar" 이런 식의 명령어(파일명)와 인자로 이루어져 있다.
process_exec가 정상적으로 실행되면, 즉 파일이 열리면 다시 exec로 리턴하지 않는다. 그래서 성공 시 리턴값이 필요가 없다. 그 이유는 파일 로딩이 성공하면 CPU의 실행 컨텍스트(레지스터 상태, 프로그램 카운터 등)가 새로운 프로그램 시작 지점으로 변경되고, 코드의 실행 흐름을 새로운 프로그램으로 전환시키기 때문이다.
근데 마지막에 return 0이 있긴 한데, 어차피 그 위에 NOT_REACHED()가 있어서 사실상 없는 거라고 봐도 무방하다. NOT_REACHED는 정상적인 프로그램 흐름이었을 경우 절대 도달해서는 안될 곳을 의미하니까.
syscall.c
wait() 시스템 콜은 process_wait()를 호출하며, pid에 해당하는 자식 프로세스가 종료되기를 기다리게 된다.
process_wait와 process_exit는 밀접하게 관련되어 있어서, 코드를 한번에 올리겠다.
process.c
process_wait() 함수를 통해 부모 프로세스는 자식 프로세스가 종료되기를 기다리며, 자식 프로세스가 종료될 준비를 마칠 때 까지 sema_down 을 통해서 BLOCKED된 상태로 기다린다.
자식 프로세스가 process_exit() 함수에서 파일 디스크럽터 및 메모리 반환 같은 종료 준비 작업을 마치면 sema_up을 통해서 부모를 깨운다. 그리고 다시 부모가 child_list에서 자신을 제거하고 exit status를 리턴하는 작업을 마칠 수 있도록 sema_down을 통해 기다리고 있는다.
부모는 일어나서 process_wait()을 이어서 실행하며, 자신의 child_list에서 자식을 제거하고 sema_up으로 자고 있는 자식을 깨운 후 자식의 exit status를 리턴한다.
마지막으로 자식은 process_cleanup() 함수로 자신의 페이지 테이블을 반환하고 process_exit() 함수는 종료된다. 이후 상세한 종료 절차는 운영체제에 의해 진행된 후 프로세스는 종료된다.
========================
이번 Project 2 User Program은 정말 매우 많이 어려웠다. 도저히 구현에 대한 감이 안 잡혀서 다른 사람이 정리해 둔 정답 코드들을 엄청 많이 참고했다. 하지만 분명 정답 코드를 보고 따라 쳤는데도 테스트가 전부 통과되지 않았다. 디버깅도 너무 어렵고, 어디가 문제인 지 알아도 뭘 어떻게 고쳐야 할 지도 모르겠고. User Program은 10일동안 진행되었는데, 정말 아무것도 안 남고 시간만 낭비한 기분이 들었었다.
이렇게 하는 게 무슨 의미인가 싶었고... 뭐라도 남기기 위해, 확실하게 이해하기 위해 Project 3이 시작된지 3일차인 지금까지 User Program 내용을 하나하나 이해하며 정리했다. 그랬더니 이제서야 흐름이 보이고, 내가 지난 10일 간 무엇을 했는지를 깨달을 수 있었다. 동료들은 대부분 Project 3를 서로 열심히 토론해가며 진행하고 있는데, 지금까지 저번주 프로젝트를 정리만 한 나는 무슨 소리인지 하나도 모르겠다.
하지만 이게 나에게 맞는 방향인 것 같다. 남들이 먼저 앞서나가는 것 같아 조금 불안하더라도... 진도를 조금 못 나가더라도... 공부했던 걸 복습하고 기록해서 확실히 내 것으로 만드는 것이 장기적으로 나에게 훨씬 이득이라고 생각했다.
Project 3 Virtual Memory는 굉장히 어렵다고 하던데, Project 2도 제대로 못 했던 내가 잘 할 수 있을지 모르겠다. 저번주엔 급하게 쫓기듯이 구현한 느낌이 있는데, 이번엔 그냥 내 방식대로 확실히 이해해가며 천천히 진행하려고 한다.
