거북이의 쉼터

(2022.02.15) System Calls 구현 (2/4) 본문

코딩 삽질/KAIST PINTOS (CS330)

(2022.02.15) System Calls 구현 (2/4)

onlim 2022. 2. 15. 16:58

1. 서론 및 필요 내용 설명

기숙사 1인실 떨어져서 대전에 방 계약하고 오느라 늦었다. 내 운이 그렇지 뭐... 나머지 syscall들을 구현한다.

 


2. 구현해야 하는 것

read, write, fork만 남아있고, 이들을 구현한 후, 나머지 syscall들과 함께 syscall_handler에 종합하기로 한다. 그리고 지난 포스팅까지 구현했던 syscall 함수들이 기존 함수들과 충돌할 것이 우려되어 내가 구현한 함수의 이름에는 _(언더바)를 앞에 붙여 구분할 수 있도록 수정하였다.

 


3. 구현 과정

3-1. read

지난 번 코드를 검토하다가 주어진 fd에 해당하는 fd_entry를 가져오는 코드에서 fd를 찾지 못할 경우 처리가 미흡하다는 사실을 알게 되었다. 우선 fd_search를 수정한다.

/* search for fd_entry corresponding to given fd 
 * returns NULL if fails to find */
struct fildes *fd_search (int fd)
{
	struct list_elem *e;
	struct thread *curr = thread_current ();
	struct fildes *fd_entry, *res = NULL;

	for (e = list_begin(&curr->fd_list); e != list_end(&curr->fd_list); e = list_next(e))
	{
		fd_entry = list_entry(e, struct fildes, fd_elem);
		if (fd_entry->fd == fd)
		{
			res = fd_entry;
			break;
		}
	}

	return res;
}

주어진 fd에 해당하는 fd_entry를 가져온다. fd가 만약 stdin에 연결이 되어 있다면 input_getc로 입력을 받아오라고 매뉴얼에 적혀있다. input_getc의 코드를 살펴보면 키보드 입력 버퍼에서 한 글자씩 반환한다는 것을 볼 수 있다. 

/* 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;
}

이를 사용해 루프를 돌면서 주어진 버퍼에 한 글자씩 채우면 된다는 것을 알았다.

 

fd로 연결된 파일이 stdout일 경우에는 어떻게 해야할까? 테스트 케이스에서 제시한 바로는 조용히 실패하거나 코드를 종료하라고 한다. 

/* Try reading from fd 1 (stdout), 
   which may just fail or terminate the process with -1 exit
   code. */

실제 리눅스에서 테스트했을 때는 아무 exit도 발생하지 않으므로 그냥 조용히 실패하도록 코드를 구현하자.

 

일반적인 파일일 경우, 이전 포스팅에 언급한대로 file_read를 사용하여 버퍼를 채워넣도록 한다. 여기까지 설명한대로 그대로 구현하면 아래와 같다.

int _read (int fd, void *buffer, unsigned size)
{
	int res;
	unsigned i;

	filesys_enter();
	struct fildes *fd_entry = fd_search(fd);
	
	if (fd_entry == NULL) // not found
		res = -1;

	else
	{
		if (fd_entry->fp == 0) // stdin
		{
			for (i = 0; i < size; i++)
			{
				*(uint8_t *)(buffer + i) = input_getc();
				if (*(uint8_t *)(buffer + i) == 0)
					break;
			}

			res = (int)i;
		}

		else if (fd_entry->fp == 1) // stdout
			res = -1;

		else
			res = (int)file_read(fd_entry->fp, buffer, size);
	}

	filesys_exit();
	return res;
}

다음으로 넘어간다.

3-2. write

stdin에 대해서는 조용히 실패할 것, 그리고 stdout에 대해서는 putbuf를 사용할 것, 일반 파일에 대해서는 file_write를 사용한다는 것을 염두하고 구현한다. 참고로 putbuf는 콘솔창에 주어진 버퍼의 내용을 주어진 길이만큼 작성하는 함수이다. 

