WIL - PintOS:Project 2 / System Call

박상우·2024년 6월 9일
0

📝 TIL

목록 보기
37/45
post-thumbnail

⚽️ Main Goal

시스템 콜 구현하기

  • 현재 PintOS에서는 system call handler가 없어서 시스템 콜이 처리되지 않는다.
  • 시스템 콜을 구현하고, 해당 기능을 제공한다.

🐮 필요한 개념

System Call

  • 운영체제에서 제공하는 서비스를 위한 프로그래밍 인터페이스
  • 사용자 프로그램이 커널의 기능을 사용할 수 있게 해줌
  • 시스템 호출을 위해 커널 모드로 전환 되고, 실행 후 사용자 모드로 전환

⚙️ 구현하기

☑️ pid_t sys_halt (const char *thread_name)

  • PintOS를 종료
pid_t sys_halt (const char *thread_name) {
	/* Shut down PintOS */
	power_off();
}

☑️ void sys_exit (int64_t status)

  • 현재 실행중인 프로세스 종료
void sys_exit (int64_t status) {
	struct thread *t = thread_current();

	t -> is_exit = true; // 정적으로 종료했는지 여부를 판단하기 위해 is_exit값 저장;

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

	thread_exit();
}

⬛️ tid_t sys_exec (const char *cmd_line)

🫠 fork() 구현이 미흡해서 테스트 케이스를 통과하지 못한 코드입니다.
  • 주어진 cmd_line을 통해 실행파일로 변경
  • 파일 디스크립터는 exec 호출을 통해 변경되지 않고 열린 상태를 유지합니다.
  • cmd_line을 parsing하기 위해 copy하여 사용 ( const라서 변경 불가 )
  • 복사한 값을 인자로 하여 process_exec 함수를 호출하고, 로드되거나 실행될 수 없는 경우에는 status -1로 프로세스를 종료한다.
// syscall.h
tid_t sys_exec (const char *cmd_line) {
	check_address(cmd_line);

  char *file_name_copy = palloc_get_page(PAL_ZERO); // 페이지 할당을 통해 파일 이름을 복사할 메모리 공간을 얻습니다.
    
  if (file_name_copy == NULL)
    exit(-1); // 메모리 할당에 실패한 경우, -1을 반환하고 현재 프로세스를 종료합니다.
 
	strlcpy(file_name_copy, file_name, PGSIZE); // file_name을 file_name_copy로 복사합니다. PGSIZE는 복사할 최대 크기를 나타냅니다.
 
  if (process_exec(file_name_copy) == -1)
      exit(-1); // process_exec 함수를 호출하여 파일을 실행합니다. 실행에 실패한 경우, -1을 반환하고 현재 프로세스를 종료합니다.
}

⬛️ int sys_wait (pid_t pid)

🫠 fork() 구현이 미흡해서 테스트 케이스를 통과하지 못한 코드입니다.
  • 자식 프로세스의 종료 상태를 반환한다.
  • pid가 살아있다면 대기상태로 기다리다가, pid가 종료되었을 때의 상태를 반환한다.
// syscall.c
int sys_wait (pid_t pid) {
	return process_wait(pid);
}

// process.c
int process_wait (tid_t child_tid UNUSED) {
	struct thread *child_thread = get_child_process(child_tid); // tid에 해당하는 스레드를 검색

	if (child_thread == NULL)		return -1;
	
	sema_down(&child_thread -> wait_sema); // 자식 스레드 끝날때 까지 대기

	int exit_status = child_thread->exit_status; // 자식 스레드가 죽기 전에 자식의 정보를 저장

	list_remove(&child_thread -> child_elem); // 자식 리스트에서 자식 스레드 제거

	sema_up(&child_thread -> exit_sema); // exit을 기다리는 자식의 seam를 up

	return exit_status;
}

void
process_exit (void) {
	struct thread *curr = thread_current ();
	struct file **f = curr -> fd_table;

	for (int i = 3; i < MAX_TABLE_SIZE; i++) { // 프로세스의 모든 파일 닫음
		if (f[i] != NULL) {
			file_close(f[i]);
			f[i] = NULL;
		}
	}

	free(f); // 프로세스 내 파일 디스크립터 테이블 메모리 해제
	
  file_close(t->running);  // 실행 중인 파일 닫기

	process_cleanup (); // 프로세스 정리
	
	sema_up(&curr -> wait_sema); // 대기 중인 부모 스레드가 바라보는 sema를 up
		
	sema_down(&curr -> exit_sema); // 자식 스레드 끝날때 까지 대기
}

