Pintos 2주차 도입

정글일지 30

Project2 : User Program

유저 프로그램을 실행하게 하는 시스템에 대해 배운다.

유저프로그램 로딩, 실행은 이미 있으나, I/O,나 상호작용은 불가능한 상태

이 프로젝트에서 시스템 콜을 통해 운영체제와 프로그램이 상호작용 하도록 해야 한다.

userprog 디렉토리에서 하면 된다.

project1 위에서 진행된다.

project1의 코드가 project2 코드에 영향을 끼치진 않지만 이후에도 project1 테스트는 통과해야 한다.

추가 도전과제 옵션도 있음.

옵션에서는 기초 코드 틀이 없이 완전히 작성해야함.

userprog/Make.vars 를 수정해야함

Background

지금까지 Pintos에서 실행해온 코드는 운영체제 커널의 부분이였다. 즉, 시스템의 privileged parts까지 완전히 접근이 가능하였다. 운영체제 꼭대기에서 유저프로그램을 시작하면, 더이상은 힘들다. 이 프로젝트가 다룰 부분이다.

우리는 복수의 프로세스를 동시에 실행시키도록 한다. 각 프로세스는 단일 프로세스만 가진다. 유저프로그램은 전체 기계를 다룰 수 있다는 착각 속에서 써졌다. 이 착각을 유지하기 위해서, 한번에 복수의 프로세스를 불러오고 실행시킬때, 반드시 메모리와 스케쥴링 등을 올바르게 관리해야 한다.

이전 프로젝트에서, 우리는 직접 커널에 테스트 코드를 실행했었으니, 커널 내부에 구체적인 함수 작용을 요청해야만 했었다. 지금부터는, 유저 프로그램을 실행함으로서 운영체제를 테스트할 것이다. 이건 더 큰 자유를 보장한다. 우리는 유저 프로그램 인터페이스가 여기 서술된 조건들을 만족시켜야만 한다. 우리의 모든 코드는 #ifdef VM에 동봉된 책에 전혀 찾아볼 수 없다. 이 책은 우리가 3주차에 할 가상 메모리 subsystem을 알게된 후 보게 될 것이다. 또한 #ifndef VM로 코드가 동봉되었다면, project3부터는 누락될 것이다.

시작 전에 synchronization 과 virtual address를 반드시 읽기를 권한다.

Using the File System

유저 프로그램이 파일 시스템에서 로딩되고 우리가 꼭 써야 하는 많은 시스템 콜이 파일 시스템과 관련되었기 때문에, 이 프로젝트에서 우리는 파일 시스템을 다뤄야 한다. 그러나, 이 프로젝트의 초점은 파일 시스템이 아니기 때문에, filesys 디렉토리에 간단하지만 완성된 파일 시스템이 제공된다. 파일 시스템을 사용하는 방식, 특히 많은 한계들을 이해하기 위해 filesys.h 와 file.h를 훑어봐야 할 것이다.

이 프로젝트에서 파일 시스템 코드를 수정할 필요는 없고 그러지 않길 바란다. 파일 시스템에 대한 작업은 이 프로젝트의 목적과 멀다.

파일 시스템 작동을 향상시키기 위해 파일 시스템을 적절하게 사용하는 습관은 프로젝트 4에 엄청난 도움이 될 것이다. 그때까지 당신은 다음 한계점들은 참아야 한다.

  • 내부 synchronization이 없다. Concurrent accesses는 서로 간섭한다. 동시에 유일한 단일 프로세스만 파일 시스템 코드를 실행하도록 synchronization을 사용해야만 한다.
  • 파일 크기는 생성시에 고정된다. root directory는 파일로 표현되어, 생성되는 파일의 수 또한 제한되어 있다.
  • 파일 데이터는 단일 extent에 할당된다. 다시 말해, 하나의 파일에 있는 데이터는 반드시 연속된 범위의 디스크 섹터를 차지해야 한다. 그러므로 외부 단편화는 파일 시스템이 길게 사용될수록 심각한 문제가 된다.
  • subdirectories는 없다.
  • 파일 제목은 14 글자 이하로 제한된다.
  • system crash mid-operation(실행 중 시스템 오류)은 자동적으로 수리될 수 없는 방식으로 disk를 망가뜨릴지도 모른다. 어쨌든, 파일 시스템 수리 툴은 없다.

