sw사관학교정글/PintOS(KAIST's CS330 class)

[week09] PintOS - Project 2(User Programs) : System Calls 개요 및 File Descriptor 구현

D cron 2022. 1. 11. 23:11

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_framerax 멤버를 수정함으로써 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를 return
  • close() 시 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에서 지워버리는 함수.