티스토리 뷰
개요
시스템 콜은 소프트웨어 예외의 한 종류로 간주된다. 유저 프로그램은 시스템 콜을 통해 운영 체제에 서비스를 요청할 수 있다. x86-64 아키텍처에서는 syscall
명령어를 사용하여 시스템 콜을 호출핰다. 이는 기존의 x86 아키텍처에서 시스템 콜이 다른 소프트웨어 예외와 동일한 방식으로 처리되던 것과 대조적이다. 시스템 콜을 호출할 때는 시스템 콜 번호와 인수를 레지스터에 설정해야 한다. 시스템 콜 번호는 %rax
레지스터에 저장되며, 인수는 %rdi
, %rsi
, %rdx
, %r10
, %r8
, %r9
순서로 전달된다. 이는 네 번째 인수는 %rcx
로 전달하는 일반적인 함수 호출 규약과 약간 다르다. 시스템 콜 핸들러 syscall_handler()
는 struct intr_frame
을 통해 호출자의 레지스터에 접근할 수 있다. 이 구조체는 커널 스택에 위치한다. 시스템 콜이 값을 반환해야 하는 경우 struct intr_frame
의 rax
멤버를 수정하여 반환 값을 설정할 수 있다. 이는 x86-64 함수 호출 규약을 따르는 것이다.
구현해야하는 시스템콜 함수
void halt (void) NO_RETURN;
void exit (int status) NO_RETURN;
pid_t fork (const char *thread_name);
int exec (const char *file);
int wait (pid_t);
bool create (const char *file, unsigned initial_size);
bool remove (const char *file);
int open (const char *file);
int filesize (int fd);
int read (int fd, void *buffer, unsigned length);
int write (int fd, const void *buffer, unsigned length);
void seek (int fd, unsigned position);
unsigned tell (int fd);
void close (int fd);
/* extra */
int dup2(int oldfd, int newfd);
구현 순서
- halt: OS 종료 시스템콜
- exit: 프로세스 종료 시스템콜
- wait: 자식 프로세스를 기다리는 시스템콜, 임시로 테스트 돌릴수 있을정도로만 구현
- write: 결과를 확인해야하니 간단하게 출력을 할수 있게만 구현
- create: 파일을 만드는 시스템콜
- remove: 파일을 지우는 시스템콜 간단히 구현
- open: 파일을 열어 fd를 반환하는 시스템콜
- filesize: 파일 사이스를 반환하는 시스템콜
- seek
- tell
- read: 파일을 읽는 시스템콜
- write: 파일에다 입력하는 시스템콜
- close: open한 파일을 닫는 시스템콜
- wait, fork, exec: 까다라우니 나중에 구현
halt
power\_off()
를 호출하여 Pintos를 종료한다.(src/include/threads/init.h
에 선언되어 있음). 이 함수는 데드락 상황 등에 대한 정보를 일부 손실하기 때문에 자주 사용되어서는 안 된다.
구현
/* pintos 종료시키는 함수 */
void halt(void){
power_off();
}
exit
현재 사용자 프로그램을 종료하고 status
를 커널에 반환한다. 프로세스의 부모가 wait
하면 이 상태가 반환된다. 일반적으로 status
가 0이면 성공을, 0이 아닌 값은 오류를 나타낸다. 지금은 wait
중인 부모 프로세스를 깨울 방법이 없기 때문에 종료 상태만 저장하고 끝낸다.
구현
/* 현재 프로세스를 종료시키는 시스템 콜 */
void exit(int status){
struct thread *t = thread_current();
t->exit_status = status;
thread_exit();
}
process_exit
/* Exit the process. This function is called by thread_exit (). */
/* 프로세스를 종료합니다. 이 함수는 thread_exit ()에 의해 호출됩니다. */
void
process_exit (void) {
struct thread *curr = thread_current ();
/* TODO: Your code goes here.
* TODO: Implement process termination message (see
* TODO: project2/process_termination.html).
* TODO: We recommend you to implement process resource cleanup here. */
if(curr->exit_status != 1)
printf ("%s: exit(%d)\n", curr->name, curr->exit_status);
process_cleanup ();
}
wait
지금 process_wait은 -1을 리턴니까 무한 루프를 걸어 돌렸을 거다 하지만 그렇게하면 테스트를 실행해 볼 수 없기 때문에 무한 루프보단 반복횟수를 넉넉하게 두어서 프로그램이 실행되고 나서 루프를 탈출하게 구현하는게 낫다. wait는 나중에 구현할거다.
/* Waits for thread TID to die and returns its exit status. If
* it was terminated by the kernel (i.e. killed due to an
* exception), returns -1. If TID is invalid or if it was not a
* child of the calling process, or if process_wait() has already
* been successfully called for the given TID, returns -1
* immediately, without waiting.
*
* This function will be implemented in problem 2-2. For now, it
* does nothing. */
int
process_wait (tid_t child_tid UNUSED) {
/* XXX: 힌트) process_wait (initd)에서 pintos가 종료되는 경우,
* XXX: process_wait을 구현하기 전에 여기에 무한 루프를 추가하는 것을
* XXX: 권장합니다. */
//1000000000
//50000000
for(int i = 0; i < 500000000; i++);
return -1;
}
write
일단 임시로 표준 출력 write
를 해 출력은 가능하게 한다.
int write(int fd, const void *buffer, unsigned size)
{
check_address(buffer); // 주소 유효성 검사
if (fd == 1)
{
putbuf(buffer, size);
return size;
}
return -1;
}
create
file
이라는 이름의 새 파일을 만들고 초기 크기를 initial\_size
바이트로 설정한다. 성공하면 true
를, 그렇지 않으면 false
를 반환한다. 새 파일을 만드는 것은 파일을 열지 않는다. 새 파일을 열려면 별도의 open
시스템 콜이 필요하다.
/* 파일 생성하는 시스템 콜 */
bool create (const char *file, unsigned initial_size) {
check_address(file); // 주소 유효성 검사
bool success;
/* 성공이면 true, 실패면 false */
if (file == NULL || initial_size < 0) {
return 0;
}
success = filesys_create(file, initial_size);
return success;
}
remove
file
이라는 이름의 파일을 삭제한다. 성공하면 true
를, 그렇지 않으면 false
를 반환한다.
파일이 열려 있든 닫혀 있든 상관없이 파일을 제거할 수 있으며, 열린 파일을 제거해도 파일이 닫히지 않는다. 이게 무슨 말이냐면 핀토스는 제거할때 Unix semantics에 따라 제거를한다. Unix에서는 파일이 제거되어도 해당 파일에 대한 파일 디스크립터를 가진 프로세스는 계속해서 그 디스크립터를 사용할 수 있다. 즉, 어떤 프로세스가 그 파일을 open
해 사용중이면 다른 프로세스에 의해 그 파일을 삭제하더라도 파일에서 읽고 쓸 수 있다.
제거된 파일은 더 이상 이름을 가지지 않으므로 다른 프로세스에서 새로 열 수 없다. 그러나 해당 파일을 참조하는 모든 파일 디스크립터가 닫히거나 시스템이 종료될 때까지는 계속 존재한다. 이를 통해 파일을 사용 중인 프로세스의 작업을 보호하고, 파일 삭제로 인한 데이터 손실을 방지할 수 있다.
/* 파일을 제거하는 함수,
* 이 때 파일을 제거하더라도 그 이전에 파일을 오픈했다면
* 해당 오픈 파일은 close 되지 않고 그대로 켜진 상태로 남아있는다.*/
bool remove (const char *file) {
check_address(file); // 유효성 검사
return filesys_remove(file);
}
open
file
이라는 이름의 파일을 연다. 음수가 아닌 정수 핸들인 "파일 디스크립터"(fd)를 반환하거나, 파일을 열 수 없는 경우 -1을 반한다. 파일 디스크립터 0과 1은 콘솔용으로 예약되어 있다. fd 0(STDIN_FILENO
)은 표준 입력이고 fd 1(STDOUT_FILENO
)은 표준 출력이다. open
시스템 콜은 fd 0과 fd 1을 절대 반환하지 않는다.
각 프로세스는 독립적인 파일 디스크립터 집합을 가지고 있다. 파일 디스크립터는 자식 프로세스에 의해 상속된다. 단일 파일이 프로세스에 의해 여러 번 열리면, 각 open
은 새로운 파일 디스크립터(fd)를 반환한다. 단일 파일에 대한 서로 다른 파일 디스크립터는 별도의 close
호출에서 독립적으로 닫히며 파일 위치를 공유하지 않는다. -> 파일 디스크립터는 결국 파일의 추상화다.
이를 구현하기 위해선 struct thread
에 FDT(File descriptor Table)과 현재 프로세스가 실행중인 파일을 가리키는 포인터 변수 데이터 필드를 추가 구현해야 한다.
struct thread {
/* Owned by thread.c. */
tid_t tid; /* Thread identifier. */
enum thread_status status; /* Thread state. */
char name[16]; /* Name (for debugging purposes). */
int priority; /* Priority. */
/* Shared between thread.c and synch.c. */
struct list_elem elem; /* List element. */
#ifdef USERPROG
/* Owned by userprog/process.c. */
/* pml4는 "Page Map Level 4"를 나타내는 약어다
* x86-64 아키텍처에서는 4단계 페이지 테이블 구조를 사용하여 가상 주소를 물리 주소로 변환한다
* 이 중에서 가장 상위 레벨이 바로 PML4입니다.*/
uint64_t *pml4; /* Page map level 4 */
#endif
...
int exit_status;
struct file **fd_table; /* 파일 디스크립터 테이블 */
struct file *executable; /* 실행 중인 파일 */
};
각각의 프로세스는 각자의 FDT를 가진다.
핀토스는 단일코어 멀티 프로세스 환경이다. 이말은 프로세스는 결국 쓰레드인것이다. 하지만 쓰레드가 프로세스일까??? 그것은 아니다. 프로세스는 메인 쓰레드를 포함한 한개이상의 쓰레드를 가진다. 그렇기에 쓰레드별로 FDT를 가지게 되면 메인 쓰레드의 자식 쓰레드들도 FDT를 가지기에 불필요한 메로리 낭비가 매우 심하다.
그렇기에 fd_table
은 thread_create
함수에서 테이블을 할당해주는것이 아닌 process_init
함수에서 FDT를 동적할당해줘야 한다.
/* General process initializer for initd and other process. */
/* 프로세스 초기화 함수는 새로운 프로세스를 생성하고 초기화하는 역할을 합니다.
* 이 함수는 "initd" 프로세스뿐만 아니라 다른 프로세스를 생성할 때도 사용될 수 있습니다. */
static void
process_init (void) {
struct thread *current = thread_current ();
current->fd_table = palloc_get_multiple(PAL_USER | PAL_ZERO, INT8_MAX); // 프로세스 FDT 동적할당
}
FDT를 할당해주었으면 현재 실행중인 파일을 가리키는 변수도 참조한다. 이것은 load
함수를 실행헀을의 파일을 참조하면 된다.
/* Loads an ELF executable from FILE_NAME into the current thread.
* Stores the executable's entry point into *RIP
* and its initial stack pointer into *RSP.
* Returns true if successful, false otherwise. */
/* FILE_NAME에서 ELF 실행 파일을 현재 스레드로 로드합니다.
* 실행 파일의 진입점을 *RIP에 저장하고,
* 초기 스택 포인터를 *RSP에 저장합니다.
* 성공하면 true를, 그렇지 않으면 false를 반환합니다. */
static bool
load (const char *file_name, struct intr_frame *if_) {
struct thread *t = thread_current ();
...
switch (phdr.p_type) {
...
case PT_LOAD:
if (validate_segment (&phdr, file)) {
...
if (!load_segment (file, file_page, (void *) mem_page,
read_bytes, zero_bytes, writable))
goto done;
thread_current()->executable = file; // 현재 실행중인 파일을 참조
}
...
}
파일 디스크립터와 관련한 모든 세팅이 끝났다. 이제 open
함수를 이용해 파일을 열고 FDT에 매핑하고 해당 fd를 리턴하면된다. 이때 fd는 0에서 시작해 할당할 수 있는 fd중 가장 낮은 fd를 리턴한다.
/* open 시스템콜 */
int open (const char *file) {
struct thread *curr = thread_current();
check_address(file);
struct file *open_file = filesys_open(file);
int fd = -1;
if(open_file == NULL){
return -1;
}
for(int i = 2; i < INT8_MAX; i++){
if(curr->fd_table[i] == NULL){
fd = i;
break;
}
}
if(fd != -1)
curr->fd_table[fd] = open_file;
return fd;
}
filesize
fd에 해당하는 file의 size를 리턴한다.
int filesize (int fd){
struct file *f = get_file(fd);
off_t size = file_length(f);
return size;
}
seek
seek
함수는 파일 디스크립터 fd로 지정된 열린 파일에서 다음에 읽기나 쓰기 작업이 수행될 위치를 변경한다. position
인자는 파일의 시작점부터의 오프셋을 바이트 단위로 지정한다. 이 함수를 사용하여 파일 내에서 임의의 위치로 이동할 수 있다.
여기서 주의할 점은 파일의 현재 끝을 넘어서 seek
하는 것이 오류가 아니라는 것이다. 이 경우 이후의 read
작업은 0바이트를 반환하여 파일의 끝임을 나타내고, 이후의 write
작업은 파일을 확장하고 중간의 공백을 0으로 채운다.
하지만 Pintos에서는 프로젝트 4가 완료될 때까지 파일의 크기가 고정되어 있으므로, 파일의 끝을 넘어서는 write
작업은 오류를 반환한다고 한다.
void seek (int fd, unsigned position){
struct file *f = get_file(fd);
if(f == NULL)
return;
file_seek(f, position);
}
tell
tell
함수는 파일 디스크립터 fd로 지정된 열린 파일에서 다음 읽기나 쓰기 작업이 발생할 위치를 반환한다. 반환값은 파일의 시작점부터의 오프셋을 바이트 단위로 나타낸다.
unsigned tell (int fd){
struct file *f = get_file(fd);
if(f == NULL)
return;
return file_tell(f);
}
read
열린 파일을 fd를 사용해 size
바이트를 buffer
로 읽는다. 실제로 읽은 바이트 수(0은 파일의 끝)를 반환하거나, 파일을 읽을 수 없는 경우(파일 끝 이외의 상황으로 인해) -1을 반환한다. fd 0은 input_getc()
를 사용하여 키보드에서 읽는다.
- fd가 0인 경우, 표준 입력(키보드)에서 데이터를 읽는다.
- fd가 0이 아닌 경우, 해당 파일 디스크립터와 연결된 파일에서 데이터를 읽는다.
fd에 매핑된 파일을 사용하려면 FDT에서 fd를 조회하는 함수를 작성해야한다. struct file* get_file(int fd){ if(fd < 2 || fd >= INT8_MAX) // 요청한 fd가 표준 입출력이거나 범위를 넘어서면 NULL을 리턴 return NULL; struct thread *curr = thread_current(); return curr->fd_table[fd]; }
int read (int fd, void *buffer, unsigned length){
check_address(buffer); // 주소 유효성 검사
/* 1번의 경우 */
if(fd == 0){
return input_getc();
}
/* 2번의 경우 */
struct file *f = get_file(fd);
if(f == NULL)
return 0;
return file_read(f,buffer,length);
}
여기서 주의할 점은 read
시스템 콜은 파일의 현재 위치에서 데이터를 읽는다. 읽기 작업 후 파일 위치는 읽은 바이트 수만큼 전진한다는 것이다.
write
buffer
에서 size
바이트만큼 열린 파일 fd에 쓴다. write
함수는 실제로 쓰여진 바이트 수를 반환하며, 이는 일부 바이트를 쓸 수 없는 경우 size
보다 작을 수 있다. 파일 끝을 지나서 쓰는 것은 일반적으로 파일을 확장하지만, 기본 파일 시스템에서는 파일 증가가 구현되지 않는다. PintOS에서 요구되는 write
동작은 파일 끝까지 가능한 한 많은 바이트를 쓰고 실제로 쓰여진 바이트 수를 반환하거나, 전혀 쓸 수 없는 경우 0을 반환하는 것이다. fd 1은 콘솔에 출력한다. 콘솔에 출력을 하려면 size
가 몇 백 바이트보다 크지 않은 한 putbuf()
를 사용하면 된다(더 큰 버퍼는 나누는 것이 합리적이다).
int write(int fd, const void *buffer, unsigned size)
{
check_address(buffer); // 유효성 검사
if(fd == 0) return -1;
else if (fd == 1)
{
putbuf(buffer, size);
return size;
}
else
{
struct file *f = get_file(fd);
if(f == NULL)
return -1;
int byte_written = file_write(f, buffer, size);
return byte_written;
}
return -1;
}
close
파일 디스크립터 fd를 닫는 시스템 콜이다. 프로세스가 종료되거나 중지될 때 모든 열린 파일 디스크립터는 자동으로 이 함수를 호출하여 닫아야한다.
/* FDT에서 해당 fd의 참조를 해제하는 함수 */
void remove_fd(int fd) {
struct thread *t = thread_current();
if(fd >= 2 || fd < INT8_MAX)
return t->fd_table[fd] = NULL;
}
void close(int fd)
{
struct file *f = get_file(fd);
if(f != NULL)
{
file_close(f); // 파일을 닫는다
remove_fd(fd); // fd 참조 해제
}
}
테스트 결과
pass tests/threads/priority-donate-chain
pass tests/userprog/args-none
pass tests/userprog/args-single
pass tests/userprog/args-multiple
pass tests/userprog/args-many
pass tests/userprog/args-dbl-space
pass tests/userprog/halt
pass tests/userprog/exit
pass tests/userprog/create-normal
pass tests/userprog/create-empty
pass tests/userprog/create-null
pass tests/userprog/create-bad-ptr
pass tests/userprog/create-long
pass tests/userprog/create-exists
pass tests/userprog/create-bound
pass tests/userprog/open-normal
pass tests/userprog/open-missing
pass tests/userprog/open-boundary
pass tests/userprog/open-empty
pass tests/userprog/open-null
pass tests/userprog/open-bad-ptr
pass tests/userprog/open-twice
pass tests/userprog/close-normal
pass tests/userprog/close-twice
pass tests/userprog/close-bad-fd
pass tests/userprog/read-normal
pass tests/userprog/read-bad-ptr
pass tests/userprog/read-boundary
pass tests/userprog/read-zero
pass tests/userprog/read-stdout
pass tests/userprog/read-bad-fd
pass tests/userprog/write-normal
pass tests/userprog/write-bad-ptr
pass tests/userprog/write-boundary
pass tests/userprog/write-zero
pass tests/userprog/write-stdin
pass tests/userprog/write-bad-fd
FAIL tests/userprog/fork-once
FAIL tests/userprog/fork-multiple
FAIL tests/userprog/fork-recursive
FAIL tests/userprog/fork-read
FAIL tests/userprog/fork-close
FAIL tests/userprog/fork-boundary
FAIL tests/userprog/exec-once
FAIL tests/userprog/exec-arg
FAIL tests/userprog/exec-boundary
FAIL tests/userprog/exec-missing
pass tests/userprog/exec-bad-ptr
FAIL tests/userprog/exec-read
FAIL tests/userprog/wait-simple
FAIL tests/userprog/wait-twice
FAIL tests/userprog/wait-killed
pass tests/userprog/wait-bad-pid
FAIL tests/userprog/multi-recurse
FAIL tests/userprog/multi-child-fd
FAIL tests/userprog/rox-simple
FAIL tests/userprog/rox-child
FAIL tests/userprog/rox-multichild
pass tests/userprog/bad-read
pass tests/userprog/bad-write
pass tests/userprog/bad-read2
pass tests/userprog/bad-write2
pass tests/userprog/bad-jump
pass tests/userprog/bad-jump2
pass tests/filesys/base/lg-create
pass tests/filesys/base/lg-full
FAIL tests/filesys/base/lg-random
pass tests/filesys/base/lg-seq-block
pass tests/filesys/base/lg-seq-random
pass tests/filesys/base/sm-create
pass tests/filesys/base/sm-full
FAIL tests/filesys/base/sm-random
pass tests/filesys/base/sm-seq-block
pass tests/filesys/base/sm-seq-random
FAIL tests/filesys/base/syn-read
FAIL tests/filesys/base/syn-remove
FAIL tests/filesys/base/syn-write
FAIL tests/userprog/no-vm/multi-oom
pass tests/threads/alarm-single
pass tests/threads/alarm-multiple
pass tests/threads/alarm-simultaneous
pass tests/threads/alarm-priority
pass tests/threads/alarm-zero
pass tests/threads/alarm-negative
pass tests/threads/priority-change
pass tests/threads/priority-donate-one
pass tests/threads/priority-donate-multiple
pass tests/threads/priority-donate-multiple2
pass tests/threads/priority-donate-nest
pass tests/threads/priority-donate-sema
pass tests/threads/priority-donate-lower
pass tests/threads/priority-fifo
pass tests/threads/priority-preempt
pass tests/threads/priority-sema
pass tests/threads/priority-condvar
pass tests/threads/priority-donate-chain
25 of 95 tests failed.
참고
https://casys-kaist.github.io/pintos-kaist/project2/system_call.html
'운영체제' 카테고리의 다른 글
[PintOS] Project3-1 Supplement Page Table (0) | 2024.06.02 |
---|---|
[PintOS] Project3-0 가상메모리는 DRAM의 추상화다 (0) | 2024.06.02 |
[PintOS] Project2-2 User Memory Access (0) | 2024.05.26 |
[PintOS] Project2-1 Passing the arguments and creating a thread (2) | 2024.05.22 |
[PintOS] Project2-0 Background (2) | 2024.05.22 |