한가지 중요한 내장 기능

filesys_remove() 같은 unix_like semantic이 실행된다. 다시 말해, 삭제된 파일이 열리면, block은 비할당화 되지 않고 마지막 것이 닫히기 전까지 여전히 다른 쓰레드에 의해 처리되어 열리게 된다.

모든 테스트 프로그램이 kernel image에 이미 존재하는 project 1가 다르게, 유저 공간에서 작동하는 테스트 프로그램을 Pintos virtual machine에 넣어야만 한다. 난이도를 낮추기 위해, 테스트 명령어(ex. make check)에는 자동적으로 포함 되어있어서, 이것을 이해할 필요는 없다. 하지만 인지하고 있는것은 개인 테스트 케이스를 실행하는 데는 분명히 도움이 된다.

핀토스 가상 머신에 파일을 넣기 위해, 우선 file system partition을 가진 simulated disk를 만들어야 한다. pintos-mkdisk 프로그램이 이 기능을 제공한다. userprog/build 디렉토리에서, pintos-mkdisk filesys.dsk2를 실행하라. 이 명령어는 2MB 크기의 Pintos file system partion을 포함하며 filesys.dsk를 이름으로 하는 simulated disk를 만든다. 그 후, pintos 뒤이자 -- 전에(ex. pintos --fs-disk filesys.disk --), --fs-disk filesys.dsk를 passing 하여 disk를 구체화 해라. ----fs-disk가 simulated된 커널이 아니라 pintos 명령어이기 때문에 필수이다. 그 이후, kernel’s의 커맨드 라인에 -f -q를 passing하는 방식으로 파일 시스템 파티션을 형성해라(pintos -p file:newname -- -q를 사용). -f옵션은 파일 시스템이 형성되도록 하고 -q는 format이 형성되자마자 Pintos가 나가게 한다.

simulated된 파일 시스템의 안팎의 파일을 복사하는 방법이 필요하다. pintos -p(“put”)과 -g(“get”)옵션이 이 기능이다. Pintos 파일 시스템으로 파일을 복사하기 위해, 명령어 pintos -p file -- -q를 사용해라. newname이라는 이름으로 핀토스 파일 시스템으로 복사하기 위해, :newname을 원래 파일 제목에 추가해라 . 즉 pintos =p file:newname -- -q이다. VM(virtual memory) 밖의 파일을 복사하기 위핸 명령어는 유사하지만 -g-p로 대체한다.

그런데, 이 명령어들은 커널의 command line에 special 명령어 추출과 추가(append)를 전달하고 simulated “sscratch” particion을 안팎으로 복사하는 방식으로 작동한다. 정말 궁금하다면, 세부적인 것들은 filesys/fsutil.c와 pintos script에서 볼 수 있다.

‘onearg’ argument를 통과면서 파일 시스템 파티션이 된 디스크를 만들고, 파일 시스템형 형성하고, 새로운 디스크로 args-single 프로그램을 복사하고 실행하는 방법에 대한 요약이다. 이미 테스트 케이스를 만들었고 현재 디렉토리가 userprog/build라고 가정하자.

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

이후에 사용이나 inspection을 위해 파일 시스템 디스크를 유지하길 원하진 않는다면, 단일 명령어로 모든 4 단계를 결합할 수 있다. --filesys-size=n옵션은 pintos 실행의 지속을 위해 즉시 파일 시스템 파티션을 n 메가바이트 사이즈로 만든다. pintos automatic test suite는 이 문법을 광범위하게 사용한다.

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

