거북이의 쉼터

(2021.10.01) Alarm Clock 가이드라인 본문

코딩 삽질/KAIST PINTOS (CS330)

(2021.10.01) Alarm Clock 가이드라인

onlim 2021. 10. 1. 14:03

정말 오랫동안 블로그에 들리지 않았다. 주 요인은 귀찮음이 첫 번째요, OS 공부가 두 번째였다. 사실 거진 2년간 OS 공부를 손에서 놓고 있던 터라 매뉴얼을 읽어도 연관된 개념들이 가물가물했다. 이대로는 코딩 자체를 독자적으로 못하겠단 생각이 들어서 OS 책을 사서 다시 공부를 시작했다. 공부하다 보니 마침 9월도 끝나고, 내 전역도 확실시되었기에 이제 좀 뭐라도 다시 시작해야 하지 않을까 하는 생각이 들어 다시 글을 쓴다. 

 

우선 Pintos에 익숙하지 않은 사람들을 위해 짤막하게 설명을 하자면, Pintos에서는 모든 프로그램들이 1 Process - 1 Thread 라는 기본 전제가 있으며, 그래서 실질적으로 Process와 Thread를 동일시하는 것을 코드 곳곳에서 볼 수 있다. 그래서 후술 할 내용도 Process와 Thread에 대한 개념적 사실이 다소 섞여 있음을 감안해주길 바란다. 

 

Pintos의 가장 첫 과제는 Alarm Clock을 구현하는 것이다. 설명을 보면 timer_sleep() 함수가 busy wait을 하고 있으니 이를 고치는 것이 목적인 것 같다. 이를 이해하기 위해서는 Pintos의 기본 구현 상태에서 Thread들이 어떻게 관리되는지를 알아야 한다. 기본적인 구현에서 Pintos의 Thread는 생성된 이후, 소멸되기 이전에 두 가지 상태를 오간다. 하나는 CPU를 점유하고 있는 Running 상태, 또 하나는 언제든지 scheduler가 선택하면 실행될 수 있는 Ready 상태이다. Ready 상태에 있는 Thread들은 Ready Queue라는 자료구조에 넣어져서 관리된다. 문제는 이 두 가지 상태만으로는 CPU 효율성에 문제가 생긴다는 것이다.

 

매우 극단적인 경우이긴 하지만 예를 들어 보자. 존재하는 Thread는 CPU에서 실행되는 Thread 하나뿐이고, 이 Thread를 A라고 하자. 현재 CPU를 점유하는 A가 몇 초 후 다시 동작할 필요가 있다고 하자. 그러면 A는 지금은 CPU를 잡고 있을 필요가 없으므로, CPU를 yield (양보)한다. 현재 구현된 바에 의하면 A는 완전히 실행이 끝난 것이 아니니 소멸되지 않고, CPU를 점유하고 있는 상태도 아니므로 ready 상태로 돌아오게 된다. 문제는 이렇게 되면 CPU가 놀고 있다고 판단한 운영체제가 언제든지 실행될 수 있는 Thread들의 집합인 Ready Queue에 있는 Thread 중 하나를 선택해 CPU로 올린다는 것이다. 이 상황에서는 Ready Queue에 A밖에 없기 때문에 A가 다시 선택된다. A는 CPU를 다시 점유한 뒤, 시간이 아직 충분히 지나지 않았다는 사실을 깨닫고, 다시 CPU를 양보한다.

 

이 과정이 반복되면서, 아무런 실질적 작업이 이루어지지 않는데도 CPU는 긴 시간 동안 점유되어 있는 기이한 현상이 발생한다. 이를 busy wait이라고 하며, 결국 첫 번째 과제는 Thread가 얼마간 CPU 점유를 하지 않겠다고 하는 상황에서 이 busy wait 현상을 어떻게 해결하는지를 물어보는 것이다. 다행히 OS 업계의 선배들은 나보다 훨씬 똑똑한 덕분에 이 문제를 아주 간단히 해결하였고, 교제에도 그 내용이 나와있다. 방법은 제3의 상태를 만들고, timer_sleep을 호출한 Thread를 그 상태로 만들어 관리하는 것이다. <운영체제 : 아주 쉬운 세 가지 이야기> (후술 OSTEP)에서는 Process의 상태를 단순화하면 다음 세 상태 중 하나에 존재할 수 있다고 설명한다.

 

  • 실행 (Running) : 실행 상태에서 프로세스는 프로세서에서 실행 중이다. 즉 프로세스는 명령어를 실행하고 있다.
  • 준비 (Ready) : 준비 상태에서는 프로세스는 실행할 준비가 되어 있지만 운영체제가 다른 프로세스를 실행하고 있는 등의 이유로 대기 중이다.
  • 대기 (Blocked) : 프로세스가 다른 사건을 기다리는 동안 프로세스의 수행을 중단시키는 연산이다. 흔한 예 : 프로세스가 디스크에 의한 입출력 요청을 하였을 때 프로세스는 입출력이 완료될 때까지 대기 상태가 되고, 다른 프로세스가 실행 상태로 될 수 있다.

