[sw정글 9주차] 세상에서 가장 친절한 OS 실행과정 (코드있이 이해하는 pintos 프로젝트2 )

Date:     Updated:

카테고리:

태그:

🚀시작전

핀토스는 나에게 꽤나 큰 프로그램이다. C언어로 이루어진 3만여줄의 코드를 처음 볼 땐 정신이 혼미했으나, 훌륭한 팀원들의 도움을 통해 그 가닥을 점점 잡아나갔다. pintos는 결국 OS이다. 컴퓨터의 부팅과정과 화면에 입출력처리 등 기본적인 작업들을 해주고 컴퓨터는 (우리의 선택에 의해)종료된다. 그렇다면 그 과정을 한번 자세히 살펴보도록 하겠다.

노잼주의!!!

🚀시작전2

다음은 프로젝트를 하며 정리했던 자료들과 계획표를 담은 노션, 그리고 공부하면서 모르고 지나쳤던 기술부채를 정리한 내용이다. 혹시 핀토스프로젝트 중에 있다면 참조하면 좋을듯 하다.

Project2 공부내용 <== WIL

Project2 기술부채(청산완료!!!) <== WIL


🚀시작

❗️❗️우리가 원하는 것

fork-once

fork-once라는 테스트가 있다. 우리는 핀토스에서 이 프로그램을 테스트하기를 원한다. 간단한 프로그램을 출력시키고, 실행시키고, 종료한다. 아주 간단한 테스트이다. 그럼 그 전체적인 그림을 부팅부터 살펴보도록 하겠다.

💻부팅

다음은 핀토스의 init.c 파일이다.

init.c-1

유저가 사용하게 될 .BSS영역을 깨끗하게 밀어주고 RAM에 프로그램을 올린다. 82번줄코드에서는 메인쓰레드를 생성해 주고, 91번줄 코드를 보면, userprogram인 경우 TSS를 init해주는 것을 볼 수 있다. 커널모드인지 유저모드인지에 대한 권한설정을 해주는 부분이다. 이어서 코드를 보도록 하겠다.

init.c-2

컴퓨터가 부팅이 완료된 모습이다. 부팅이 완료된 컴퓨터는 우리가 원하는 작업을 수행하고 컴퓨터를 종료시킨다. 이 작업들은 어떻게 만들어지게 될까? 간단히 말해서, 우리가 커맨드 라인으로 입력시킨 옵션들을 parsing하고 CGI 환경변수를 인자로 만들어서 전달 및 실행시킨다. 결국 핀토스는 우리가 원하는 작업들이 담긴 쓰레드들을 만들고, 순차적으로 실행시킨다. 그렇다면 쓰레드들이 어떻게 생성되는지 그 과정들을 알아보도록 하겠다.

🧑🏼‍💻실행

👍쓰레드 생성

이어서 계속 init.c코드를 다시 보도록 하겠다.

105번째줄 코드를 보면 thread_start() 함수가 있다. 82번줄 코드에서 만들었던 메인스레드가 idle_started를 세마다운하려 시도하며 스스로 block 된다. 이윽고 ready_list의 idle 스레드가 스케줄 된다.

idle은 어떤 작업을 할까?

idle작업

idle_thread 전역변수에 자신을 등록하고, 스스로 세마다운했던 메인스레드를 깨워준다. 그리고 이후 idle_thread는 ready_list에도 존재하지 않는 상태가 된다.

위의 그림은 thread.c에 있는 next_thread_to_run() 함수이다. ready_list가 empty가 된 상황에서야 집가나갔던 idle_thread가 돌아오게 된다. 현시점에서는, 이제 다시 메인쓰레드가 스케줄링 되어있는 상황이다. 그렇다면 이제 테스트를 돌릴 차례이다. 다시 init.c로 돌아가도록 하겠다.

👍테스트의 생성

init.c-2

run_actions() 에서는 action 구조체에 run_task라는 함수를 넣어준다. run_task는 다음과 같다.

run_task

userprog이면서 thread테스트가 아니므로, process_wait( 어쩌고 )으로 빠지게 된다. process_wait()은 어쩌고가 끝날 때 까지 기다리는 함수이다.(sema를 통해, 어쩌고가 끝날 때 까지wait을 하는 기능을 우리가 구현해야한다)

