본문 바로가기

개발/PintOS

Pintos Project(핀토스 프로젝트) 1 - User Program

OS 과목에서 악명높은 프로젝트가 핀토스 프로젝트다. 그만큼 열심히하면 배워가는 것도 많다. 지금부터 핀토스 프로젝트 소개를 하려고 한다. 기준은 서강대학교에서 제공하는 Manual과 Pintos Manual이다.

 

* 0_2 프로젝트는 핀토스의 data structure를 구현하는 것이다. 공식 Pintos에서 요구하는 test는 아니고 서강대에서 만든 것 같다. (test를 일일이 만든 게 대단하다...) Data structre를 열심히 공부한 사람은 쉽게 할 수 있고, 난이도도 높지 않으니 알아서 잘 하시길 바란다. 

 

Pintos Project 1은 User Program을 짜는 것이다. 

< 초기 핀토스 상태 >

  현재 Pintos는 아주 기본적인 기능만 갖추고 있다. 할 수 있는 게 없다. 예를 들면 Boot하고 실행하고 Power off 등 시스템을 켜고 끄는 건 가능하지만 실질적으로 User program을 실행할 수 없다. 이거 컴퓨터 맞나 싶다.

 

그래서 이번 Pintos 프로젝트의 목표는 PintOS user program을 적절히 실행하도록 만드는 것이다. 현재 PintOS는 명령어를 그 전체 문자열로 인지한다. 그니까 ‘echo x’라는 명령을 입력하면

 

"아, 명령어 echo 랑 매개변수 x 가 입력됐구나. 그러니까 x를 인쇄해야지" 이게 아니라 "명령어 echo x가 들어왔구나. ?? 근데 이게 뭐야???" 딱 이 상태다. (약간 '아버지가방에들어가신다' 느낌임) 그러므로 우선 입력된 명령을 명령어와 매개변수로 구분하도록 해야한다. 이를 Argument Passing User stack 과정에서 구현할 것이다.

 

입력된 명령을 분절한 후에 명령어를 실행하는 데 이 때 User memory kernel Memory가 구분되어있음을 주의해야 한다. 메모리를 kernel memory User Memory로 구분하지 않고 사용하면 Memory를 관리하기 힘들다.. 예를 들어 각 프로세스가 서로 영역을 침범해서 오류를 발생시키거나 OS를 동작시키는 데 중요한 Kernel Code를 훼손할 수도 있다. User memory kernel memory를 구분한 상태에서 kernel의 함수를 User Program이  쓰려면 System Call이 필요하다

 

자 이제 뭘 할 지 정리해보자 Argument Passing

현재 PintOS는 명령어를 입력받았을 때 명령어 부분과 매개변수를 구분 못한다. 아까도 얘기했지만 'echo x'를 걍 echo x 전체로 받아들인다. 명령어를 적절히 분절해서 '명령어'와 '매개변수'로 나눠야 한다.

첫번째 단어는 프로그램의 이름이고 두 번째 단어부터는 N-1번째 매개변수이다. 단, 프로그램과 매개변수, 그리고 매개변수 끼리의 공백이 한 칸이 아니라 여러 칸일 수 있다. 이 여러 공백은 프로그램 상에서 하나의 공백과 같으므로 잉여 공백도 하나로 묶음처리해야한다.

 

여러 방법을 쓸 수 있겠지만 우리는 변수를 저장할 수 있는 stack을 쌓아서 접근하는 방법을 구현한다. 그러므로 변수를 stack에 쌓아서 다른 함수들이 argument를 참조할 수 있도록 한다. Userprog/process.c에 스택 포인터와 관련된 함수를 새롭게 만들어서 구현하면 된다. 이 작업은 Manual에 나와있는 것처럼

if (!setup_stack (esp))

  goto done;

다음 부분에 작성하면 된다. , stack initialize 한 후에 esp_stack(file_name, esp); 과 같은 함수를 만들어서 실행하면 filename  command line에서 받아온 데이터를 하나씩 parsing 하여 stack에 쌓으면 된다.

 

