[PintOS] 2-1 Arguments passing

2024. 6. 2. 16:42크래프톤 정글 5기/공부

GIT

 

[process.c, process.h, syscall.c, syscall.h]

 

영상 출처 : https://www.youtube.com/watch?v=RbsE0EQ9_dY

 


 

Background

 

Project 2 : User programming

Objective

- pintOS에서 user program을 실행하는 것

 

 

 

# pintOS, 다른 OS에서는 프로세스를 실행하기 위해서 argument를 받음

- process_wait() 함수에서 process_execute() 함수 인자로 받아옴
- process_execute()에서는 특정 동작을 실행하는 thread를 생성함
- 하지만 process_wait의 skeleton code에는 단순히 return만 기입되어 있음
	-> 이러면 그냥 꺼짐
	--> 자식 프로세스가 생성될 때까지 대기 + 프로세스 생성까지 대기 + 프로세스 완료까지 대기 해야함 
	
	
# Todo : process wait() 수정하기

 

현재 pintOS 동작 방식 → 목표로 해야하는 동작 흐름

- 현재 pintOS 동작 흐름도

1) init Process 생성 (PID = 0)
2) user process 생성
3) scheduling() 진행
4) execute 동작이 수행되지 않고 init process가 종료해버리는 상태
	 -> pintOS에서는 user process 실행이 안되는 상태 !!
	 
- 목표로 해야하는 pintOS 동작 흐름도

1) init process 생성 (PID = 0)
2) user process 생성
3) scheduling() 진행
4) user process가 execute 됨
5) init process는 user process가 완료될 때까지 대기 이후 종료

 

- process_execute() 함수에는 '실행할 파일 이름'을 받아와야 함
- process_execute() 함수는 'thread_create' 함수를 호출해서 새로운 thread 생성함

# 동작 흐름도

1) thread가 process_execute() 호출
2) process_execute()에서 thread_create() 호출 -> 새로운 thread 생성
3) 새로운 thread, 기존 thread는 이어서 동작 수행
# thread_create() : 새로운 thread 구조를 생성하는 함수

- 새로운 struct thread를 생성하고 초기화함
- kernel stack 할당
- 실행할 동작을 [Instruction pointer or Instruction Counter] value 로 이동한 후 읽기 목록
  에 추가 (=Register)

 

- palloc_get_page() : 4KB의 공간 할당
- init_thread() : 4KB의 할당된 공간 초기화
- allocate_tid() : thread에 ID 부여

- alloc_frame() : kernel stack 할당
- 이후 kernel stack 초기화

- thread_unblock() : thread를 읽기 목록에 추가

 

- start_process() 호출 시 [실행하고자 하는 binary 파일의 이름]을 받아와야 함
	-> binary 파일을 Disk에서 Memory로 load 해와야 함

- load() : 실행할 binary 파일을 Disk에서 Memory로 가져옴 (# 실패 시 thread 종료)
           file_name : 실행할 파일 이름
           &if_.eip  : 실행할 Instruction 위치
           &if_.esp  : User stack의 최상위 포인터

- load() 호출 시 [binary 파일을 메모리로 load + User stack 초기화 + 초기 포인터 설정] 동작 수행

 

1) '실행할 file 이름'과 함께 start_process() 호출
2) load() 호출이 성공하면, 그대로 thread 동작 수행
3) load() 호출이 실패하면, 그대로 thread_exit()

 

- if (!setup_stack (esp))
  : User stack 초기화하는 코드

- *eip = (void (*) (void)) ehdr.e_entry;
	: 스택의 entry point를 초기화하는 코드

 

- load 기능이 완료되면 OS는 프로그램 파일을 memory로 읽어옴
- 이후 Stack, Data, Text 영역을 초기화함

 


Passing the arguments and creating a thread

 

- 기존 pintOS에서는 commandline arguments를 토큰화 하는 메커니즘이 결여되어 있음
- 따라서 프로세스 실행을 위해 '전체 command line'을 전달하는 동작만 수행됨

- command line에서 개별 토큰화 하는 동작으로 수정해야 함
- 이후 User stack으로 argument를 전달하는 동작이 수행되어야 함

ex) echo x y z -> echo / (x / y / z)

 

- tid_t process_execute() / static void start_process() 수정해야 함

 

- 가장 중요한 것 : arguments를 파싱하여, User stack으로 pushing 하는 동작

