process.c에 있는 함수에서 문제가 발생한 것 같은데, 하나씩 무식하게 뜯어보는 방법밖에 없을 것 같다. . .. . . ..
현재 핀토스의 실행 흐름이다.
run_action() 에서 run_task()를 호출하고, 그 내부에서 process_wait(process_exec(argv))를 호출한다. process_exec은 유저 프로세스를 생성하는데, 함수 내부의 thread_create(...)가 스레드를 생성한 후 이전에 짜준 스케줄링 코드에 맞춰 ready_list에 넣어주는 스케줄링을 실행한다. process_exec()에서 tid를 반환하면, process_wait()에서 -1을 반환하고, shutdown_power_off()를 통해 pintos가 종료된다. 이걸 이제 어떻게 바꿔야할까.
process_wait 내부에 sema_down을 호출한다. sema_down은 semaphore의 값이 양수가 될 때까지 재워놓는 함수이다. 유저 프로세스가 끝날 때까지 init 프로세스를 재워놓음으로써 인터럽트 충돌이 나지 않게 한다.(?? 확인 필요함;; 막 일단 쓰고 보기;) 그리고 사용자 프로그램이 다 실행된 뒤 exit()을 통해 thread_exit()을 반환한다. thread_exit에는 ifdef userprog 안에 process_exit()을 호출하는 코드밖에 없었던 것 같다. 그리고 sema_up() 함수를 통해 유저 프로세스가 다 끝났다는 신호를 주면 다시 초기의 프로세스가 exit_status를 받아 실행을 완료하고, 원래의 과정을 마무리한다.
process execution의 과정
init.c: main()
이 함수로부터 핀토스가 시작된다. main() 함수 내부의 run_action() -> run_task() -> process_wait(process_execute(task)) 순서로 호출한다. process_wait을 호출하는 스레드는 커널 스레드이며, task를 수행하는 자식 스레드를 만들고 자식 스레드(프로세스)가 종료될 때까지 기다리게 한다.
process_create_initd()
새로운 thread를 생성해 함수 인자로 넘겨진 file_name에 해당하는 유저 프로그램을 로드하고 실행시키는 함수. file_name은 커맨드 라인을 통해 입력된 string이다. 이 커맨드 라인 전체가 파라미터로 들어오기 때문에 argument passing을 통해 스택에 잘 쌓아주는 과정을 거쳐야 한다. race condition을 막기 위해 file_name을 fn_copy라는 값으로 strlcpy하고, file_name과 fn_copy를 동시에 인자로 넘겨 thread_create(file_name, PRI_DEFAULT, start_process, fn_copy)함수를 호출해야한다.
process_exec()
유저 프로세스를 메모리에 로드하고 시작시키는 함수. 함수 내부에서는 interrupt stack frame(if_)를 선언하고 초기화한다. 그 후 실행하고자 하는 프로세스의 이름과 인터럽트 스택 프레임의 eip, esp를 함께 load()의 인자로 넘긴다. load() 실행이 끝난 뒤 로드에 성공했으면 어셈플리 명령어를 수행한다. asm volatile ("movl %0, %%esp; jmp intr_exit" : : "g" (&if_) : "memory")는 류저 프로세스를 실행시키는데, 이는 intr_exit 함수를 이용해 인터럽트를 마치고 반환한 것처럼 행동했기 때문이다. 로드에 실패했을 경우 thread_exit() -> process_exit()을 통해 현재 스레드를 삭제한다.
load()
process_exec()내부에서 호출한 load()를 통해 argument passing을 진행한다. < 이건 내가 짜준 부분이고, 원래 load의 용도는 file_name이라는 ELF 바이너리 파일을 현재의 메모리에 로드해주는 것이다. 현재 실행 중인 thread의 page directory를 초기화하고, file_name executable 파일을 연다. 파일의 오류 여부를 확인한 뒤 validity가 확인되면 setup_stack 함수를 통해 user vm에 스택을 생성한다. 인자로 전달된 interrupt stack frame rip와 rsp에는 각각 executable(파일)의 엔트리 포인트(프로세스의 시작 코드), 스택 포인터의 시작 위치가 저장된다. load가 성공적으로 이루어지면 true, 실패하면 false를 반환한다.
system call의 과정
아무 것도 건드리지 않은 상태의 핀토스는 시스템콜 핸들러가 구현되어 있지 않기 때문에 시스템 콜이 호출되지 않아 유저 프로그램이 정상적으로 작동하지 않는다. 핀토스 내 현재 구현되어 있는 부분은 threads/init.c에서 실행되고 있는 syscall_init() 함수이다. 이 함수는 syscall_handler() 함수를 시스템콜 핸들러로서 세팅해준다
시스템 콜이란, 유저 프로그램이 작동하기 위해 커널 기능을 사용할 수 있도록 운영 체제가 제공하는 인터페이스이다. 특정 유저 프로그램이 작동하면서 메모리 읽기나 쓰기같은 커널 기능에 대한 리퀘스트를 보내면 커널 영역에서 스스템 콜이 실행되어 처리한 후 결과 값을 넘겨주는 방식이다.
시스템콜 함수는 argument 개수에 따라 syscall0, syscall1, syscall2, syscall3을 호출하고, argument와 시스템 콜 넘버를 유저 스택에 push한 후 인터럽트를 발생시켜 커널의 syscall_handler()을 실행시킨다. syscall_handler()는 switch-case문을 이용해 시스템 콜 넘버에 해당하는 시스템 콜을 호출한다. 시스템 콜 넘버는 lib/syscall-nr.h에 enum 타입으로 선언되어있다.
시스템 콜이 처리되는 동안 커널에서 유저 프로세스로의 접근이 필요하다. 이 과정에서 유저 프로세스가 넘겨준 주소가 유저 영역을 벗어난 주소인지 확인해야한다. 유저 프로그램 동작을 위해 process_create_initd 함수가 실행되면서 프로세스가 실행할 자식 스레드를 생성하고, 자식 스레드 내 pagedir에 유저 프로세스 페이지를 생성한다. setup_stack()함수 내 palloc을 통해 유저 스택을 할당받는다. 이 때 반환되는 주소는 virtual kernel address로 kpage 변수에 저장된다. 핀토스는 physical memory에 직접적으로 접근하는 것을 허용하지 않기 때문에, virtual kernel memory를 physical memory에 대응해 사용해야 한다.
핀토스에서 유저 프로그램은 파일 시스템을 통해 로드된다. 핀토스에는 간단한 형태의 파일 시스템들이 filesys 폴더에 고현되어 있는데, filesys.h와 file.h의 함수를 잘 이해하고 사용하는 것이 중요한다. 핀토스 가이드에는 이 파일 시스템의 한계에 대해서도 언급하고 있다. internal synchronization이 구현되어 있지 않기 때문에, 동시에 여러 프로세스가 접근하면 예상치 못한 에러가 날 수 있고, 파일 사이즈가 생성 시간에 고정되어 있고, 루트 디렉토리가 파일 형태로 표현되어 있어 생성할 수 있는 파일의 수가 제한되어 있다. 서브 디렉토리 생성이 되지 않고, 파일 이름이 14자를 넘길 수 ㅇ벗으며, 파일 시스템 repair 툴이 없다는 점 등을 한계로 꼽는다. 이것들은 모두 프로젝트 2를 진행하는 데 아무 문제가 없다.
exit()
exit 시스템 콜이 호출될 때 (유저 프로세스가 직접 호출하며, 유저 프로세스가 정상적으로 종료됨을 의미) exit된 프로세스 이름과 exit code를 출력 해야 한다.
deny_write
프로그램은 디스크에 binary executable 파일로 저장되어 있다. load() 함수는 프로그램을 메모리에 로드하는데, 운영 체제는 실행 중인 유저 프로그램의 데이터가 변경되지 못하게 막아야 한다. 이를 위해서 load() 함수가 호출되기 전에 핀토스가 제공하고 있는 file_deny_write 함수를 호출해 write를 실행하지 못하게 막아야한다. load()가 끝나고 file이 닫힌 후 file_allow_write()를 삽입해 다시 write가 가능하도록 해준다.
2월 13일 ㅎㅎ 지금까지 짰던 (7 / 95 failed)의 코드를 다 버리고 프로젝트 2를 처음부터 시작하기로 마음 먹었다. 아래서부터 process.c 공부 + 코드 짠거 기록해야지 룰루
struct thread <- thread.h
struct thread *parent;
struct list child_list;
struct list_elem child_elem;
struct file *running_file;
struct semaphore load_semaphore;
struct semaphore orphan_semaphore;
struct semaphore exit_semaphore;
필드를 추가해주고, 초기화해준다.
process_exec()
/* Switch the current execution context to the f_name. * Returns -1 on fail. */
현재 프로세스를 실행하고 싶은 새로운 실행 가능한 프로세스로 바꾸는 것이다. 그렇기 때문에 보통 fork() 뒤에 exec()을 호출한다. process_exec()에서 load()를 호출하는데, load에서 argument passing을 해주었기 때문에 process_exec()은 더 건드릴 게 없다는 게 현재까지의 내 생각.
process_wait()
/* Waits for thread TID to die and returns its exit status. If it was terminated by the kernel (i.e. killed due to an exception), returns -1. If TID is invalid or if it was not a child of the calling process, or if process_wait() has already been successfully called for the given TID, returns -1 immediately, without waiting. This function will be implemented in problem 2-2. For now, it does nothing. */
Hint) The pintos exit if process_wait (initd), we recommend you to add infinite loop here before implementing the process_wait. */
부모 프로세스가 자식 프로세스를 기다리는 동작. 일반적으로 자식이 종료된 뒤 SIGCHLD 시그널을 보내면 기다리고있던 부모 프로세스가 받아서 처리하고, 자식 프로세스가 완전히 종료되는 과정이다. 이런 과정이 없으면 좀비 프로세스(자식의 자원은 모두 해체되지만 커널에 의해 프로세스 테이블에는 존재하고 있는 상태)가 되는 것이다.
자식 프로세스의 pid를 인자로 받고, 자식의 exit status를 기다린다. 자식 프로세스가 exit되지 않은 상태라면 종료될 때까지 기다리고, 종료되었을 때의 exit status를 반환한다. 자식 프로세스가 exit() 시스템 콜을 호출하지 않았지만 다른 이유로 커널에 의해 강제적으로 종료되었을 떄는 -1을 반환한다. 이렇게 wait이 실패하는 경우는 아래 두 가지이다.
1. 인자로 받은 pid를 가진 프로세스가 wait()을 호출한 프로세스의 직계 자식이 아닐 때
만약에 A가 B를 fork(), B가 C를 fork()했다면, A는 B가 종료되었을지라도 C를 기다릴 수 없다.
2. 하나의 프로세스는 주어진 자식에 대해 최대 한 번까지만 wait()을 호출할 수 있다.
syscall.c에서 호출하는 sys_wait()에서는 process_wait()을 호출하는 코드가 전부이다. 따라서 이런 과정 구현은 process_wait() 내부에서 해야한다.
이제부터 세마포어를 다뤄야하는데, 프로젝트 2를 완성하기 위해서는 최소 3개의 세마포어가 필요하다고 이전 글에서 다룬 적이 있다. 파일을 열기 전에 쓰는 load semaphore, 중간에 에러가 발생했을 때 부모와 자식 관계를 끊도록 하는 orphan semaphore, 프로세스가 종료되는 것을 알리는 exit semaphore 이렇게 세가지는 필요하다.
sema_down: 세마포어의 값이 0일 때 현재 스레드를 THREAD_BLOCK 상태로 변경한 후 schedule()
sema_up: ready_list에 스레드가 존재하면 리스트 가장 처음에 위치한 스레드를 THREAD_READY 상태로 변경한 후 schedule()
적당한 곳에 semaphore 잘 써주고, 디버깅 하다보니 multi-oom 하나 남았다.
close 관련 테케들에서 커널 패닉이 자꾸 발생했던 것은 close_file을 써야하는 자리에 file.h의 file_close를 썼기 때문이었고, 갈아엎기 전에도 안됐던 close-twice 테케가 자꾸 fail이었던 것은 fd_table에서 해당 파일을 닫고 NULL 처리를 해줬어야하는데,,,,, = NULL 대신에 == NULL을 썼기 때문이었다............... 이걸 왜 못봤지............ 정말 환장할 뻔 했다.
이제 남은건 multi-oom 하나! 아래의 것들을 하나씩 확인해주려고 한다.
1. file, fd table 제대로 다 닫는지 확인
2. thread 잘 종료하는지 확인
3. 모든 에러 케이스에 exit or return -1 넣었는지 확인
4. memory allocation 후, 모두 free 해주는지 확인
1. file, fd table 제대로 다 닫는지 확인
void
process_exit (void) {
struct thread *curr = thread_current ();
for (int i = 2; i <= curr -> index; i++){
if (curr -> fd_table[i] != NULL && curr -> running_file != curr -> fd_table[i]){
close_file(curr -> fd_table[i]);
}
}
// fd_table 비워주기
if (curr -> running_file != NULL){
file_close(curr -> running_file);
curr -> running_file = NULL;
}
// 실행 중인 파일 닫아주기
palloc_free_page(curr -> fd_table); // fd_table 할당 해줬던 거 free
if (!list_empty(&curr -> child_list)){
for (struct list_elem *e = list_begin(&curr -> child_list); e != list_end(&curr -> child_list); e = list_next(e)){
struct thread *t = list_entry(e, struct thread, child_elem);
e = list_remove(&t -> child_elem);
palloc_free_page(t);
}
}
ㄴ 여기에 palloc_free_page(t) 뺴고 sema_up(&curr -> zombie_semaphore); 해줬는데도 결과는 똑같이 나옴
2. thread 잘 종료하는지 확인
thread가 잘 종료되는지 확인하라는 게 무슨 말인지 잘 모르겠다. 함수 내에서 struct thread 를 해줬으면 그 이후에 그것들을 다 palloc_free_page(thread) 이런식으로 해줘야한다는 의미일까? struct한 모든 스레드를?????
3. 모든 에러 케이스에 exit or return -1 넣었는지 확인
다 확인한 것 같음;;
4. memory allocation 후, 모두 free 해주는지 확인
palloc_get_page: fn_copy, newpage, arg_addr, kpage
palloc_free_page: fn_copy, newpage, fd_table, child_list의 child, arg_addr, kpage
malloc: file_name, argv
free: file_name, argv
결국 process.c에 있는 fork, wait, exit 관련된 함수랑 multi-oom.c에 있는 테스트 파일에 print문을 다 적어서 하나씩 확인했다. 할당과 free를 적절한 위치에서 해주지 못해서 메모리가 부족했고, 그래서 child를 210개까지 못만들어주는 것은
1. process_wait()에서 list_remove(&t -> elem);을 지우기
2. process_exit()에서 child_list를 돌면서 e = list_remove(e) 해준 다음 palloc_free_page(t) 해주던걸 지우고, 대신에 remove(e) 해주기 전에 sema_up(&curr -> zombie_semaphore) 해주기
했더니 고쳐졌다. 하지만 210번 자식까지 만든 뒤 child를 지워주는 과정에 문제가 있는 것 같았다.
child_210_X 까지는 정상적으로 exit(-1)이 출력됐는데
child_210_O에서 exit(211)이 아니라 exit(-1)이 콜되면서 테스트가 fail 되는 것이 계속됐다.
처음에는 process_wait에 문제가 있는 줄 알고 process_wait에 있는 코드들을 계속 봤지만, 조금씩 이렇게 저렇게 바꿔봐도 원래 코드가 맞게 짜진 것 같았다ㅠ process_wait에서 반환하는 값은 child의 exit_status인데, exit_status가 -1이 됐다는 말은 어디에서 에러문이 걸렸다는 것 같았다.
그래서 process.c에 있는 함수들 중 return -1과 exit(-1)이 있는 부분들을 하나씩 다 뜯어봤고, child_tid == TID_ERROR일 때 return -1을 해줬어야하는데 여기에서 exit(-1)로 빠져버려서 계속 리턴값 없이 그냥 exit(-1)로 끝나버리는 것 같았다.
그리고 이게 아니더라도 thread_exit()에서 파일들을 닫을 때 index 바로 전 순서까지만 닫았어야하는데, index 까지도 닫아주고 있길래 그 부분도 고쳐줬더니!
모든 테스트 케이스가 통과됐다. !
'KAIST PINTOS' 카테고리의 다른 글
syscall.c (0) | 2022.02.13 |
---|---|
argument passing (0) | 2022.02.13 |
Project 2 디버깅 (0) | 2022.02.07 |
system call (0) | 2022.01.21 |
PROJECT 1 - Alarm Clock, Priority Scheduling (0) | 2022.01.20 |