시스템 콜은 도움을 주는 영역이 아니고 사용자 영역에서 자원을 사용하기 위한 유일한 방법이다. 그런 만큼 군더더기 없이 기능면에선 시비 걸리지 않게 구현 할 수 있어야 한다. 파일 시스템 콜부터 구현을 시도해 보기로 한다.
2025.05.28 - [구현하기] - PintOS P2 #4. System Call 서론
PintOS P2 #4. System Call 서론
유저 프로그램이 작동하기 위해서는 직접 내용을 요청하기보단 시스템 콜을 통한 본래의 기계 내의 서비스를 이용하는 것이 필요하다. 그에 대한 대리 수행은 System Call로 이루어진다. 그 내용을
hyeonistic.tistory.com
이전 글에서 이어진다.
제일 급한 것부터
최초로 구현해야 할 것은 write와 exit이다. 왜냐하면, 기본적인 출력이 이루어져야 테스트 케이스의 상태를 확인 할 수 있기 때문이다. 그래서 write를 한 번 만들어보자.
write는 매개변수의 내용에 따라 파일에 write하기도 하고 운영체제의 출력 콘솔에 write하기도 한다. 매개변수는 운이 좋게도 그 정보가 주어져있다. 우린 사전 정보를 간단히 확인하고 이어서 코드 구현은 어떻게 이루어졌는지를 확인 할 것이다.
- write는 매개 변수로 세 가지를 받는다 : fd 번호, write 할 내용, 내용의 크기
- fd가 1번이면 운영체제의 출력 콘솔로 출력한다. 그에 해당하지 않는 경우 fd에 해당하는 파일에 write 한다.
- 이건 후에 확장 할 것이다.
write(f->R.rdi, f->R.rsi, f->R.rdx);
int write(int fd, const void *buffer, unsigned size)
{
if(fd == 1)
{
putbuf(buffer, size);
return size;
}
}
이렇게 하면 기본적인 출력을 만들 수 있다. 이어서 exit을 만들어보자.
exit(f->R.rdi);
void exit(int status)
{
struct thread *curr = thread_current();
printf("%s: exit(%d)\n", curr->name, status);
thread_exit();
}
이제 대부분의 테스트 케이스에서 요구하는 출력은 작동한다. 축하한다. 당신은 윈도우 12 개발자가 되었다.
이어서.. 파일 관련된 내용을 만들어보자. 우선 파일을 제대로 논하기 전에는 파일 디스크립터를 쓰레드 단위에서 정의해야한다.
우리는 크게 다루지 않을 것 같다는 생각에 PintOS 내 file 형식의 fd_table을 64개 칸을 정의해서 사용했다.
file과 filesys를 하나하나 설명하기엔 그 제목이 직관적이니 그런가보다 하고 받아들이길 기대한다.
sturct thread
{
...
struct file *fd_table[64];
int next_fd;
...
}
범용성 있는 내용들
- 모든 시스템 콜 테스트 케이스에는 불량한 데이터를 고의로 넣는 행위가 다량 발생 할 것이다. 그래서 다들 적절한 값이 배정되었는지를 확인하는 과정을 거쳐야한다. 나같은 경우에는 이미 존재하는 is_user_vaddr 형식을 사용했고, 모든 시스템 콜의 분기점에 배치하였다.
case SYS_WRITE:
if(is_user_vaddr(f->R.rdi) && is_user_vaddr(f->R.rsi) && is_user_vaddr(f->R.rdx))
f->R.rax = write(f->R.rdi, f->R.rsi, f->R.rdx);
break;
이것은 write 시스템 콜의 단편적인 예제이다. 꼭 이렇게가 아니더라도 다양한 형식으로 적절한 주소인지만큼은 확인 해낼 수 있어야한다.
리턴값 있는 시스템 콜들도 빠질 수가 없다. 이런 것들은 일단 리턴을 하는데, 리턴 값도 rax라는 레지스터에 도로 배치하는 것이 약속이다.
즉, 이런 형태가 최종적인 시스템 콜 형태가 된다.
f->R.rax = write(f->R.rdi, f->R.rsi, f->R.rdx);
write 확장
- 파일에도 write 행위를 수행 할 수 있어야 한다.
- 일부 부적절한 write 시도에 대해 방지 할 수 있어야 한다.
int write(int fd, const void *buffer, unsigned size)
{
if ((buffer == NULL) || !(pml4_get_page(thread_current()->pml4, buffer))) exit(-1);
if(fd == 0) exit(-1);
if(fd >= 64) exit(-1);
if(fd == 1)
{
putbuf(buffer, size);
return size;
}
else
{
struct file *targetWrite = thread_current()->fd_table[fd];
if(targetWrite == NULL) exit(-1);
int writed = file_write(targetWrite, buffer, size);
return writed;
}
}
exit(-1)로 유도 된 모든 상황은 적절하지 못한 실행이다. 나중에 테스트 케이스에 대한 정보를 간단히 정리해볼텐데, 그 글과 대응해서 본다면 도움이 될 것이다.
read
- fd == 0 인경우, 입력을 받는다.
- 그 외의 fd는 file에서 내용을 읽어온다.
- 적절치 못한 호출은 exit(-1) 처리 해야한다.
int read(int fd, void *buffer, unsigned size)
{
if(fd >= 64 || fd == 1 || fd == 2) exit(-1);
if ((buffer == NULL) || !(pml4_get_page(thread_current()->pml4, buffer))) exit(-1);
if(!is_user_vaddr(buffer)) exit(-1);
if(fd == 0)
{
uint8_t inputData = input_getc();
return inputData;
}
else
{
off_t inputData = file_read(thread_current()->fd_table[fd], buffer, size);
return inputData;
}
재밌는 것은 read도 write를 호출 할 때와 동일한 매개변수를 적는다는 것이다. 다시 말해, 사용 할 시스템 콜 분기 번호가 달라짐으로써 그릇 자체는 같을 지언정 내용은 시스템 콜에 맞게 내용도 적절히 바뀐다고 간주 할 수 있다.
open
- 파일을 열어서 OS가 사용 할 수 있게 한다. 열려서 나온 결과를 현재 쓰레드의 파일 디스크립터 테이블에 매핑한다.
int open(const char *file)
{
if (pml4_get_page(thread_current()->pml4, file) == NULL) exit(-1);
struct thread *curr = thread_current();
struct file *targetFile = filesys_open(file);
if(targetFile == NULL) return -1;
int i = curr->next_fd;
curr->fd_table[i] = targetFile;
curr->next_fd += 1;
return i;
}
사실 이것은 상당히 부적절 한 것이, 비어있는 것 fd_table[]을 생각 할 수 있지만.. NULL을 적절히 찾아서 배치하는 형태를 보이는 것이 좋다.
close
- 파일을 닫고 해당 하는 파일 디스크립터 테이블을 초기화 한다.
void close(int fd)
{
if(fd > 64) exit(-1);
struct file *closeTarget = thread_current()->fd_table[fd];
if (!is_user_vaddr(closeTarget)) return;
file_close(closeTarget);
}
create
- 파일을 생성하는 filesys_create의 wrapping 함수 같이 생각하면 좀 할만하다.
bool create(const char *file, unsigned initial_size)
{
if (pml4_get_page(thread_current()->pml4, file) == NULL) exit(-1);
if(strlen(file) == 0) exit(-1);
if(strlen(file) > 128) return false;
bool isCreated = filesys_create(file, initial_size);
return isCreated;
}
remove
- create와 동일하게, wrapping 함수를 만든다고 생각해보면 빠르게 구성 할 수 있다.
bool remove(const char *file)
{
if (pml4_get_page(thread_current()->pml4, file) == NULL) exit(-1);
if(strlen(file) == 0) exit(-1);
if(strlen(file) > 128) return false;
bool isRemoved = filesys_remove(file);
return isRemoved;
}
seek, tell, filesize
- 각 부분이 모두 wrapping 함수 처럼 작성하면 된다.
// Changes the next byte to be rtead or written in open file fd to position.
void seek(int fd, unsigned position)
{
struct file *targetSeek = thread_current()->fd_table[fd];
if(targetSeek == NULL) exit(-1);
file_seek(targetSeek, position);
}
// Return the position of the next byte to be read or written in open file fd.
unsigned tell(int fd)
{
struct file *targetTell = thread_current()->fd_table[fd];
if(targetTell == NULL) exit(-1);
off_t value = file_tell(targetTell);
return value;
}
// 파일 사이즈 반환
int filesize(int fd)
{
struct file *targetView = thread_current()->fd_table[fd];
if(targetView == NULL) exit(-1);
off_t fileSize = file_length(targetView);
if(fileSize == 0) return -1;
return fileSize;
}
이것이 완전한 끝은 아니지만, 일단 이대로 두면 큼직큼직 한 파일 관련 테스트 케이스에서는 재미를 좀 볼 수 있을 것이다.
'구현하기' 카테고리의 다른 글
PintOS P3 #1 : Virtual Memory 서론 (0) | 2025.05.29 |
---|---|
PintOS P2 #6 : System Call : Fork, Exec, Wait (0) | 2025.05.28 |
PintOS P2 #4. System Call 서론 (0) | 2025.05.28 |
PintOS P2 #3 : Argument Passing 본편 (0) | 2025.05.21 |
PintOS P2 #2 : Argument Passing을 다루기 전 (0) | 2025.05.21 |