PintOS, userprog 강의 논하기

Alarm Clock 과 Scheduler를 이어 해야 할 것은 유저 프로그램을 PintOS에서 돌리게 하는 것이다.

그 목표를 달성하기 위해서는 argument를 받아오고, 시스템 콜을 구현하며, 파일을 어떻게 저찌 할 수 있는 코드를 구현해야 한다.

PintOS를 비롯한 모든 운영체제는 프로세스를 시작하기전에 argment를 부른다. 그것이 여기서 process_execute로 구현된다. 그리고 process_execute 내에는 thread_create로 실질적인 준비가 이루어진다.
문제는, 현 상태의 process_wait는 딱히 process_execute가 뭘 하는지 관심 없고 그냥 -1를 반환한다. 즉 자식 프로세스가 뭘 생성해서 할 일을 하는데에 있어 대기 해야하는데 그냥 끝내 버린다는 것.
이제부터 이것을 만들어 나가야한다.

결과적으로 핀토스는 우측으로 만드는 것이 목표이다. 우선 좌측의 구조를 먼저 파악해보자.
init이라는 이름의 process는 pid가 0인 모든 프로세스의 부모가 되는 프로세스이다.
새로운 프로세스를 만든다음, scheduling(), 스케줄링 한 뒤에 종료된다. 종료되면 다음 껄 어떻게 틀까? 즉, 하나 틀고 그냥 꺼진다.
그러니 오른쪽의 구도로 만들어야한다. Wait for the completion 으로 빠지고, 자식 프로세스의 완료를 기다리고 다시 진행하는 것.
할 일이 무척 많다. 하지만 강의와 슬라이드가 발자국 단위로 설명 할 수 있도록 도와주겠다고 한다.

PintOS의 프로그램 실행에 대해 설명해보겠다.
파일 이름을 매개변수로 받아서 실행을 한다. a.out, 또는 ls 뭐시기 명령을 수행하려는 경우를 생각해보자.
여기서 thread_create에 file_name이 그대로 담겨 실행이 이루어진다. 그렇게 새 쓰레드가 만들어진다. 따라서 그림 아래에 새로운 쓰레드를 볼 수 있다. 그리고 거기서도 쓰레드를 또 만들게 된다. 오른쪽으로 화살표가 빠지면서 순식간에 새 쓰레드가 생성되고 실행이 계속 이루어지게 된다.

thread_create()의 자세한 내용을 보자. 새 쓰레드 구조를 만드는 함수이다. 초기화, 커널 스택 할당을 하고, 함수가 실행하도록 돕는다.
여기서 레지스터 언급이 되는데 너무 이론적이라 코드를 직접 보는게 좋겠다.

설정하려는 기본 우선순위와 함수 이름이 제공된다. 보조 매개변수 코드를 확인해보자.
첫 번째는 커널 공간에서 단일 페이지 4KB를 할당한다. 그렇게 쓰레드 구조를 초기화하게 된다. 이것은 64바이트 또는 128바이트 크기를 가진 쓰레드가 될 것이다. 그 다음 tid를 배정한다.
그 다음 커널 스택도 할당한다. 실행하려는 함수를 포함해 다양한 필드를 초기화 하게 된다. 커널 스택은 실행해야하는 함수의 주소가 포함ㅎ된다. 그런 다음 커널은 unblock하여 ready_list에 투입된다. 이게 쓰레드가 생성되는 방법이다.

프로세스를 시작하는 방법이다. ready_list에 프로그램이 추가 되었다. 파일 이름을 우선 입력 받아야한다. 즉, 이것은 실행하려는 바이너리 파일의 이름이다. 첫 번째 할 것은 디스크에서 메모리로 이진 파일을 로드 한다음, 명령어의 위치 정보를 가져온다음, 최상위 포인터를 가져온다.
해당 작업이 성공하지 못하면 즉시 쓰레드를 종료하고, 그렇지 않다면 어셈블리 코드로 접근해서 유저 프로그램이 시작되는 것이다. 내용에 jmp를 볼 수 있는데, 이것은 이후에 설명 하도록 하겠다.

쓰레드 종료는 앞에서 준비하려고 했던 걸 다시 정리하고 끝내야한다.

로드 함수가 작동되는 과정을 보자. 한 파일이 있는데 그 이름은 a.out 이다. 그리고 메모리가 하나 있다고 하자.
load가 호출되면, 커널은 쓰레드를 위해 페이지 테이블을 생성한다. 그리고 파일을 연다. 이것은 파일이므로 파일을 열고 elf 포맷의 헤더를 읽는다. 이것을 메모리로 가져온다음, elf 헤더를 보면서 이 파일이 어떻게 구성되어있는지에 대한 정보를 확인한다. data, BSS sections, location of binarys. load를 통해 파일을 구문 분석한다음에, data를 data segment에 로드한다.
이미 생성된 쓰레드 구조를 메모리에 가지고 있기 때문에, 쓰레드 구조에서 이제 a.out를 load를 통해 가져온 것들을 이어주게 된다.
- 즉 load로 파일을 메모리에 올린다.

맨 아래만 보자. 프로그램의 사용자 스택을 초기화하는 것이다.
그리고 함수 시작점에서 파일 이름을 비롯한 세 개의 매개 변수를 받는데,
- **eip라고 적힌 변수에는 함수의 시작 진입점을 포함하게 되고,
- **esp는 실행해야 하는 사용자 스택의 맨 위로 위치시키게 된다.
After load Function finished,

