핀토스 프로젝트2 인트로

Run·2021년 10월 5일
0

이번 프로젝트는 시스템콜을 통해 프로그램이 OS와 연결될 수 있도록 한다.
userprog 폴더에서 작업할 거고 물론 다른 부분에서도 할 거고.

이젠 유저 프로그램을 OS에서 실행해도 더이상 안 돌아갈거임.
이번 프로젝트에선 하나 이상의 프로세스를 돌릴건데 각 프로세스엔 하나의 쓰레드만 있음.
여러 프로세스를 한 번에 로드하거나 실행할 때 메모리, 스케줄링 그리고 기타 상태들을 정확하게 다뤄야 한다.

프로젝트 1에서는 커널에다가 우리 테스트 코드를 바로 컴파일 해서 커널 내의 특정 함수 인터페이스가 필요했다.
이번 프로젝트부턴 유저 프로그램들을 실행시키면서 테스트할거임.요렇게 하면 더 자유롭게 짤 수 있을거임.
유저 프로그램 인터페이스는 여기서 기술한 사양을 만족시켜야 한다. 물론 그 제약 조건 하에서 커널 코드를 너. 원하는대로 다시 짜도 된다.

#ifdef VM 이걸로 쌓인 블록에다간 코드 넣지 마셈.

소스 파일

  • process.c, process.h
    ELF 파일 로드하고 프로세스 실행
  • syscall.c, syscall.h
    user process가 커널 기능에 접근하고 싶을 때 syscall을 발생시킨다.
    syscall handler 뼈대이다.
    너가 채워넣어라
  • syscall-entry.S
    syscall handler 알아서 다 실행시키는 어셈블리 코드임. 몰라도 됨
  • exception.c, exception.h
    유저 프로세스가 우선순위화되거나 제한된 동작을 수행할 때 exception이나 fault로 커널로 트랩을 일으킨다.
    이 파일에서 이런 exception들을 처리한다.
    page_fault()를 너가 고치셈
  • gdt.c, gdt.h
    The x86-64 is a segmented architecture.
    고칠 필요 없음. GDT(Global Descriptor Table) 파일을 실행하는 파일인듯
  • tss.c, tss.h
    TSS(Task-State Segment)는 x86 구조 스위칭에서 사용된다. x86-64에선 안 쓰이는데 ring switching할 동안 스택 포인터를 찾기 위해 사용된다.
    즉, 유저 프로세스가 인터럽트 핸들러에 들어갈 때 그 하드웨어는 커널의 스택 포인터를 찾기 위해 tss를 이용한다.

파일 시스템 사용하기

이번 프로젝트에서 파일 시스템 코드를 인터페이스할 필요가 있는데 유저 프로그램은 파일 시스템으로부터 로드되고 너가 구현할 시스템콜들은 대부분 파일 시스템을 다루기때문이다.
근데 이번 프로젝트는 파일 시스템이 메인이 아니니까 안 봐도 되긴함.
근데 파일 시스템 루틴을 지금 적절히 사용해보는 건 프로젝트4 할 때 훨씬 편하게 만들어줄 거임.
아니면 아래와 같은 상황들을 참고 버텨야됨.

  • No internal synchronization. Concurrent accesses will interfere with one another. You should use synchronization to ensure that only one process at a time is executing file system code.
  • File size is fixed at creation time. The root directory is represented as a file, so the number of files that may be created is also limited.
  • File data is allocated as a single extent, that is, data in a single file must occupy a contiguous range of sectors on disk. External fragmentation can therefore become a serious problem as a file system is used over time.
  • No subdirectories
  • File names are limited to 14 characters.
  • A system crash mid-operation may corrupt the disk in a way that cannot be repaired automatically. There is no file system repair tool anyway.

중요한 거 하나 있는데 그게 뭐냐면

유닉스 같은 filesys_remove()가 구현됐는데 파일이 삭제된 상태에서 파일이 열린다면 그 블럭은 deallocated되지 않고 마지막 스레드가 닫힐 때까지 어떤 쓰레드도 여기에 접근할 수 있다.

