[PintOS] 2-2 System Call

2024. 6. 2. 16:58크래프톤 정글 5기/공부

GIT

 

[process.c, process.h, syscall.c, syscall.h, thread.c, thread.h, exception.c]

 

영상 출처 : https://www.youtube.com/watch?v=sBFJwVeAwEk

 


 

- 현재 pintOS에는 system call handler 부분이 비어있음

- handler 구현 이후, system call 동작도 구현해야 함

 

- System Call : OS에서 제공하는 서비스를 위한 programming Interface
                User mode program이 Kernel 기능을 사용할 수 있게 해줌

- System Call은 kernel mode에서 실행되고, 다시 user mode로 전환시킴

 

 

- syscall3 : 3개의 parameter가 존재하는 시스템콜이라는 뜻

- arg 2, 1, 0 (3개의 parameter) pushing
- pushing parameter number
- 0x30 (system call 나타내는 Interrupt number)

# TODO : 0x30을 전달받는 syscall_handler() 함수 비어있으니 고쳐야 함 !

 

- 각 systemcall number에 대한 내용은 syscall_nr.h 파일에 존재

 

 

- system call handler가 system call number를 사용하도록 / system call 호출하게 만들기
- User program이 호출한 system call이 유효한 주소를 입력받았는지 체크해야 함
  - user program이 전달한 주소는 "User area" 이내로 한정되어야 함
  - 유효한 주소가 아니라면, page fault가 발생되어야 함

- 주소 유효성 검사가 완료되면, User stack에 있는 arguments를 kernel stack으로
  복사해야 함
- system call의 return value를 eas 레지스터에 저장해야 함

** Important thing

1. User는 Kernel에 유효한 User address를 제공해야 함
2. Kernel은 실행될 때 User area에 Access하면 안됨
3. User area에 존재하던 arguments들을 무조건 kernel로 옮겨와야 함
   -> kernel이 User area로 접근할 일을 만들면 안됨

 

 

- 사용자가 systemcall 호출 시 '유효하지 않은 주소'를 전달할 경우 :
	OS는 process를 종료시켜야 함(Kernel or 다른 process에 데미지를 주지 않게)
	system call handler가 유효성을 검사할 수 있어야 함
	
- 유효성 검증 2가지 방법
    1. User가 제공한 주소의 유효성을 검사하는 것
       Page table로 확인하는 것 + 주어진 주소가 Mapping 되었는지 확인하는 것
       pagedir.c / vaddr.h 파일에서 함수 사용해라
		 
    2. PHYS_BASE (코드에서는 USER_STACK) 이내의 주소값인지 확인하는 것

 

- process는 lock을 보유하거나 malloc을 요청함

- page fault가 발생하면, global value인 lock은 process에 의해 보류됨
- page fault가 발생하면, memory allocate는 받았지만 free하지는 못함

--> Page fault가 발생하면 resource leak 발생 가능

- 따라서 process exit 이전에 [lock release] [memory free] 동작이 필요함

유효성 검증 방법 1) : address 유효성을 확인한 이후에 [lock, malloc] 동작 이루어짐
유효성 검증 방법 2) : memory access에서 error code 반환할 방법이 없어서 어려움

 

 

- 유효성 검증 방법 2)의 문제를 해결하기 위한 2가지 함수가 있음

1) get_user
 : 지정된 주소에서 1바이트를 읽음
 
2) put_user 
 : 사용자가 지정한 주소에 1바이트를 씀

 

1. halt : OS 종료함 / pintOS는 명시적으로만 종료되어야 함

2. exit : process 종료함 / process 종료 시 [process name + process status] 출력함

3. exec : 자식 process 생성한 후 프로그램을 실행 (UNIX의 exec 함수와 다름)
					fork() + exec() 함수의 동작을 합친 것
					
4. wait : 입력받은 pid인 process의 종료를 기다림

 

 

- 계층구조를 형성해야 함
- 부모 / 자식 관계 + 형제 관계를 추상화하여 포인터로 지정해야 함
	-> thread 구조체에 변화가 있어야 함

❗️ 부모가 모든 자식에 대한 포인터를 가지고 있어야 하는 게 아님

 

 

- 자식 process인 pid가 종료되기를 대기하고, 자식 process의 exit status를 검색
- 자식 process가 살아있다면, 종료될 때까지 기다림 / 종료되면 exit status 반환함
- 자식 process가 kernel에 의해 종료되면, -1 반환
- 이미 종료된 자식 process에 대해서도 wait 함수를 호출할 수 있음
- 자식 process가 종료된 이후, 부모 process는 해당 process descriptor 할당 해제해야 함

 

 

