[TIL] [WEEK9-10] Pintos Project(2) System Call(1)

woo__j·2024년 6월 3일
0

Pintos Project

목록 보기
7/14

시스템 콜 종류에 따라 파일 관련 시스템 콜 & 프로세스 관련 시스템 콜로 나누어서 정리하겠다.

📍 파일 관련 시스템 콜 구현

구현 목록: create, remove, open, filesize, read, write, seek, tell, close

parsing을 할 때와 마찬가지로, 파일을 다루는 걸 어떻게 구현해야 할까? 싶지만 이것도 pintos에서 제공하고 있다. filesys/filesys.c를 보면 파일 조작에 대한 함수가 이미 구현되어 있는 걸 볼 수 있다.
Pintos에 있는 것들을 최대한 활용하자. 방대한 파일들 속에서 잘 찾아내면 이미 제공되는 것들이 많다.

또한 시스템 콜의 인자가 포인터 형식이라면, 주소값이 잘못된 접근을 하진 않는지 check_address( )를 통해 주소 유효성 검사를 한 후 진행되도록 구현해야 함을 주의하자.

또한 시스템 콜에서 반환을 하는 함수들은 핸들러에서 반환 값을 rax에 저장하도록 구현해야 함을 주의하자.

case SYS_EXIT: /* 반환을 하지 않는 경우 */
	exit(f->R.rdi);
	break;
case SYS_FORK: /* 반환을 하는 경우 */
	f->R.rax = fork(f->R.rdi, f);
	break;

1. create()

bool create(const char *file, unsigned initial_size)
{
	check_address(file);
	/* file을 이름으로 하고, 크기가 initial_size인 새로운 파일 생성, 여는 건 X */
	/* 성공 시 true, 실패 시 false 반환 */
	lock_acquire(&filesys_lock);
	bool result = filesys_create(file, initial_size);
	lock_release(&filesys_lock);
	return result;
}

2. remove()

bool remove(const char *file)
{
	check_address(file);
	/* file이라는 이름을 가진 파일 삭제 */
	/* 성공 시 true, 실패 시 false 반환 */
	/* 파일이 열려있는지 닫혀있는지 여부와 관계없이 삭제될 수 있음 */
	lock_acquire(&filesys_lock);
	bool result = filesys_remove(file);
	lock_release(&filesys_lock);
	return result;
}

create() & remove()를 제외한 파일 관련 system call들은 파일 디스크립터를 이용해 파일에 대한 작업을 수행한다. 이 때 kernel은 파일 디스크립터와 실제 파일 구조체를 매핑해 관리하는데, 이를 위한 도구가 fd_table이다.

파일 입출력을 위해선 파일 디스크립터의 구현이 필요하다.
즉.. 우리가 해야 할 일이 늘었다.
파일 디스크립터를 구현한 후 나머지 시스템 콜들을 구현하자.

🛠️ file_descriptor 구현

  • thread 구조체에 파일 디스크립터+파일 디스크립터 테이블 추가
	#ifdef USERPROG
	/* Owned by userprog/process.c. */
	uint64_t *pml4;	   /* Page map level 4 */
	struct file **fdt; /* 파일 디스크립터 테이블 */
	int next_fd;	   /* 파일 디스크립터 값 */
  • thread 생성 시 파일 디스크립터 테이블 메모리 할당 & 나머지 멤버 변수 값 할당
	/* Project(2) */
	/* 파일 디스크립터 테이블 메모리 할당 */
	t->fdt = palloc_get_multiple(PAL_ZERO, FDT_PAGES);
	if (t->fdt == NULL)
	{
		palloc_free_page(t);
		return TID_ERROR;
	}

	t->next_fd = 2;
	t->fdt[0] = 1; /* STDIN_FILENO: 표준 입력 */
	t->fdt[1] = 2; /* STDOUT_FILENO: 표준 출력 */
  • thread 초기화 시 종료 상태 초기화
	t->exit_status = 0;
  • fdt에 파일 추가하는 함수 추가
	/* fdt에 파일을 추가하는 함수 */
    int process_add_file(struct file *f)
    {
        /* 파일 객체에 대한 파일 디스크립터 생성 */
        struct thread *t = thread_current();
        /* 파일 객체를 파일 디스크립터 테이블에 추가 */
        /* 파일 디스크립터의 최대값 1 증가 next_fd++ */
        /* 파일 디스크립터 반환 */
        struct file **fdt = t->fdt;
        int fd = t->next_fd;

        while (fd < FDTCOUNT_LIMIT && fdt[fd] != NULL) /* fdt의 빈자리 탐색 */
        {
            fd++;
        }

        if (fd >= FDTCOUNT_LIMIT) /* 예외처리 - FDT 꽉 찼을 경우 */
        {
            return -1;
        }

        t->next_fd = fd;
        t->fdt[fd] = f;

        return fd;
    }
  • fdt에서 fd에 해당하는 파일을 반환하는 함수 추가
    /* fdt에서 fd에 해당하는 파일 반환하는 함수 */
    struct file *process_get_file(int fd)
    {
        struct thread *t = thread_current();
        struct file **fdt = t->fdt;
        /* 프로세스의 파일 디스크립터 테이블을 검색하여 파일 객체의 주소를 반환, 없으면 NULL 반환 */

        if (fd >= FDTCOUNT_LIMIT || fd < 2)
        {
            return NULL;
        }

        return fdt[fd];
    }
  • fdt에서 fd에 해당하는 파일 닫고, index에 null을 할당해 파일과의 연결 끊는 함수 추가
    /* fdt에서 fd에 해당하는 index에 NULL을 할당해 파일과의 연결 끊기 */
    void process_close_file(int fd)
    {
        struct thread *t = thread_current();
        struct file **fdt = t->fdt;

        if (fd >= FDTCOUNT_LIMIT || fd < 2)
        {
            return NULL;
        }

        fdt[fd] = NULL;
    }