1. esp_stack

esp_stack()의 매개변수로 받은 file_name에 명령어 전체가 들어가 있다. 즉 'echo x'라는 명령어 전체가 들어가 있다. for문을 돌면서 전체 문자 개수를 구한다. echo x를 예로 들면 

case1) file_name[0] = e일 때 //// blanking = 0;

case2) file_name[1] = c일 때 //// blanking = 0;

case3) file_name[2] = h일 때 //// blanking = 0;

case4) file_name[3] = o일 때 //// blanking = 0;

case5) file_name[4] = ' '일 때 //// argc = 1, blanking = 1;

case6) file_name[5] = x일 때 //// blanking = 0;

즉 끝나면 argc = 1 이고 blanking = 1이다. 그리고 argc가 1이라는 건 변수가 두 개라는 의미이므로 argc += 1을 해준다. 

 

그러므로 변수 개수는 두 개, 즉 echo, x 두개이다!(맞았다!)

 

argc에 변수의 개수가 들어있으므로 그 개수만큼 malloc을 해준다. argv에 string이 들어갈 것이고("echo", "x" 등) arg_addr은 그 주소가 들어갈 것이다. 그 후 strlcpy()를 통해 esp_stack()함수의 매개변수로 들어온 file_name을 temp에 복사한다. argc가 변수 개수이므로 그 변수개수만큼 strtok_r()를 이용해서 잘라낸다!! 즉 echo x를 echo, x로 나누고 그 둘을 argv[]에 넣어준다!! 

argv[0] = echo

argv[1] = x 

이런 형식으로 되어있을 것이다.

이제 주소를 쌓을 차례다. stack의 특성상 거꾸로 집어넣어야 하므로 i  = argc-1로 설정한다. esp가 stack pointer이다. stack에 각 명령어와 매개변수를 넣어줄 것이므로 argv를 활용한다. 그리고 각 명령어와 매개변수를 넣을 때마다 그 주소를 arg_addr에 넣는다. 나중에 argument의 address도 stack에 넣을 거기 떄문이다. 

 

이제 word_align을 설정하는 차례다. 주소값을 4의 배수인가?로 맞춰줘야하기 때문에 넣는 값이다. word_align을 수정하는 과정은 어디에 NULL 문자를 넣을지 결정하는 과정이다. esp가 stack Pointer이기 때문에 esp의 위치를 조정 중이다. 그 위치를 찾으면 거기에 0을 집어넣는다.

 

아까 말한대로 스택에 각 명령어 및 매개변수의 주소를 집어넣고 뒷정리를 하는 과정이다. esp stack pointer의 변화는 조금 복잡하긴 한데, 조금 생각하면 알 수 있을 정도이므로 설명을 생략한당! 

 

User Memory Access 

1. User Program이 Kernel Address를 직접적으로 참조하지 않도록 한다. 만약 참조하면 에러를 발생시켜야한다. User Program은 OS를 통해서만 System Call을 활용할 수 있다.(Write 등) 이를 위하여 syscall.c에서 is_user_vaddr(addr)를 활용하는 함수를 만들어서 system calll이 발생할 때마다 user memory 이외의 주소를 참고하고 있지는 않은지 확인해야 한다

위 사진은 system call handler이다. 각 System call을 활용할 때마다 호출된 주소가 정상인지(Kernel Address를 참조하고 있는 건 아닌지) 검사한다. 

고맙게도 PintOS에서 is_user_vaddr이라는 함수를 제공한다. 이 함수는 매개변수로 들어온 주소가 User 영역인지 검사하는 함수다. 이걸 활용하면 Protection에 관한 함수를 구현할 수 있다. 

System Calls 

드디어 대망의 System call이다. 지금까지 esp_stack 함수를 통해 명령어와 매개변수를 분절했고 is_user_vaddr() 함수를 활용해서 메모리 보호를 구현했다. 이제 하이라이트인 System call을 구현할 차례다.  이번엔 read, write 등 file에 관한 함수는 굉장히 간단하게 구현한다. 이건 Project 1-2에서 구현할 계획이다.

 