파일에 대한 standard Unix semantics을 구현하면, 파일이 삭제됐는데도 그 파일에 대한 파일 descriptor를 갖고 있는 프로세스면 그 디스크립터를 계속 사용할 수 있다. 즉, 그 파일로부터 읽고 쓰기를 계속할 수 있다. 그 파일은 이름도 없고 프로세스들은 그 파일을 열 수도 없지만 그 파일을 가리키는 모든 파일 디스크립터가 닫히거나 머신 자체가 꺼지기 전까지는 그 파일은 계속 존재한다.

커널 이미지에 모든 테스트 프로그램이 이미 있었던 프로젝트1과는 달리 (유저 영역에서 실행되는) 테스트 프로그램을 핀토스 가상 머신에 넣어야 한다.
테스팅 스크립트(make check 같은 거)가 자동적으로 처리해주긴하지만 알아두면 개별 테스트 케이스를 돌리는 데 많은 도움 될 거임.

핀토스 가상 머신에 넣기 위해 파일 시스템 파티션과 simulated된 디스크를 만들 수 있어야 한다. pintos-mkdisk 프로그램이 이 기능을 해준다.
pintos-mkdisk filesys.dsk 2 명령은 2MB 핀토스 파일 시스템 파티션을 포함한 filesys.dsk 이름의 simulated disk를 만든다.
--fs-disk filesys.dsk 명령을 통해 디스크를 specify 해준다.
--은 --fs-disk가 simulate된 커널이 아니라 핀토스 크스립트를 위한 것이기 때문에 필요하다.
그리고 나서 커널 명령어 라인에 -f -q 을 넣음으로써 파일 시스템 파티션을 포멧시켜준다.
-f는 파일 시스템을 포멧시켜주고
-q는 포멧되자마자 핀토스를 끝나게 해준다.

The pintos -p ("put") and -g ("get") options은 파일을 simulated된 파일 시스팀으로 그리고 에서 복사시킨다.
새로운 이름으로 복사시키고 싶으면 pintos -p file:newname -- -q
VM에서부터 복사시키고 싶으면 명령어 똑같이 쓰고 -g를 -p로 바꿔준다.

테스트 케이스를 이미 만들었고 지금 디렉토리가 userprog/build라는 가정하에서
파일 시스템 파티션으로 디스크를 만들고 파일 시스템을 포멧하고, args-single program을 새로운 디스크로 복사하고 'onearg' 파라미터를 주어 실행시키는 명령어는 애라와 같다.

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

파일 시스템 디스크를 앞으로 안 볼 거면 한방에 실행시키는 명령어가 있음.
pintos --fs-disk=10 -p tests/userprog/args-single:args-single -- -q -f run 'args-single onearg'

유저 프로그램이 어떻게 일하느냐

핀토스는 C 프로그램 사용할 수 있음.
malloc()은 구현될 수 없는데 이 프로젝트에서 필요한 시스템 콜은 메모리 할당이 허용되지 않는다.
핀토스는 소수점 오퍼레이션을 사용하는 프로그램은 실행할 수 없는데 커널은 스레드를 스위칭할 때 프로세서의 소수점 단위를 저장하거나 복구시키지 않기 때문이다.

핀토스는 ELF 실행파일들을 userprog/process.c에 있는 로더를 사용해서 로드할 수 있다. ELF는 많은 OS에서 오브젝트 파일, shared libraries,와 실행 파일로서 사용되는 파일 포맷이다.

VM 레이아웃

핀토스에서 VM은 유저 vm과 커널 vm으로 나뉜다.
유저 vm은 0번지에서 디폴트로 0x8004000000이고 include/threads/vaddr.h에 정의되어 있는 KERN_BASE까지이다.
커널 vm은 가상 주소 공간에서 나머지 영역이다.

유저 vm은 프로세스당 하나씩 주어진다. 커널에서 다른 프로세스로 넘어갈 때 프로세서의 페이지 디렉토리 베이스 레지스터를 바꿔줌으로써 유저 가상 주소 공간도 바뀐다.
스레드 구조체는 프로세스의 페이지 테이블을 가리키는 포인터를 포함한다.

커널의 가상 메모리는 전역적이다. 어떤 유저 프로세스나 커널 스레드가 돌고있든 커널 가상 메모리는 항상 같은 방법으로 mapping된다.
핀토스에서 커널 vm은 KERN_BASE에서 시작해서 피지컬 메모리에 일대일로 매핑된다.
즉, 가상 주소 KERN_BASE는 물리 주소 0에 접근한다. 가상 주소에서 KERN_BASE + 0x1234dlaus 물리 주소 0x1234에 접근한다는 거다.