struct thread *get_child_process (int pid) { // 자식 스레중 pid에 해당하는 스레드를 검색
	struct thread *curr = thread_current();
	struct list_elem* e;
	
	// child list이 처음부터 끝까지 순회
 	for (e = list_begin(&curr -> child_list); e != list_end(&curr -> child_list); e = list_next(e)) {
		struct thread *child = list_entry(e, struct thread, child_elem);
		
		// 전달 받은 Pid와 일치하는 프로세스(스레드)가 있는지 검사
		if (child -> tid == pid) {
			return child;
		}
	}

	return NULL;
}

☑️ bool sys_create (const char *file, unsigned initial_size)

  • Initial_size만큼의 크기를 가지는 파일을 만든다.
bool sys_create (const char *file, unsigned initial_size) {
	bool success;

	check_address(file); // 파일에 대한 포인터 주소를 검증한다.

	lock_acquire(&filesys_lock); // 파일 동시접근을 제어하기 위해 LOCK 사용
	success = filesys_create(file, initial_size); // 파일 생성 성공 여부 할당
	lock_release(&filesys_lock); // Lock 해제

	return success;
}

☑️ bool sys_remove (const char *file)

  • 파일 이름에 해당하는 파일을 제거
// syscall.c
bool sys_remove (const char *file) {
	bool res;

	lock_acquire(&filesys_lock); // 파일 동시접근을 제어하기 위해 LOCK 사용
	res = filesys_remove(file); // 파일 삭제
	lock_release(&filesys_lock); // Lock 해제

	return res;
}

☑️ int sys_open (const char *file)

  • 파일 경로에 대한 파일을 열고, 파일의 fd를 반환한다.
// syscall.c
int sys_open (const char *file) {
	struct file *f;

	check_address(file); // 파일 포인터 주소 검증

	lock_acquire(&filesys_lock); // 파일 동시접근을 제어하기 위해 LOCK 사용
	f = filesys_open(file); // 해당 파일을 open한다.
	lock_release(&filesys_lock); // Lock 해제

	if (f == NULL) // 파일 열기 실패시
		return -1;

	int fd = process_add_file(f); // 파일을 파일 디스크립터 테이블(fdt)에 추가하고 fd를 반환

	if (fd == -1) // fdt에 추가할 수 없는 경우, 파일 닫기
		file_close(f);

	return fd;
}

// process.c
int process_add_file (struct file *f) {
	struct thread *t = thread_current();

	for (int fd = 3; fd < MAX_TABLE_SIZE; fd++) { // FD Table을 3부터 순회하며 비어있는 가장 작은 값을 Return 함.
		if ( t -> fd_table[fd] == NULL ) {
			 t -> fd_table[fd] = f;

			return fd;
		}
	}

	return -1;
}

☑️ int sys_filesize (int fd)

  • 파일 객체를 조회해서, 해당 파일이 유효하면 파일의 크기(바이트)를 반환한다.
// syscall.c
int sys_filesize (int fd) {
	struct file* f = process_get_file(fd); // fd에 대한 파일 조회

	if (f != NULL) // 파일이 존재한다면
		return file_length(f); // 파일의 길이를 반환

	return -1;
}

// file.c
off_t file_length (struct file *file) {
	ASSERT (file != NULL);
	return inode_length (file->inode); // inode( 파일의 메타데이터 )에서 파일의 길이를 참조한다.
}

☑️ int sys_read (int fd, void *buffer, unsigned size)

  • fd로 파일을 열고, 실제로 읽은 바이트 수를 반환하고, 읽기에 실패한 경우 -1을 반환한다.
  • fd가 0 ( == 표준 입력 )인 경우 input_getc() 함수를 활용하여 키보드입력을 받는다. 문자열 종단 문자 \0 를 입력받는 경우 입력을 중단하고, 그 크기를 반환한다.
  • fd가 1 ( == 표준 출력 )인 경우, -1을 반환한다.
  • 그 외, fd에 해당하는 파일을 읽어서 buffer에 size 만큼 읽은 바이트 수를 저장한다.