#TODO : return -1로 되어있는 부분을 발전된 형태로 구현해야 함

 

 

- 'child_tid' parameter 이용해서 child process의 descriptor 검색
- process_wait() 함수 호출자는 [child process 종료] 까지 block 되어야 함
- chile process가 종료되면, descriptor 할당 해제 및 child process의 exit status를 반환함

- 부모 process가 자식 process를 기다리고, 자식 process 간 동기화를 시켜줘야 함
- struct thread 구조체 안에 'wait을 위한 semaphore' 추가해야 함
- 해당 semaphore는 처음 생성될 때 0으로 초기화 되야 함
- wait()에서 sema down / exit()에서 sema up 해야 함

- struct thread 구조체 안에 'exit status 기록'할 수 있는 Field 추가해야 함

 

 

- process_wait() 안에서 sema_down()
- thread_exit() 안에서 sema_up()

- sema_down() 하면 호출자(부모 process)는 자식 process 종료까지 block됨
- sema_up() 하면 semaphore 값이 증가하길 기다리던 process들 모두 unblock됨

 

 

- pintOS에서 존재하는 exec()는 fork() + exec() (UNIX)

Function :

- thread 생성 + binary 파일 실행
- command line을 인자로 받고, command line을 실행하는 프로그램을 실행함
- 생성된 새로운 process의 pid를 반환함
- exec() 호출자(부모 process)는 자식 process가 동작을 완료할 때까지 대기함
	-> 이건 exec system core가 할 일
- exec system core는 '실행 파일 생성 & load' 이후 반환됨

 

 

- 부모 process는 자식 process의 성공적 생성 여부 / binary file load의 성공 여부를 알아야 함
- 부모 process와 자식 process를 semaphore 사용으로 동기화 할거임

- struct thread 구조체에 'exec()을 위한 semaphore' 추가함
- 해당 semaphore value는 생성될 때 0으로 초기화
- 자식 process의 실행파일이 성공적으로 load 될때까지 기다리기 위해 sema_down 함
- 자식 process의 실행파일이 성공적으로 load 되면, sema_up 함

- struct thread 구조체에 '파일이 성공적으로 load 되었는지 여부' 확인을 위한 Field 필요

 

 

- 부모 process가 process_execute() 호출
- process_execute() 안에 있는 start_process() 호출 -> 자식 process 생성
- 자식 process는 thread를 생성하고 파일을 load 함
- 파일 load가 완료되면 sema_down() 호출
- sema_down() 하게되어 [부모 process + 자식 process] 병렬적 동작이 가능함

 

 

- sema_down()을 어디에 위치시켜야 하나 ? #TODO

 

 

- argument process의 종료 상태를 반환해야 함
- process가 exit()을 호출하면, OS는 current user program 종료 후 상태를 kernel에 반환함
- 부모 process가 wait 중이라면, user program 종료 후 상태를 반환함

- 종료 상태를 저장하는 동작을 추가해야 함
- 종료 상태와 thread 이름을 출력해야 함
- 이후 thread_exit() 호출

 

 

- thread의 exit status를 process status에 저장해야 함
- sema_up() 호출하여, 해당 thread 종료를 기다리던 process가 동작할 수 있게 함

- thread 목록을 조작할 때마다 intr_disable() 해줘야 함 !!!!

 


What I did

 

- user program에서 syscall 호출 시 전달하는 ‘address’ 유효성 검사를 1번 방식으로 하도록 결정
- 1번은 [syscall_handler() 함수 내에서, 각 syscall이 호출될 때마다 유효성 검사] 하는 동작
- mmu.c 파일 내의 ‘pml4_get_page’ 함수 + vaddr.h 파일 내의 ‘is_user_vadder) 사용하면 됨
1. method 1으로 address valid check 동작 추가하기 
	
     함수를 새로 만들거임 bool address_checker(void *address)
     여기서 걸어줄 조건문은 [address가 NULL인지] [pml4_get_page 반환값이 NULL인지]

     pml4_get_page() 진입 시 (is_user_vaddr) 여부를 확인함
     이후 pml4e_walk를 통해서 입력된 virtual address를 테이블을 타고 타고 타고 타고 들어가서 
     PTE에 있는 physical address 가져옴
     해당 동작을 했는데 NULL이 뜬거면, thread_exit 시키면 될 듯
	 