- process_execute() 함수에 'file_name 전체' 를 전달받음
- 전달받은 문자열을 파싱해서, 첫 번째 토큰을 thread_create()의 '새 프로세스 이름'으로 전달

- start_process() 함수에 'file_name 전체' 를 전달받음
- 전달받은 문자열을 파싱해서, 새로 생성된 process의 User stack에 Pushing

 

- C library 함수인 *strtok_r() 함수를 이용해서 command line을 토큰화 할 수 있음

 

 

- process_execute() 함수는 thread_create() 함수를 실행시킴
- thread_create() 함수는 2개의 중요한 동작으로 구분할 수 있음

1. 실행하려는 파일의 이름을 '실행하려는 thread 이름'으로 전달하는 것
2. start_process라는 '실행할 함수'를 전달하는 것

 

 

* start_process에서
- Interrupt Frame을 할당함
- 프로그램을 load하고, interrupt frame과 user stack을 초기화함
- user stack에 arguments를 설정함
- interrupt_exit()을 통해 user program으로 Jumping 함

- 현재 pintOS에는 [user stack 초기화] + [user stack에 arguments 설정] 동작이 결여됨

#Todo : 해당 동작 구현하기

 

- start_process는 load() 함수에 '프로그램 이름' 전달함
- load() 함수는 전달받은 '프로그램 이름'으로 '실행시킬 파일'을 찾고, '메모리로 load' 함

file_name : load 하려는 파일의 이름
&if_.eip : Function entry point, 프로그램이 로드된 이후 실행되어야 하는 함수의 주소
					 load() 함수는 해당 Field를 초기화 해야함
&if_.esp : User stack의 최상단 주소 / load() 함수는 이 Field를 초기화 해야함

 

- success = load() : 실행 파일을 메모리에 load 함 / User stack 초기화

- set up stack : User program을 실행할 때 arguments를 user program에 전달해야 함
  # TODO : 해당 부분은 누락되있어서, 구현해야 함
	
- asm volatile() : kernel -> user 전환 / 프로그램 실행

- 2번을 구현하려면, 1 / 3번 동작을 잘 이해해야함

 

- Kernel에서 빠져 나올 때 사용하는 asm volatile()

- 2개의 기본 Assembly Instruction으로 구성됨 (move / jump)

- move : 현재 스택 (kernel stack)의 맨 위를 가리키도록 설정 -> Interrupt frame의 맨 위 가리킴
- jump : intr_exit으로 이동 -> kernel에서 빠져나오게 됨

 

# intr_exit function

- ESP는 Kernel stack의 최상단 (= Interrupt frame의 최상단)을 가리키게 됨
- OS에 의해 정의되었던 Register들 pop() (=터뜨림)
- 12 Byte 이동함 (Field 건너 뜀)
- iret() 함수 호출

# iret() function

- CPU에 의해 정의되었던 Register들 pop()
- User process에서 사용되던 Register들 (pop한 5개의 register) 복원
- Kernel mode -> User mode로 전환
- ESP는 User stack의 최상단을 가리키게 됨
- Thread를 처음 만들면, Interrupt Frame이 비어있음
- Process가 Kernel로 진입하면, Interrupt Frame을 어떤 값으로 채움
  int() 함수의 동작 -> User process의 register 값을 pushing
- 하지만 처음으로 생성되는 process는, [기존 User process의 register 값]이 존재하지 않음
- 따라서 start process (처음으로 생성된 process)는 Interrupt Frame 구조를 초기화 해야함

 

- 스택을 설정하는 동작을 추가해야 하는데, 함수를 새로 적지는 않아도 됨
- user process의 stack에 esp field가 있다고 가정함
- 우리가 할 건 &if_.esp - 4 (?)

 

- %bin/ls -l foo bar 이라는 commdline은 4개의 인자로 구성됨

- Arguments pushing에는 다양한 규칙들이 존재

 

- Stack 최상단부터 pushing 하게 됨
- 오른쪽부터 pushing 
- 우리가 4바이트로 정렬해야 하는 게 중요함
- 예시 arguments는 19 Byte이고, 1 Byte padding이 필요함
- argv[4] == padding (NULL) : 문자열 인자의 끝임을 알리는 역할

- 이후 argv[3] ~ argv[0]이라는 이름으로 argument 들의 주소값 저장
- argv[0] 주소값을 저장한 필드의 주소값 argv 저장
- 새로운 process를 생성하면 return 값이 없어도 됨

 