/* Writes the N characters in BUFFER to the console. */
void
putbuf (const char *buffer, size_t n) {
	acquire_console ();
	while (n-- > 0)
		putchar_have_lock (*buffer++);
	release_console ();
}

이를 기반으로 write를 작성하면 아래와 같다.

int _write (int fd, const void *buffer, unsigned size)
{
	int res;
	unsigned i;

	filesys_enter();
	struct fildes *fd_entry = fd_search(fd);

	if (fd_entry == NULL)
		res = -1;

	else
	{
		if (fd_entry->fp == 0) // stdin
			res = -1;

		else if (fd_entry->fp == 1) // stdout
		{
			putbuf(buffer, size);
			res = (int)size;
		}

		else
			res = file_write(fd_entry->fd, buffer, size);
	}

	filesys_exit();
	return res;
}

3-3. fork 

이제 대망의 fork다. 이전 포스팅에서 언급한대로 작업을 시작한다. thread 구조체에 우선 fork에 사용될 semaphore인 fork_sema를 추가한다. 지난 포스팅에서 thread_create와 init_thread에서 parent_tid 부분이 제대로 수정이 안 되어 있어 해당 부분도 고쳐주었다. idle과 main thread는 parent_tid를 고려하지 않도록 TID_ERROR를 부여한다.

static void
init_thread (struct thread *t, const char *name, int priority) {
	...
	if (strcmp(name, "idle") == 0 || strcmp(name, "main") == 0)
		t->parent_tid = TID_ERROR;
	else
		t->parent_tid = thread_current()->tid;
	...
	sema_init(&t->fork_sema, 0);
	...
}

thread_create가 실패할 경우 fork는 바로 TID_ERROR를 반환하면 된다. 실패하지 않았을 경우에는 완전한 복제가 이루어질때까지 기다리도록 sema_down을 한다. 복제의 성공/실패 여부를 판단할 때가 된다면 해당 thread에서 sema_up이 호출되어 현재 thread가 block 상태에서 벗어날 것이다. 실패했다면 해당 thread의 exit_status에 -1이 들어있을 것이므로, TID_ERROR를 반환하도록 한다. 성공했다면 가장 처음에 thread_create를 통해 얻은 child_tid를 반환하도록 한다.

 

문제는 인자 intr_frame *if_의 정보를 어떻게든 __do_fork로 넘겨야 한다는 것이다. thread 구조체에 넣으려고 했는데 주석의 해당 문구가 좀 걸린다.

/*
 *    1. First, `struct thread' must not be allowed to grow too
 *       big.  If it does, then there will not be enough room for
 *       the kernel stack.  Our base `struct thread' is only a
 *       few bytes in size.  It probably should stay well under 1
 *       kB.
 */

intr_frame의 크기가 생각보다 크긴한데, 일단 다른 방법이 마땅치 않으므로 intr_frame 구조체를 thread 구조체 내에 내장시켜서 정보를 복사하도록 하자. 이름은 fork_frame이라 정한다.

/* 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_) {
	/* Clone current thread to new thread.*/
	memcpy (&thread_current()->fork_frame, if_, sizeof (struct intr_frame));
	tid_t child_tid = thread_create (name, PRI_DEFAULT, __do_fork, thread_current ());
	
	if (child_tid == TID_ERROR)
		return TID_ERROR;

	// search for child process thread
	struct thread *child_thread = search_child (child_tid);

	// semaphore down
	sema_down(child_thread->fork_sema);
	
	if (child_thread->exit_status == -1)
		return TID_ERROR;

	return child_tid;
}

이제 __do_fork를 수정할 차례이다. 메모리 매핑 복사는 duplicate_pte에서 이루어지므로 열린 파일을 복사하는 것부터 작업하자. 우선 현재 새로운 thread가 생성될 때 0과 1 fd를 할당한 fildes 구조체가 thread에 들어가지 않고 있다. 해당 파일들을 넣어줄 필요가 있다. 이는 후에 프로세스 단계에서 할당 해제해주는 것이 적합하므로 프로세스를 시작(initiate)할 때마다 호출되는 process_init 함수에 해당 코드를 넣어준다.