유저 프로그램은 자신의 유저 vm에만 접근할 수 있다.
커널 vm에 접근하면 page fault를 일으킨다. 그리고 userprog/exception.c에 있는 page_fault()에 의해 처리되고 그 프로세스는 종료된다.
커널 스레드들은 커널 vm은 물론 유저 프로세스가 실행중이면 실행중인 유저 vm에도 접근가능하다.
물론 커널이라고 하더라도 매핑되지 않은 유저 가상 주소에 접근하면 page fault가 뜬다.

전형적인 메모리 레이아웃

개념적으로 각 프로세스는 어떻게 선택하느냐에 따라 본인의 유저 vm를 마음대로 설정할 수 있다.
실제론 유저 vm은 아래와 같이 구성되어 있다.

이 프로젝트에서 유저 스택 사이즈는 고정되어 있지만 프로젝트 3에서는 키울 수 있다. 전통적으로 initialize되지 않은 데이터 세그먼트 사이즈는 시스템콜로 조정될 수 있지만 이걸 구현할 필요는 없다.

핀토스에서 코드 세그먼트는 유저 가상 주소 0x400000에서 시작한다. 이는 대략 주소 공간의 바닥으로부터 128MB정도 된다. 이 값은 우분투에서 전형적인 값이며 별 큰 의미는 없다.

링커는 여러 프로그램 세그먼트의 이름과 위치를 알려주는 "링커 스크립트"에 따라 메모리에서 유저 프로그램의 레이아웃을 설정한다.

링커 스크립트에 대해 더 알고 싶으면 Scripts 챕터 보셈

유저 메모리 접근

시스템콜의 한 부분으로, 커널은 유저 프로그램에 의해 제공된 포인터를 통해 메모리에 접근해야 된다.
커널은 반드시 조심스럽게 유저 프로그램이 제공한 메모리 포인터에 접근해야된다.
왜냐하면 유저는 널포인터나 물리 주소에 연결되지 않은 vm에 연결된 포인터, 또는 (KERN_BASE 아래에 있는) 가상 주소 공간을 가리키는 포인터를 넘겨줄 수도 있다.
이런 유효하지 않은 포인터들은 offending 프로세스를 종료시키거나 그 자원들을 free시켜줌으로써 커널이나 실행중인 프로세스들에 아무 영향이 가지 않게 접근이 거절되어야 한다.

정확하게 위와 같이 진행할 수 있는 방법이 최소 두가지 있다.
첫 번째 방법은 유저가 제공한 포인터의 유효성을 확인하고 역참조하는 방식이다.
이 방법은 include/threads/vaddr.h과 thread/mmu.c에 있는 함수들을 보면 좋을거다.
이 방법은 유저 메모리 접근을 처리하는 데 가장 쉬운 방법이다.

두번째 방법은 유저 포인터가 KERN_BASE 아래를 가리키는지 확인하고 역참조하는 것이다.
유효하지 않은 포인터는 userprog/exception.c에 있는 page_fault()의 코드를 변경해줌으로써 당신이 해결할 수 있는 page fault를 일으킬 것이다.
이 방법은 프로세서의 MMU의 이점을 취하기 때문에 보통 더 빠르다. 그래서 실제 커널에서 자주 쓰인다.

두 방법 모두에서 자원들이 새지 않도록 해줘야 한다.
당신의 시스템콜이 lock을 요청하거나 malloc()으로 메모리를 할당했다고 해보자.
만약에 유효하지 않은 포인터를 만났을 때 너는 lock을 release해주거나 메모리 페이지를 free시켜줘야 한다.
만약에 역참조 하기 전에 유저 포인터를 확인하기로 했으면 더 쉬워야 한다.
유효하지 않은 포인터가 page fault를 일으키녀 더 어려울 것이다.
왜냐하면 메모리 접근으로부터 에러 코드를 리턴할 방법이 없기 때문이다.
따라서 후자와 같은 방법을 쓰고 싶은 이들을 위해 우리는 도움이 될만한 코드를 제공하려 한다.

/* 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가 그저 rax를 -1로 설정하고 이전 값을 %rip로 복사할 수 있게 page_fault()를 고쳤다고 가정한다.

profile
정글에서 살아남기

0개의 댓글