// syscall.c
int sys_read (int fd, void *buffer, unsigned size) {
	// 열린 파일의 데이터를 읽는 시스템 콜
	int readed_byte;
	
	check_address(buffer);

	if (fd == STDIN_FILENO) { // 표준 입력 FD (0)을 전달 받은 경우
		unsigned i;
		for (i = 0; i < size; i++) { // 최대 size 만큼 문자를 입력받는다.
				((uint8_t *)buffer)[i] = input_getc();
				if (((uint8_t *)buffer)[i] == '\0') {
						break;
				}
		}
		readed_byte = i; // 입력한 사이즈에 대해서 할당

	} else if (fd == STDOUT_FILENO) { // 파일 디스크립터가 표준 출력인 경우, 읽기 작업을 지원하지 않으므로 -1을 반환
        return -1;
  } else { // 표준 입출력 FD (0) 이외의 값을 전달 받은 경우
		lock_acquire(&filesys_lock); // 파일 동시접근을 제어하기 위해 LOCK 사용

		struct file *f = process_get_file(fd); // 파일 디스크립터를 통한 파일 객체 검색

		if (f == NULL) { // 파일이 없는 경우 예외처리
			lock_release(&filesys_lock); // Lock 해제
			return -1;
		}

		readed_byte = file_read(f, buffer, size); // 파일의 데이터를 size만큼 저장하고 읽은 바이트 수 리턴

		lock_release(&filesys_lock); // Lock 해제
	}

	return readed_byte;
}

☑️  int sys_write (int fd, void *buffer, unsigned size)

  • fd에 해당하는 파일을 열고, buffer에 size만큼 데이터를 적는다.
  • 실제 적힌 만큼의 바이트 수를 return.
  • 만약 fd가 1 ( == 표준 출력 )인 경우, putbuf() 함수를 사용하여 콘솔에 적는다.
  • 만약 fd가 2 ( == 표준 입력 )인 경우, -1을 return
  • 그 외의 fd가 입력되면, file_write() 를 활용하여 파일에 기록한다.
// syscall.c
int sys_write (int fd, void *buffer, unsigned size) {
	int write_byte = -1; // 값 갱신이 없는 경우 -1을 반환하기 위해 초기값 -1로 설정
	
	if ( fd >= MAX_TABLE_SIZE ) return -1; // 파일 테이블 최대 크기보다 큰 fd가 들어오는 경우 -1을 return한다.
	
	lock_acquire(&filesys_lock); // 파일 동시 접근을 막기 위해 Lock 사용
	
	if (fd == STDOUT_FILENO) {  // 표준 출력 FD (1) 을 전달받은 경우
		putbuf(buffer, size); // 버퍼에 저장된 값을 화면에 출력후 

		write_byte = size; // 버퍼의 크기 반환

	}  else if (fd == STDIN_FILENO) {
    // 쓰기가 표준 입력인 경우, 파일 시스템 잠금 해제 후 -1 반환
    lock_release(&filesys_lock);
    return -1;
	} else {  // 표준 출력 FD (1)외의 FD를 전달받은 경우

		struct file *f = process_get_file(fd); // 파일 디스크립터를 통한 파일 객체 검색

		if (f != NULL) { // 파일이 없는 경우 예외 처리
			write_byte = file_write(f, buffer, size); // 버퍼에 저장된 크기만큼 파일에 기록한 바이트 수 할당
		}
	}
	
	lock_release(&filesys_lock); // Lock 해제 

	return write_byte;
}

// console.c
void putbuf (const char *buffer, size_t n) {
	acquire_console (); // 콘솔 잠금을 얻는다. 다른 프로세스의 콘솔에 대한 동시 접근을 막음
	while (n --> 0)
		putchar_have_lock (*buffer++); // 버퍼의 내용을 출력
	release_console (); // 콘솔 잠금을 해제.
}

☑️ void sys_seek ( int fd, unsigned position )

  • fd에 해당하는 파일을 열어서 읽거나 쓸 바이트로 이동한다.
// syscall.c
void sys_seek ( int fd, unsigned position ) {
	if ( fd < 3 ) { // fd가 3이하인 경우 표준 입,출력,에러를 의미하기 떄문에 반환
		return;
	} 
	
	struct file *file = process_get_file(fd); // fdt 조회

	if (f != NULL) {
		file_seek(f, position); // 파일 위치로 이동
	}
}

// file.c
void
file_seek (struct file *file, off_t new_pos) {
	ASSERT (file != NULL);
	ASSERT (new_pos >= 0);
	file->pos = new_pos;
}

☑️ unsigned sys_tell ( int fd )

  • fd로 연 파일에 대해서 읽거나 쓸 위치의 다음 byte를 반환한다.
  • seek와 하는 기능이 거의 비슷하지만 tell의 경우 위치를 반환한다.
// syscall.c
unsigned sys_tell ( int fd ) {
	if ( fd < 3 ) { // fd가 3이하인 경우 표준 입,출력,에러를 의미하기 떄문에 반환
		return;
	} 
	struct file *file = process_get_file(fd); // fdt 조회

	if (f != NULL) {
		return file_tell(f, position); // 파일 위치를 반환
	}
	
	return NULL;
}