How User Programs Work

Pintos는 메모리에 알맞고 당신이 실행한 시스템 콜만 사용한다면 일반적인 C 프로그램을 실행할수 있다. 특히, 이 프로젝트를 위해 요구되는 시스템 콜 중 어느것도 메모리 할당을 하지 않기 때문에, malloc()은 실행될 수 없다. Pintos가 쓰레드를 전환할 때,커널이 저장하고 프로세서의 floating-point unit을 복구하지 않기 때문에 floating point operation을 사용하는 프로그램을 실행할 수 없다.

Pintos는 ELF executables를 userprog/pocess.c에서 제공되는 loader로 로딩할 수 있다. ELF는 Lunux, Solaris, 그리고 다른 많은 운영체제에서 객체 파일, 공유 libraries 그리고 excutables를 위해 사용하는 파일 format이다.

Pintos를 위한 프로그램을 생산하는 x86-64 ELF executables를 출력하는 linker나 compiler는 모두 사용할 수 있다(사용해도 괜찮은 compilers와 linkers만 제공된다.). simulated된 파일 시스템에 테스트 프로그램을 복사하기 이전까지, Pintos가 유용한 동작을 할수 없다는 것을 즉시 깨달아야 한다. 파일 시스템으로 다양한 프로그램들을 복제하기 전까지 흥미를 끌만한 일들을 할 수 없을 것이다. 깔끔한 reference file system disk를 만들고 싶거나 디버깅 하는 동안 발생할지 모르는 유용한 상태를 너머 filesys.dsk를 엉망으로 만들때마다 복사하고 싶을지도 모른다. (You might want to create a clean reference file system disk and copy that over whenever you trash your filesys.dsk beyond a useful state, which may happen occasionally while debugging.)

Virtual Memory Layout

Pintos에서 가상 메모리는 두 지역으로 나누어져 있다. 유저 가상 메모리와 커널 가상 메모리. 유저 가상 메모리는 include/threads/vaddr.h에 정의된 대로 범위는 가상 주소 0부터 KERN_BASE까지, 기본 설정은 0x8004000000이며 커널 가상 메모리는 가상 주소 공간의 나머지를 차지한다.

유저 가상 메모리는 프로세스마다이다. 커널이 한 프로세스에서 다른 프로세스로 전환하면, 유저 가상 메모리 공간도 프로세서의 page directory base register를 바꿈으로써 변환된다(thread/mmu.c 내의 pml4_activate()를 보면 됌). struct thread는 프로세스의 page table을 가리키는 포인터를 갖고 있다.

