거북이의 쉼터

(2022.02.16) System Calls 구현 (3/4) 본문

코딩 삽질/KAIST PINTOS (CS330)

(2022.02.16) System Calls 구현 (3/4)

onlim 2022. 2. 16. 12:27

1. 서론 및 필요 내용 설명

지난 포스팅 말단부터 계속 디버깅을 하고 있다. 그러다가 또 걸린 케이스가 rox 케이스이다. rox 케이스에서는 주석에서 적힌 것처럼 실행 중인 파일의 변경 불가 여부를 검사한다. 아마 rox라는 것 자체가 read-only executable의 약자라고 생각한다. 해당 매뉴얼은 여기에 나와 있다. 실행 파일에 대해서는 deny_write을 임시적으로 걸라는 것이다.

 

또 중요한 사항이 있는데 실행에 사용된 파일은 프로세스가 실행될 동안에는 계속 열어두라는 것이다. 

You can use file_deny_write() to prevent writes to an open file. Calling file_allow_write() on the file will re-enable them (unless the file is denied writes by another opener). Closing a file will also re-enable writes. Thus, to deny writes to a process's executable, you must keep it open as long as the process is still running.

 

어찌 보면 당연한 것이다. 파일을 중간에 닫아버리면 무슨 문제가 발생할 지 알 수 없기 때문에 한 번 실행에서 계속 deny_write가 걸려있는 것이 예상치 못한 동작을 피하는 데 도움을 줄 것이다.

 

해당 수정이 가해져야 할 부분은 process_exec, process_fork 등 실행 파일을 open하고 load하는 등의 조작을 가하는 함수들이다. 또한 프로세스를 종료할 때 실행 파일을 닫는 코드도 추가해야 한다. 이번 포스팅은 해당 부분들을 수정하고 지난 포스팅에 이어 마저 프로젝트 2를 디버깅하도록 하겠다.

 


2. 구현해야 하는 것

  • process_exec, 나아가 load 함수에서 실행 파일을 열 때 file_deny_write을 걸고 해당 파일을 닫지 않도록 수정
  • process_fork에서 열린 실행 파일에 대해 open_cnt를 올려줄 것
  • process_exit에서 실행 파일을 닫을 것

 


3. 구현 과정

우선 프로세스 시작부터 끝까지 실행 파일을 관리하기 위해서는 해당 파일에 대한 포인터를 가지고 있어야 한다. thread 구조체에 또 추가해준다. 이제 그만 추가해야 하지 않을까...

struct thread {
	...
	struct file *exec_file;
	...
}