위와 같이 file_descriptor 구현이 끝났다면 나머지 시스템 콜들을 구현할 수 있다.

3. open

int open(const char *file)
{
	check_address(file);
	/* file이라는 이름을 가진 파일을 열기 */
	/* 성공 시, 파일 식별자로 불리는 비음수 정수(0 이상) 반환, 실패 시 -1 반환 */
	lock_acquire(&filesys_lock);
	struct file *target_f = filesys_open(file);

	if (target_f == NULL)
	{
		lock_release(&filesys_lock);
		return -1;
	}

	/* fd table에 파일 추가 후 fd 반환 */
	int fd = process_add_file(target_f);
	/* fd table이 꽉 찼을 경우 파일 닫기 */
	if (fd == -1)
	{
		file_close(target_f);
	}
	lock_release(&filesys_lock);
	return fd;
}

4. filesize

int filesize(int fd)
{
	/* fd로서 열려 있는 파일의 크기가 몇 바이트인 지 반환 */
	struct file *target_f = process_get_file(fd);

	if (target_f == NULL)
	{
		return -1;
	}

	return file_length(target_f);
}

5. read

int read(int fd, void *buffer, unsigned size)
{
	/* buffer 안에 fd로 열려있는 파일로부터 size 바이트를 읽고 읽어낸 바이트의 수를 반환 */
	/* 파일 끝에서 시도하면 0, 파일이 읽어질 수 없었다면 -1 반환 */

	check_address(buffer);
	/* 버퍼 끝 주소도 유저 영역 내에 있는지 체크 */
	check_address(buffer + size - 1);
	int read_bytes = 0;

	/* 파일에 동시 접근이 발생할 수 있기 때문에 lock 걸기 */
	lock_acquire(&filesys_lock);
	if (fd == STDIN_FILENO)
	{
		char *read_buf = (char *)buffer;
		/* 표준 입력 = 키보드의 데이터를 읽어 버퍼에 저장 */
		for (read_bytes = 0; read_bytes < size; read_bytes++)
		{
			char c = input_getc(); /* input_getc(): 키보드로부터 입력받은 문자 반환 함수  */
			*read_buf++ = c;	   /* 버퍼에 입력받은 문자 저장 */
			if (c == '/0')
			{
				break;
			}
		}
	}
	else
	{
		if (fd < 2) /* fd = STDOUT_FILENO, 표준 출력 */
		{
			lock_release(&filesys_lock);
			return -1;
		}
		struct file *target_f = process_get_file(fd);
		if (target_f == NULL)
		{
			lock_release(&filesys_lock);
			return -1;
		}
		read_bytes = file_read(target_f, buffer, size); /* 파일의 데이터를 size만큼 읽어 buffer에 저장 후 */
	}
	lock_release(&filesys_lock);
	return read_bytes; /* 읽은 바이트 수 return */
}

read( )는 fd로 얻어온 파일(즉, 열려있는 파일)로부터 값을 읽어 buffer에 넣는다.
이 때, fd값으로 예외처리를 해준다.

fd == 0일 경우 ) 표준 입력을 의미하므로 input_getc( )를 이용해 키보드 입력을 읽어와 buffer에 담는다.
fd == 1일 경우 ) 표준 출력을 의미하므로, 파일을 읽어들일 수 없어 -1을 반환하도록 한다.
나머지 케이스들 ) file_read( )를 이용

6. write

