User Program _System Call(exit, write, open, close, create, remove)

sys_exit

이제 기본적인 시스템콜 구현을 보도록 하겠습니다

유저 프로그램이 exit을 호출하면 내부적으로 다음과 같은 시스템 콜 래퍼 함수가 호출됩니다:

void exit (int status) {
    syscall1(SYS_EXIT, status); // 시스템 콜 호출
    NOT_REACHED();              // 이후 코드는 절대 도달하지 않음
}

즉, SYS_EXIT이라는 번호를 가진 시스템 콜이 커널에 요청되지요~

void syscall_handler(struct intr_frame *f UNUSED)
{
	uint64_t syscall_num = f->R.rax;
	uint64_t arg1 = f->R.rdi;
	uint64_t arg2 = f->R.rsi;
	uint64_t arg3 = f->R.rdx;
	uint64_t arg4 = f->R.r10;
	uint64_t arg5 = f->R.r8;
	uint64_t arg6 = f->R.r9;

	switch (syscall_num)
	{
	case SYS_HALT:
		sys_halt();
		break;
	case SYS_EXIT:
		sys_exit(arg1);
		break;
	case SYS_FORK:
		f->R.rax = process_fork((const char *)arg1, f);
		break;
	case SYS_EXEC:
		f->R.rax = sys_exec((void *)arg1);
		break;
	case SYS_WAIT:
		f->R.rax = sys_wait((tid_t)arg1);
		break;
	case SYS_CREATE:
		f->R.rax = sys_create(arg1, arg2);
		break;
	case SYS_REMOVE:
		f->R.rax = sys_remove(arg1);
		break;
	case SYS_OPEN:
		f->R.rax = sys_open(arg1);
		break;
	case SYS_FILESIZE:
		f->R.rax = sys_filesize(arg1);
		break;
	case SYS_READ:
		f->R.rax = sys_read(arg1, arg2, arg3);
		break;
	case SYS_WRITE:
		f->R.rax = sys_write(arg1, arg2, arg3);
		break;
	case SYS_SEEK:
		sys_seek(arg1, arg2);
		break;
	case SYS_TELL:
		f->R.rax = sys_tell(arg1);
		break;
	case SYS_CLOSE:
		sys_close(arg1);
		break;
	default:
		thread_exit();
		break;
	}
}

커널은 syscall 번호에 따라 sys_exit()함수를 호출하게 됩니다

sys_exit

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

	printf("%s: exit(%d)\n", thread_name(), status);
	thread_exit();
}

 스레드 이름: exit(상태)의 형태를 출력하고 스레드를 종료하는 흐름인디

현재 실행 중인 스레드의 종료상태를 저장해두고,

메세지를 출력한 후 thread_exit()를 호출합니다!~

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

#ifdef USERPROG
	process_exit();
#endif

	/* Just set our status to dying and schedule another process.
	   We will be destroyed during the call to schedule_tail(). */
	intr_disable();
	do_schedule(THREAD_DYING);
	NOT_REACHED();
}

현재 스레드가 더이상 실행되지 않도록 THREAD_DYING상태로 표시하고

스케줄러가 다음 스레드를 선택하고

해당 스레드는 완저니 소멸되게됩니다....

결론적으로!

exit() 시스템 콜은 유저가 호출하면 커널이 처리하고,

현재 프로세스를 종료하고 스레드를 제거하는 흐름이다.

종료 상태는 커널이 추적할 수 있도록 exit_status에 저장된다.

로 정리할 수 있겠습니다


sys_write

/* write:
 * 열린 파일 디스크립터에 데이터를 쓴다.
 * fd: 쓸 대상 파일 디스크립터
 * buffer: 쓸 데이터가 담긴 메모리 주소
 * size: 쓸 바이트 수
 * SYS_WRITE: write syscall 번호
 * syscall3을 통해 fd, buffer, size를 전달한다.
 * 반환값은 실제로 쓴 바이트 수이고, 실패 시 -1을 반환할 수 있다. */
int
write (int fd, const void *buffer, unsigned size) {
	return syscall3 (SYS_WRITE, fd, buffer, size);
}
static int sys_write(int fd, const void *buffer, unsigned size)
{
	
	if (fd == 1)
	{
		putbuf(buffer, size);
		return size;
	}

}
  • 커널은 파일 디스크립터 fd의 값에 따라 어떤 출력 장치에 쓸지를 결정함
  • fd == 1일 경우:
    • stdout (표준 출력) 을 의미함
    • 따라서 putbuf(buffer, size)를 호출해 콘솔에 출력
    • putbuf는 buffer에 담긴 내용을 size만큼 터미널에 출력하고 끝
  • 수정 예정입니다~