load는 이제 실패했을 경우와 성공했을 경우를 분리해서 구현하자. 성공했을 경우에는 deny_write를 파일에 걸고 빠져 나오고, 실패했을 경우에는 파일을 닫고 리소스를 해제해면서 반환한다.

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 fail;
	}

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

	/* 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 fail;
		file_seek (file, file_ofs);

		if (file_read (file, &phdr, sizeof phdr) != sizeof phdr)
			goto fail;
		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 fail;
			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 fail;
				}
				else
					goto fail;
				break;
		}
	}

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

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

	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);
	free(file_name_copy);

	success = true;
	t->exec_file = file;
	file_deny_write (file);
	filesys_exit();
	return success;
	
fail:
	free(file_name_copy);
	file_close(file); // release the resource if failed
	filesys_exit();
	return success;
}

load 함수 너무 길어...

 

다음으로 process_fork의 경우, 새롭게 여는 프로세스에 대해서 open_cnt만 증가시켜주면 된다. file_duplicate를 exec_file에 대해서도 적용하는 것이다. 기존에 filesys_lock으로 보호된 구역 내에 코드를 넣는다.

static void
__do_fork (void *aux) {
	...
	fp = file_duplicate(parent->exec_file);
	if (fp == NULL)
	{
		succ = false;
		goto file_copy_done;
	}
	current->exec_file = fp;
    
file_copy_done:
	filesys_exit();
	...
}

file_duplicate의 경우 deny_write까지 복사가 되면서 내부의 inode의 deny_write 카운터도 1 증가되므로 따로 손 볼 것은 없어 보인다.

 

마지막으로 process_exec과 process_exit에서 기존 실행 파일을 닫는 코드를 넣어준다. 공통적으로 process_cleanup을 호출하므로 여기에 넣으면 딱 좋을 것 같다는 생각을 했다. 

static void
process_cleanup (void) {
	struct thread *curr = thread_current ();

#ifdef VM
	supplemental_page_table_kill (&curr->spt);
#endif

	uint64_t *pml4;

	pml4 = curr->pml4;
	if (pml4 != NULL) {
		curr->pml4 = NULL;
		pml4_activate (NULL);
		pml4_destroy (pml4);
	}

	filesys_enter();
	if (curr->exec_file != NULL)
	{
		file_close(curr->exec_file);
		curr->exec_file = NULL;
	}
	filesys_exit();
}

이제 얼추 해당 내용은 끝난 것 같으니 다시 디버깅 지옥으로 입성해보자.


4. 디버깅

실행 결과 rox는 수월하게 넘어간다. 다만 또 bad_read 쪽에서 걸린다.

FAIL tests/userprog/bad-read
Test output failed to match any acceptable form.

Acceptable output:
  (bad-read) begin
  bad-read: exit(-1)
Differences in `diff -u' format:
  (bad-read) begin
+ Interrupt 0x0e (#PF Page-Fault Exception) at rip=4000f1
+ rax 0000000000000000 rbx 0000800424104800 rcx 00000000000000ff rdx 0000000000000000
+ rsp 000000004747ff90 rbp 000000004747ff90 rsi 00000000006050ee rdi 00000000006051df
+ rip 00000000004000f1 r8 0000000000000000  r9 0000000000000000 r10 0000000000000000
+ r11 0000000000000206 r12 00008004240f3000 r13 00008004229dd800 r14 00008004240f1000
+ r15 0000800420a8e200 rflags 00000212
+ es: 001b ds: 001b cs: 0023 ss: 001b
  bad-read: exit(-1)
  
  Page fault at 0: not present error reading page in user context.

NULL에 접근하면서 page fault가 나버리는 상황인데 메시지 출력을 하면 안 된다는 것 같다. 그러면 kill에서 exit이 되기까지 기다리는 것이 아닌, page_fault 함수에서 잡아서 exit을 시켜야 한다. 현재 page_fault에 있는 변수들은 다음과 같으며, 이들을 활용해야 한다.

static void
page_fault (struct intr_frame *f) {
	bool not_present;  /* True: not-present page, false: writing r/o page. */
	bool write;        /* True: access was write, false: access was read. */
	bool user;         /* True: access by user, false: access by kernel. */
	void *fault_addr;  /* Fault address. */
	...
}

해당 변수들 옆에 나와있는 설명에 기반해 바로 exit을 시킬 조건으로는 다음을 생각할 수 있다.

  • not_present가 false로 설정이 됐을 때 (read-only 페이지에 write 시도) 
  • user가 true로 설정이 되었고 (user에 의한 접근) fault_addr가 정상적으로 접근할 수 있는 주소가 아닌 경우

다음 프로젝트에서 볼 것이지만 page_fault에 들어갔다고 해서 반드시 죽어야 하는 것은 아니다. 접근하려는 페이지가 메모리에 단순히 올라와있지 않은 경우에는 not_present가 true라 할지라도 해당 페이지를 메모리에 불러옴으로서 page_fault에서 벗어나 정상적 실행으로 복귀할 수 있다. 일단 프로젝트 2까지는 무조건 page_fault에서 프로세스가 exit하게 되므로, 위의 조건들이 성립하는 경우 _exit(-1)을 호출하도록 page_fault를 수정한다.

 

KERN_BASE 상위의 주소는 is_user_vaddr로 검증할 수 있다. 그러나 해당 케이스처럼 NULL 등 KERN_BASE 하위 주소에 대해서는 not_present로 검증해야 한다. 물론 위에서 언급한 것처럼 프로젝트 3에 들어가서는 not_present에 해당하는 경우는 추가 검증이 필요하다.

static void
page_fault (struct intr_frame *f) {
	...
	/* Determine cause. */
	not_present = (f->error_code & PF_P) == 0;
	write = (f->error_code & PF_W) != 0;
	user = (f->error_code & PF_U) != 0;

	if (!not_present)
		_exit(-1);

	if (user && (not_present || !is_user_vaddr(fault_addr)))
		_exit(-1);
	...
}

이렇게 수정해 놓고 다시 실행했다.

 

다행히 나머지 테스트 케이스에서 안 걸리고 multi-oom까지 간다. 더럽게 오래 걸리는 multi-oom의 실행 결과를 기다린다. 그리고 당연히 실패... 오오미 우선 건너뛰고 계속 돌려보았다.