2. syscall_handler 수정하기

     기존 skeleton code에는 그냥 printf만 찍힘
     입력으로 받아오는 interrupt frame에서 Register -> rax에서 syscall_number 받아옴
     switch - case로 구분해서 [유효성 검사] + [syscall 동작 구현]해야 함

     여기서 중요한 게, 유효성 검사를 하려면 [해당 syscall이 주소값(buffer) 전달하는지 여부가 중요]
     syscall마다 전달하는 인자가 다름 (주소 안쓰는 애들도 있음)
     그래서 syscall case마다 검사를 다 따로 달아줘야 함
     -> 그래서 영상에서 '2번 방법은 코드의 간결성을 유지할 수 있다는 장점이 있다' 라고 한듯 ?
 	 
     case로 들어가서, 인자를 가져오는 동작을 하려면 f->R로 들어가서 값을 가져와야 함
     rdi / rsi / rdx / r10 / r8 / r9 순서
 	 
3. exit 구현
	 
     1) struct thread 구조체에 int exit_code 추가
     2) case : exit 추가
     3) thread_exit -> process_exit 내부에 printf ("%s: exit(%d)\n", ...); 추가
     4) printf ("%s: exit(%d)\n", curr->name, curr->exit_code);
	 
	 
4. process_exit() 에서 printf 찍던거 exit() 안으로 옮김

 

0. 나랑 재혁님 서로 pass 뜨는게 다르지만, syscall.c 파일의 차이라고 판단하고 분업함
   0523 2023 기준 git에 올린 파일(=수정된 파일) [thread.c / thread.h / process.c / syscall.c]

1. syscall 부르는 형식 바꿈 (switch - case 안에서 동작하지 않고, 함수 밖으로 빼서 호출하는 식)

2. fd 함수 하나 만들고, thread.h에서 [struct thread에 필드 하나 추가] [FDT_MAX 선언]

... 생략 ...

 

코드 수정 -> 이전 테스트 터짐 -> 코드 수정 복원 실패 -> 재도전 -> 코드 수정 -> 이전 테스트 터짐 -> ...

fork(), wait(), exec() 함수 이외는 모두 어찌저찌 성공

 

1. thread.h : thread 구조체 수정

    struct file* fdt[FDT_MAX]; // 부모 FD
    struct list_elem child_elem; // 자식 list elem
    struct list child_set; // 자식 list
    int next_fd; // 다음 fd 값
    int exit_code; // 종료 status
    bool loaded; // 적재 유무
    bool exited; // 종료 유무
    struct semaphore exit_sema; // exit semaphore
    struct semaphore load_sema; // laod semaphore
    struct thread *parent; // 부모 thread 포인터
	
2. init_thread : list_init(&t->child_set) 추가

3. thread_create :

    /* customed 0524 */
    t->parent = thread_current();
    sema_init(&t->exit_sema, 0);
    sema_init(&t->load_sema, 0);
    t->loaded = false;
    t->exited = false;
    list_push_back(&thread_current()->child_set, &t->child_elem);

4. process_fork() -> __do_fork() -> duplicate_pte()

    1. process_fork에서 __do_fork 동작할 자식 thread 생성 
    2. sema_down 호출 -> 자식 process load까지 대기
	
    3. 자식 process는 __do_fork 동작 수행
    4. 부모의 if_ 복제 / 자식 PTE 생성 / 부모 PTE 복제 / 부모 FDT 복제 / 부모 File 복제
    5. 복제 다 했으면 sema 올리고 종료

 

📎  함수들 몇개
더보기

 

tid_t
process_fork (const char *name, struct intr_frame *if_) {
    /* Clone current thread to new thread.*/
    struct thread *parent = thread_current();
    tid_t tid = thread_create (name, PRI_DEFAULT, __do_fork, parent); // 현재 thread 복제하여 새로운 thread 생성
    if (tid == TID_ERROR) // thread_create 동작이 완료되지 않으면, TID_ERROR 출력
    {
        return TID_ERROR;
    }

    struct thread *child = get_thread(tid); // all_list에서 자식 tid로 자식 thread 가져옴
    sema_down(&child->load_sema); // 자식 process load 완료까지 대기

    if (child->loaded == false) // 자식 process가 load되지 않으면 에러 반환
    {
        return TID_ERROR;
    }

    return tid;
}

 

