시스템 콜 종류에 따라 파일 관련 시스템 콜 & 프로세스 관련 시스템 콜로 나누어서 정리하겠다.
구현 목록: 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;
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이다.
파일 입출력을 위해선 파일 디스크립터의 구현이 필요하다.
즉.. 우리가 해야 할 일이 늘었다.
파일 디스크립터를 구현한 후 나머지 시스템 콜들을 구현하자.
#ifdef USERPROG
/* Owned by userprog/process.c. */
uint64_t *pml4; /* Page map level 4 */
struct file **fdt; /* 파일 디스크립터 테이블 */
int next_fd; /* 파일 디스크립터 값 */
/* 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: 표준 출력 */
t->exit_status = 0;
/* 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에 해당하는 파일 반환하는 함수 */
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을 할당해 파일과의 연결 끊기 */
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을 초기화하는 코드를 추가하면 된다.
그런데 또 한 가지 문제가 하나 더 있다.
실행 중인 파일에 쓰기 작업을 수행한다면, 예상치 못한 결과를 낳을 수 있기 때문에 이를 방지해주는 걸 구현해줘야 한다.
이는 역시 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가 가장 편하고 직관적이긴 하다.