운영체제는 프로그램을 메모리로 읽고, 초기화하고, 데이터와 BSS를 초기화 한다음, 텍스트 메모리도 초기화한다. 이렇게 로드 함수가 끝난다.
PintOS의 전반적인 user prog 작동 형태를 보았다. 이제 인수를 전달하고 쓰레드를 생성하는 메커니즘을 구현해야한다.

현재 PintOS에서는 명령줄 인수를 토큰화하는 메커니즘이 없다. 프로세스 실행을 위해 전체 명령줄만 전달하는 것이 가능하다.
따라서 토큰화 할 수 있도록 코드를 만들어야한다. 개별 토큰화한다음, 세번째 이름을 식별하고 파일 이름이 Echo인 프로그램을 찾은 다음 사용자 스택에 넣을 수 있어야 한다. 이 경유엔 x y z가 있으니 스택에 푸쉬하는 메커니즘이 필요하다.

두 개가 수정 대상이다.

중요한 것은 파싱하여 그 내용을 유저 스택에 넣는 것이다.
첫 번째 토큰을 새 프로세스의 이름으로써 실행해야한다. argv[0]이 해당이라고..
process_excute()는 file_name으로 실행하려고 시도하게 된다.
start_process()는 개별 토큰을 토큰화한다음에 유저스택에 넣게 된다.

정석 라이브러리에서 제공하는 strtok_r을 보면 이해가 빠를 것.

첫 번째는 실행하려는 파일의 이름으로 쓰레드에서 이름으로 전달하고, 그리고 start_process라는 함수를 쓰레드에게 주어서 시작하게 끔한다.

현재의 PintOS는 argment로 사용자 스택을 초기화하는 내용이 없어서 이 내용을 구현해야 한다.

제일 일반적인 구조 int를 예를 들자. user program이 int를 호출하면 운영체제에서 trap을 만든다. 그리고 커널에서 빠져나올 때는 iret를 써서 빠져 나올 수 있다. 두 명령어에 대한 세부 사항을 이제부터 보자.

좌측은 가상 구조이다. 좌측에서 User Space는 정확히 지정하진 않았지만 프로그램이 담겨있을 것이다.
Remember when you ececute in interrupt instruction it switches your kernel from the user to the kernel and it pushes register to the interrupt frame that resides to the kernel spaces. this is executed in a single instruction.

아래 블럭부터 보자. 5개의 레지스터. 가운데에는 12바이트의 내용, 그리고 위에 많은 범용 레지스터를 볼 수 있다.
인터럽트 프레임의 데이터 구조는 운영체제에서 정의된다.
맨 아래만 CPU에 의해 정의되고 나머지 두 블럭은 운영체제가 담당한다.
- x86 기준으로 아래는 유지되지만 운영체제에 따라 위의 두 블럭 내용이 달라질 여지는 충분하다.

int n이 어떻게든 불리면, esp를 사용자 스택에서 Kernel Stack Top 으로 전환한다음 push 한다.

esp의 위치가 옮겨가는 과정을 보다 자세하게 보겠다. 처음 esp는 완전 맨 아래에 박혀있다. int가 불리면서 esp는 CPU의 영역을 완전히 지나치고 두번째 블럭의 시작점에 놓여진다.

로드의 역할을 확인 할 수 있다.

굵게 되었거나 빨간 줄이 중요하다.
1. success
- load에서 로드하고 스택을 초기화한다.
2. 사용자 프로그램을 실행할때 사용자 프로세스에 인수 집합을 전달해야한다. 직접 만들어야한다.
3. 커널에서 점프하여 jump로 해당 영역으로 이동한다.
2번 영역이 직접 구현되어야하는데 그 전에 1과 3을 잘 이해해두길 기대한다.

인터럽트 프레임의 현재 단계를 가리키도록 설정한다. 이것은 인터럽트 종료로 이동한다. 코드를 통해 확인하면 :
레지스터를 팝하고 12바이트를 점프한다음 iret를 불러버린다. mov %, esp에 해당하는 것이다.

앞에서 이야기 했던 2번 구현에 대한 추가 설명이다.
- 오른쪽에 보이는 것은 주소 공간이다. 텍스트, 데이터, BSS 영역이 있다.
- 그리고 위에 있는 &if_.esp는 인터럽트 프레임이 존재한다고 가정해보겠다.
- 시작 프로세스에서 해야 할 일은 사용자 스택을 설정하는 것이다.
- 프로세스가 재개 하는 타이밍에..
push the parmeters from the stack top one by one.

- 80x86 Calling Convention이다.
- 문자열을 오른쪽에서 왼쪽으로 푸쉬해야한다. 그래서 [3]에 위치한 "bar"부터 푸시가 이루어져야한다.
- 모두 푸시한 후 4바이트로 정렬되어야한다.
- 마지막에 위치한 Push the address of the next instruction은 꼭 지켜지도록 하자.

bar, foo, -l, bin/ls 를 순서대로 푸시하였다. Argment를 4바이트로 정렬해야한다. 19바이트이니 20바이트가 되도록 패딩을 추가한다. 그 다음
이후 word-align이 투입되고 다시 argv[n]이 Name 필드에 언급되는데, 여기서 Data가 0으로 언급되는경우 argment의 끝을 표시하는 것이다.
- Why is "return address" here is 0?
- 방금 새로 만들어진 프로세스이기 때문이다. 돌아갈 곳이 없어서 꺼지는 의미로써 작성된 fake address 인 것.


hex_dump 함수가 제공된다. 이걸로 스택에 올바르게 담겼는지 확인 할 수 있다. 잘 해봐라.