그렇다면 어쩌고작업에 대해 살펴보도록 하겠다. 어쩌고process_create_initd(task) 이다. 그리고 이 task는 우리가 그토록 원했던, fork-once 실행파일(의 이름)인 것이다.

process_create_initd

process_create_initd() 함수는 fork-once라는 이름의 쓰레드를 만들어준다. 이 쓰레드는 스케줄링 되는 순간 initd() 라는 함수를 실행시키는 일을 하게된다. initd함수는 다음과 같다.

initd

process_create_initd()에서 thread_create()에서 fn_copy로 인자를 넣어주었다. 이 인자가 initd함수의 f_name으로 들어오게 된다. 결국 initd에서 fork-once라는 인자를 받아process_exec(fork-once)로 프로그램을 실행시키는 것이다.

그리고 이 process_exec 함수는, fork-once라는 바이너리 파일을 찾아서 메모리에 올리고 실행시키는 역할을 하는 것이다. 마침내 우리의 테스트가 실행되는 것이다.

‘어쩌고’는 새로운 쓰레드를 만들고, 그 위에 우리가 원하는 fork-once라는 테스트케이스를 올려주는 작업인 것이다.

새로이 만들어진 fork-once라는 이름의 쓰레드는, 스케줄링이 되는 순간 본인의 업무를 다하게 된다. fork-once라는 테스트를 돌리게 된다. 그렇다면 fork-once라는 이름의 쓰레드가 생성되고, 스케줄된 이 시점에서는… 메인쓰레드가 fork-once가 끝날 때 까지 기다리고 있는 상황인 것이다. 그리고 fork-once라는 이름의 쓰레드는, fork-once.c 파일의 내용을 수행하고자 한다.

그렇다면, 위에서 말한 ‘어쩌고’가 전체 그림에서 어느 위치에 있는지 알아보도록 하겠다. 참고로 어쩌고process_create_initd(task)를 나타낸다.

big-picture

조금 이해가 되었는가? 위 그림에서 가장 오른쪽열의 파일실행이, fork-once를 실행하는 내용인 것이다.

👍테스트(fork)

fork-once

fork-once함수의 내부이다. 포크가 일어나고, wait을 한다. 어떤 일이 일어나는 것일까? 테스트를 계속 해 나가는 fork-once라는 이름의 스레드와, 이 스레드가 만들어 내는 child스레드, 두가지 스레드의 관점에서 이야기를 해 나가도록 하겠다.

테스트를 수행하고있는 fork-once라는 이름의 쓰레드는 fork를 만나면 child스레드를 생성한다. 생성된 child스레드는 본인의 pid를 fork-once스레드에게 전달해준다.pid는 0이 아닌값(여태까지 있어왔던 프로세스의 개수)을 리턴받게된다. 자식의 pid를 전달받은 fork-once 스레드는 본인의 일을 계속 해 나간다. fork를 하고나니 if 문을 만나게 된다. pid는 양수이므로 if 문 안으로 들어가게 된다. if문안으로 들어가니 잠시 wait을 하라고 한다. 누구를 wait해야하나? 라고 인자로 받은 pid를 보니, 자식놈을 기다려야 하는 것 같다. 잘 모르겠지만 fork-once는 이렇게 잠에 빠지게 된다….

웬걸? 잠에서 일어나니 자식(child 스레드)놈이, 81을 나한테 리턴하고 죽어있다. fork-once는 다시 묵묵히 일을 진행 해 나간다. wait을 한 결과 child로부터 받은 81을 stauts에 넣어준다. 그리고 14번째 라인의 msg(“Parent… %d”)을 출력하고 프로그램을 종료한다

이번에는 child입장에서 다시 보도록 하겠다.

fork-once

child스레드는 fork함수로 태어나게 된다. fork함수로 태어나지만 아직 일은 못하는 상황인 것이다. 무슨일을 해야하는고… 하니, 12번 라인에서 fork(“child”)가 불린 시점 이후로의 일을 하면 된다. 아무런 일을 하지 못하던 child 스레드는, 엄마 스레드인 fork-once가 wait을 통해 잠이들면서 기회가 생기게 된다. 엄마가 자식을 wait하는 동안 child 스레드는 힘을 내서 본인의 일을 하러 간다.