☑️ void sys_close (int fd)

  • fd에 해당하는 파일을 닫음
// syscall.c
void sys_close (int fd) {
	process_close_file(fd);
}

// process.c
void process_close_file (int fd) {
	struct thread *t = thread_current();
	struct file *f = t -> fd_table[fd];

	if (f != NULL) {
		file_close(f);
		t -> fd_table[fd] = NULL;
	}

}

☑️ void syscall_handler (struct intr_frame *f UNUSED)

  • 인터럽트 프레임 내부에 있는 첫번째 인자 ( R.Rax )를 통해 시스템 콜 넘버를 확인한다.
  • 시스템 콜 넘버에 맞는 시스템 콜을 호출한다.
void syscall_handler (struct intr_frame *f UNUSED) {
	 // 시스템 호출 번호와 인자들을 저장할 변수들
	int call_num_ptr = f->R.rax;

	// 시스템 콜 넘버를 통해 해당하는 시스템 콜을 호출한다.
	switch (call_num_ptr) {
		case SYS_HALT:                 /* Halt the operating system. */
			sys_halt();
			break;

		case SYS_EXIT:                /* Terminate this process. */
			sys_exit(f -> R.rdi);
			break;    

		case SYS_EXEC:                /* Switch current process. */
			f -> R.rax = sys_exec(f -> R.rdi);
			break; 

		case SYS_WAIT:                /* Wait for a child process to die. */
			f -> R.rax = sys_wait(f -> R.rdi);
			break; 

		case SYS_CREATE:              /* Create a file. */
			f -> R.rax = sys_create(f -> R.rdi, f -> R.rsi);
			break;

		case SYS_REMOVE:              /* Delete a file. */
			f -> R.rax = sys_remove(f -> R.rdi);
			break;

		case SYS_OPEN:                /* Open a file. */
			f -> R.rax = sys_open(f -> R.rdi);
			break;

		case SYS_FILESIZE:            /* Obtain a file's size. */
			f -> R.rax = sys_filesize(f -> R.rdi);
			break;

		case SYS_READ:                /* Read from a file. */
			f -> R.rax = sys_read(f -> R.rdi, f -> R.rsi, f -> R.rdx);
			break;

		case SYS_WRITE:              /* Write to a file. */
			f -> R.rax = sys_write(f -> R.rdi, f -> R.rsi, f -> R.rdx);
			break;

		case SYS_SEEK:                /* Change position in a file. */
			sys_seek(f -> R.rdi, f -> R.rsi);
			break;

		case SYS_TELL:                /* Report current position in a file. */
			f -> R.rax = sys_tell(f -> R.rdi);
			break;

		case SYS_CLOSE:               /* Close a file. */
			sys_close(f -> R.rdi);
			break;

		default:
			thread_exit();
			break;
	}
}

☑️ check_address()

void check_address ( void *addr ) {
	// 전달받은 주소값이 유효한지, 사용자 영역의 메모리 주소인지 검사
	if ( addr == NULL || is_user_vaddr(addr) == false )
		sys_exit(-1);
}

thread 구조체 요소 추가


💫 트러블 슈팅

  • 시스템 콜 로 만든 함수들을 각 위치에서 호출하지 못하고 있는 경우
→ 기존 내장 함수인 `fork` 함수와 중복되어 생성되어 충돌이 발생하고 있다. → syscall.h, process.c에서 만든 함수가 참조되지 않는 현상
  1. 함수명 제대로 확인좀…

    위에서 확인할 수 있었던 sys_* 함수들은 모두 기존에 sys_접미사가 없는 함수들이었다. 하지만 기존 시스템 콜과 이름이 같아서 아래와 같은 confilct error가 발생했었고, 직접 만든 시스템 콜의 경우 함수 명을 sys_를 붙여 기존의 시스템 콜 함수와 다른 이름을 사용했다.

    바꾸는 과정에서 함수를 선언해둔 헤더파일에는 모두 수정했지만, 실제 함수 구현부의 함수 이름을 채 바꾸지 않고, 테스트를 돌려서 에러가 발생했었다.

    꼼꼼하게 확인하지 못했다.

  2. VM Block

    #ifndef VM
    
    /* code... */
       
    #endif /* VM */

    해당 블록은 컴파일 옵션으로 VM 이 정의된 상황에서만 내부 코드가 컴파일 된다. remove_child_process , get_child_process 의 경우 이 블록 내부에 선언해주어서 헤더파일에 포함되어있더라도 실행시 함수가 없다고 인식하게 된다.