static void
__do_fork (void *aux) {
    struct intr_frame if_;
    struct thread *parent = (struct thread *) aux;
    struct thread *current = thread_current ();
	
    struct intr_frame *parent_if = &parent->tf; // 부모의 interrupt frame 가져오기
    bool succ = true;

    /* 1. Read the cpu context to local stack. */
    memcpy (&if_, parent_if, sizeof (struct intr_frame)); // 부모 process의 interrupt frame 자식 process에게 복사

    /* 2. Duplicate PT */
    current->pml4 = pml4_create(); // 자식 process의 page table 생성
    if (current->pml4 == NULL)
        goto error;

    process_activate (current);
#ifdef VM
    supplemental_page_table_init (&current->spt);
    if (!supplemental_page_table_copy (&current->spt, &parent->spt))
		goto error;
#else
    // pml4 전체를 순회하면서 각 유효한 PTE에 대해 PTE 복제하는 함수 호출
    // return value가 false라면 error
    if (!pml4_for_each (parent->pml4, duplicate_pte, parent))
        goto error;
#endif

    /* TODO: Your code goes here.
     * TODO: Hint) To duplicate the file object, use `file_duplicate`
     * TODO:       in include/filesys/file.h. Note that parent should not return
     * TODO:       from the fork() until this function successfully duplicates
     * TODO:       the resources of parent.*/

    if (parent->next_fd != FDT_MAX)
    {
        // 부모 process의 FDT -> 자식 process의 FDT 복제
        for (int i = 2; i < parent->next_fd; i++)
        {
            if (parent->fdt[i] != NULL)
            {	
                // 부모 process의 파일 -> 자식 process의 파일 복제
                struct file *duplicate_file = file_duplicate(parent->fdt[i]); 
                if (duplicate_file == NULL)
                {
                    succ = false;
                    break;
                }
                current->fdt[i] = duplicate_file;
            }
        }
        current->next_fd = parent->next_fd;
    }
    else
    {
        succ = false;
    }

    // load 동작 완료 표시
    sema_up(&current->load_sema);
    current->loaded = true;

    /* Finally, switch to the newly created process. */
    if (succ)
        do_iret (&if_); // 자식 process 시작
error:
    current->loaded = false;
    sema_up(&current->load_sema);
    thread_exit (); // 자식 process 종료
}

 

static bool
duplicate_pte (uint64_t *pte, void *va, void *aux) {
    struct thread *current = thread_current ();
    struct thread *parent = (struct thread *) aux;
    void *parent_page;
    void *newpage;
    bool writable;

    /* 1. TODO: If the parent_page is kernel page, then return immediately. */
    // va가 커널영역에 위치하면 return true
    if (is_kernel_vaddr(va))
    {
        return true;
    }
	
    /* 2. Resolve VA from the parent's page map level 4. */
    // 부모 page table에서 va에 대한 pa 반환
    parent_page = pml4_get_page (parent->pml4, va);

    /* 3. TODO: Allocate new PAL_USER page for the child and set result to NEWPAGE. */
    // 자녀 process page 할당
    newpage = palloc_get_page(PAL_USER);
	
    /* 4. TODO: Duplicate parent's page to the new page and
     *    TODO: check whether parent's page is writable or not (set WRITABLE
     *    TODO: according to the result). */
    memcpy(newpage, parent_page, PGSIZE); // 부모 page 내용을 자식 page에 복사
    writable = is_writable(pte); // 쓰기 동작 가능 여부 저장

    /* 5. Add new page to child's page table at address VA with WRITABLE
     *    permission. */
    if (!pml4_set_page (current->pml4, va, newpage, writable)) { // 자식 process page table에 새 페이지 추가
        /* 6. TODO: if fail to insert page, do error handling. */
        palloc_free_page(newpage); // 실패할 경우 할당 페이지 해제
        return false; 
    }
    return true;
}

 

// pml4 전체를 순회하면서 각 유효한 PTE에 대해 주어진 func 함수를 호출
bool
pml4_for_each (uint64_t *pml4, pte_for_each_func *func, void *aux) {
	
    // pml4에 있는 모든 entry 순회)
    for (unsigned i = 0; i < PGSIZE / sizeof(uint64_t *); i++) {
		
        // 현재 pml4 entry에 해당하는 PDPE(Page Directory Point Entry) 가져옴
        // ptov : physical address -> kernel virtual address translate
        uint64_t *pdpe = ptov((uint64_t *) pml4[i]);

        // PDPE가 유효한지 확인 / PTE_P 비트가 설정되어 있으면 유효하다고 판단
        if (((uint64_t) pdpe) & PTE_P)

            // pdf_for_each : 현재 PDPE에 매핑된 모든 PD(Page Directory) 순회
            // 순회하면서 func 함수 호출하는데, return value가 false라면 false return
            if (!pdp_for_each ((uint64_t *) PTE_ADDR (pdpe), func, aux, i))
                return false;
    }
    return true;
}
동기화 동작 추가 -> 이전 테스트 터짐 -> 코드 복원 실패 -> 재도전 -> 동기화 동작 수정 -> 이전 테스트 더터짐 -> ...

 