본인의 일은 if (pid = )에서부터 분기를 하는것이다. 분기를 하려고 본인의 rax를 확인해본다. 하지만 rax 레지스터에는 웬걸… 0이라는 숫자가 적혀있는것이다.(이 숫자는 자식스레드를 fork할 때 미리 넣어준 것임) 0번 pid라는 충격적인 숫자를 본 child는 자신이 child스레드라는 사실을 알게된다. 따라서 fork의 리턴값이 0인 child스레드는 else문으로 향하게 된다. else문으로 간 child는 16번줄의 msg(“child run”)과, exit(81)을 수행한다.

그리고 이 81은 시스템콜 핸들러를 통해 나를 기다리고 있는 부모님에게 전달된다. 이렇게 child는 81을 부모님에게 남긴채 죽게된다.

🍴fork

child는 왜 만들어졌지만, 부모님이 child를 wait하기 전까지는 일을 하지 못했을까? 다음 그림은 child_thread가 행하게 된 __do_fork 함수의 일부분 이다.

do_fork

fork함수는 본인이 해야할 일(fork-once.c의 fork된 이후시점의 코드 실행)을 하고싶지만 문앞에서 그 기회를 놓치게 된다. 330번줄 코드에서 do_iret()을 수행하기만 하면, 할일을 할 수 있었겠지만, sema_up()을 함으로써 부모스레드가 잠에서 일어나게 된다. 본인이 스스로 block되어가며, child스레드를 활성화시키고, child가 fork를 하라고 시켰지만, child보고 실행하라고 한건 아닌 것이다.

process_fork

위의 그림은, child가 본인을 fork할 수 있도록 부모스레드가 스스로 세마다운하며 잠에 드는 그림이다. 하지만 child보고 실행하라는 말은 하지 않았다. 직전에 보았듯이 child는 실행되기 직전에 멈추게 되고, 부모스레드가 wait으로 child가 실행될 수 있도록 잠시 blocked되는 순간이 되어서야 child는 do_iret함수를 마저 실행하며 본인의 작업을 수행하게되는 것이다.

__do_fork(추가사항)

__do_fork까지 오기 직전과정에서의 오개념이 있었어서 이번 과제에서 애를 먹었었다. 자식이 부모의 정보를 fork해올 때, 부모스레드가 넘겨준 본인스레드의 inturrupt_frame(이하 if_)를 받아오고자 하였는데, 이는 잘못된 행위였다. 부모가 fork를 수행하고자, 본인이 잠에들고 자식이 본인을 fork할 수 있도록 잠드는 그 시점!! fork 시스템콜을 호출하는 그 시점의, 부모의 if_를 복사하고자 했었다. 하지만 자식이 fork한 부모의 if_는 엉뚱한 값을 들고 있었다.

sungtae_fork

위 그림은, 헬스를 열심히 하는 동료 성태 블로그의 일부분이다. 나도 동일한 시행착오를 겪었었다. 그를 위해서는 do_iret()에 대해 좀더 알 필요가 있다.

user-kernel

유저영역에서 커널영역으로 가서 작업을 수행할 때, 어떻게 다시 유저영역으로 돌아올 수 있을까? 유저영역으로 돌아갈 if_를 알고 있어야 한다. 그렇다면 커널영역역에서 작업하던 도중, (context_switching이 일어나서)다른 스레드에게 주도권을 빼앗기면 어떻게 될까?

추후, 본인이 다시 일을 할 기회를 얻었을 때, 어디서부터 시작하면 되는지에 대한 커널에서 작업하던 부분을 기록할 if가 또 필요한 것이다. 이처럼 context_switching을 위한 if가 필요한 것이고, 본인의 스레드 내에서 유저↔️커널을 위한 if_도 필요한 것이다.