🎉 결과

fork() 시스템 콜에 대한 구현이 미흡해서 구현할 수 있는 부분까지 진행했다.

22 of 95 tests failed. 🫥


🙃 후기

분하다… ALL PASS를 다짐했었는데 마지막에 이렇게 무기력하게 무너질 줄은 몰랐다. 저번 프로젝트 1을 1.5주간 하면서도 많은 것을 배웠지만 이제는 익숙해지지 않았을까? 하는 안일한 생각을 해버렸다. 매주 그랬듯 새로운 주제와 난이도의 과제를 진행했고, 처참히 꺠졌다. 후회되는 것들이 많았던 프로젝트인 만큼 고민했던 것들을 적어보려고 한다. (과제 외적인 것들이 많긴 하네..)

개념 vs 구현

이번 프로젝트 2를 진행하면서 가장 깊게 생각했던 부분은 구현하는 것과 개념을 아는 것 그 사이를 어떻게 찾아야하는가 였다.

시간이 충분하게 많다면 두마리 토끼를 잡을 수 있었겠지만 그렇지 못하는 여건에서 어떻게 효과적으로 PintOS를 통해서 많은 것을 배울 수 있을까 고민했던 것 같다. (그마저도 해야할 케이스들이 많아서 짧게 그치긴했다…) 과제 초기에는 시스템 콜에 대한 흐름과 시스템 콜 코드의 흐름을 충분히 파악하지 못하고 그저 빠르게 구현에 뛰어들어버려 이해하는데 많은 시간이 걸렸다. 이런 상태에서 구현을 하더라도 이 운영체제의 한 흐름을 내가 충분하게 이해했는지 나에게 물었을 때 자신있게 이해했다고 말할 수 없을 것 같았다. 그래서 조금 깊게 공부하고 있지만 이 흐름을 정리하면서 과제를 하고 있던 동료들이 멋있어 보였다.

하지만 과제 마감 며칠을 남겨두고 코치님들께서 깊게 이해할 필요없고, 무조건 시간내 구현하는 것이 1차적인 목표다라고 말씀하셨다. 그리고 PintOS의 흐름을 깊게 이해한다고 해서 OS를 전부 이해하는 것이 아니기 때문에, 다른 사람이 작성한 코드를 빠르게 이해하고, 주어진 요구사항을 구현을 하고, 그 과정에서 맞닥뜨린 어려움, 버그들을 핸들링해보는 경험을 빨리 하셨으면 좋겠다라고 덧붙이셨다. 말씀을 들으면서 다시 내가 왜 이 곳에 오게 됐는지를 상기하게 됐고, 개발자라면 당연 추구해야하는 성장 방식이라는 생각이 들었다.

어느 것이 더 정답이다라는 것은 없지만, 정글에서 의도하는 것이 무엇인지, 어떤 것이 내가 개발자 커리어를 다시 시작함에 있어서 나은 것인지 빨리 결정내릴 필요가 있을 것 같다.

체력

PintOS 주차가 거듭되면서 체력의 중요성을 느끼고 있고, 점차 더 느껴질 것 같다.

하지만 체력이 떨어지는 것보다 체력이 떨어지면서 스스로 예민해지고 있다고 느껴지는 날들이 있었다. 이런 나를 보면서 놀래기도 했고, 기분이 태도가 되지 말아야 된다며 스스로 반성도 하고 있다. 다들 힘들고, 각자의 사정이 있을텐데 동료들에게 폐를 끼치고 싶진 않다.

기분과 태도는 말에서 드러난다. 항상 말을 조심하고, 줄이려고 노력하자.

프로젝트 3에서는…

무조건 ALL PASS를 목표로 할 것이다. VM이라는 엄청 어려울 것 같은 주제가 기다리고 있지만, 이번 2주차 마지막날인 지금처럼 ALL PASS로 완료하지 못한 것을 후회하고 싶지 않다.

그러기 위해서 이번 미션에서는 흐름을 빨리 이해할 것이다. 초반에 전반적인 흐름을 정리하지 않고 그냥 무작정하다보니 서로 떨어져있는 개념을 연결하지 못하면서 어려움을 많이 느꼈던 것 같다.

흐름을 빨리 파악하겠다.

무엇보다 같은 조가 된 동료들과의 조합도 너무 기대된다. 내가 힘든 만큼 동료들에게 더 힘이 되기 위해 노력해야겠다. 마무리 잘하자.

profile
나도 잘하고 싶다..!

0개의 댓글