본문 바로가기
개발자 도전기/[OS] pintOS

pintOS | Project 2 | 시스템콜 구현 (fork, wait, read, write, seek, tell, close)

by 답수 2021. 10. 13.
728x90
반응형

 

pintOS | Project 2 | 시스템콜 구현 (halt, exit, exec, create, remove, open, filesize)

 

pintOS | Project 2 | 시스템콜 구현 (halt, exit, exec, create, remove, open, filesize)

pintOS | 명령어 실행 기능 구현(Command Line Parsing) (수정 중) pintOS | 명령어 실행 기능 구현(Command Line Parsing) pintos project2 설명 보기 프로젝트1에서 실행했던 alarm clock, scheduling 등 모든..

dapsu-startup.tistory.com

 

 

저번 글에 이어 시스템 콜 구현 계속 작성 ㄱㄱ 일단 시스템콜 핸들러 함수를 다시 한 번 보자

void syscall_handler (struct intr_frame *f) {
	// TODO: Your implementation goes here.
	char *fn_copy;

	/*
	 x86-64 규약은 함수가 리턴하는 값을 rax 레지스터에 배치하는 것
	 값을 반환하는 시스템 콜은 intr_frame 구조체의 rax 멤버 수정으로 가능
	 */
	switch (f->R.rax) {		// rax is the system call number
		case SYS_HALT:
			halt();			// pintos를 종료시키는 시스템 콜
			break;
		case SYS_EXIT:
			exit(f->R.rdi);	// 현재 프로세스를 종료시키는 시스템 콜
			break;
		case SYS_FORK:
			f->R.rax = fork(f->R.rdi, f);
			break;
		case SYS_EXEC:
			if (exec(f->R.rdi) == -1) {
				exit(-1);
			}
			break;
		case SYS_WAIT:
			f->R.rax = process_wait(f->R.rdi);  // <-- process_wait 함수 호출
			break;
		case SYS_CREATE:
			f->R.rax = create(f->R.rdi, f->R.rsi);
			break;
		case SYS_REMOVE:
			f->R.rax = remove(f->R.rdi);
			break;
		case SYS_OPEN:
			f->R.rax = open(f->R.rdi);
			break;
		case SYS_FILESIZE:
			f->R.rax = filesize(f->R.rdi);
			break;
		case SYS_READ:
			f->R.rax = read(f->R.rdi, f->R.rsi, f->R.rdx);
			break;
		case SYS_WRITE:
			f->R.rax = write(f->R.rdi, f->R.rsi, f->R.rdx);
			break;
		case SYS_SEEK:
			seek(f->R.rdi, f->R.rsi);
			break;
		case SYS_TELL:
			f->R.rax = tell(f->R.rdi);
			break;
		case SYS_CLOSE:
			close(f->R.rdi);
			break;
		default:
			exit(-1);
			break;
	}
}

 

 

| fork()

현재 pintos는 프로세스 구조체에 부모와 자식 관계를 명시하는 코드가 없다. 즉 부모와 자식의 구분이 없고, 자식 프로세스의 정보를 알지 못 하기 때문에 자식의 시작/종료 전에 부모 프로세스가 종료되는 현상이 발생하여 프로그램이 실행되지 않는 경우가 있다. 이를 위해 fork() 함수를 사용한다. fork는 자식 프로세스를 생성하고 프로그램을 실행시키는 시스템 콜이다.

 

여기서 본질적인 질문이 생긴다. 자식 프로세스를 왜 복제하는 것일까?

 

그 이유 첫 번째로, 프로세스를 생성하는 과정이 간단하지 않기 때문이다. 프로세스를 생성하고 이와 관련된 많은 자료 구조들이 있는데 이것들을 다 새로 만드는 것보다는 기존에 생성된 프로세스의 자료 구조를 복사하는 것이 더 효율적이다. 

 