그리고 내가 했던 시행착오의 결정적인 실수를 떠올려 보겠다. 부모가 시스템콜로 fork를 수행하는 시점의 if*가 thread->if* 에 업데이트 되어 있을 것이라고 착각한 것이다. 뭐.. cpu야 pc를 넘겨가며 신나게 작업을 하겠지만, ‘그 정보’가 스레드에 기록되지 않고 있는것은 당연한 것이다. 당연히 될것이라고 생각하고 코드작성을 했던것이 나의 결정적인 실수였다.

sungtae_fork2

따라서 성태의 블로그에서 오개념이였음을 정확히 확인할 수 있었다. 시스템콜이 호출된, 해당 시점의 if를 따로 저장해둠으로써 문제를 해결할 수 있었다. 그 시점의 if를 기준으로 __do_fork를 수행할 수 있는 것이다. 작업이 어느 시점에서 수행되는지에 대한 명확한 논리를 꼼꼼히 따졌어야 했던 것이다.

💻테스트종료

무튼, 다시 pintos의 큰 흐름으로 돌아와서, 테스트가 종료된다. pintos를 하는 많은 분들이 알듯, 테스트코드가 콘솔에 쏟아낸 텍스트들과 .ck파일의 내용을 비교하여 테스트의 pass/fail여부를 결정짓게 된다. 이부분은 간단한 로직이며, 우리가 따로 구현해야 할 부분이 아니므로 가볍게 넘어가겠다.

pintos-big-picture

pintos는 이처럼 부팅이 되고, 스레드를 셋팅하고, 테스트들을 수행하고 종료된다. 스레드 관점에서만 최대한 간단하게 설명하고자 하였으며, 기타 다른부분(PML4를 셋팅하는 부분, page_table 복사, sema, wait_list 등) 설명하면 끝도 없는 부분들이 많다. (반대로, 필자도 모든부분을 알지는 못한 상태이다) 하지만 결국 컴퓨터를 켜서 일련의 작업들을 수행하고 컴퓨터는 종료된다. 이러한 맥락에서, 스레드 관점에서 설명한 위의 그림이 핀토스를 이해하는데 도움이 되면 좋겠다.

😃회고

image

이번 프로젝트도 정말 재미있게 했던 것 같다. USB허브를 이용하여, 한개의 컴퓨터에 3명분의 키보드 마우스를 연결하였다. 류석영 교수님이 알려주셨던 pair programming을 실천 해 본 것이다. 한명의 엄청난 시니어가 있어서 그로부터 순식간에 코딩실력이 는다거나… 그런일은 발생하지 않았다. 그럼에도 불구하고 3명중 1명은 운전대를 잡고 코딩을 진행한다.

내가 코드를 짜야하는 순간이 오거나, 새로운 아이디어가 있어서 2명이 보는 와중 코딩을 진행하는 경험은 정말 좋았던 것 같다. 양쪽에서 들리는 소리들과, 내가 생각하던 아이디어를 합쳐가며 아이디어를 코드로 (팀원들이 기다리지 않게)빠르게 옮겨야 했으며, 최고의 집중력을 유지해야 했다. 여기서 말하는 최고의 집중력이란, 논리적으로 틀리지 않는 상태를 말한다.

결과적으로 말하면, 2.5주를 팀원들과 즐겁게 코딩을 했다는 점이 가장 큰 장점인 것 같다. 3명이서 바라보니… 논리적 결함이 줄어들고… 순간적인 집중력이.. 새로운 아이디어에 대한 토론 어쩌고… 등은 모두 부수적인 요인이였다. 가장 좋았던것은 즐겁게 프로젝트를 순항시켰고 의미있는 결과까지 뽑아내었다는 것이다. 그 어떤 자료들도 참고하지 않고, 추측과 실험, 토론으로 코드를 완성시켜나갔다. 엄청난 양의 코드로 느껴졌지만 challenging한 미션을 이토록 즐겁게 수행해 나갈 수 있었다는 점에서….


Pair Programming? 합격! 또 다시 도전해 볼 의향이 있다.

image



😵배우면서 깨달은 내용을 정리해 보았습니다. 틀린 것 같은 개념을 아래 댓글에 달아주시면 감사합니다😵

🌜 Thank you for reading it. Please leave your comments below😄

맨 위로 이동하기

swjungle survive 카테고리 내 다른 글 보러가기

댓글 남기기