FAIL tests/userprog/no-vm/multi-oom
pass tests/threads/alarm-single
pass tests/threads/alarm-multiple
FAIL tests/threads/alarm-simultaneous
FAIL tests/threads/alarm-priority
pass tests/threads/alarm-zero
pass tests/threads/alarm-negative
FAIL tests/threads/priority-change
FAIL tests/threads/priority-donate-one
FAIL tests/threads/priority-donate-multiple
FAIL tests/threads/priority-donate-multiple2
FAIL tests/threads/priority-donate-nest
FAIL tests/threads/priority-donate-sema
FAIL tests/threads/priority-donate-lower
pass tests/threads/priority-fifo
FAIL tests/threads/priority-preempt
FAIL tests/threads/priority-sema
FAIL tests/threads/priority-condvar
FAIL tests/threads/priority-donate-chain

나머지 프로젝트 1의 여러 테스트 케이스들도 왜 인지는 모르겠지만 실패한다. 실패한 원인을 살펴보니 exit 메시지가 시도때도 없이 불리는 것을 볼 수 있다. exit 메시지의 요구 사항을 다시 보자.

Do not print these messages when a kernel thread that is not a user process terminates, or when the halt system call is invoked.

 

유저 프로세스가 아닌 것이 종료될 때는 exit 메시지를 출력하지 말라는 것이다. 사실 후반부 테스트를 할 때는 USERPROG 없이 진행될 줄 알고 무시했었던 조건인데 역시 그렇게 쉬울리 없다. 어떻게 조건을 맞출 수 있을까?

 

모든 유저 프로세스는 process_init을 반드시 거치는 반면, 단순 커널 thread는 process_init을 거치지 않는다. 따라서 process_init에서 유저 프로세스인지 표시를 해 준다면 process_exit이 진행될 때 확인하고 process_exit을 실행할지 여부를 판단할 수 있다. 코드를 이에 맞춰 수정하고 다시 실행해준다. __do_fork에서 process_init을 제외했기 때문에 __do_fork에는 따로 코드를 넣어준다. 만약 추후 process_init에서 해야 할 작업들이 더 추가된다면 __do_fork에도 process_init을 넣고 대응작업을 해주는 것이 편할 것이다.

struct thread {
	...
	bool is_userprog;
	...
}

static void
process_init (void) {
	...
	current->is_userprog = true;
}

static void
__do_fork (void *aux) {
	...
	current->is_userprog = true;
	...
}

void
thread_exit (void) {
	ASSERT (!intr_context ());

#ifdef USERPROG
	if (thread_current()->is_userprog)
		process_exit ();
#endif
	...
}

여전히 실패하는 테스트 케이스가 있다. thread 이름이 틀렸다고 지적하는 테스트 케이스인데, 프로세스에서는 이름을 파싱해야 하고 thread에서는 이름을 파싱하면 안된다고 한다. 하하..

 

어차피 프로세스의 이름을 요구하는 상황은 exit 메시지 뿐이다. exit 메시지 용도로 프로세스 이름을 따로 저장하고 기존 thread 이름을 저장하던 name 멤버에서 파싱을 제거하도록 init_thread 코드를 수정한다.

struct thread {
	...
	char proc_name[16]; // for exit message
	...
}

static void
init_thread (struct thread *t, const char *name, int priority) {
	...
	char *ptr, *real_name;
	memset (t, 0, sizeof *t);
	t->status = THREAD_BLOCKED;
	strlcpy (t->name, name, sizeof t->name);
	real_name = strtok_r(name, " ", &ptr);
	strlcpy (t->proc_name, real_name, sizeof t->proc_name);
	t->tf.rsp = (uint64_t) t + PGSIZE - sizeof (void *);
	...
}

void
process_exit (void) {
	...
	sema_up(&curr->child_ready_sema);
	printf("%s: exit(%d)\n", curr->proc_name, curr->exit_status);
	sema_down(&curr->parent_ready_sema);
}

이제 이전 프로젝트 테스트케이스는 전부 통과한다.

pass tests/threads/alarm-single
pass tests/threads/alarm-multiple
pass tests/threads/alarm-simultaneous
pass tests/threads/alarm-priority
pass tests/threads/alarm-zero
pass tests/threads/alarm-negative
pass tests/threads/priority-change
pass tests/threads/priority-donate-one
pass tests/threads/priority-donate-multiple
pass tests/threads/priority-donate-multiple2
pass tests/threads/priority-donate-nest
pass tests/threads/priority-donate-sema
pass tests/threads/priority-donate-lower
pass tests/threads/priority-fifo
pass tests/threads/priority-preempt
pass tests/threads/priority-sema
pass tests/threads/priority-condvar
pass tests/threads/priority-donate-chain

드디어 multi-oom에 집중할 수 있는 환경이 만들어졌다. 포스팅이 길어진 관계로 끊고 다음 포스팅에 본격적으로 multi-oom를 잡아보자.

 


5. 후기

이번엔 좀 편히 가나 싶었더니 역시 오오미야 실망시키지 않아.

Comments