-
[WEEK09~10] syscall wait()의 구현 - 구조 설계를 중점으로 (Pintos PROJECT2 : USER PROGRAMS)SW Jungle/TIL (Today I Learned) 2022. 11. 29. 16:15
복잡한 기능을 구현하려고 하면 막막하다는 느낌을 받을 때가 많다.
고려해야 할 요소들이 너무 많아서, 머릿속에서 마구 섞이기 때문이다.
무엇을 먼저 고려해야 구현이 쉬울까?
구현을 하는데는 다양한 방법이 있을 것이고, 각자 자기에게 맞는 방법이 있을 테니 나의 방식만이 옳다고 할 수는 없다.
하지만 아예 감조차 잡지 못하는 분들을 위해 조금이라도 도움이 되길 바라면서,
이번 Pintos Project 2 - User Programs를 구현할 때 내가 썼던 방식을 공유하려고 한다.
구현한 여러 System Call 중, wait() 기능을 구현할 때의 경험을 바탕으로 설명하겠다.
아래 이미지들은 SW정글에서 발표할 때 썼던 장표들이다.
wait 함수가 올바르게 작동하기 위해서는 위와 같은 조건들을 만족해야 한다.
이들을 맥락없이 한 번에 구현하려고 하면, 웬만한 고수가 아니고서는 머릿속이 복잡해지고 뭐 부터 시작해야 할지 감이 안 잡힐 것이다.
그럼 어떤 순서로 구현하는 것이 좋을까?
일단, 돌아가게 만드는 것이 중요하다.
돌아간다는 말은 문제가 해결될 수 있어야 한다는 뜻이다.
가장 단순한 방법을 써서라도 일단 돌아가면 좋은 코드이다.
그 다음 고려해야 할 것이 시간복잡도와 공간복잡도이다.
당연한 얘기일지도 모르지만, 빠른 시간 안에 해결될 수 있으면 좋은 코드이다.
애초에 우리가 컴퓨터에게 작업을 맡기는 이유가 빠르게 문제를 해결하기 위해서이지 않은가.
그리고 컴퓨터의 자원은 한정되어 있기 때문에 공간도 적게 차지할수록 좋다.
하지만 오늘날에는 메모리의 크기가 충분히 커졌기 때문에 공간을 약간 희생하더라도 시간복잡도를 낮추는 쪽으로 코딩하는 경향이 있는 것 같다.
마지막으로, 남들이 알아보기 쉽고 유지보수도 용이한 클린 코드를 작성할 수 있으면 좋을 것이다.
클린 코드를 작성하는 능력이 날이 갈수록 중요해지고 있지만, 아무리 읽기 쉬운 코드라고 해도 문제를 해결하지 못하거나 시간복잡도가 높다면 좋은 코드라고 할 수 없다. 즉, 클린 코드는 우선순위가 높지 않으므로 제일 나중에 고려해도 되는 문제라는 것이다.
그럼 일단 문제가 해결될 수 있게 만들어야 하겠다.
아까 위에서 봤던 여러가지 문제들이 있었는데, 나는 먼저 그 문제들을 작은 단위로 쪼개서 하나씩 해결한다 (용어를 쓰자면 분할 정복법이다).
그 해결 과정에서 구조를 짜게 되는데 일단은 최대한 간단하게 짠다.
그러다가 더 이상 지금의 구조로는 해결되지 않는 문제를 만나면, 그때 구조를 바꾼다.
다음 장표에서 예시와 함께 보자.
wait 함수에서 가장 먼저 해야 할 일은 자식 프로세스의 종료 상태를 받는 것이다.
나는 종료 상태를 이미 thread 구조체의 exit_code로 관리하고 있었기 때문에, 구조의 변경이 필요하지 않다.
단지 자식의 종료 과정에서 부모에게 exit_code를 전달해주기만 하면 된다.
전달 방법에 대한 고민이 필요하지만, 일단은 자식의 exit_code가 생겼을 때 부모에게 알리면 된다는 가정만 하고 넘어가보자.
자식이 아직 살아있다면, 종료를 기다려야 한다.
가장 간단한 방법으로 해결해보자.
자식 프로세스의 exit_code에 초기값을 주고, 무한루프를 돌리며 값에 변화가 있는지를 확인하자.
나의 scheduling 방식에서는 timer interrupt에 의해 context switching이 주기적으로 일어나기 때문에, 부모가 busy wait를 하더라도 자식 프로세스가 작업을 진행하고 종료에까지 다다를 수 있다.
exit_code 값에 변동이 생기면 자식이 종료된 것이므로 그때 busy wait loop를 종료하고 return을 해주면 된다.
다음 구현 과제로, 커널에 의해 종료되더라도 종료 상태를 리턴해야 한다.
NULL pointer 처럼 유효하지 않은 포인터 참조가 발생하거나, 포인터가 kernel 영역을 참조하려고 할 때 커널이 -1 코드로 프로세스를 종료시키는 기능은 구현해 둔 상태다.
커널이 종료시키는 자식 프로세스의 exit_code에 -1을 넣어줄 것이므로, exit_code를 부모에게 전달하는 데에 문제가 없다.
아직까지는 큰 구조변경 없이 구현이 가능할것만 같다.
그런데 이제 자식이 먼저 종료되어도 커널이 종료상태를 리턴할 수 있어야 한다고 한다.
지금까지는 자식 프로세스의 thread가 살아있을 수 있다는 가정 하에, thread 구조체 안에 exit_code를 넣어서 관리했었다.
그런데 만약 자식이 부모가 wait()를 호출하기 전에 종료되어 버린다면, 자식 thread는 page free가 되어버릴 것이기 때문에 부모가 wait()를 호출하는 시점에 exit_code를 보존할 수 없게 된다.
그러면 이제 구조를 바꿔야 할 때가 온 것이다.
핵심은, 자식이 종료되어도 종료 상태를 남기고 가야 한다는 것이다.
그러려면 자식 프로세스 바깥에서 exit_code를 관리해야 한다.
바깥 공간 중에 가장 적절한 공간을 고민해 봤을때, 어차피 exit_code를 알아야 하는 주체가 부모 프로세스이므로 부모 프로세스의 메모리 공간에서 할당을 해 주는게 좋다고 생각했다.
그리고 여기서 이어진 생각으로, 부모 프로세스가 여러 개의 자식 프로세스를 관리해야 할 수 있기 때문에, child_list를 만들어서 관리해야 겠다는 생각을 했다.
그리고 이 리스트의 원소로 각 chlid의 exit_code를 넣는 것이다.
하지만 exit_code만으로는 어떤 프로세스의 exit_code인지 알 수 없으므로, tid(핀토스에서는 프로세스 당 하나의 thread밖에 없으므로, pid를 tid로 대체할 수 있다)도 같이 넣어주기로 하자.
두 개 이상의 원소를 묶어서 리스트로 관리하려면 구조체를 사용하는 것이 용이하다. 그러므로 구조체 child를 만들기로 한다.
그러면 언제 구조체를 만들어 주어야 하는가?
자식이 언제 종료되어도 exit_code를 받을 수 있게끔 해야 하기 때문에, 자식이 생성되는 과정(fork 또는 initd 과정)에서 넣어주는 것이 좋다고 생각했다.
이제는 자식 프로세스의 page가 free 되어도 부모는 자식의 exit_code를 알 수 있게 되었다.
다음 구현 과제로, 즉시 -1을 리턴해야 하는 경우가 있다 (wait요청 자체가 잘못되었을 경우).
하나는 직속 자식이 아닌 pid를 기다리려고 하는 경우, 다른 하나는 같은 pid에 대해 중복으로 wait 호출을 하는 경우이다.
아까 만든 구조에 따르면, 부모가 자식을 생성할 때만 child_list에 넣기 때문에, 직속 자식이 아닌 경우 (손자 프로세스, 또는 아예 관련이 없는 프로세스)에는 child_list에 존재하지 않게 된다. 그러므로 child_list에 없을 때 -1을 리턴하면, 직속자식이 아닌 pid를 기다리는 일은 없게 된다. 그리고 child_list에서 자식을 찾았을 때 해당 원소를 remove해준다면, 중복된 wait 요청이 있을 때는 child_list에서 찾을 수 없게 되므로 이 상황에서도 -1을 리턴할 수 있게 된다.
논리와 구조를 만들었으니, 이제 코드로 옮길 차례다.
코드로 옮기는 것에는 왕도가 없는 것 같다. 많이 부딪혀보고 많이 경험해본 사람이 코드를 잘 짠다.
하지만 이렇게 구조를 먼저 짜고 코드로 옮기게 되면 확실히 그냥 코드부터 짜는 것에 비해 고민해야 할 지점이 반으로 줄게 된다.
다시 말해, 코드로 옮기는 작업에만 온전히 집중할 수 있게 되는 것이다.
이렇게 복잡한 구현이라도 분할해서 하면, 부분은 전체에 비해 덜 복잡하므로 보다 쉽게 구현할 수 있다.
한걸음 더 나아가 얘기해보자면, 코드의 완성도를 높이기 위한 몇 가지 고려사항들이 있다.
대부분의 케이스에서 돌아가는 코드더라도, 특수한 케이스에서 실패할 수 있기 때문에 가능한 모든 케이스를 고려해서 코딩하는 것이 좋다.
이런 고민은 프로그램의 흐름(flow)을 따라가며 해보면 좋다. 일단 대강의 코드를 작성해 놓고, 코드의 진행 순서를 line by line으로 보면서, 생길 수 있는 특이 케이스를 상상해 보길 바란다.
그리고 비효율적인 부분들, 예를 들면 여기서는 부모가 자식을 busy wait 하는 과정이 있을 수 있는데, 이런 부분을 시간 복잡도 등을 고려해서 적절히 처리해주는 것도 코드의 완성도를 높이는 방법이다.
'SW Jungle > TIL (Today I Learned)' 카테고리의 다른 글
[WEEK12] Lazy Loading (Pintos PROJECT3 : VIRTUAL MEMORY) (0) 2022.12.13 [WEEK11] 페이징은 왜 하는걸까 (Pintos PROJECT3 : VIRTUAL MEMORY) (0) 2022.12.06 [WEEK08] Pintos PROJECT1 : THREADS (0) 2022.11.17 [WEEK07] C언어에서 문자열 다루기. 근데 이제 포인터와 파싱(parsing)을 곁들인 (0) 2022.11.10 [WEEK06] Malloc Lab 설명서 번역 (CS:APP) (2) 2022.10.28