static void
process_init (void) {
	struct thread *current = thread_current ();

	struct fildes *stdin_fildes = (struct fildes *)malloc(sizeof(struct fildes));
	struct fildes *stdout_fildes = (struct fildes *)malloc(sizeof(struct fildes));

	stdin_fildes->fd = stdin_fildes->fp = 0;
	stdout_fildes->fd = stdout_fildes->fp = 1;

	list_insert(&current->fd_list, &stdin_fildes->fd_elem);
	list_insert(&current->fd_list, &stdout_fildes->fd_elem);
}

현재 process_init 에서는 stdin과 stdout를 fd_list에 할당하는 역할 외에는 하고 있지 않으며, fork는 부모의 fd_list를 그대로 복사해오면 되기에 process_init을 호출할 필요가 없다. 해당 코드를 호출하지 않으면 비어있는 fd_list에 별도의 작업없이 복사할 수 있게된다.

	/* 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.*/
	struct list_elem *e;
	struct fildes *fd_entry, *fd_entry_copy;
	struct file *fp;

	filesys_enter();

	/* copy files from parent fd_list */
	for (e = list_begin(&parent->fd_list); e != list_end(&parent->fd_list); e = list_next(e))
	{
		fd_entry = list_entry(e, struct fildes, fd_elem);
		fp = file_duplicate(fd_entry->fp);
		if (fp == NULL)
		{
			succ = false;
			goto file_copy_done;
		}

		fd_entry_copy = (struct fildes *)malloc(sizeof(struct fildes));
		if (fd_entry_copy == NULL)
		{
			succ = false;
			goto file_copy_done;
		}

		fd_entry_copy->fd = fd_entry->fd;
		fd_entry_copy->fp = fp;
		list_insert_ordered(&current->fd_list, &fd_entry_copy->fd_elem, cmp_fd, NULL);
	}

file_copy_done:
	filesys_exit();

이제 파일 복사도 끝났으니, duplicate_pte를 보자. 가이드라인에서 설명한 그대로 구현하면 된다. 물론, 에러 핸들링과 자원 할당/해제를 염두하면서 코딩한다.

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 false;

	/* 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)
		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. */
		/* Should free newpage, every other pages will be handled in process_cleanup */
		palloc_free_page(newpage);
		return false;
	}

	return true;
}

이제 복사는 전부 끝났다. 이제 몇 개만 더 고치면 fork도 끝이다. 자식 프로세스에서는 fork의 반환값이 0이어야 하므로, __do_fork에서 로컬 변수 if_의 rax 값을 0으로 바꿔준다.

/* 1. Read the cpu context to local stack. */
memcpy (&if_, parent_if, sizeof (struct intr_frame));
if_.R.rax = 0;

복사 작업의 성공/실패 여부를 알게 된 때에 fork_sema를 up시켜야 부모 프로세스가 block에서 해제될 수 있다. 또한 실패한 경우에는 exit_status에 -1을 저장해 실패한 사실을 부모 프로세스에 알려야한다.

	/* Finally, switch to the newly created process. */
	if (succ)
	{
		sema_up(&current->fork_sema);
		do_iret (&if_);
	}
		
error:
	current->exit_status = -1;
	sema_up(&current->fork_sema);
	thread_exit();

드디어 길고 긴 fork도 끝났다.

3-4. 잔여 작업

우선 프로세스가 종료될 때, 가지고 있던 fd_list의 파일 중 close가 되지 않은 파일이 있다면 전부 닫으면서 할당된 공간을 해제해주어야 한다. 이는 process_exit에서 구현해주어야 하므로 process_exit을 다시 수정한다.

/* Exit the process. This function is called by thread_exit (). */
void
process_exit (void) {
	struct thread *curr = thread_current ();
	struct list_elem *e;

	process_cleanup ();
	nowait_terminate ();

	filesys_enter ();

	while (!list_empty (&curr->fd_list))
	{
		e = list_pop_front (&curr->fd_list);
		struct fildes *fd_entry = list_entry (e, struct fildes, fd_elem);

		if (fd_entry->fp != 0 && fd_entry->fp != 1)
			file_close (fd_entry->fp);

		free (fd_entry);
	}

	filesys_exit ();

	sema_up(&curr->child_ready_sema);
	printf("%s: exit(%d)\n", curr->name, curr->exit_status);
	sema_down(&curr->parent_ready_sema);
}