sys_open

open은 파일 열고 파일 디스트립터(fd)를 반환하고 실패시 -1을 반환하는 함수입니다

우선 파일 디스크립터를 사용하려면 각 프로세스 또는 스레드가 자신만의 파일 디스크립터테이블을 가져야합니다

Why?

  • 유저는 파일을 “이름”으로 열지만,
  • 유저 프로그램이 이후 접근할 땐 “숫자 fd”를 통해 접근함
  • 그래서 내부적으로 fd → file 구조체 포인터 매핑을 유지해야 함

아무튼간 그래서 thread구조체에 추가해줍니다

#define MAX_FD 64

struct thread {
    ...
    struct file **fd_table;  // fd 번호로 파일에 접근하는 테이블
    int fd_idx;              // 다음 사용할 fd 번호
    ...
};

그리고 process_init에서 fd_table할당합니다

process_init(void)
{
	struct thread *current = thread_current();
	current->fd_table = calloc(MAX_FD, sizeof(struct file *));
	current->fork_sema = malloc(sizeof(struct semaphore));
	sema_init(current->fork_sema, 0);
	ASSERT(current->fd_table != NULL);
}

참고로 MAX_FD는 32입니다

이제 sys_open함수를 보면

int sys_open(const char *file) {
	check_address(file); // 사용자 영역 유효성 검사

	if (file == NULL || strcmp(file, "") == 0)
		return -1;

	struct file *file_obj = filesys_open(file); // 내부 파일 시스템에서 열기
	if (file_obj == NULL)
		return -1;

	int fd = find_unused_fd(file_obj); // 빈 FD 찾고 저장
	return fd;
}

sys_open함수는 이런 흐름

filesys_open

struct file *filesys_open(const char *name) {
	struct dir *dir = dir_open_root();
	struct inode *inode = NULL;
	bool exist = dir && dir_lookup(dir, name, &inode);
	dir_close(dir);

	if (!exist)
		return NULL;

	return file_open(inode);
}
  • dir_lookup()을 통해 실제로 존재하는 파일인지 확인
  • 존재한다면 inode를 얻고 file_open()으로 파일 객체 반환
bool
dir_lookup (const struct dir *dir, const char *name,
            struct inode **inode) {
    struct dir_entry e;

    ASSERT (dir != NULL);
    ASSERT (name != NULL);

    if (lookup (dir, name, &e, NULL))       // 디렉터리 내 검색
        *inode = inode_open (e.inode_sector);  // 찾으면 inode 열기
    else
        *inode = NULL;                         // 못 찾으면 NULL

    return *inode != NULL; // 성공 여부 반환
}
  • 디렉터리 객체 dir에서 주어진 이름 name의 엔트리를 검색
  • 찾으면 inode 포인터에 해당 파일의 inode를 열어서 저장
  • 못 찾으면 inode는 NULL이 됨
  • 성공 여부는 true / false로 반환

lookup()

  • 디렉터리 파일을 한 엔트리씩 읽음 (inode_read_at)
  • name이 일치하고 in_use라면 성공

inode_read_at()

  • 디스크에서 엔트리 정보를 읽어오는 함수
  • sector 단위로 읽고 필요한 부분만 복사

이러한 흐름을 거쳐서

파일이 존재하고 inode 사용 가능하면 *inode = inode_open(...) 결과를 저장하게 됩니다

file_open

주어진 inode를 기반으로 파일 객체(struct file)를 동적 생성하여 반환

struct file *
file_open (struct inode *inode) {
	struct file *file = calloc (1, sizeof *file);
	if (inode != NULL && file != NULL) {
		file->inode = inode;
		file->pos = 0;
		file->deny_write = false;
		return file;
	} else {
		inode_close (inode);
		free (file);
		return NULL;
	}
}
  1. sizeof(struct file)만큼 heap에서 메모리 할당 (calloc)
  2. inode가 NULL이 아니고, 메모리 할당도 성공했으면:
    • file->inode = inode : 해당 inode를 연결
    • file->pos = 0 : 파일 포인터 시작 위치 초기화
    • file->deny_write = false : 쓰기 허용 상태로 시작
    • → file 반환
  3. 실패한 경우:
    • inode_close(inode) 호출 → 참조 수 감소
    • file 메모리 해제
    • NULL 반환

그럼 이제 find_unused_fd함수로 사용되지 않은 파일 디스크립터 번호를 찾아 할당하고 반환해줍니다

