PintOS

PintOS Project 2 : system call (1)

wook's note 2024. 9. 18. 16:47

 

user program은 컴퓨터 전체를 소유한다는 환상 하에 쓰여진다. 예를 들어서 메모리, cpu 등 자원들을 사용하는데 우리는 이것을 추상화 하여 각 프로세스가 진짜로 자원을 다 사용하고 있다고 착각하게 만든다. 또는 이 환상을 만족시키는 방향으로 관리를 해야 한다. 따라서 프로세스가 하면 안되는 작업을 하기 위해 시스템 콜을 호출하게 된다.

 

시스템 콜이란 유저프로그램이 못하는 작업을 커널에게 부탁을 하는 것이라고 생각할 수 있다. 

따라서 project 2 의 목표는 OS와 user program이 상호작용을 할 수 있도록 시스템 콜 함수들을 구현하는 것이다.

 

 

함수의 흐름

 

 

위의 그림을 간단히 설명해보자면,

  1. 유저프로그램이 exec() 함수를 실행한다.
  2. exec()는 유저프로그램이 실행하지 못하는 작업이므로 커널에게 요청하는 시스템 콜을 보낸다. 이 때 어떤 시스템 콜인지 알려주는 것이 있어야 한다. 
  3. 커널은 시스템 콜이 어떤 경우인지를 확인하여 해당하는 작업을 처리한다. 위의 경우 exec 요청을 exec함수를 통해 처리해야 한다.
  4. 커널 함수인 process_exec()에서 로딩과 같은 작업을 진행하고 완료했으면 반환한다. 

 

lib/syscall-nr.h 에는 어떤 시스템 콜이 몇 번의 숫자와 연결 되어있는지 확인할 수 있다.

 

 

 


 

 

코드구현

void syscall_handler (struct intr_frame *f UNUSED) {
switch (f->R.rax)
	{
	case SYS_HALT:
		halt_syscall();
		break;
	case SYS_EXIT:
		exit_syscall(f->R.rdi);
		break;
	case SYS_FORK:
		f->R.rax =fork_syscall(f->R.rdi);
		break;
	case SYS_EXEC:
		if (exec_syscall(f->R.rdi) == -1)
        {
            exit_syscall(-1);
        }
		break;
	case SYS_WAIT:
		f->R.rax =wait_syscall(f->R.rdi);
		break;
	case SYS_CREATE:
		f->R.rax = create_syscall(f->R.rdi,f->R.rsi);
		break;
	case SYS_REMOVE:
		f->R.rax = remove_syscall(f->R.rdi);
		break;
	case SYS_OPEN:
		f->R.rax = open_syscall(f->R.rdi);
		break;
	case SYS_FILESIZE:
		f->R.rax = filesize_syscall(f->R.rdi);
		break;
	case SYS_READ:
		f->R.rax = read_syscall(f->R.rdi, f->R.rsi, f->R.rdx);
		break;
	case SYS_WRITE:
		f->R.rax = write_syscall(f->R.rdi, f->R.rsi, f->R.rdx);
		break;
	case SYS_SEEK:
		seek_syscall(f->R.rdi, f->R.rsi);
		break;
	case SYS_TELL:
		f->R.rax = tell_syscall(f->R.rdi);
		break;
	case SYS_CLOSE:
		close_syscall(f->R.rdi);
		break;
	default:
		exit_syscall(-1);
		break;
	}
}

 

다양한 system call  경우들을 case로 관리하는 system call handler 함수.

여기서 인자들은 f->R.rdi ... 이러한 방식대로 넘겨주게 된다. 이는 user level application에서 인자들을 %rdi, %rsi, %rdx, %r10, %r9, %r8  순서대로 담아놓기 때문이다.또한 rax는 return 값을 담아놓는 레지스터이므로 시스템 콜의 번호가 담겨져 있다.

 


 

시스템 콜 기능을 구현하기 위해, 유저 가상 주소 공간에 데이터를 읽고 쓰기 위한 방법을 제공해야한다. 시스템 콜의 인자로 들어온 포인터로부터 데이터를 읽어야 할 때 가상주소공간에 데이터를 읽어야하는 방법을 알아야한다.

유효하지 않은 포인터는 예를들어 커널 메모리를 가르키는 포인터 등을 가리키면 유저프로세스를 종료시킴으로써 대응할 수 있어야 한다.

 

유효하지 않은 포인터 종류

  1. invalid pointer : NULL 등 mapping 되지 않은 주소
  2. kernel pointer : KERN_BASE 상위 주소
  3. block partially in one of those regions : 영역의 일부가 비정상 블록

정리하자면 시스템 콜에서 주어지는 인자가 유저 메모리에 포함되는 주소여야 한다는 의미이다. 