int write(int fd, const void *buffer, unsigned size)
{
	check_address(buffer);
	/* 버퍼 끝 주소도 유저 영역 내에 있는지 체크 */
	check_address(buffer + size - 1);
	int write_bytes = 0;

	if (fd == STDOUT_FILENO)
	{
		/* 표준 출력, 버퍼에 저장된 값을 console에 출력 후 버퍼 크기 반환 */
		putbuf(buffer, size);
		return size;
	}
	else
	{
		if (fd < 2) /* fd = STDIN_FILENO, 표준 입력 */
		{
			return -1;
		}
		/* 버퍼에 저장된 데이터를 크기만큼 파일에 기록 후 기록한 바이트 수 반환 */
		struct file *target_f = process_get_file(fd);
		if (target_f == NULL)
		{
			return -1;
		}
		lock_acquire(&filesys_lock);
		write_bytes = file_write(target_f, buffer, size);
		lock_release(&filesys_lock);
	}

	return write_bytes;
}

write( )는 buffer에 있는 값을 출력해주는 함수다.
마찬가지로 fd값으로 예외처리를 해준다.

fd == 0일 경우 ) 표준 입력을 의미하므로, -1을 반환한다.
fd == 1일 경우 ) 표준 출력을 의미하므로, 화면에 출력해줘야 하는 경우다. buffer에 저장된 값을 console에 출력한 후 버퍼 크기를 반환한다.
나머지 케이스들 ) file_write( )를 이용

7. seek

void seek(int fd, unsigned position)
{
	/* 열린 파일의 위치(offset)을 이동하는 시스템 콜 */
	/* position: 현재 위치(offset)을 기준으로 이동할 거리 */
	if (fd < 2)
	{
		return;
	}
	/* 파일 디스크립터를 이용해 파일 객체 검색 */
	struct file *target_f = process_get_file(fd);
	if (target_f == NULL)
	{
		return;
	}
	/* 해당 열린 파일의 위치(offset)을 position만큼 이동 */
	file_seek(target_f, position);
}

8. tell

unsigned
tell(int fd)
{
	/* 열린 파일의 위치(offset)을 알려주는 시스템 콜 */
	/* 성공 시 파일 위치를 반환, 실패 시 -1 반환 */
	if (fd < 2)
	{
		return;
	}
	/* 파일 디스크립터를 이용해 파일 객체 검색 */
	struct file *target_f = process_get_file(fd);
	if (target_f == NULL)
	{
		return;
	}
	/* 해당 열린 파일의 위치 반환 */
	return file_tell(target_f);
}

9. close

void close(int fd)
{
	struct file *target_f = process_get_file(fd);

	if (target_f == NULL)
	{
		return;
	}
	lock_acquire(&filesys_lock);
	file_close(target_f);
	lock_release(&filesys_lock);
	process_close_file(fd);
}

이렇게 파일 관련 시스템 콜을 모두 구현할 수 있다.
그런데 설명하지 않은 부분이 하나 있을 것이다. lock_acquire( )와 lock_release( )?

⚙️ 동기화 문제 해결

동시에 한 파일에 접근한다면, Race-Condition 문제가 발생할 수 있기 때문에 동기화 작업이 필요하다.
이 때 lock을 사용해 해결할 수 있다.

filesys_lock을 하나 생성해 파일에 접근할 때 lock을 받고, 작업이 끝난 후 lock을 해제한다.

해당 lock은 userprog/syscall.h에 선언하고, 시스템 콜을 초기화할 때(syscall_init( )) lock을 초기화하는 코드를 추가하면 된다.

그런데 또 한 가지 문제가 하나 더 있다.

⚙️ Deny Write on Executables (실행 파일에 쓰기 거부)

실행 중인 파일에 쓰기 작업을 수행한다면, 예상치 못한 결과를 낳을 수 있기 때문에 이를 방지해주는 걸 구현해줘야 한다.

이는 역시 Pintos에서 제공하는 함수를 활용하면 된다.

file_deny_write(): 열려 있는 파일에 쓰기 작업을 하는 것을 막기
file_allow_write(): 해당 파일의 쓰기 작업을 다시 활성화
-> 파일을 닫으면 쓰기 작업도 활성화 된다.
그러니 실행 파일에 대한 쓰기 작업을 방지하려면 프로세스가 실행 중인 동안 열린 상태로 유지되어야 함을 유의하자.

load()에서 파일을 실행시키니 thread 구조체에 running_file 멤버 변수를 추가해준 후, file_deny_write()를 통해 쓰기 작업을 막아준다. 또한 file_close에서 다시 file_allow_write()를 통해 쓰기 작업을 활성화 해주도록 하자.

이렇게 하면 정말 파일 관련 시스템 콜의 완성이다.
올바르게 작성했다면, 이제부터 test-case를 돌려볼 수 있을 것이다.

실패 시, Kernel-PANIC이거나 fail이 뜨는데
PANIC은 코드가 잘못된 경우(이는 잘못된 순서도 포함),
fail은 어차피 올바른 결과와 내 코드의 결과를 대조해주니 그를 바탕으로 수정할 수 있을 것이다.
디버깅을 적극 활용하자. 여러 방법이 있겠지만 아무래도 역시 printf가 가장 편하고 직관적이긴 하다.

0개의 댓글

관련 채용 정보