본문 바로가기

프로그래밍/JUNGLE

PintOS project2. User Program

Project2. User Program

1. Argument passing

첫 과제는 Argument passing이다. 이를 이해하기에 앞서, 2주차 과제에서 우리가 배워야 할 것이 무엇이고 목적이 뭔지 생각해보자.
쉘에서 우리는 프로그램을 실행할 때,

echo 'Hello World'

처럼 echo라는 프로그램을 'Hello World'라는 인자값을 주어 실행한다.

혹은 더블 클릭으로 폴더를 열거나, 파일을 실행하는 것도 같은 맥락일 것이다.
이렇게 사용자가 어떤 프로그램을 실행하겠다고 명령했을 때, 그 명령을 컴퓨터가 이해할 수 있게 잘 전달해줘야 한다.
즉, project2의 목표는 User Program을 어떻게 컴퓨터가 식별하고 실행하는가! 에 관한 것이다.

더 구체적으로 말하자면,  PintOS는 1개의 프로세스에 1개의 쓰레드만 존재하지만, CPU는 여러개의 프로세스를 동시에 실행할 수 있다. 그리고 각 프로세스는 CPU가 오직 해당 프로세스를 위해 동작한다고 착각하지만, 실제로는 그렇지 않다
이런 착각을 만들어 주는게 목표이다. 즉, 각 프로세스가 메모리, 스케쥴링 및 기타 상태를 설정하고 있어야한다.

 

돌아와서, 그럼 argument passing을 알아보자.
우리가 해야 할 일은, echo x y 와 같은 command line을 입력 받았을 때 이를 parsing(문자열을 쪼갬, 프로그램 이름 / 인자1 / 인자 2)과 passing(파싱한 인자들을 스택 포인터에 저장하는 일)을 수행하면 된다.

parsing의 경우는 strtok_r 함수를 이용해 구현했다. (즉, arg[0] = echo, arg[1] = x, arg[2] = y 이런 식으로 대입했다.)
이후 passing은 stack pointer (rsp)에 해당 값들을 넣어주면 된다!

kaist-pintos document에 있는 이 그림이 passing의 정확한 과정을 보여준다.

PintOS Document 

위 표를 보면, argv의 진짜 값 즉, echo x y  각각의 문자열을 먼저 저장한다. 당연하게도 이는 각각 4, 1, 1 byte를 차지할 것이다. 주의할 것은 실제 크기는 '\0'을 문자열에 추가해야하기 때문에 5, 2, 2 byte를 차지한다.
이렇게 각 크기만큼 문자열을 저장한 후, 더블 워드 정렬을 해준다. 이는 앞서 공부했던 것처럼 64bit에서 주소값은 8byte로 표현이 되기 때문에, 8의 배수로 정렬을 해준다. 즉 5 2 2 는 총 9 byte이고, 8의 배수로 정렬하기 위해 7byte의 더미 값을 넣어준다.
마지막으로, 방금 저장한 각 문자열의 실제 주소값을 다시 저장해준다.
정말 정말 마지막으로, 모든 과정이 끝났음을 알려주기 위해 return address 인 0을 넣어 마무리한다.

이렇게 stack pointer(rsp)에 Argument의 값을 적재(load)했다.  그 후, document에 명시된 것과 같이
rdi -> argc, rsi -> rsp값을 대입하여 준다.
이렇게 값을 기입한 if (interupt frame)을 do_iret을 통해 레지스터에 올려준다!
그러면 사용자가 입력한 command line을 토대로, 해당 프로그램을 실행해주는 것이다.

(document와 코드 내 주석에 설명히 정말 잘 되어있다. 꼼꼼히 읽어보길 바란다.)

2. User memory

2번 과제에서 해야하는 것은, 잘못된 메모리에 대한 접근을 막는 것이다.
즉 1번에서 유저가 준 stack pointer의 위치가 잘못된 주소를 줬을 때이다.
1. Null pointer일 때

2. kernel 가상 메모를 참조 했을 때 (즉, KERN_BASE보다 큰 값일 때)

3. 맵핑되지 않은 가상 메모리를 참조했을 때


이 3가지 경우에는 메모리에 참조할 수 없게 해야한다.
그래서 우리는 syscall.c에 check_address 함수를 만들어야 한다.