void check_address(void *addr) {
    struct thread *curr = thread_current();

    if (is_kernel_vaddr(addr) || addr == NULL || pml4_get_page(curr->pml4, addr) == NULL)
        exit_syscall(-1);
}

 

kernel 주소이거나 NULL 주소이거나 페이지 테이블에 매핑이 되지 않은 주소면 프로세스를 종료하는 작업을 하는 함수.

이를 통해 유저 메모리에 포함되는 주소를 확인한다.

 

 


thread struct에 추가해야 할 사항이 있다. 

struct thread{
    #ifdef USERPROG
        /* Owned by userprog/process.c. */
        uint64_t *pml4;                     /* Page map level 4 */


        /** #Project 2: System Call */
        int exit_status;

        int fd_idx;              // 파일 디스크립터 인덱스
        struct file **fd_table;       // 파일 디스크립터 테이블
        struct file *running_file;  // 실행중인 파일

        struct intr_frame parent_if;  // 부모 프로세스 if
        struct list child_list;
        struct list_elem child_elem;

        struct semaphore fork_sema;  // fork가 완료될 때 signal
        struct semaphore exit_sema;  // 자식 프로세스 종료 signal
        struct semaphore wait_sema;  // exit_sema를 기다릴 때 사용
    #endif
};

------
init_thread() 에 추가
#ifdef USERPROG
    /** #Project 2: System Call  */
    t->running_file = NULL;

    list_init(&t->child_list);
    sema_init(&t->fork_sema, 0);
    sema_init(&t->exit_sema, 0);
    sema_init(&t->wait_sema, 0);
    /** -----------------------  */
#endif
------

thread_create() 에 추가 _ thread 초기화

#ifdef USERPROG
    /** #Project 2: System Call - 구조체 초기화 */
    t->fd_table = palloc_get_multiple(PAL_ZERO, FDT_PAGES);
    if (t->fd_table == NULL)
        return TID_ERROR;

    t->exit_status = 0;  // exit_status 초기화

    t->fd_idx = 3;
    t->fd_table[0] = STDIN;   // stdin 예약된 자리 (dummy)
    t->fd_table[1] = STDOUT;  // stdout 예약된 자리 (dummy)
    t->fd_table[2] = STDERR;  // stderr 예약된 자리 (dummy)
    /** ---------------------------------------- */

    /** #Project 2: System Call - 현재 스레드의 자식 리스트에 추가 */
    list_push_back(&thread_current()->child_list, &t->child_elem);
#endif

 

 

 

 

System Call functions

 

1. halt() : 프로그램 종료

void halt_syscall(){
	power_off();
}

 

2. exit() : 프로세스 종료

void exit_syscall(int status){
	struct thread *cur = thread_current();
	
	cur->exit_status = status;

	printf("%s: exit(%d)\n", cur->name, cur->exit_status);

	thread_exit();
}


----------

void process_exit (void) {
	struct thread *curr = thread_current ();
	/* TODO: Your code goes here.
	 * TODO: Implement process termination message (see
	 * TODO: project2/process_termination.html).
	 * TODO: We recommend you to implement process resource cleanup here. */


	for (int fd = 0; fd <curr->fd_idx; fd++){
		close_syscall(fd);
	}
	palloc_free_multiple(curr->fd_table,FDT_PAGES);

	file_close(curr->running_file);

	process_cleanup ();

	sema_up(&curr->wait_sema); // 자식 프로세스가 종료될 때 부모에게 signal

	sema_down(&curr->exit_sema); // 부모가 종료시켜줄 때 까지 대기

}

 

 

3. fork() : 자식 프로세스 생성하고 부모 프로세스의 값을 복제

pid_t fork_syscall(const char *thread_name){
	check_address(thread_name);

	return process_fork(thread_name,NULL);
}


----------