결론적으로 timer_sleep()이 불린 Thread를 Blocked 상태로 만든 뒤, 일정 시간이 지났다고 판단이 되면 다시 Ready 상태로 만들어 주면 된다. 이렇게 하면 Blocked된 Thread가 Ready Queue에 들어가지 않아 시간이 다 지나지 않았을 때는 scheduler에 의해 선택될 수 없기 때문에 busy wait 현상이 발생하지 않기 때문이다. 여기까지 계획이 세워졌으면, 이 시점에서 해결해야 하는 문제들은 다음과 같다.

 

  • Thread를 어떻게 Blocked 상태로 만들고 (Block) Blocked 상태에서 어떻게 해제 (Unblock) 할 수 있는가?
  • Thread를 Unblock하는 과정은 어떤 시점에서 어떻게 실행되어야 하는가?

첫 번째 질문에 대해서 친절한 Pintos는 다 계획이 있다. 이미 Thread를 Block 하는 thread_block() 함수와 Unblock 하는 thread_unblock() 함수가 구현이 되어 있다. 수정할 필요가 있을지도 모르지만, 일단 이를 기반으로 과제를 수행하면 될 것이다.

void
thread_block (void) {
	ASSERT (!intr_context ());
	ASSERT (intr_get_level () == INTR_OFF);
	thread_current ()->status = THREAD_BLOCKED;
	schedule ();
}

void
thread_unblock (struct thread *t) {
	enum intr_level old_level;

	ASSERT (is_thread (t));

	old_level = intr_disable ();
	ASSERT (t->status == THREAD_BLOCKED);
	list_push_back (&ready_list, &t->elem);
	t->status = THREAD_READY;
	intr_set_level (old_level);
}

두 번째 질문은 꽤나 중요하다. 위의 예시를 다시 생각해보면 timer_sleep() 함수를 thread_block()으로 제대로 구현했을 때, timer_sleep()이 호출된 이후 CPU는 아무것도 하지 않는 상태, 즉 idle 상태로 돌입하게 된다. 다른 Process가 CPU에서 실행되어 Block 된 Thread를 Unblock 하도록 지시하지 않는다면 이 Thread는 영원히 Block 되어 있을 것이고, 영면할 것이다. 

 

여기서 다행인 점은 운영체제 또한 하나의 Process이기에, 운영체제가 일정 시간이 지난 뒤에 해당 Thread가 Unblock되도록 지시를 내리면 된다는 것이다. 다만 운영체제가 이를 위해 너무 많은 시간을 CPU에서 잡아먹고 있으면, busy wait와 비교해서 더 나은 점이 없기에, 우리의 현명한 OS 선배들은 운영체제가 일정 시간마다 CPU를 점유할 수 있도록 하는 방법을 개발해냈다. 그것이 바로 타이머 인터럽트(timer interrupt)이다. 

 

수 밀리 초마다 인터럽트라 불리는 하드웨어 신호가 발생하도록 프로그래밍하고, 인터럽트가 발생했을 때 이를 처리하도록 운영체제를 설계한다면, 인터럽트 신호가 발생할 경우 운영체제는 CPU를 점유해 일정 작업을 진행할 수 있다. 따라서 이 특성을 이용해 타이머 인터럽트를 처리하는 타이머 인터럽트 핸들러(timer interrupt handler)에 일정 시간이 경과했는지 판단하여 Thread를 Unblock 시키는 기능을 추가한다면 원하는 시간에 Thread를 깨울 수 있게 된다. 설령 CPU가 idle상태가 아니더라도 인터럽트가 발생하면 운영체제는 현재 수행 중인 Process를 중단시키고 인터럽트 핸들러를 실행하기 때문에 CPU 제어권을 얻어 해당 작업을 수행할 수 있게 된다.

 

따라서 두 번재 질문에 대해서는 타이머 인터럽트 핸들러에 thread_unblock()과 관련된 루틴을 추가하는 방식으로 답을 할 수 있으며, 해당 코드는 devices/timer.c에서 찾을 수 있었다.

/* Timer interrupt handler. */
static void
timer_interrupt (struct intr_frame *args UNUSED) {
	ticks++;
	thread_tick ();
}

결론적으로 Alarm Clock을 위해 해야 할 일을 나열하면 크게 다음과 같다.

 

  • timer_sleep()은 thread_block()을 활용해 수정한다.
  • timer_interrupt()에서 thread_unblock()을 활용해 시간이 경과한 Thread를 Unblock 시킨다.

이제 여기까지 개괄적인 가이드라인을 정했으니 다음 포스팅부터 본격적으로 코딩을 시작할 것이다.

Comments