User Program Kernel Function을 사용하려면 System Call을 반드시 거쳐야 한다. 만약 User Program이 직접 Kernel Access 하면 앞서 얘기했던 문제들이 발생하기 때문이다. 현재 PintOS System Call이 구현되어있지 않기 때문에 우리가 System Call을 구현해야 한다.

 

System Call을 구현하려면

lib/user/syscall.c

userprog/syscall.c userprog/syscall.h userprog/process.h, userporg/process.h

threads/thread.c, threads/thread.h

를 수정해야한다. 특히 userprog/syscall.c에 있는 syscall_handler System Call을 총괄하는 역할을 하므로 매우 중요하다. System call Function 자체는 새롭게 추가해야하지만 그 구현은 process.h process.c thread.h thread.c 등을 사용하므로 프로그램이 엉키지 않게 주의하자.

얘가 syscall이다. syscall1은 매개변수가 하나이다. push 명령을 통해서 필요한 매개변수 등을 stack에 집어넣은다음에 int $0x30을 통해서 Systemcall Handler를 호출한다.(난 그렇게 이해함) add1 $8이 무슨 의미일지 잘 생각하면 좋을 듯

System Call은 다음과 같이 stack을 이용한다. 그 후 INT $0x30을 통해 System Call Handler를 호출한다. 그 호출된 Handler userprog/syscall.c에 있다.  

바로 얘가 userprog/syscall.c에 있는 handler이다.

System Call은 User Program이 곧바로 Kernel 영역의 중요한 프로그램을 실행시킬 수 없기 때문에 Interface 역할을 한다. 그니까 과정을 보면 

UserProgram이 Write나 Read 같은 Systemcall을 호출하면 저기에 define 되어있는 syscall을 통해서 해당 sytemcall의 정보를 push 하고 $0x30 Interrupt를 발생시킨다. 그러면 userprog/syscall.c에 있는 systemcall handler가 발동되고 systemcall Handler에서 해당 Syscall을 적절히 처리한다. 하지만 현재는 Syscall이 아무것도 구현이 되어있지 않으므로 우리가 다 구현해야 한다. User Program은 이러한 System Call을 활용해서 많은 Kernel 프로그램을 실행하고 그 결과값을 받을 수 있다.

 

lib/syscall-nr.h에 위처럼 여러 SYS가 enum에 묶여있다.

 

lib/user/syscall.c에 각 syscall 들이 매개변수의 개수별로 정의되어 있다.

 

그리고 이런 식으로 syscall을 활용하고 있다. 즉 user program이 exec를 호출하면 바로 Kernel의 system call이 발동되는 게 아니라 lib/user/syscall.c에 있는 exec()가 호출되고 이 함수는 define 되어 있는 syscall1을 부른다. 그 syscall1은 위에서 #define syscall1로 정의되어 있다. 그 #define syscall1을 잘 보면 int $0x30을 호출한다. 이 명령어를 통해 userprog/syscall.c에 있는 (kernel) syscall handler를 호출하고 esp를 통해서 내가 어떤 system call을 썻는지와 그 system call을 실행하는 데 필요한 매개변수를 전달한다. f->esp가 명령어가 있는 주소를 가리킨다고 생각하면 된다. 가령 echo x를 했다면 현재 f->esp에는 'echo'가 있을 것이다. 

int $0x30을 통해서 호출되는 syscall_handler이다. 

그러면 syscall handler를 통해서 위와 같은 함수가 호출되어서 syscall을 처리하게 된다. 

 