커널 가상 공간은 전역적이다. 어떤 유저 프로세스나 커널 쓰레드가 작동중이던 상관없이 항상 같은 곳을(same way) mapping한다. Pintos에서, 커널 가상 메모리는 KERN_BASE + 0x1234에서 시작하는 실제 물리적인 메모리와 일대일 대응되어 mapping된다. 다시 말해, 기계의 실제 메모리의 크기에 따라, 가상 주소 KERN_BASE가 실제 물리적인 주소 0에 접근하고, 가상 주소 KERN_BASE+0x1234가 실제 물리적 주소 0x1234`에 접근하는 식이다.

유저 프로그램은 오직 자신의 유저 가상 메모리에만 접근할 수 있다. 커널 가상 메모리에 대한 접근 시도는 userprog/exception.cpage_fault()가 다루는 page fault를 야기하고, 프로세스는 종결된다. 커널 쓰레드들은 커널 가상 메모리와 유저 프로세스가 작동중이라면, 현재 프로세스의 유저 가상 메모리도 접근할 수 있다. 그러나, 커널안에서라도, mapping되지 않은 유저 가상 주소의 메모리에 접근하는 시도는 page fault를 초래한다.

Typical Memory Layout

개념적으로, 각 프로세스는 자신의 유저 가상 메모리에 자유롭게 놓인다. 실제로, 유저 가상 메모리는 이렇게 놓여진다.

그림

이 프로젝트에서, 유저 스택은 고정된 사이즈이지만 프로젝트 3에서는 커질 수 있다. 전통적으로, 초기화되지 않은 데이터 세그먼트의 크기는 시스템 콜로 조절될 수 있지만, 여기서 실행할 일은 없다.

Pintos에서 코드 segment는 주소 공간의 바닥부터 약 128MB정도 되는 유저 가상 주소 0x400000에서 시작한다. 이 값은 ubuntu에서 일반적이고 깊은 의미는 없다.

linker는 메모리 내의 유저 프로그램의 레이아웃을 만드는데, “linker script”라고도 지칭되며, 다양한 프로그램 segment의 위치와 제목을 말해준다. info ld로 볼 수 있는 linker 메뉴얼얼에서 “Scripts” 챕터를 읽으면 linker scripts에 대해 많이 좀더 배울 수 있다.

특별한 excutable의 뼈대를 보기 위해 -p로 odjdump를 실행해라.

Accessing User Memory

시스템 콜의 부분으로서, 종종 커널은 유저 프로그램이 제공하는 포인터를 통해 메모리에 반드시 접근해야만 한다. 커널은 그럴때 굉장히 조심해야만 하는데, 유저가 mapping되지 않은 가상 메모리를 가리키는 포인터인 null 포인터나, KERN_BASE상위 같은 커널 가상 주소 공간을 가리키는 포인터를 줄 수도 있기 때문이다. 이러한 타당하지 않은 모든 포인터들은 그들의 resource를 free하게 하거나 offending하는 process를 종결시키는 방법으로 커널이나 다른 작동중인 프로세스들에게 피해를 끼치지 않고 거부되어야만 한다.

이렇게 올바르게 하는 합리적인 방식이 최소한 두 가지는 있다. 첫번째 방법은 유저가 제공하는 포인터의 타당성을 검증하고 나서 그것을 역참조(dereference)하는 것이다. 이 방식을 선택한다면, thread/mmu.cinclude/threads/vaddr.h내에 있는 함수들을 볼 필요가 있다. 이게 유저 메모리 접근을 다루는 가장 간단한 방법이다.

두번째 방법은 그저 유저 포인터가 KERN_BASE하위를 가리키는지만 확인하고 역참조하는 것이다. 타당하지 않은 유저 포인터는 “page fault”를 초래할 것이고, userprog/exception.c 내의 page_fault()에 대한 코드를 수정함으로써 처리할 수 있다. 이 기술은 보통 더 빠른데, 프로세서의 MMU에 대한 이득이 있어 Linux를 포함하는 실제 커널에서 사용되는 경향이 있기 때문이다.

두 방법 어디든, 자원이 “누수”되지 않도록 해야 한다. 예를 들어, 시스템 콜이 lock이나 malloc()으로 할당된 메모리를 얻었다고 가정하자. 만약 타당하지 않은 유저 포인터를 이후에 맞닥뜨리면, 그럼에도 확실히 lock을 release하거나 메모리 페이지를 free 할 수 있어야만 한다. 만약 역참조 하기전에 유저 포인터를 확인한다면, 간단해야 한다. 타당하지 않은 포인터가 page fault를 야기한다면 처리하기에 더욱 어려운데, 메모리 접근에서 에러 코드를 리턴할 방법이 없기 때문이다. 그러므로 두 가지 중 후자의 방식을 하고자 한다면, 유용한 코드가 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/* 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;
}

위 각각의 함수들은 유저 주소가 이미 KERN_BASE 아래에 있다고 확인되었다고 가정한다. 또한 page_fault()를 수정해서 커널의 page fault rax를 -1로 설정하고 이전의 값을 %rip로 복사한다고 가정한다. (They also assume that you’ve modified page_fault() so that a page fault in the kernel merely sets rax to -1 and copies its former value into %rip.)