fork, wait, exec 동작 추가 직전 git에 올려둔 코드로 복원 후 다시 진행

1. thread struct 수정

	struct thread *parent; // 부모 thread 포인터
	struct list child_set; // 자식 list
	struct list_elem child_elem; // 자식 list elem
	int exit_code; // 종료 status
	
	bool is_dead;
	struct semaphore wait_sema; // wait semaphore
	struct semaphore load_sema;
	
2. thread_create 수정

	list_pushback(&thread_current()->child_set, &t->child_elem)
	
3. thread_exit 수정

	list_remove(&thread_current()->a_elem);
	if (thread_current()->parent != NULL)
		list_remove(&thread_current()->child_elem);
	thread_current()->is_dead = true;
	// palloc_free_page(thread_current());
	sema_up(&thread_current()->wait_sema);
	do_schedule (THREAD_DYING);
	NOT_REACHED ();
	
4. init_thread 수정

	for (int i = 2; i < FDT_MAX; i++)
	{
		t->fdt[i] = NULL;
	}
	t->next_fd = 2;

	t->is_dead = false;
	t->parent = NULL;
	list_init(&t->child_set);
	sema_init(&t->wait_sema, 0);
	sema_init(&t->load_sema, 0);

	t->magic = THREAD_MAGIC;
	
-------- 여기까지는 fix해도 되고, process 수정하면 다터지는 현상 발생 --------

5. process_wait 수정

int process_wait (tid_t child_tid UNUSED) {
	/* XXX: Hint) The pintos exit if process_wait (initd), we recommend you
	 * XXX:       to add infinite loop here before
	 * XXX:       implementing the process_wait. */
	/* customed 0523 */
	struct thread* child = get_child_process(child_tid);
	if (child == NULL)
		return -1;
	
	if (child->is_dead)
		return -1;
	
	sema_down(&child->wait_sema);

	if (child->is_dead)
		return child->exit_code;

	return -1;
}

-------- 여기까지는 fix해도 되고, fork ㄱㄱ --------

6. exception -> page_fault 수정

	if (not_present || write || user) 
	{
		exit(-1);
	}

22개 fail 남아서, 일단 commit
1. sema_init(load_sema)를 다시 init_thread 에서 처리

2. process_fork 함수에서 sema_down 밑줄 / return tid 윗줄에 printf 찍어봄

    -> sema down도 잘 되고 sema up도 잘 되는듯 ? + tid도 생성이 되는듯 ? (4로찍힘)
	 
3. printf 찍으면 thread_current에서 ASSERT 걸림 (status 확인 과정에서) 
    -> 주석처리하면 잘찍힘
	 
4. 준혁이형 코드랑 최대한 비슷하게 ㄱㄱ

    1) thread struct에서 copied_if 추가
    2) tf 주지 말고 copied_if 전달
    3) file_deny, filesys_lock 제거

 

1. fork-multiple 이새끼 make check 하면 exit status 5 떠야 할 때 -1 뜨면서 틀리는데
   개별실행 돌리면 정답 그대로 출력됨 뭐하는새끼임 ?
	 
2. 갑자기 args 테스트들이 진행 안될 때가 있는데, 그냥 make clean 하고 다시 make check 하면 됨

3. fork-multiple / multi-recurse 둘이 되다 안되다 함

4. 확정적으로 안되는 건 rox 3문제 + syn-read + syn-write + multi-oom

 

1. filesys_lock 사용함
   file_read / file_write 할때 앞뒤로
	 
2. file_deny 사용함
   open 할때 / close 할때 allow 
	 
3. syscall.c 에서 case / switch 들어갈 때, exec에서 
   break; 안걸어줌 레전드사건
	 
4. multi-recurse + syn-read 과정에서 계속 backtrace 해보니
   exec에서 뭔가 문제가 발생한 것 같음

 

1. file_deny 해놓고 돌리니까 5 fail 뜸

2. filesys lock 걸고 다시 ㄱㄱ

3. exit_sema라는 semaphore를 process_wait에서 down, process_exit에서 up

4. process_exit 할 때 file resource 초기화하는 과정이 없다 .?

 

sync-read 테스트 통과를 해결하지 못하고, 

multi-oom 테스트 트라이 중이던 재혁님 코드를 이식해와서 마지막 문제 해결을 해보려 했으나

실패 ..