Implement system call infrastructure
userprog/syscall.c
에 있는 system call handler를 구현하라. system call 번호를 검색한 다음 시스템 호출 인수를 검색하고 적절한 작업을 수행해야 한다.
System Call Details
첫 번째 프로젝트는 운영 체제가 사용자 프로그램으로부터 제어권을 되찾을 수 있는 한 가지 방법을 이미 다루었다: 타이머와 입출력 장치로부터의 인터럽트. 이것들은 CPU 외부의 엔티티에 의해 발생하므로 "외부(external)" 인터럽트이다.
운영 체제는 프로그램 코드에서 발생하는 이벤트인 소프트웨어 예외도 처리한다. 그것들은 page fault 또는 division by zero와 같은 error일 수 있다. 예외(Exceptions)는 사용자 프로그램이 운영 체제에게 서비스("system call")를 요청할 수 있는 수단이다.
기존 x86 아키텍처에서 system call은 다른 소프트웨어 exceptions와 동일하게 처리되었다. 그러나 x86-64에서 제조업체들은 system call에 대한 특별한 명령(instruction)인 syscall
을 소개한다. 이것은 system call handler를 호출하는 빠른 방법을 제공한다.
현재 syscall
명령어는 x86-64에서 시스템 호출을 호출하기 위해 가장 일반적으로 사용되는 수단이다. Pintos에서 사용자 프로그램은 시스템 호출을 위해 syscall
을 호출한다. system call number와 추가 인수는 syscall 명령을 호출하기 전에 일반적인 방식으로 레지스터에 설정되어야 한다. 딱 두개를 제외하고 :
%rax
는 system call number- 네 번째 argument는
%rcx
가 아니라%r10
이다.
따라서 syscall_handler()
가 제어권을 잡으면, system call number는 %rax
안에 있게 되고, arguments들은 %rdi
, %rsi
, %rdx
, %r10
, %r8
, %r9
순서로 전달된다.
caller의 register는 struct intr_frame
에 접근할 수 있다(struct intr_frame
은 kernel stack에 있다).
함수 반환 값에 대한 x86-64 convention은 이 값을 RAX레지스터에 배치하는 것이다. 값을 반환하는 system call은 struct int_frame
의 rax
멤버를 수정함으로써 convention을 지킨다.
대략적 흐름 및 현재상황
다음은 system call 호출 과정을 나타낸 것이다. 그리고 구현할 부분은 빨간 점선으로 되어 있다.
(참고로 우리의 stack pointer는 esp
가 아니라 rsp
이다 - 레지스터의 이름이 32bit는 e로 시작하고 64bit는 r로 시작)
코드를 따라가면서 어떻게 진행되는지 알아보자.
예를 들어서 user program이 exec
이라는 system call을 호출하면
lib/user/syscall.c
에서 정해놓은 규칙에 따라 exec() 함수가 호출된다.
int
exec (const char *file) {
return (pid_t) syscall1 (SYS_EXEC, file);
}
이 때 SYS_EXEC는 include/lib/syscall-nr.h
에 enum으로 정의되어 있고(즉, SYS_EXEC=3)
이 번호와 받아온 argument값들을 가지고 syscall1()
을 호출한다.
syscall1()
은 syscall()
을 호출하는데, syscall()
함수는 아래와 같다.
레지스터에 rgument값을 넣고, 어셈블리어로 syscall을 호출한다.
그러면 userprog/syscall-entry.S
로 가게 되고
rsp(stack pointer) 값을 바꿔줌으로써 kernel stack으로 진입하게 된다.
(이 때 CPU는 단순히 rsp
의 값을 바꿈으로써 user 영역에서 kernel 영역으로 진입하게 됨)
kernel thread에 stack으로 쌓은 intr_frame 값들을 rdi
에 넣어서 userprog/syscall.c
안에 있는 syscall_handler()
에게 전달해준다. 그러면 우리는 전달받은 값을 가지고 각각의 system call에 맞는 동작을 실행시키는 코드를 짜면 된다.
현재상황
파일 디스크립터 부분이 누락되어 있다(사실 다 누락되어있다...ㅋㅋㅋ). 파일 입출력을 위해서는 파일 디스크립터의 구현이 필요하다.
/* The main system call interface */
void
syscall_handler (struct intr_frame *f UNUSED) {
// TODO: Your implementation goes here.
printf ("system call!\n");
thread_exit ();
}
저기 보이는 printf(”system call!\n”)
과 thread_exit()
을 지워주지 않으면 나중에 test를 pass 할 수 없기 때문에 지워버리고 시작하자!
참고) printf()
는 write system call을 부른다. 따라서 write() system call을 구현하기 전까지는 test를 한 개도 통과할 수 없다.
helper function 구현
잠깐! File Descriptor system call들을 구현하기 전에 system call을 구현하는데 필요한 helper function들을 먼저 만들어보자.
check_address()
void check_address(void *addr)
{
// kernel VM 못가게, 할당된 page가 존재하도록(빈공간접근 못하게)
struct thread *cur = thread_current();
if (is_kernel_vaddr(addr) || pml4_get_page(cur->pml4, addr) == NULL)
{
exit(-1);
}
}
check_address()
함수는 받아온 주소값이 user 영역을 가리키고, 할당된 페이지를 가리키는지 확인한다. 이 함수를 통해 우리는 user가 kernel 영역에 접근하는등의 허용되지 않은 접근을 막는다.
find_file_by_fd()
static struct file *find_file_by_fd(int fd)
{
struct thread *cur = thread_current();
if (fd < 0 || fd >= FDCOUNT_LIMIT)
{
return NULL;
}
return cur->fd_table[fd];
}
이 함수는 fd를 이용해서 file을 찾는 함수이다.
add_file_to_fdt()
int add_file_to_fdt(struct file *file)
{
struct thread *cur = thread_current();
struct file **fdt = cur->fd_table;
// Find open spot from the front
// fd 위치가 제한 범위 넘지않고, fd table의 인덱스 위치와 일치한다면
while (cur->fd_idx < FDCOUNT_LIMIT && fdt[cur->fd_idx])
{
cur->fd_idx++;
}
// error - fd table full
if (cur->fd_idx >= FDCOUNT_LIMIT)
return -1;
fdt[cur->fd_idx] = file;
return cur->fd_idx;
}
이 함수는 현재 프로세스의 fd table에 file을 추가하는 함수다.
remove_file_from_fdt()
void remove_file_from_fdt(int fd)
{
struct thread *cur = thread_current();
// error : invalid fd
if (fd < 0 || fd >= FDCOUNT_LIMIT)
return;
cur->fd_table[fd] = NULL;
}
이 함수는 fd table에서 현재 thread를 제거하는 함수다.
File Descriptor 구현
구현할 함수
open()
: 파일을 열 때 사용
filesize()
: 파일의 크기를 알려줌
read()
: 열린 파일의 데이터를 읽음
write()
: 열린 파일의 데이터를 기록함
seek()
: 열린 파일의 위치(offset)를 이동
tell()
: 열린 파일의 위치(offset)를 알려줌
close()
: 열린 파일을 닫음
File Descriptor table 추가 (thread.h 의 thread 구조체 안에)
- 프로세스는 자기만의 파일 디스크립터 테이블을 하나씩 갖고 있다.
- 파일 객체 포인터의 배열이다.
- file descriptor 값은 file descriptor 테이블을 검사하여, 2부터 순차적으로 할당
open()
시 file descriptor를 returnclose()
시 file descriptor 테이블에 해당 entry 값을 NULL로 초기화palloc_get_page()
를 사용하여 file descriptor 테이블을 위한 메모리 영역 할당받음- 프로세스 디스크립터에 file descriptor 테이블을 위한 포인터 할당
- 모든 프로세스의 file descriptor 테이블은 커널 메모리에 위치(현재 pintos heap영역 없음)
- file descriptor 최대 64개 파일 객체 포인터를 가짐
struct thread
{
...
// project 2 : system call
struct file **fd_table; // file descriptor table의 시작주소 가리키게 초기화
int fd_idx; // fd table에 open spot의 index
...
};
system call handler
/* The main system call interface */
void syscall_handler(struct intr_frame *f UNUSED)
{
// project 2 : system call
/*
함수 return 값에 대한 x86-64 convention은 이 값을 rax레지스터에 배치하는 것이다.
값을 반환하는 system call은 struct int_frame의 rax 멤버를 수정함으로써 convention을 지킨다.
*/
switch (f->R.rax) // rax는 system call number이다.
{
case SYS_OPEN:
f->R.rax = open(f->R.rdi);
break;
case SYS_FILESIZE:
f->R.rax = filesize(f->R.rdi);
break;
case SYS_READ:
f->R.rax = read(f->R.rdi, f->R.rsi, f->R.rdx);
break;
case SYS_WRITE:
f->R.rax = write(f->R.rdi, f->R.rsi, f->R.rdx);
break;
case SYS_SEEK:
seek(f->R.rdi, f->R.rsi);
break;
case SYS_TELL:
f->R.rax = tell(f->R.rdi);
break;
case SYS_CLOSE:
close(f->R.rdi);
break;
default:
exit(-1);
break;
}
}
R.rax에 system call number가 적혀있고, 이를 통해서 어떤 system call을 실행할지 구분한다.
open()
int open(const char *file)
{
check_address(file);
struct file *open_file = filesys_open(file);
if (open_file == NULL)
{
return -1;
}
// fd table에 file추가
int fd = add_file_to_fdt(open_file);
// fd table 가득 찼을경우
if (fd == -1)
{
file_close(open_file);
}
return fd;
}
check_address를 통해서 주소값이 user 영역인지, 접근 가능한지 확인하고 진행한다. 여기서 fd table에 file을 추가하고 fd를 받아온다.
성공시 fd값, 실패시 -1을 return
filesize()
int filesize(int fd)
{
struct file *open_file = find_file_by_fd(fd);
if (open_file == NULL)
{
return -1;
}
return file_length(open_file);
}
fd를 가지고 file의 크기를 return 한다.
read()
int read(int fd, void *buffer, unsigned size)
{
check_address(buffer);
off_t read_byte;
uint8_t *read_buffer = buffer;
if (fd == 0)
{
char key;
for (read_byte = 0; read_byte < size; read_byte++)
{
key = input_getc();
*read_buffer++ = key;
if (key == '\0')
{
break;
}
}
}
else if (fd == 1)
{
return -1;
}
else
{
struct file *read_file = find_file_by_fd(fd);
if (read_file == NULL)
{
return -1;
}
lock_acquire(&filesys_lock);
read_byte = file_read(read_file, buffer, size);
lock_release(&filesys_lock);
}
return read_byte;
}
fd가 0이면 STDIN이니까 키보드로 들어온 값을 읽는다, fd가 1이면 STDOUT이다. 그리고 그게 아닐 경우 open된file을 찾아서 file을 읽는다.
file을 읽을 때 lock을 걸어주는데(다른 프로세스의 접근 막기 위해), 저 lock은 syscall.h에 선언해준다.
// userprog.syscall.h
struct lock filesys_lock;
그리고 초기화를 해줘야 하는데 syscall_init()에서 초기화 해준다.
void syscall_init(void)
{
...
lock_init(&filesys_lock);
}
seek()
void seek(int fd, unsigned position)
{
struct file *seek_file = find_file_by_fd(fd);
// 0,1,2는 이미 정의되어 있음
if (seek_file <= 2)
{
return;
}
seek_file->pos = position;
}
파일에서 position으로 이동하는 함수
tell()
unsigned tell(int fd)
{
struct file *tell_file = find_file_by_fd(fd);
if (tell_file <= 2)
{
return;
}
return file_tell(tell_file);
}
파일의 현재 위치를 알려주는 함수
close()
void close(int fd)
{
struct file *fileobj = find_file_by_fd(fd);
if (fileobj == NULL)
{
return;
}
remove_file_from_fdt(fd);
}
fd로 file을 찾아서 fd table에서 지워버리는 함수.