두 번째, 새로 생성된 프로세스의 엔트리 포인트를 어디로 할 것인가의 이슈도 있다. 일반적으로 프로그램이 실행될 때 main()함수를 엔트리 포인트로 하는데, 새로 프로세스가 생성될 때마다 main()함수부터 시작하는 것은 매우 비효율적이고 프로세스간 통신에서 문제가 발생할 수 있다. 그래서 부모 프로세스를 복제하면 부모 프로세스가 호출한 시점을 엔트리 포인트로 가지기 때문에 더욱 직관적이고 발생할 수 있는 문제를 줄일 수 있다.

나중에 다시 정리할 때 참고할 블로그: https://young-blog.tistory.com/29

 

부모 프로세스에서 자식 프로세스가 생성될 때, 부모 프로세스는 자식 프로세스가 끝날 때까지 wait()하고, 자식 프로세스는 생성되면서 exec()함수를 호출하게 된다. 

 

코드로 과정을 살펴보자면, init에서 main() 함수를 통해 run_actions() 함수가 실행되고, 현재 스프로세스가 'run'이면 run_task() 함수가 호출된다. 그리고 실행 중인 프로세스가 유저 프로세스라면 process_wait() 함수를 통해 process_create_initd() 함수를 호출한다.

// ../threads/init.c


/* Runs the task specified in ARGV[1]. */
static void run_task (char **argv) {
		...
		process_wait (process_create_initd (task));
		...
}

 

process_create_initd()  함수에서 스레드를 생성하면서 initd() 함수를 호출한다.

// ../userprog/process.c


tid_t process_create_initd (const char *file_name) {
	...
	tid = thread_create (file_name, PRI_DEFAULT, initd, fn_copy);
	...
	return tid;
}

 

initd() 함수를 통해 process_exec() 함수를 호출하게 되고, 새로 생성된 스레드(자식 프로세스)를 실행한다.

// ../userprog/process.c


static void initd (void *f_name) {
	... 
	if (process_exec (f_name) < 0)
	...
}

 

그 후 자식 프로세스 실행이 종료되면, wait하던 부모 프로세스가 다시 실행된다.

 

오른쪽 그림과 같이 실행시키기 위해 코드를 구현하고 있는 것임!

 

 

 

 

본론으로 다시 돌아와서! fork() 함수는 아래와 같이 구현하면 된다.

// ../userporg/syscall.c


// 부모: 성공 시 자식 pid 반환, 실패 시 -1
// 자식: 성공 시 0 반환
tid_t fork(const char *thread_name, struct intr_frame *f) {
	return process_fork(thread_name, f);
}

 

process_fork()를 타고 들어가서 다음과 같이 구현한다.

// ../userprg/process.c


/* Clones the current process as `name`. Returns the new process's thread id, or
 * TID_ERROR if the thread cannot be created. */
tid_t process_fork(const char *name, struct intr_frame *if_) {
	// 현재 프로세스를 새 프로세스로 복제
	struct thread *cur = thread_current();
	memcpy(&cur->parent_if, if_, sizeof(struct intr_frame));

	tid_t tid = thread_create(name, PRI_DEFAULT, __do_fork, cur);
	if (tid == TID_ERROR) {
		return TID_ERROR;
	}

	struct thread *child = get_child_with_pid(tid);
	sema_down(&child->fork_sema);
	if (child->exit_status == -1) {
		return TID_ERROR;
	}

	return tid;
}

  + thread.h에서 thread 구조체에 struct semaphore fork_sema;추가

 

 

 

여기서 또 __do_fork 함수로 이동한다. pintos에서 fork 부분이 제일 어렵고 복잡하다;; 함수 여러 개가 다 연결되어 있다 보니 아직도 이해하기 어려운 부분들이 많다... 여하튼 일단 __do_fork 함수로 따라가자.