- 스택 설정이 제대로 이루어졌는지 체크하려면, hex_dump() 함수 호출하면 됨

 

- User stack 설정이 완료되면, 위 그림과 같은 모습을 보이게 됨

 


int() / iret()

 

✅ - Kernel 안으로 무엇이 들어가고, Kernel 밖으로 무엇이 나가나?

 

- int() 함수를 사용하면, user program이 operating system으로 트랩됨 (user -> kernel)
- iret() 함수를 사용하면, (kernel -> user) 전환이 가능

 

- Virtual address space는, [kernel space + User space] 로 구성됨
- [text + DATA + BSS] 세그먼트로 구분되지만, OS의 경우 [DATA, BSS] 위치가 바뀔 수 있음

- 일반적인 상황에서는, ESP는 User stack의 최상단을 가리킴
- interrupt Instruction (int()와 같은)을 호출하면, ESP는 자동으로 kernel stack 최상단 가리킴
- 이후 'User process'가 사용한 Register을 저장함 (kernel stack에 저장한다는 듯 ?)
- 해당 동작을 수행하는 (User process가 사용한 Register를 저장하는) 데이터 구조 == 'Interrupt Frame'

* INT() 호출 이후의 동작 흐름

- Interrupt Instruction 실행
- User -> Kernel 전환 (ESP는 자동으로 User stack -> Kernel stack 가리킴)
- Register (User process가 사용한) 를 Kernel space에 있는 'Interrupt Frame'으로 Pushing

 

 

- Interrupt handler, systemcall같은 kernel function을 실행시킴
- OS는 현재 실행 중인 process의 register를 kernel stack 안에 저장함
- 이 때 ESP는 user stack의 상단을 가리키다가, kernel stack 상단을 가리킴

 

 

- Interrupt frame 안에는 5개의 Register 존재
- 이후 12 Byte의 Field 존재
- 이후 공간은 Registser 존재 / OS에 따라 Register 구성이 정해짐
- OS가 달라지면, Retister 구성이 달라질 수 있음 

- kernel stack 안에 존재하는 데이터 구조임
- user process의 register 저장함

 

- Interrupt Instruction (int()) 호출 시, ESP는 kernel stack을 가리킴
- CPU에 의해 정의되는 5개의 register pushing
- ESP가 계속 변경되며, 12 Byte field와 OS에 의해 정의되는 register pushing
- 모든 pushing이 끝나면, ESP는 kernel stack 최상단을 가리키게 됨
- 이 과정이 끝나야 kernel로 진입이 가능해짐

 


What I did

 

5/20(월)

- Keyword 학습 / 팀 간 공유
- Git book 공부

---

5/21(화)

- Git book 마무리 이후 Argument passing 영상 시청 및 정리
- Argument passing 구현 트라이
1. process_wait 수정

int
process_wait (tid_t child_tid UNUSED) {
	/* customed 0521 */
	while (1)
	{
		if (child_tid == NULL)
		{
			return -1;
		}
	}
}

2. process_create_initd 수정 (Parsing method)

	/* customed 0521 */
	// Parse command line and get program name
	char *save_ptr;
	

	strtok_r(fn_copy, " ", &save_ptr);
	
	/* Create a new thread to execute FILE_NAME. */
	tid = thread_create (file_name, PRI_DEFAULT, initd, fn_copy);
	
3. pro

4. load 수정 (Arguments passing 구현)

1) filesys_open할 때 전달해야 하는 인자는 file_name을 자른 첫 번째 토큰
2) 따라서 해당 함수 호출 윗부분에 token_1이라는 포인터를 선언하고, 데이터를 담아서 넘김
3) 밑부분에 다시 parsing -> pushing 동작 전에, argv[] 배열에 아까 잘라서 넘긴 토큰 넣어줘야 함
4) 영상과는 다르게, [arguments / padding / argv[4](NULL) address / arguments address
	 / return address] Pushing해주고 rdi, rsi에 argc, argv를 직접 넣어줘야 함
	 
5. thread.h 수정 (#define USERPROG 추가)
마주한 문제

1. 영상과 함수명이 다르고, 함수 속 내용들이 조금씩 달라서 동기화에 애먹음

2. load() 함수에서 parcing을 해주고, 잘린 token을 배열에 다시 넣어줘야 하는 동작을 생각하지 못해서
   [실행되는 파일 이름이 자꾸 잘리는 것] + [값이 모두 pushing되지 않는 것] 해결하는 데 오래걸림