파일 관련 syscall에서 fd와 연결된 fd_entry가 없을 때와, 연결된 fp가 0 또는 1일때의 예외처리도 코드에 넣어준다. 수정하는 과정에서 fd_deallocate는 필요가 없을 것 같아 _close와 통합하면서 지웠다.

int _filesize (int fd)
{
	int res = -1;
	filesys_enter();
	struct fildes *fd_entry = fd_search(fd);
	if (fd_entry != NULL && fd_entry->fp != 0 && fd_entry->fp != 1)
		res = (int)file_length(fd_entry->fp);
	filesys_exit();
	return res;
}

void _seek (int fd, unsigned position)
{
	filesys_enter();
	struct fildes *fd_entry = fd_search(fd);
	if (fd_entry != NULL && fd_entry->fp != 0 && fd_entry->fp != 1)
		file_seek(fd_entry->fp, position);
	filesys_exit();
}

unsigned _tell (int fd)
{
	unsigned res = 0;
	filesys_enter();
	struct fildes *fd_entry = fd_search(fd);
	if (fd_entry != NULL && fd_entry->fp != 0 && fd_entry->fp != 1)
		res = (unsigned)file_tell(fd_entry->fp);
	filesys_exit();
	return res;
}

void _close(int fd)
{
	filesys_enter();

	struct fildes *fd_entry = fd_search(fd);
	struct file *fp;

	if (fd_entry == NULL) // not valid fd
		goto close_done;

	list_remove (&fd_entry->fd_elem);
	fp = fd_entry->fp;
	free (fd_entry); // always free what you malloc-ed;

	if (fp != 0 && fp != 1)
		file_close (fp);

close_done:
	filesys_exit();
}

또 load 함수에서 메모리 누수 등의 문제와 함께 파일을 열고 닫는 과정에 있어 filesys_lock 반영이 안 되어 있어 해당 사항을 수정하였다.

