pintos에서 Threads 스케줄러를 구현해보자 (alarm clock)

Jifrozen·2024년 9월 30일

정글

목록 보기
1/4

핀토스 기간이 찾아왔다.
첫 번째 핀토스 과제는 다중 스레드 환경에서 Trheads 스케줄러를 구현해야하는 과제였다.

Alarm clock

Alarm : 호출한 프로세스를 정해진 시간 후에 다시 시작하는 커널 내부 함수
기본적으로 운영체제에서 스레드를 관리하기 위해 선점방식과 비선점 방식의 스케줄러를 사용할 수 있다.
또 이를 위해 timer라는 하드웨어 장치를 가져 시분할 스케줄링을 제공한다.
시분할 스케줄링이란 작업들이 CPU를 사용하고 있는 시간을 주기적으로 확인하고, 주어진 시간동안 실행되었는지 확인한다. 그리고 타이머 인터럽트가 발생하면 현재 실행중인 프로세스를 중단시키고, 스케줄러가 실행된다. 이러면 마치 동시에 여러 작업을 수행하는 것 처럼 보인다.
pintos의 경우 timer를 사용하여 일정 시간마다 타이머 인터럽트를 발생시켜 선점 방식의 스레드 스케줄러를 제공한다.
우리가 수정해야하는 부분은 다음과 같다.
현재 pintos 타이머 기능은 Busy waiting으로 구현되어 있는데 sleep/wake up 방식을 이용해서 다시 구현한다.
Busy waiting이란 Thread가 CPU를 점유하면서 대기하고 있는 상태를 의미한다.
스레드가 대기하는 동안 CPU를 반납하여 다른 작업을 수행하는게 효율적이지만 현재는 CPU를 붙잡고 대기하고 있는 부분을 수정해야한다.

코드로 구현되어있는 스레드는 다음과 같은 주요 행동을 한다.

Thread

1. thread_init

멀티 스레드 환경에서 가장 처음 생성되는 스레드는 main 스레드이다. 메인 스레드의 경우 다른 피어 스레드가 생기기전에 cpu를 점유하고 있다.
우선순위의 경우 default로 31을 가진다.

2. thread_create

스레드를 생성합니다.
스레드가 생성하고 스레드 상태는 ready로 변경합니다.

3. thread_yield

cpu를 양보하는 함수입니다.
현재 진행중인 running 상태 스레드를 ready상태로 바꿉니다. 그리고 다시 schedule을 실행합니다.

4. schedule

스케줄러를 진행하는 코드입니다.
ready에서 가장 우선순위가 높은 스레를 가져와 running상태로 변경합니다.
thread_launch를 통해 컨텍스트 스위칭에서 발생하는 레지스터상에서 스레드의 데이터를 바꿔주는 과정이 일어납니다.

Timer

타이머에서 주요함수는 다음과 같다.

1. timer_sleep

ticks만큼 스레드 실행을 중단하는 함수이다.
tick은 타이머 인터럽트가 발생한 횟수이고
1초 동안 TIMER_FREQ(100개)만큼 인터럽트가 발생한다.

해당 함수의 경우 1시간마다 기온을 측정해야하는 스레드가 있다면
1시간동안 스레드 실행을 멈췄다가 1시간이 지나면 다시 깨어나 기온을 측정하는 스레드와 같은 경우 사용한다.

현재는 busy waiting으로 구현되어있다.

void timer_sleep(int64_t ticks)
{
	// 현재 시점 기록
	int64_t start = timer_ticks();

	// 현재 인터럽트 상태가 켜져있는지 검사
	ASSERT(intr_get_level() == INTR_ON);

	// 경과 시간이 원하는 타이머 틱(ticks) 수보다 작으면 계속 CPU를 양보
	// 현재 timer_ticks - start 틱보다 작으면 계속 중단

	while (timer_elapsed(start) < ticks)
	 	thread_yield();

}

while (timer_elapsed(start) < ticks) ticks가 해당 시점부터 ticks만큼 시간이 지나지 않으면 스레드를 양보한다.
readyList에 스레드가 들어가게되고 해당 스레드가 가장 우선순위가 크기 때문에 다시 while문을 돌게된다. 이를 busy_waiting이라고 한다.

해당 문제를 해결하기 위해 cpu를 계속 점유하면서 ticks를 계산하는것이 아니라
스레드를 잠들게하여 ticks만큼 cpu를 점유하지 못하게 한다.

이를 위해 while문을 삭제하였고 thread_sleep(start + ticks);을 구현하여 스레드를 blocked상태로 변경하고 sleep_list안에 넣어주었다. 그리고 일어나야하는 시간을 스레드 구조체 상에서 관리해야하기 때문에 wake_ticks 필드를 추가하여 ticks를 저장해주었다.
또 sleep_list에 잠자고있는 스레드를 넣어줄때는 wake시간이 적은순으로 insert해주었다.

void thread_sleep(int64_t ticks)
{
	struct thread *th = thread_current();
	th->wake_ticks = ticks; // ticks에 도달하면 깨우도록, 깨워야 하는 시점을 저장한다.

	enum intr_level old_level = intr_disable();							// 인터럽트 비활성화
	list_insert_ordered(&sleep_list, &th->elem, less_wake_ticks, NULL); // sleep list에 넣기 (ticks순 오름차순)
	thread_block();														// 현재 쓰레드를 waiter 리스트에 넣기
	intr_set_level(old_level);											// 인터럽트 활성화
}

다음으로 스레드를 깨우는 과정이 필요하다.

static void
timer_interrupt(struct intr_frame *args UNUSED)
{
	// 인터럽트를 실행했으니 틱 증가

	ticks++; // 시스템이 시작된 이후 경과한 타이머 틱 수를 증가시킴
	check_thread_tick(ticks);
	thread_tick(); // 스레드 관련 타이머 기능을 처리 // 스레드 틱도 증가시킴
}

타이머 인터럽트 함수는 말그대로 타이머 인터럽트를 발생시키는 함수이고 이는 8254 타이머라는 하드웨어를 통해 정기적으로 실행된다.

특정 ticks 시점에 자고있는 스레드들중에 깨워야하는 시간이 ticks와 일치한다면 스레드를 blocked 상태에서 ready상태로 변경해야한다.

이를 위해 thread에 check_thread_tick을 추가하였디.

void check_thread_tick(int64_t ticks)
{
	struct list_elem *e;
	struct thread *t;

	while (!list_empty(&sleep_list) && list_entry(list_front(&sleep_list), struct thread, elem)->wake_ticks <= ticks)
	{
		struct thread *awake_thread = list_entry(list_pop_front(&sleep_list), struct thread, elem);

		thread_unblock(awake_thread);
	}
}

해당 함수는 지금까지 재운 스레드를 관리하고 있는 sleep_list에서 깨워야하는 ticks이 된 스레드의 상태를 unblock해주는 함수이다.

결과적으로 idle 스레드의 tick이 증가하였다.
여기서 idel이란 유휴 스레드로
다중 스레드 환경에서 더 이상 cpu에 작업할 스레드가 존재하지 않다면 idle 스레드를 작업하게 하여 CPU가 잠들지 못하게 하는 스레드이다.
따라서 idel 스레드가 실행되었다는 의미는 cpu가 다른 스레드의 작업들을 빨리 끝냈다는 의미이고 성능이 좋아졌다는 의미이다!

0개의 댓글