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

[week09] PintOS - Project 2(User Programs) : Introduction

D cron 2022. 1. 11. 01:18

Project 2 : User Programs

The base code already supports loading and running user programs,

but no I/O or interactivity is possible.

기본 코드는 이미 사용자 프로그램 로드 및 실행을 지원하지만 I/O 또는 상호 작용은 불가능하다.

In this project, you will enable programs to interact with the OS via system calls.

이 프로젝트에서는 프로그램이 시스템 호출을 통해 OS와 상호 작용할 수 있도록 한다.

Background

지금까지 핀토스에서 실행했던 모든 코드는 운영 체제 커널의 일부였다.

운영 체제 위에서 사용자 프로그램을 실행하기 시작하면 더 이상 사실이 아니다.

이전 프로젝트에서는 테스트 코드를 커널에 직접 컴파일했기 때문에 커널 내에 특정 기능 인터페이스가 필요했다. 이제부터 사용자 프로그램을 실행하여 운영 체제를 테스트한다.

당신의 모든 코드는 절대 다음 블록(#ifdef VM)안에 위치해서는 안 된다. 이 블록은 프로젝트 3에서 구현할 가상 메모리 하위 시스템을 활성화한 후 포함된다.

Source Files

process.c, process.h Loads ELF binaries and starts processes

ELF binaries가 무엇일까?

ELF는 Executable and Linkable Format의 약어로 실행파일, 목적파일, 공유 라이브러리 그리고 코어 덤프를 위한 표준 파일 형식이다. x86기반 유닉스, 유닉스 계열 시스템들의 표준 바이너리 파일 형식이다.


syscall.c syscall.h

user process가 kernel 함수에 접근하고자 할 때 system call을 호출한다

현재상태는 message를 출력하고 user process를 끝낸다.

우리는 여기에 system call에 필요한 다른 모든 작업을 수행하기 위한 코드를 추가할 것이다.


exception.c, exception.h

user process가 특권 동작이나 금지된 동작을 할 경우 exception 혹은 fault로 들어간다.

현재상태는 message를 호출하고 process를 종료시키게 되어있다.

여기서 page_fault() 함수를 약간 수정해야 한다.


tss.c, tss.h

TSS는 Task-State Segment의 약자로 x86 구조의 task switching을 위해 사용되었다. 그러나 x86-64에서는 task switching이 더 이상 사용되지 않는다. 그럼에도 불구하고, TSS는 ring switching 중에 stack pointer를 찾기 위해 여전히 존재한다.

Using the File System

There is no need to modify the file system code for this project

파일 시스템 코드를 수정할 필요가 없다.

file system implementaion를 imporve하면 더 쉽게 사용가능하다. 그러나 그때까지는 다음 제한 사항을 허용해야 한다.

  • 내부 동기화(internal synchronization)가 없다. 동시 액세스는 서로 간섭한다. 동기화(synchronization)를 사용하여 한 번에 하나의 프로세스만 파일 시스템 코드를 실행하도록 해야 한다.
  • 파일 크기는 생성 시 고정된다. 루트 디렉터리는 파일로 표시되므로 생성할 수 있는 파일 수도 제한된다.
  • 파일 데이터는 단일 익스텐트로 할당된다. 즉, 단일 파일의 데이터는 디스크의 연속적인 섹터 범위를 차지해야 한다. 따라서 시간이 지남에 따라 파일 시스템이 사용됨에 따라 외부 단편화가 심각한 문제가 될 수 있다.
  • 하위 디렉토리 없음
  • 파일 이름은 14자로 제한.
  • 작동 중 시스템 충돌은 자동으로 복구할 수 없는 방식으로 디스크를 손상시킬 수 있다. 어쨌든 파일 시스템 복구 도구는 없다.

한 가지 추가 기능

filesys_remove()

파일이 제거될 때 열려 있으면 해당 블록이 할당 해제되지 않으며 마지막 스레드가 닫을 때까지 열려 있는 스레드에서 계속 액세스할 수 있다.

pintos는 어떻게 disk를 사용하는가?

커널 이미지에 모든 테스트 프로그램이 이미 있었던 프로젝트 1과 달리 핀토스 가상 머신에 테스트 프로그램(사용자 공간에서 실행되는)을 넣어야 한다. 사용자의 편의를 위해 테스트 스크립트(즉,make check)에서 이 작업을 자동으로 처리하므로 대부분의 경우 이 작업을 이해하지 않아도 된다. 그러나 이 사실을 알면 개별 검사 사례를 실행하는 데 상당한 도움이 된다.


핀토스 가상 시스템에 파일을 넣으려면 먼저 파일 시스템 파티션(file system partition)이 있는 시뮬레이션 디스크(simulated disk)를 생성해야 한다. pintos-mkdisk 프로그램은 이 기능을 제공한다. userprog/build 디렉토리에서 pintos-mkdisk filesys.dsk 2를 실행해보자. 이 명령은 2MB(2^20) Pintos 파일 시스템 파티션이 포함된 filesys.dsk라는 시뮬레이션 디스크를 생성한다. 그런 다음 pintos뒤에(그리고 --앞에) --fs-disk filesys.dsk를 전달하여 디스크를 지정한다(예: pintos --fs-disk filesys.disk -- KERNUL_COMMAND...). --fs-disk는 시뮬레이션 커널이 아닌 핀토스 스크립트용이기 때문에 --가 필요하다. 그런 다음 커널의 명령줄(kernel’s command line)에 -f -q를 전달하여 파일 시스템 파티션을 포맷한다: pintos SCRIPT_Commands -- -f -q. -f 옵션을 사용하면 파일 시스템이 포맷되고-q 옵션을 사용하면 포맷이 완료되는 즉시 핀토스가 종료된다.


simulated file system 안팎으로 파일을 복사하는 방법이 필요하다. 핀토스 -p ("put")와 -g ("get") 옵션은 이 작업을 수행한다. 'file'을 Pintos 파일 시스템에 복사하려면 pintos -p file -- -q 명령을 사용한다. newname이라는 이름으로 핀토스 파일 시스템에 복사하려면 원래 파일 이름 뒤에 :newname을 추가한다. pintos -p file:newname -- -q. VM에서 파일을 복사하는 명령은 유사하지만 -p-g로 대체한다.


다음은 파일 시스템 파티션으로 디스크를 만들고, 파일 시스템을 포맷하고, args-single 프로그램을 새 디스크에 복사한 다음, 인수 'onearg'를 전달하는 방법에 대한 요약이다. tast case를 이미 작성했으며 현재 디렉토리가 userprog/build라고 가정한다.


pintos-mkdisk filesys.dsk 10
pintos --fs-disk filesys.dsk -p tests/userprog/args-single:args-single -- -q -f run 'args-single onearg'

나중에 사용하거나 검사하기 위해 파일 시스템 디스크를 주변에 두지 않으려면 네 단계를 모두 단일 명령으로 결합할 수도 있다. --filesys-size=n 옵션은 핀토스 실행 기간 동안만 약 n메가바이트 크기의 임시 파일 시스템 파티션을 만든다. 핀토스 자동 테스트 스위트는 다음과 같은 문법을 광범위하게 사용한다.

pintos --fs-disk=10 -p tests/userprog/args-single:args-single -- -q -f run 'args-single onearg'

How User Programs Work

malloc() 은 사용할 수 없다. 이 프로젝트에서 system call에 memory 할당에 관한건 없다.

floating point operations을 사용하는 program을 돌릴 수 없다. kernel이 processor의 floating point를 저장하지 않기 때문이다.

테스트 프로그램을 모의 파일 시스템에 복사하기 전까지는 핀토스가 유용한 작업을 수행할 수 없다는 것을 즉시 깨달아야 한다.

Virtual Memory Layout

PintOS에서 Virtual Memory 는 두 영역으로 나뉘어진다.

User Virtual Memory

  • 0 ~ KERN_BASE(0x8004000000)

Kernel Virtual Memory

  • virtual memory의 나머지 부분

사용자의 virtual memory는 프로세스별로 다르다. kernel이 하나의 프로세스에서 다른 프로세스로 switch 할 때, 사용자의 virtual memory도 함께 switch 된다. 프로세서의 page directory base register를 바꿈으로서(pml4_activate() in thread/mmu.c 참고). thread 구조체는 프로세스 page table에 대한 pointer를 포함하고 있다.


kernel virtual memory는 전역이다. user process가 동작하든 kernel thread가 동작하든 항상 같은 방식으로 mapped 된다. Pintos에서는 kernel virtual memory가 physical memory(물리 메모리)와 1대1 대응으로 매핑된다. (KERN_BASE에서 시작)


virtual address KERN_BASE 는 physical address 0 에 mapping된다. virtual address KREN_BASE + 0x1234 는 physical address 0x1234 에 대응된다.


user program은 자신의 user virtual memory에만 접근이 가능하다. kernel의 virtual memory에 접근하면 page fault를 일으킨다( page_falut() 함수에 의해 in userprog/exception.c ). 그리고 process는 종료된다.


kernel thread는 kernel virtual memory에 접근가능하고 만약 process가 running 상태라면 user virtual memory에도 접근가능하다. 그러나 unmapped user virtual address에 접근하면 page fault가 일어난다.

Typical Memory Layout

일반적인 user virtual memory의 모습

USER_STACK +----------------------------------+
           |             user stack           |
           |                 |                |
           |                 |                |
           |                 V                |
           |           grows downward         |
           |                                  |
           |                                  |
           |                                  |
           |                                  |
           |           grows upward           |
           |                 ^                |
           |                 |                |
           |                 |                |
           +----------------------------------+
           | uninitialized data segment (BSS) |
           +----------------------------------+
           |     initialized data segment     |
           +----------------------------------+
           |            code segment          |
 0x400000  +----------------------------------+
           |                                  |
           |                                  |
           |                                  |
           |                                  |
           |                                  |
       0   +----------------------------------+

이 프로젝트에서 stack 의 크기는 고정되어 있다. 그러나 project 3에서 자라도록 허용될 것이다.

Accessing User Memory

system call의 부분으로서, kernel은 user program이 제공한 pointer로 memory에 access해야하는 경우가 존재한다. 이 때 매우 주의해야 하는데, 왜냐하면 user는 NULL pointer, unmapped virtual memory에 대한 pointer, kernel virtual address space에 대한 pointer(above KERN_BASE)를 kernel에게 전달할 수 있기 때문이다. 이러한 모든 유형의 유효하지 않은 포인터는 문제가 되는 프로세스를 종료하고 해당 리소스를 해제하여 커널이나 실행 중인 다른 프로세스에 해를 끼치지 않고 거부되어야 한다.


첫 번째 방법

user-provided pointer의 유효성을 검증하고 난 후 역참조(dereference) 하는 것이다.

  • 역참조: 주소값을 통해 값에 접근

이 방법을 사용한다면 thread/mmu.c (in include/threads/vaddr.h)의 함수들을 봐라.

이 방법이 user memory access를 다루는 가장 단순한 방법이다.


두 번째 방법

KERN_BASE 아래의 pointer들인지 확인한 후, 역참조한다.

유효하지 않은 user pointer는 page fault를 일으킨다. 이 코드는 page_fault() (in userprog/exceptions.c) 함수를 수정함으로써 다룰 수 있다.

이 기술은 일반적으로 프로세서의 MMU(Memory Management Unit)를 이용하기 때문에 더 빠르며, 따라서 실제 커널(리눅스 포함)에서 사용되는 경향이 있다.


두 경우 모두, 자원을 leak 하지 않도록 주의해야 한다. system call이 lock을 요청했거나, malloc() 함수를 통해 메모리 할당을 요청했을 경우를 예로 들어보자. 만약 유효하지 않은 user pointer를 받고 나서도 lock을 release하거나 page memory를 free 시켜줘야 한다. 왜냐하면 memory 접근에서 error code를 반환하는 방법이 없기 때문이다. 따라서, 두 번째 방법을 시도하려는 사람에게 유용한 코드를 제공한다.


/* Reads a byte at user virtual address UADDR.
 * UADDR must be below KERN_BASE.
 * Returns the byte value if successful, -1 if a segfault
 * occurred. */
static int64_t
get_user (const uint8_t *uaddr) {
    int64_t result;
    __asm __volatile (
    "movabsq $done_get, %0\n"
    "movzbq %1, %0\n"
    "done_get:\n"
    : "=&a" (result) : "m" (*uaddr));
    return result;
}

/* Writes BYTE to user address UDST.
 * UDST must be below KERN_BASE.
 * Returns true if successful, false if a segfault occurred. */
static bool
put_user (uint8_t *udst, uint8_t byte) {
    int64_t error_code;
    __asm __volatile (
    "movabsq $done_put, %0\n"
    "movb %b2, %1\n"
    "done_put:\n"
    : "=&a" (error_code), "=m" (*udst) : "q" (byte));
    return error_code != -1;
}

각 함수들은 user pointer가 KERN_BASE 아래에 있다는 것이 증명되었다고 가정한다.

또한 당신이 page_fault()를 수정해서 kernel 안에 있는 page fault는 rax를 -1로 설정하고 이전값을 %rip에 복사하도록 했다는 것을 가정한다.