static bool
load (const char *file_name, struct intr_frame *if_) {
	struct thread *t = thread_current ();
	struct ELF ehdr;
	struct file *file = NULL;
	off_t file_ofs;
	bool success = false;
	int i;

	/* Allocate and activate page directory. */
	t->pml4 = pml4_create ();
	if (t->pml4 == NULL)
		return false;
	process_activate (thread_current ());

	/* Parse file_name into real name and arguments */
	char *file_name_copy = (char *) malloc (strlen (file_name) + 1);
	if (file_name_copy == NULL)
		return false;

	strlcpy (file_name_copy, file_name, strlen (file_name) + 1);
	
	char *r_file_name = NULL;
	char *ptr = NULL;
	
	r_file_name = strtok_r(file_name_copy, " ", &ptr);

	filesys_enter();

	/* Open executable file. */
	file = filesys_open (r_file_name);
	if (file == NULL) {
		printf ("load: %s: open failed\n", r_file_name);
		goto done;
	}

	/* Read and verify executable header. */
	if (file_read (file, &ehdr, sizeof ehdr) != sizeof ehdr
			|| memcmp (ehdr.e_ident, "\177ELF\2\1\1", 7)
			|| ehdr.e_type != 2
			|| ehdr.e_machine != 0x3E // amd64
			|| ehdr.e_version != 1
			|| ehdr.e_phentsize != sizeof (struct Phdr)
			|| ehdr.e_phnum > 1024) {
		printf ("load: %s: error loading executable\n", r_file_name);
		goto done;
	}

	/* Read program headers. */
	file_ofs = ehdr.e_phoff;
	for (i = 0; i < ehdr.e_phnum; i++) {
		struct Phdr phdr;

		if (file_ofs < 0 || file_ofs > file_length (file))
			goto done;
		file_seek (file, file_ofs);

		if (file_read (file, &phdr, sizeof phdr) != sizeof phdr)
			goto done;
		file_ofs += sizeof phdr;
		switch (phdr.p_type) {
			case PT_NULL:
			case PT_NOTE:
			case PT_PHDR:
			case PT_STACK:
			default:
				/* Ignore this segment. */
				break;
			case PT_DYNAMIC:
			case PT_INTERP:
			case PT_SHLIB:
				goto done;
			case PT_LOAD:
				if (validate_segment (&phdr, file)) {
					bool writable = (phdr.p_flags & PF_W) != 0;
					uint64_t file_page = phdr.p_offset & ~PGMASK;
					uint64_t mem_page = phdr.p_vaddr & ~PGMASK;
					uint64_t page_offset = phdr.p_vaddr & PGMASK;
					uint32_t read_bytes, zero_bytes;
					if (phdr.p_filesz > 0) {
						/* Normal segment.
						 * Read initial part from disk and zero the rest. */
						read_bytes = page_offset + phdr.p_filesz;
						zero_bytes = (ROUND_UP (page_offset + phdr.p_memsz, PGSIZE)
								- read_bytes);
					} else {
						/* Entirely zero.
						 * Don't read anything from disk. */
						read_bytes = 0;
						zero_bytes = ROUND_UP (page_offset + phdr.p_memsz, PGSIZE);
					}
					if (!load_segment (file, file_page, (void *) mem_page,
								read_bytes, zero_bytes, writable))
						goto done;
				}
				else
					goto done;
				break;
		}
	}

	/* Set up stack. */
	if (!setup_stack (if_))
		goto done;

	/* Start address. */
	if_->rip = ehdr.e_entry;

	/* TODO: Your code goes here.
	 * TODO: Implement argument passing (see project2/argument_passing.html). */

	char *token = NULL;
	int argc = 1; // already parsed real file name

	while ((token = strtok_r(NULL, " ", &ptr)) != NULL)
		argc += 1;

	char **argv = (char **) malloc (sizeof(char *) * argc);
	if (argv == NULL)
		goto done;

	strlcpy(file_name_copy, file_name, strlen(file_name) + 1);

	ptr = NULL;
	int idx = 0, len;
	
	for (token = strtok_r(file_name_copy, " ", &ptr); token != NULL; 
		 token = strtok_r(NULL, " ", &ptr), idx++)
		argv[idx] = token;

	for (idx = argc - 1; idx >= 0; idx--)
	{
		len = strlen(argv[idx]);
		if_->rsp -= (len + 1);
		strlcpy(if_->rsp, argv[idx], len + 1);
		argv[idx] = if_->rsp; // for filling in argv part of stack
	}
	
	if_->rsp -= (((uint64_t)if_->rsp) & 7); // word-align
	if_->rsp -= 8; // argv[argc] = NULL

	for (idx = argc - 1; idx >= 0; idx--)
	{
		if_->rsp -= 8;
		*((uint64_t *)if_->rsp) = argv[idx];
	}

	// fill in rdi, rsi
	if_->R.rdi = argc;
	if_->R.rsi = if_->rsp;

	// fill in fake return address
	if_->rsp -= 8;

	free(argv);
	success = true;

done:
	/* We arrive here whether the load is successful or not. */	
	free(file_name_copy);

	file_close (file);
	filesys_exit();

	return success;
}

이제 모든 함수들을 syscall_handler에 종합하면 이 긴 여정도 일단 끝이다. 반환값은 rax에 담아야 하고, 반환값이 없는 함수는 단순히 실행만 하는 식으로 작성한다. 포인터에 대해서는 미리 작성해둔 validate_uaddr과 validate_uaddr_region 함수를 활용해 유효한 포인터인지 미리 검증하는 것을 염두한다.