int find_unused_fd(const char *file)
{
	struct thread *cur = thread_current();

	while (cur->fd_table[cur->fd_idx] && cur->fd_idx < MAX_FD)
		cur->fd_idx++;

	if (cur->fd_idx >= MAX_FD)
		return -1;

	cur->fd_table[cur->fd_idx] = file;

	return cur->fd_idx;
}
  1. 현재 스레드의 파일 디스크립터 테이블(fd_table)을 확인
  2. fd_idx 위치가 비어 있지 않으면 다음 인덱스로 이동
  3. fd_idx가 MAX_FD 이상이면 → 더 이상 빈 공간이 없음 → -1 반환 (에러)
  4. 비어 있는 위치를 찾으면 해당 인덱스에 파일을 등록하고, 그 인덱스를 반환

핵심 포인트

  • fd_idx는 테이블에서 탐색을 시작할 위치이며, 사용될 때마다 증가한다
  • 실제로는 fd_table[2]부터 사용하게 구현되는 게 일반적 (0: stdin, 1: stdout 예약됨)
  • 반환된 fd_idx는 유저 프로그램이 이후 read, write 등에서 사용할 파일 핸들

sys_close

// lib>user>syscall.c
int
open (const char *file) {
	return syscall1 (SYS_OPEN, file);
}
//userprog>syscall.c
void sys_close(int fd)
{
	struct thread *curr = thread_current();
	if (fd < 2 || fd >= MAX_FD)
		return NULL;
	struct file *file_object = curr->fd_table[fd];
	if (file_object == NULL)
		return;

	file_close(file_object);
	curr->fd_table[fd] = NULL;
}

파일 디스크립터 fd를 닫는 함수입니다 프로세스가 종료되면 모든 fd는 자동으로 닫힙니다

file_close - 파일 구조체를 해제하고, 해당 inode도 닫아준다

void
file_close (struct file *file) {
	if (file != NULL) {
		file_allow_write (file);
		inode_close (file->inode);
		free (file);
	}
}
  1. file_allow_write()로 쓰기 금지 해제
  2. inode_close()로 inode 참조 정리
  3. file 구조체 자체를 free()

inode_close

해당 Inode의 열려있는 참조 수(open_cnt)를 관리하고, 마지막 참조일 경우 자원을 완전히 해제

void
inode_close (struct inode *inode) {
	/* Ignore null pointer. */
	if (inode == NULL)
		return;

	/* Release resources if this was the last opener. */
	if (--inode->open_cnt == 0) {
		/* Remove from inode list and release lock. */
		list_remove (&inode->elem);

		/* Deallocate blocks if removed. */
		if (inode->removed) {
			free_map_release (inode->sector, 1);
			free_map_release (inode->data.start,
					bytes_to_sectors (inode->data.length)); 
		}

		free (inode); 
	}
}
  1. open_cnt를 하나 감소
  2. 참조가 모두 끊기면:
    • inode 리스트에서 제거
    • inode->removed == true일 경우, 디스크 블록까지 해제
    • inode 자체를 free()

더이상 열려 있는 파일이 없으면, inode 및 디스크 자원을 완전히 해제한다


sys_create

사용자 입력으로 들어온 file이름의 파일을 커널이 완전한지 검사한 뒤, 내부적으로 inode를 생성하고 디렉토리에 등록하여 파일 생성

/* create:
 * 파일 시스템에 새 파일을 생성하는 시스템 콜을 호출한다.
 * file: 생성할 파일의 이름 (문자열 포인터)
 * initial_size: 파일의 초기 크기 (바이트 단위)
 * SYS_CREATE: create syscall 번호
 * syscall2를 통해 인자 2개를 전달하고, 생성 성공 여부를 bool로 반환한다. */
bool
create (const char *file, unsigned initial_size) {
	return syscall2 (SYS_CREATE, file, initial_size);
}

크기 initial_size의 새 파일을 생성합니다. 성공 시 true, 실패 시 false 반환

bool sys_create(const char *file, unsigned initial_size)
{
	check_address(file);
	if (file == NULL || strcmp(file, "") == 0)
	{
		sys_exit(-1);
	}
	return filesys_create(file, initial_size);
}
  • file 포인터가 유저 영역의 올바른 주소인지 검증
  • file이 비었거나 NULL이면 종료
  • 실제 생성은 filesys_create()에게 위임

check_address

// 주소값이 유저 영역(0x8048000~0xc0000000)에서 사용하는 주소값인지 확인하는 함수
void check_address(const uint64_t *addr)
{
	struct thread *cur = thread_current();

	if (addr == "" || !(is_user_vaddr(addr)) || pml4_get_page(cur->pml4, addr) == NULL)
	{
		sys_exit(-1);
	}
}

주소 유효성 검사 함수

  • 현재 스레드의 page table(pml4)을 기준으로,
    • 주소가 유저 영역에 있는가
    • 페이지 테이블에서 매핑되어 있는가
  • 조건을 하나라도 만족 못하면 sys_exit(-1)로 프로세스 종료