tid_t process_fork (const char *name, struct intr_frame *if_ UNUSED) {
	THREAD *curr = thread_current();
	struct intr_frame *tf = (pg_round_up(rrsp()) - sizeof(struct intr_frame));
	memcpy(&curr->parent_if, tf,sizeof(struct intr_frame));

	tid_t tid = thread_create(name,PRI_DEFAULT,__do_fork,curr);

	if (tid == TID_ERROR)
		return TID_ERROR;
	
	THREAD *child = get_thread(tid);

	sema_down(&child->fork_sema); // waiting for child fork complete

	if(child->exit_status == TID_ERROR)
		return TID_ERROR;

	return child->tid; // 부모 프로세스의 리턴값 : 생성한 자식 프로세스의 tid
}
static void __do_fork (void *aux) {
	struct intr_frame if_;
	struct thread *parent = (struct thread *) aux;
	struct thread *current = thread_current ();
	/* TODO: somehow pass the parent_if. (i.e. process_fork()'s if_) */
	struct intr_frame *parent_if;
	bool succ = true;

	
	parent_if = &parent->parent_if;

	/* 1. Read the cpu context to local stack. */
	memcpy (&if_, parent_if, sizeof (struct intr_frame));
	if_.R.rax = 0; // 자식 프로세스의 return 값 0 

	/* 2. Duplicate PT */
	// 0으로 만들어준 페이지 생성
	current->pml4 = pml4_create();
	if (current->pml4 == NULL)
		goto error;

	// 페이지 활성화 , kernel stack 연결
	process_activate (current);
#ifdef VM
	supplemental_page_table_init (&current->spt);
	if (!supplemental_page_table_copy (&current->spt, &parent->spt))
		goto error;
#else
	// 페이지 테이블 4단계 확인하기
	if (!pml4_for_each (parent->pml4, duplicate_pte, parent))
		goto error;
#endif
	// 부모가 가진 fd 복사 
	if(parent->fd_idx >= FDCOUNT_LIMIT)
		goto error;

	current->fd_idx = parent->fd_idx;
	struct file *file;
	for (int fd = 0; fd < FDCOUNT_LIMIT ; fd ++){
		file = parent->fd_table[fd];
		if (file == NULL)
			continue;

		if (file >STDERR){
			current->fd_table[fd] = file_duplicate(file);
		}else{
			current->fd_table[fd] = file;
		}
	}
	// signal to parent , fork complete
	sema_up(&current->fork_sema);

	process_init ();

	/* Finally, switch to the newly created process. */
	if (succ)
		do_iret (&if_);
error:
	sema_up(&current->fork_sema);
	exit_syscall(-1);
}
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. */
	if (is_kernel_vaddr(va))
		return true;

	/* 2. Resolve VA from the parent's page map level 4. */
	parent_page = pml4_get_page (parent->pml4, va);
	if (parent_page == NULL)
		return false;

	/* 3. TODO: Allocate new PAL_USER page for the child and set result to
	 *    TODO: NEWPAGE. */
	newpage = palloc_get_page(PAL_ZERO);
	if (newpage == NULL)
		return false;

	/* 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);
	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)) {
		/* 6. TODO: if fail to insert page, do error handling. */
		return false;
	}
	return true;
}

 

 

4. exec() : 명령을 실행하는 함수

int exec_syscall(const char *cmd_line){
	check_address(cmd_line);

	off_t size = strlen(cmd_line)+1;
	char *cmd_copy = palloc_get_page(PAL_ZERO);

	if(cmd_copy == NULL){
		return -1;
	}

	memcpy(cmd_copy,cmd_line,size);

	return process_exec(cmd_copy);
}


----------

int process_exec (void *f_name) {
	char *file_name = f_name;
	bool success;

	/* We cannot use the intr_frame in the thread structure.
	 * This is because when current thread rescheduled,
	 * it stores the execution information to the member. */
	struct intr_frame _if;
	_if.ds = _if.es = _if.ss = SEL_UDSEG;
	_if.cs = SEL_UCSEG;
	_if.eflags = FLAG_IF | FLAG_MBS;

	/* We first kill the current context */
	process_cleanup ();

	// ==== argument parsing ====
	// argv_list , argc_num 만들기
	char *argv_list[64]; // pintos에서 command line의 길이에는 128바이트 제한이므로 인자를 담을 배열을 64로 지정
	char *token, *save_ptr;
	int agrc_num = 0;

	token = strtok_r(file_name, " ", &save_ptr);
	argv_list[agrc_num] = token; // arg_list[0] = file_name_first

	while (token != NULL)
	{
		token = strtok_r(NULL, " ", &save_ptr);
		agrc_num++;
		argv_list[agrc_num] = token;
	}
	// ==== argument parsing ====

	/* And then load the binary */
	success = load (file_name, &_if);
	if (!success){
		palloc_free_page (file_name);
		return -1;
	}
	setup_argument(argv_list,agrc_num,&_if);
	/* If load failed, quit. */
	palloc_free_page (file_name);
	/* Start switched process. */
	do_iret (&_if);
	NOT_REACHED ();
}

 

 

5. wait() : 부모 프로세스가 자식 프로세스의 작업 완료와 프로세스 종료를 기다리는 함수

int wait_syscall(pid_t tid){
	return process_wait(tid);
}


-----------

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. */
	struct thread *child = get_thread(child_tid);
	if(child == NULL)
		return -1;
	
	// 현재 부모프로세스 시점
	sema_down(&child->wait_sema); // 자식 프로세스가 종료될 때 까지 대기
	list_remove(&child->child_elem);
	sema_up(&child->exit_sema); // 자식 프로세스가 죽을 수 있도록 signal

	return child->exit_status;
}