pintos
이번 주 과제는 운영체제 만들기였고, 나는 곧바로 이런 의문을 품었다.
내가 운영체제를 왜 만들어야 하는가?
운영체제는 CPU 자원을 관리하고 나눠주면서 사용자가 원하는 일들을 스레드라는 태스크로 만들어서 차례대로 처리해주는 역할을 한다. 컴퓨터를 사면 그 안에 운영체제가 들어있고, 컴퓨터를 부팅한다는 것은 곧 운영체제에 시동을 거는 것이다. 사용자가 원하는 일을 얼마나 매끄럽게, 지연 없이 처리해주는지가 곧 운영체제의 성능이 된다.
수십년 전 이미 만들어진 window, mac 등의 좋은 운영체제 위에서 문서작성도 하고, 영상도 보고, 프로그래밍도 하고 있는 나는 좋은 성능의 운영체제의 이점을 이미 충실히 누리고 있다. '바퀴를 재발명하지 말라'는데, 왜 나는 운영체제를 내 손으로 만들어야 할까? 그것도 요즘 시대에 맞지 않는 싱글 코어 방식으로? (요즘은 CPU 를 여러개 동시에 돌릴 수 있는 멀티 코어 기술이 활용되고 있다)
일단은 과제를 시작했고, 사용자가 시킨 태스크가 스레드로 만들어져 스케줄링되는 과정을 핸들링하면서 이 과제의 의미를 서서히 이해할 수 있었다. 컴퓨터의 자원을 가장 효율적으로 관리하기 위해 정교하게 설계하는 훈련을 통해, 고수준 언어로 프로그램을 설계할 때도 편리함에 기대지 않고 효율성을 극대화할 수 있게 되는 것이다.
운영체제는 CPU 자원을 관리하고, CPU 위에서 돌아가며 일처리를 하는 스레드를 관리한다. 생성된 스레드는 ready_list 혹은 run queue 라고 불리우는 대기열에 들어가서 자기 차례를 기다린다. 스레드가 대기열에 들어가 차례대로 실행되는 것을 스레드 스케줄링이라고 하며, 스케줄링 방법은 busy waiting과 sleep-wake 방식으로 나뉜다.
busy waiting 방식에서 스케줄링을 위해 사용되는 자료구조는 ready_list 하나뿐이다. CPU에서 실행을 끝낸 스레드는 다시 ready_list의 맨 뒤로 삽입되고, 완전히 종료되거나 혹은 대기열에서 순서를 기다리다가 다시 CPU로 올라간다. 이와 달리 sleep-wake에서는 sleep_list라는 자료가 하나 더 등장한다. OS가 부팅될 때 register 에 타이머 인터럽트 함수가 등록되고 이 함수는 일정간격으로 호출된다. 이 간격을 tick이라고 부른다. tick은 CPU 에서 시간을 세는 단위인데, 인터럽트 함수가 CPU tick에 맞추어서 호출되며 프로그램에 전역변수로 설정되어있는 tick을 1씩 올려주면서 타이머 기능을 해주는 것이다.
타이머 인터럽트 함수의 또다른 역할은 바로 sleep_list 안에 있는 스레드 중에서 깨워야할 스레드가 있는지 확인하고 깨우는 것이다. 현재 tick 기준으로 깨워야할 스레드가 있을 때에만 sleep_list에서 해당 스레드를 깨워 ready_list에 스케줄링 해준다. busy waiting 방식에서는 매 tick마다 (혹은 그보다 적은 사이클마다) 조건을 확인하고 CPU 상의 스레드와 ready_list의 후보 스레드를 교체해주는 동작을 반복하기 때문에 전력 소모가 심한데, sleep-wait은 일정 시간에 도달하기 전까지는 새로 스케줄링을 하지 않기 때문에 상대적으로 부하가 적은 방식이다.
스레드는 우선순위를 갖는다. 더 높은 우선순위의 스레드가 먼저 처리되어야 하기에, ready_list는 우선순위대로 정렬되어야 한다. 우선순위만을 가지고 스케줄링될 때는 문제가 복잡하지 않지만, 스레드들이 하나의 자원을 동시에 필요로 할 때는 일이 복잡해진다.
만약 우선순위가 높은 스레드가 CPU 위에 올라가서 수행을 시작했지만 이전에 수행되던 스레드가 공유 자원을 내놓지 않는다면 이 스레드는 일을 시작할 수 없기에 공유자원 대기리스트에 들어가서 대기한다. 자원을 사용한다는 것은 atomic 해서 중간에 끊을 수 없으므로, 이전 스레드는 잠시 높은 우선순위로 변경된 후 자신이 하던 일을 모두 완료하고 나서 공유자원을 내놓는다. 드디어 자원이 비면, 자원 대기리스트에 있던 스레드 중 가장 높은 우선순위의 스레드가 해당 자원을 차지한다.
우선순위 교체와 자원 양보가 적절하게 이루어지지 않으면 우선순위가 높은 스레드임에도 영원히 자원 권한을 얻지 못해 CPU 와 ready_list를 왕복하는 CPU 낭비가 발생할 수 있다. 이를 priority inversion이라고 하며, 이러한 현상을 방지하기 위해 실행중이던 스레드의 우선순위를 임시로 높여서 무사히 자원이용을 끝낼 수 있게 해주는 priority donation을 설계해주어야 한다.
Photo by Francesco Vantini on Unsplash
NOT too much information
많은 반성을 하는 한 주였다. 혼자서 오래 고민하는 습관은 나를 여기까지 성장시켰지만, 주변 사람에게 도움을 요청해서 빠르게 더 나은 해결방법을 얻을 수도 있다는 것을 인식하기까지 시간이 걸렸고, 인식한 후에도 실제로 행동으로 옮기기는 어려웠다. 남이 아는 것을 나는 스스로 알아내지 못한다는 사실이 자존심이 상했다. 하지만 생각해보면 내가 아는 것, 책이나 검색으로 습득하는 것들은 결국 모두 타인의 도움으로 이루어지는 것들이다. 누군가 생산한 지식이기에 내가 배울 수 있는 것이다. 그러니 주변사람에게 도움을 청하는 것에 어떠한 불필요한 감정을 실어서 고민하지 말도록 하자. 그럴 시간에 어떻게 하면 더 정확하고 제대로 물어볼지, 얻어낸 답을 어떻게 내것으로 소화할 것인지에 대해서 고민하자.