void
syscall_handler (struct intr_frame *f) {
	int syscall_num = f->R.rax;
	void *argument_ptr;

	switch (syscall_num)
	{
		case SYS_HALT:
			_halt();
			break;
		case SYS_EXIT:
			_exit (f->R.rdi);
			break;
		case SYS_FORK:
			argument_ptr = f->R.rdi;
			validate_uaddr (argument_ptr);
			f->R.rax = _fork (argument_ptr, f);
			break;
		case SYS_EXEC:
			argument_ptr = f->R.rdi;
			validate_uaddr (argument_ptr);
			f->R.rax = _exec (argument_ptr);
			break;
		case SYS_WAIT:
			f->R.rax = _wait((tid_t)f->R.rdi);
			break;
		case SYS_CREATE:
			argument_ptr = f->R.rdi;
			validate_uaddr (argument_ptr);
			f->R.rax = _create (argument_ptr, f->R.rsi);
			break;
		case SYS_REMOVE:
			argument_ptr = f->R.rdi;
			validate_uaddr (argument_ptr);
			f->R.rax = _remove(argument_ptr);
			break;
		case SYS_OPEN:
			argument_ptr = f->R.rdi;
			validate_uaddr (argument_ptr);
			f->R.rax = _open (argument_ptr);
			break;
		case SYS_FILESIZE:
			f->R.rax = _filesize (f->R.rdi);
			break;
		case SYS_READ:
			argument_ptr = f->R.rsi;
			validate_uaddr_region (argument_ptr, f->R.rdx);
			f->R.rax = _read (f->R.rdi, argument_ptr, f->R.rdx);
			break;
		case SYS_WRITE:
			argument_ptr = f->R.rsi;
			validate_uaddr_region (argument_ptr, f->R.rdx);
			f->R.rax = _write (f->R.rdi, argument_ptr, 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;
	}

	return;
}

이제 큰 틀은 완성된 것 같다. 그러나 아직 디버깅이 남아있지 ㅎㅎㅎ....

 


4. 디버깅

일단 make까진 성공한다. 근데 바로 커널 패닉이 뜬다.

FAIL tests/userprog/args-none
Kernel panic in run: PANIC at ../../lib/kernel/list.c:158 in list_insert(): assertion `is_interior (before) || is_tail (before)' failed.
Call stack: 0x80042185fa 0x8004218b43 0x800421bbe6 0x800421bcbd 0x80042078d4
Translation of call stack:
0x00000080042185fa: debug_panic (lib/kernel/debug.c:32)
0x0000008004218b43: list_insert (lib/kernel/list.c:159)
0x000000800421bbe6: process_init (userprog/process.c:44)
0x000000800421bcbd: initd (userprog/process.c:80)
0x00000080042078d4: kernel_thread (threads/thread.c:554)

리스트 관련해서 문제가 생긴듯 하다. 다시 코드를 살펴봤더니 정말 바보같은 실수를 저지르고 있었다.

static void
process_init (void) {
	...
	list_insert (&current->fd_list, &stdin_fildes->fd_elem);
	list_insert (&current->fd_list, &stdout_fildes->fd_elem);
	...
}

// list.c
void
list_insert (struct list_elem *before, struct list_elem *elem) {
	ASSERT (is_interior (before) || is_tail (before));
	ASSERT (elem != NULL);

	elem->prev = before->prev;
	elem->next = before;
	before->prev->next = elem;
	before->prev = elem;
}

void
list_push_back (struct list *list, struct list_elem *elem) {
	list_insert (list_end (list), elem);
}

당연히 list_insert를 쓰니까 호환이 안 되는 것이었다. list_push_back을 썼어야지.. 피곤한가보다... 바로 수정하였다.


이제 커널 패닉은 해결이 되었지만 두 번째 케이스부터 다시 문제가 생긴다.

Test output failed to match any acceptable form.

Acceptable output:
  (args) begin
  (args) argc = 2
  (args) argv[0] = 'args-single'
  (args) argv[1] = 'onearg'
  (args) argv[2] = null
  (args) end
  args-single: exit(0)
Differences in `diff -u' format:
  (args) begin
  (args) argc = 2
  (args) argv[0] = 'args-single'
  (args) argv[1] = 'onearg'
  (args) argv[2] = null
  (args) end
- args-single: exit(0)
+ args-single one: exit(0)

exit 메시지가 이상하게 뜨고 있는 것 같은데 해결하도록 한다. thread의 이름은 process_create_initd에서 thread.c의 init_thread로 넘겨져 복사되는 것이므로, 파싱을 한 뒤 복사하도록 하게 한다.

static void
init_thread (struct thread *t, const char *name, int priority) {
	ASSERT (t != NULL);
	ASSERT (PRI_MIN <= priority && priority <= PRI_MAX);
	ASSERT (name != NULL);

	char *ptr, *real_name;
	memset (t, 0, sizeof *t);
	t->status = THREAD_BLOCKED;
	real_name = strtok_r(name, " ", &ptr);
	strlcpy (t->name, real_name, sizeof t->name);
	...
}

이제 exit 메시지도 수정되었다.


다시 make check로 결과를 확인해보았다. 주욱 잘 가는 듯 하다가 open-normal에서 걸린다.

FAIL tests/userprog/open-normal
Kernel panic in run: PANIC at ../../userprog/exception.c:98 in kill(): Kernel bug - unexpected interrupt in kernel
Call stack: 0x80042186ad 0x800421d550 0x800421d6cf 0x800420968b 0x8004209aa9 0x800421dc3f 0x800421d744 0x400106 0x40018d 0x400c2e
Translation of call stack:
0x00000080042186ad: debug_panic (lib/kernel/debug.c:32)
0x000000800421d550: kill (userprog/exception.c:104)
0x000000800421d6cf: page_fault (userprog/exception.c:160 (discriminator 12))
0x000000800420968b: intr_handler (threads/interrupt.c:352)
0x0000008004209aa9: intr_entry (threads/intr-stubs.o:?)
0x000000800421dc3f: syscall_handler (userprog/syscall.c:148)
0x000000800421d744: no_sti (userprog/syscall-entry.o:?)
0x0000000000400106: (unknown)
0x000000000040018d: (unknown)
0x0000000000400c2e: (unknown)

open 쪽 코드를 보니 나의 ㅄ력이 다시 한 번 빛을 발한다.

int _open (const char *file)
{
	int fd;
	filesys_enter();
	struct file *fp = filesys_open(file);
	struct fildes *res = fd_allocate(fp);
	filesys_exit();
	return fd;
}

int fd_allocate (struct file *fp)
{
	struct list_elem *e;
	struct fildes *fd_entry, *res;
	struct thread *curr = thread_current();

	list_sort(&curr->fd_list, cmp_fd, NULL);
	int fd = 0;

	for (e = list_begin(&curr->fd_list); e != list_end(&curr->fd_list); e = list_next(e))
	{
		fd_entry = list_entry(e, struct fildes, fd_elem);
		if (fd_entry->fd == fd)
			fd++;
		else
			break;
	}

	res = (struct fildes *)malloc(sizeof(struct fildes));
	res->fd = fd;
	res->fp = fp;
	list_insert_ordered(&curr->fd_list, &res->fd_elem, cmp_fd, NULL);

	return fd;
}

ㅁㅊ fd를 반환하도록 했으면 저것도 따라서 고쳤어야 하는데 왜 안 고쳤을까... 빨리 수정한다. 겸사겸사 filesys_open을 했을 때 NULL이 나오는 경우도 예외처리가 안 되어 있어서 고쳐준다.

int _open (const char *file)
{
	int fd = -1;
	filesys_enter();
	struct file *fp = filesys_open(file);
	if (fp != NULL)
		fd = fd_allocate(fp);
	filesys_exit();
	return fd;
}

이제 open 관련은 모두 통과한다.


다시 make check한다. ㅠㅠㅠㅠ 언제까지 해... 또 주욱 잘 가다가 write-normal에서 죽는다.

FAIL tests/userprog/write-normal
Kernel panic in run: PANIC at ../../userprog/exception.c:98 in kill(): Kernel bug - unexpected interrupt in kernel
Call stack: 0x80042186ad 0x800421d550 0x800421d6cf 0x800420968b 0x8004209aa9 0x800421e1be 0x800421dd2d 0x800421d744 0x4001bd 0x40024c 0x400ced
Translation of call stack:
0x00000080042186ad: debug_panic (lib/kernel/debug.c:32)
0x000000800421d550: kill (userprog/exception.c:104)
0x000000800421d6cf: page_fault (userprog/exception.c:160 (discriminator 12))
0x000000800420968b: intr_handler (threads/interrupt.c:352)
0x0000008004209aa9: intr_entry (threads/intr-stubs.o:?)
0x000000800421e1be: _write (userprog/syscall.c:313)
0x000000800421dd2d: syscall_handler (userprog/syscall.c:162)
0x000000800421d744: no_sti (userprog/syscall-entry.o:?)
0x00000000004001bd: (unknown)
0x000000000040024c: (unknown)
0x0000000000400ced: (unknown)

살펴보니 fp라고 쓸 것을 fd라고 써 놨다. 바로 고쳐서 해결되었다. 제발 오타 좀 내지 마라.


또 주욱 가다가 fork에서 죽는다. 올 것이 왔구나

Acceptable output:
  (fork-once) begin
  (fork-once) child run
  child: exit(81)
  (fork-once) Parent: child exit status is 81
  (fork-once) end
  fork-once: exit(0)
Differences in `diff -u' format:
  (fork-once) begin
- (fork-once) child run
- child: exit(81)
- (fork-once) Parent: child exit status is 81
+ child: exit(-1)
+ (fork-once) Parent: child exit status is -1
  (fork-once) end
  fork-once: exit(0)

디버깅해본 결과, duplicate_pte에서 문제가 생기는 듯 하다. 생각보다 duplicate_pte 자체는 간단한 함수여서 문제가 어딘지 고민했다. 결론적으로 va가 커널 영역인지 검사하는 코드에서 반환값이 false가 아닌 true가 되어야 할 것 같았다. 해당 조건이 만족할 때 바로 반환하라는 주석의 의도는 커널 주소가 인자로 들어오면 바로 fork에 실패하라는 것은 아닐 것이다. 애당초 pml4_for_each의 동작 원리를 생각하면 duplicate_pte의 va인자로 커널 주소는 반드시 들어가도록 되어 있다.

 

따라서 해당 주석의 의미는 아마도 커널 영역은 모든 프로세스를 통틀어 공통이기 때문에 복사할 필요가 없다는 것이다. 복사할 영역에 해당하는 페이지들은 유저 페이지로 한정되어 있다는 것이다. 따라서 인자로 커널 영역이 들어올 경우 true를 반환하도록 수정하면 문제가 해결될 것이다.

static bool
duplicate_pte (uint64_t *pte, void *va, void *aux) {
	...
	/* 1. TODO: If the parent_page is kernel page, then return immediately. */
	if (is_kernel_vaddr (va)) 
		return true;
	...
}

duplicate_pte 문제는 해결이 됐지만 이제는 파일 복사쪽에서 또 문제다. 여기도 0/1 예외 처리를 안 해줘서 그런 것 같다. 바로 수정해준다. 

/* copy files from parent fd_list */
for (e = list_begin(&parent->fd_list); e != list_end(&parent->fd_list); e = list_next(e))
{
	fd_entry = list_entry (e, struct fildes, fd_elem);
	if (fd_entry->fp != 0 && fd_entry->fp != 1)
	{
		fp = file_duplicate (fd_entry->fp);
		if (fp == NULL)
		{
			succ = false;
			goto file_copy_done;
		}
	}

	fd_entry_copy = (struct fildes *)malloc(sizeof(struct fildes));
	if (fd_entry_copy == NULL)
	{
		file_close(fp);
		succ = false;
		goto file_copy_done;
	}

	fd_entry_copy->fd = fd_entry->fd;
	if (fd_entry->fp != 0 && fd_entry->fp != 1)
		fd_entry_copy->fp = fp;
	else
		fd_entry_copy->fp = fd_entry->fp;

	list_insert_ordered(&current->fd_list, &fd_entry_copy->fd_elem, cmp_fd, NULL);
}

나머진 점심 나가서 먹을 것 같으니 다음 포스팅 때 잡자. 


5. 후기

WA! 신나는 버그 잡기! 다음 시간에도 이어집니다 ㅋㅋㅋ

Comments