여기서 앞서 말한 3가지 경우를 체크해주면 된다!!! (syscall 함수를 호출할 때 주소값을 확인해야해서 syscall.c에서 만들어준다.

그리고 잘못된 주소를 참조한 경우 exit(-1)을 호출한다.


3. System Calls

프로젝트 2의 핵심이자 꽃이라고 생각된다.
시스템 콜은 일반 함수와는 다르다. read, write와 같은 함수를 호출하면 SYS_CALL을 호출하게 되고 이 SYS_CALL 함수가 하는 일은 호출한 함수가 받은 인자값 ( e.g close(0), read write는 3개의 인자를 가진다)을 레지스터에 올리는 것이다!!!

rax에는 syscall 이름을 올려 syscall_handler에서 어떤 syscall을 호출했는지 알 수 있다.
이 과정에서 우리는 사용자 영역에서 syscall을 호출하고 호출되면서 kernel 영역으로 넘어간다.

PintOS에서 이 과정을 살펴보자면, User Program에서 syscall을 호출하면, syscall_handler로 넘어가서 해당 syscall을 처리하게 된다.
여기서 우리는 syscall_handler를 implement 해주면 된다!! (현재는 halt, open, close, read, write 등 하나도 구현이 안되어 있다.) 때문에 syscall.h에 선언되어 있는 모든 syscall을 구현하고 syscall_handler에 추가해야한다.

Write를 구현해야 비로소 make check를 실행하여 결과를 확인할 수 있다. (test의 msg 함수가 write을 이용하기 때문)

구현해야 할 syscall은 HALT, EXIT, FORK, EXEC, WAIT, CREATE, REMOVE, OPEN, FILESIZE, READ, WRITE, CLOSE, SEEK, TELL, DUP2로 총 15개다. (DUP2는 extra 과제이다.)

 

Halt는 PintOS를 종료한다. 때문에 구현되어 있는 power_off() 함수를 이용하면 된다.

Exit은 current thread를 종료하는 것이다. thread_exit()을 사용하면 되는데, 이때 exit하기 전 종료하는 쓰레드의 status를 기록하고 부모에게 이를 알려준 후 종료한다. (이 과정은 wait에서 연결된다. 즉, 부모는 wait 중이고 자식이 종료하며 자신의 exit_status를 기록하고 부모는 wait에서 다시 실행이 재개되면 exit_status를 받아 종료가 성공적인지 확인한다.)


Fork, Exec, Dup2 는 우선 패스하겠다.

Wait은 process_wait 부분을 구현해주면 된다. 이를 위해서는 thread 구조체에 wait semaphore를 만들어줘야 하고,  부모는 sema down을 하고 자식이 sema up을 하기 전까지 대기한다.

Create, Remove는 2번에서 했던 address check 이후 filesys_create 와 filesys_remove 함수를 이용하면 된다.

Open은 마찬가지로 address check 이후, filesys_open 함수를 이용해 해당 파일을 연다. 파일이 존재하는지 확인하고, 해당 파일을 thread 구조체의 fdTable에 추가하고, fdIDX도 1 추가해준다. 

Filesize는 해당 fd가 존재하는지 먼저 체크하고, 존재한다면 그 file_obj (파일 오브젝트? 파일 구조체!)를 file_length 함수를 이용하여 크기를 측정한다.

Read는 address_check를 한 후, 읽고자 하는 fd에 해당하는 file이 존재하는지 확인한다. 파일이 stdin일 경우 input_getc 함수를 이용해서 buffer에 값을 넣어주고, stdout이라면 -1을 리턴한다. 그리고 이외의 파일이라면 read / write를 할 때 필요한 lock을 생성하여 lock을 걸고, file_read를 이용해 파일을 읽은 후 lock을 풀어준다. (read / write가 동시에 일어나는 것을 방지하기 위함이다.)

Write는 read와 유사하다. putbuf 함수를 이용해서 stdout일 경우를 처리하고, 아닌 경우는 file_write를 이용한다. (전체적인 구조는 동일함)

Seek, Tell은 각각 파일이 존재하는지만 확인하고, fileobj -> pos 를 변경하고 file_tell을 이용하면 된다.

Close는 open과 유사하다. Extra 문제를 고려하면 조금 복잡해지지만, 일반적인 경우에는 file이 존재하는지 체크하고 fdTable에서 해당 file을 제거해주면 된다. 마찬가지로 file_close를 이용해 파일을 닫는다.

4. Process Termination Messages

앞서 얘기했던 exit 시스템 콜을 구현할 때,  Process Termination Message를 출력하라는 의미로 받아들였다.
앞선 과제를 했다면 매우 간단히 해결 가능하다!

5. Denying Writes to Executables

내가 fd = 5인 파일을 읽고 있다고 하자. 읽는 중에 파일의 아랫부분 (아직 읽지 않은 부분)이 변경되거나 하면 안되기 때문에, 현재 load된 파일은 write를 할 수 없게 처리해줘야한다. 다행히 이는 file_deny_write라는 함수를 이용하면 된다.
즉 load  후에 처리해주고, 마찬가지로 close를 하게 되면 다시 풀어주면 된다!
thread 구조체에 현재 running 중인 파일을 추가해주면 좋다.

6. Extend File Descriptor (Extra)

마지막으로 EXTRA 과제는 동일한 파일을 여러번 open하거나, 복사 (dup)을 하게 될 경우 어떻게 처리를 해야하나에 관한 문제이다. 현재 구현된 방식으로는 duplicate를 하거나 open을 한다면 계속해서 파일이 추가되기만 할 뿐이다. 즉 같은 파일이 fd 2 5 7 8 순으로 계속 생겨날 수 있는데, 우리가 원하는 것은 동일한 파일이 몇개인지 count하고 다른 fd지만 같은 file 구조체를 바라보게 하는 것이다!! (설명이 괴랄하다 ㅠㅠ)

요지는 thread 구조체에는 stdin_count, stdout_count를 추가하고 (stdin, stdout이 여러개인지, 혹은 0이 되어버렸는지 확인하기 위함이다) file 구조체에 dup_Count를 추가한다. (이는 해당 파일이 몇개의 복제를 가지고 있는지 확인하는 것이다.) 예를 들면 dup_Count가 0이라면 우리가 했던 일반적인 경우이다. 때문에 Close를 할 경우 그냥 close를 하면 된다. 그러나 dup_Count > 0 이라면 복제된 파일이 존재하는 것이기 때문에 close가 호출돼도 파일을 닫으면 안된다!
그래서 dup_Count -- 를 해주고, 이런 식으로 duplicate된 파일의 수를 control한다.

 

 

'프로그래밍 > JUNGLE' 카테고리의 다른 글

PintOS Project 4. File System  (0) 2021.11.02
Project3. Virtual Memory (PintOS)  (0) 2021.10.28
PintOS project1. Threads  (0) 2021.10.04
[C] 재귀함수를 만드는 Global 변수 vs argument  (0) 2021.09.08
RB Tree 삽입  (0) 2021.09.08