// ../userporg/process.c


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;

	/* 2. Duplicate PT */
	current->pml4 = pml4_create();
	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
	if (!pml4_for_each (parent->pml4, duplicate_pte, parent))  // to copy entire user memory space including corresponding pagetable structures
		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.*/

	// multi-oom) Failed to duplicate
	if (parent->fd_idx == FDCOUNT_LIMIT)
		goto error;

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

		// If 'file' is already duplicated in child, don't duplicate again but share it
		bool found = false;
		if (!found) {
			struct file *new_file;
			if (file > 2)
				new_file = file_duplicate(file);
			else
				new_file = file;

			current->fd_table[i] = new_file;
		}
	}
	current->fd_idx = parent->fd_idx;

	// child loaded successfully, wake up parent in process_fork
	sema_up(&current->fork_sema);

	/* Finally, switch to the newly created process. */
	if (succ)
		do_iret(&if_);

error:
	current->exit_status = TID_ERROR;
	sema_up(&current->fork_sema);
	exit(TID_ERROR);
}

pintos에서 모든 프로세스 생성의 대부분 작업은 여기 __do_fork()에서 처리된다.

 

그 다음 부모 프로세스의 가상 메모리 주소 공간을 복사하는 duplicate_pte() 함수는 아래와 같이 구현한다.

/* Duplicate the parent's address space by passing this function to the
 * pml4_for_each. This is only for the project 2. */
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; // return false ends pml4_for_each, which is undesirable - just return true to pass this kernel va
	}

	/* 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_USER);
	if (newpage == NULL) {
		printf("[fork-duplicate] failed to palloc new page\n"); // #ifdef DEBUG
		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); // *PTE is an address that points to parent_page

	/* 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. */
		printf("Failed to map user virtual page to given physical frame\n"); // #ifdef DEBUG
		return false;
	}

	return true;
}

 

 

| wait()

wait() 함수는 자식 프로세스가 모두 종료될 때까지 대기하고, 자식 프로세스가 올바르게 종료되었는지 확인하는 기능을 한다.

// ../userprog/process.c


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_child_with_pid(child_tid);

	// [Fail] Not my child
	if (child == NULL)
		return -1;

	// Parent waits until child signals (sema_up) after its execution
	sema_down(&child->wait_sema);

	int exit_status = child->exit_status;

	// Keep child page so parent can get exit_status
	list_remove(&child->child_elem);
	sema_up(&child->free_sema); // wake-up child in process_exit - proceed with thread_exit
	return exit_status;	
}

 

 

| read()

read()는 열린 파일의 데이터를 읽는 시스템 콜이다. 

int read(int fd, void *buffer, unsigned size) {
	check_address(buffer);

	int read_result;
	struct thread *cur = thread_current();
	struct file *file_fd = find_file_by_fd(fd);

	if (fd == 0) {
		// read_result = i;
		*(char *)buffer = input_getc();		// 키보드로 입력 받은 문자를 반환하는 함수
		read_result = size;
	}
	else {
		if (find_file_by_fd(fd) == NULL) {
			return -1;
		}
		else {
			lock_acquire(&filesys_lock);
			read_result = file_read(find_file_by_fd(fd), buffer, size);
			lock_release(&filesys_lock);
		}
	}
	return read_result;
}

buffer 파라미터는 읽은 데이터를 저장할 버퍼의 주소값이고, size는 읽을 데이터의 크기이다.

 

그리고 race conditon을 피하기 위해 syscall.h에 strcut lock filesys_lock  을 선언해준다.

 

fd값이 0일 때는 표준입력이기 때문에 input_getc() 함수를 이용하여 키보드의 데이터를 읽어 버퍼에 저장한다.

 

이 때, 나는 계속 fail되어 -1이 출력되었었다. 그래서 함수를 하나씩 따라가면서 디버깅을 해봤다. 먼저 input_getc()함수를 따라가봤다.

// ../devices/input.c


/* Retrieves a key from the input buffer.
   If the buffer is empty, waits for a key to be pressed. */
uint8_t
input_getc (void) {
	enum intr_level old_level;
	uint8_t key;

	old_level = intr_disable ();
	key = intq_getc (&buffer);
	serial_notify ();
	intr_set_level (old_level);

	return key;
}