1. 그러니 우선 lib/syscall-nr.h 에 가서 enum{ }에 적절한 System call을 확인하고 lib/user/syscall.h와 lib/user/syscall.c, 그리고 userprog/syscall.c를 통해 system call의 뼈대를 만들어야한다. 이 작업까지는 굉장히 수월하고 신나고 내가 시스템 프로그래밍에 재능있나 싶다. 아주 즐겁게 끝낼 수 있을 것이다. 문제는... 이 system call 각각을 구현하는 게 좀 에바쎄바다. 아마 대부분의 시간을 이걸 구현하는 데 쓸 것이다. 

명세서에 적힌 대로 shutdown_power_off 함수를 실행한다 .

 

1) 현재 동작중인 thread를 받는다.

2)  thread의 이름 부분만 추출해서 이름과 입력 받은 status를 출력하고 그 status를 현재 thread에 추가한다.

3) thread_exit()을 호출한다.(thread_exit() 은 thread/thread.c에 있다.) 

 

1) 입력받은 cmd_lines에서 명령어 부분만 추출한다.  
2) 그 file이 open된 file인지 확인하고 아니라면 return -1을 실행한다.  (open 되지도 않았는데 어떻게 실행하겠는가??)
3) process_execute()를 매개변수 cmd_lines에 대해 실행한다.  

 

4) thread_create를 통해 tid를 얻고 그 tide return 한다. 이 값이 process_execute()를 호출한 exec return 값이기도 하다.

1) 입력받은 pid를 이용해서 process_wait((tid_t)pid) 함수를 호출한다.

 

2) 현재 thread child_thread list를 돌면서 입력받은 child_tid와 같은 tid를 지닌 thread가 있는지 검사한다. 만약 있으면 sema_down을 해서 Parent Process가 죽지않도록 한다. 자식 process의 작업이 다 끝나면 현재 process도 죽을 수 있다. exit_status에 현재 thread child_exit_status를 삽입하고 그 thread list에서 제거한다. 그 후 sema_up을 한다. process_exit에서 sema_up sema_down을 하기 때문이다. 그리고 exit status return 한다.

 

2-1) 만약 tid가 일치하는 thread가 없으면 -1 return 한다.

 

이번 프로젝트의 read용 fd는 STDIN이다.

 

이번 프로젝트의 write용 fd는 STDOUT이다.

 

Synchronization

OS를 배웠다면 알겠지만 부모 Process는 fork()를 통해 Child Process를 생성한다.(COW와 관련...File Management에서 배울 듯, 지금은 상관없다.) 이번 Project에 그 Child Process가 발생한다. -> Synchronization 문제를 반드시 해결해야한다!!!

Thread 구조체에 Child_thread와 관련된 구조체와 동기화를 위한 semaphore 구조체 등을 추가로 삽입하고 thread 구조체가 바뀌었으므로 init_thread 함수도 수정한다.  

 

#ifdef ~ #endif 까지가 이번 프로젝트에서 필요한 구조체들이다. 
sema_init()은 synch.c에 있다. 어떤 함수인지 확인해야 한다. 그래야 PintOS가 돌아가는 과정이 머리속에 그려진다.  아무튼 thread.h/thread 구조체에 만든 list 및 struct sema 등을 초기화한다. 이 init_thread는 thread가 만들어질 때 발동되는 함수다. 

 

Process가 만들어지는 과정은 다음과 같다. 어떤 Job을 요청받으면 thread/init.c에 있는 run_task() 의 process_wait(processs_execute(task))가 호출된다. 그러면 userprog/process.c 에 있는 proess_execute 가 호출되면고thread_create()가 호출된다. 이 함수를 통해 thread가 만들어진 뒤 thread/thread.c에 있는 init_thread() 함수를 통해 해당 thread가 초기화 된다. 위에 첨부한 사진이 init_thread() 함수이고 이 init_thread()에서 struct seam 등을 초기화한다.

 

Parent Process Child Process가 죽을 때까지 죽으면 안된다. Child Process가 끝난 후 그 뒷정리를 해야하기 때문이다. 이를 위해 thread 구조체에 semaphore를 추가하고 process_wait 함수를 수정해서 Parent Process Child Process가 성공적으로 동기화할 수 있도록 해야한다.

-