filesys_create

파일 시스템 레벨에서 inode와 디렉터리 엔트리를 직접 조작

bool
filesys_create (const char *name, off_t initial_size) {
	// printf("FILE NAME :%s\\n", name);
	disk_sector_t inode_sector = 0;
	struct dir *dir = dir_open_root ();
	bool success = (dir != NULL
			&& name !=NULL
			&& strlen(name)<=14
			&& free_map_allocate (1, &inode_sector)
			&& inode_create (inode_sector, initial_size)
			&& dir_add (dir, name, inode_sector));
	if (!success && inode_sector != 0)
		free_map_release (inode_sector, 1);
	dir_close (dir);

	// printf("sucees? :%d\\n", success);
	return success;
}
  1. dir_open_root() → 루트 디렉토리 열기
  2. 유효성 검사:
    • name이 비어있지 않은지
    • 이름 길이가 14글자 이하인지 (Pintos 내부 제한)
  3. inode 할당 (free_map_allocate)
  4. inode 생성 (inode_create)
  5. 디렉토리에 추가 (dir_add)
  6. 실패 시: 할당된 inode 공간 반환

성공 시: true, 실패 시: false


sys_remove

파일을 삭제합니다. 열려 있든 닫혀 있든 상관없이 삭제 가능. 성공 시 true

/* remove:
 * 파일 시스템에서 주어진 파일을 삭제하는 시스템 콜을 호출한다.
 * file: 삭제할 파일 이름
 * SYS_REMOVE: remove syscall 번호
 * syscall1로 파일 이름을 넘기고, 삭제 성공 여부를 bool로 반환한다. */
bool
remove (const char *file) {
	return syscall1 (SYS_REMOVE, file);
}
bool sys_remove(const char *file)
{
	return filesys_remove(file);
}

음….. 이게 다에요..

filesys_remove

bool
filesys_remove (const char *name) {
	struct dir *dir = dir_open_root ();
	bool success = dir != NULL && dir_remove (dir, name);
	dir_close (dir);

	return success;
}
  • 루트 디렉터리를 열고
  • dir_remove()로 해당 파일 삭제 시도
  • 디렉터리 닫고 결과 반환
/* dir_remove - 디렉터리에서 주어진 이름(name)의 파일을 제거합니다.
 *
 * 동작 순서:
 * 1. 디렉터리에서 name을 가진 파일을 검색 (lookup).
 * 2. 해당 엔트리의 inode를 열어 실제 파일 메타데이터 접근.
 * 3. 엔트리의 사용 상태를 false로 설정해 디렉터리에서 삭제.
 * 4. 해당 inode를 제거하여 실제 파일 삭제 처리.
 * 5. 성공 여부 반환.
 */
bool
dir_remove(struct dir *dir, const char *name)
{
	struct dir_entry e;			// 디렉터리 엔트리 구조체 (name, inode_sector 등 포함)
	struct inode *inode = NULL; // 파일의 inode 포인터
	bool success = false;		// 성공 여부 초기화
	off_t ofs;					// 디스크 오프셋 (엔트리 위치)

	ASSERT(dir != NULL);
	ASSERT(name != NULL);

	/* 1. 디렉터리에서 해당 이름의 엔트리를 찾는다. */
	if (!lookup(dir, name, &e, &ofs))
		goto done; // 찾지 못하면 실패

	/* 2. 해당 엔트리의 inode를 연다. (파일의 메타데이터 접근) */
	inode = inode_open(e.inode_sector);
	if (inode == NULL)
		goto done;

	/* 3. 디렉터리 엔트리에서 삭제 표시 (in_use 플래그 해제) */
	e.in_use = false;

	/* 디스크 상의 디렉터리 파일에서 해당 엔트리 정보 갱신 */
	if (inode_write_at(dir->inode, &e, sizeof e, ofs) != sizeof e)
		goto done;

	/* 4. 해당 inode를 삭제 (inode의 제거 플래그 설정 + 디스크 해제) */
	inode_remove(inode);

	/* 5. 삭제 성공으로 설정 */
	success = true;

done:
	/* 열었던 inode를 닫고 메모리 해제 */
	inode_close(inode);
	return success;
}

대충 흐름 설명하자면

  1. filesys_remove(name)
    • 루트 디렉터리에서 dir_remove() 호출
    • 성공하면 true 반환
  2. dir_remove(dir, name)
    • 디렉터리 엔트리(파일 목록)에서 해당 파일 검색 → 삭제 플래그 설정
    • 해당 파일의 inode 열어서 삭제 처리
    • 디스크에서도 inode 제거