이 함수를 보면 key값을 리턴하는데, key는 intq_getc()라는 함수의 리턴값을 받는다. 저게 뭘까? 당연히 모른다 ㅋ 또 따라가야 한다.

// ../devices/intq.c


/* Removes a byte from Q and returns it.
   Q must not be empty if called from an interrupt handler.
   Otherwise, if Q is empty, first sleeps until a byte is
   added. */
uint8_t
intq_getc (struct intq *q) {
	uint8_t byte;

	ASSERT (intr_get_level () == INTR_OFF);
	while (intq_empty (q)) {
		ASSERT (!intr_context ());
		lock_acquire (&q->lock);
		wait (q, &q->not_empty);
		lock_release (&q->lock);
	}

	byte = q->buf[q->tail];
	q->tail = next (q->tail);
	signal (q, &q->not_full);
	return byte;
}

음~ 일단 intq 구조체는 buffer 사이즈를 담는 큐인 것을 알았고, q의 tail은 기존 데이터를 읽기 위해 저장하는 정수값이다. (참고로 q는 int head도 가지고 있는데, 이는 새로운 데이터가 써지는(written) 정수값)

 

어찌됐든 읽어야 하는 데이터를 반환해야 하는데 중요한 점은 input_getc() 함수 내에서 q가 lock를 부여 받는다는 것이다. 즉 내가 read 시스템 콜을 구현할 때 lock을 부여하는 것과 중복되면 안 된다는 것이다. 그래서 lock_aquire() 함수 위치를 file_read() 함수를 호출하는 코드 위에 배치하니 pass되었다.

 

 

| write()

wirte() 함수는 열린 파일의 데이터를 기록하는 시스템 콜이다. 역시 fd값이 1일 때는 표준 출력이기 때문에 1일 시 putbuf() 함수를 사용하여 버퍼에 저장된 데이터를 화면에 출력한다.

// ../userprog/syscall.c


// buffer로부터 사이즈 쓰기
int write(int fd, const void *buffer, unsigned size) {
	check_address(buffer);

	int write_result;
	lock_acquire(&filesys_lock);
	if (fd == 1) {
		putbuf(buffer, size);		// 문자열을 화면에 출력하는 함수
		write_result = size;
	}
	else {
		if (find_file_by_fd(fd) != NULL) {
			write_result = file_write(find_file_by_fd(fd), buffer, size);
		}
		else {
			write_result = -1;
		}
	}
	lock_release(&filesys_lock);
	return write_result;
}

 

 

| seek()

seek()을 열린 파일의 위치(offset)를 이동하는 시스템 콜이다.

// ../userprog/syscall.c


// 파일 위치(offset)로 이동하는 함수
void seek(int fd, unsigned position) {
	struct file *seek_file = find_file_by_fd(fd);
	if (seek_file <= 2) {		// 초기값 2로 설정. 0: 표준 입력, 1: 표준 출력
		return;
	}
	seek_file->pos = position;
}

 

 

| tell()

열린 파일의 위치를 알려주는 시스템 콜!

// ../userprog/syscall.c


// 파일의 위치(offset)을 알려주는 함수
unsigned tell(int fd) {
	struct file *tell_file = find_file_by_fd(fd);
	if (tell_file <= 2) {
		return;
	}
	return file_tell(tell_file);
}

 

 

| close()

close()는 열린 파일을 닫는 시스템 콜이다. 파일을 닫고 fd를 제거한다.

// 열린 파일을 닫는 시스템 콜. 파일을 닫고 fd제거
void close(int fd) {
	struct file *fileobj = find_file_by_fd(fd);
	if (fileobj == NULL) {
		return;
	}
	remove_file_from_fdt(fd);
}

 

계속 구현을 하다 보니 all pass 나옴

 

그런데 아직도 __do_fork()나 duplicate_pte() 등 이해하기 어려운 부분들이 많다ㅠㅠ 목요일이 오기 전까지 확실하게 잡고 넘어가자!

 

+ 추가로 dup2 부분도 구현가즈아...

728x90
반응형

댓글