정글/Pintos

[pintos] 1주차 - Priority Scheduling 구현 과정

nkdev 2025. 5. 20. 21:51

alarm 테스트는 모두 pass했다!

이제 priority 테스트를 통과시켜보자.

구현 요구 사항

ready_list에 있는 스레드를 스케줄하여 running 상태로 만들 때 우선 순위에 맞게 스케줄링되도록 만들어야 한다. schedule()함수를 보면 항상 ready_list의 가장 앞에 있는 스레드가 실행되기 때문에 ready_list에 스레드를 넣을 때 마다 priority순서로 정렬되게 만들어야 한다.

그러면 저 세 가지 경로로 ready_list에 스레드를 넣는 부분을 변경해야 한다. alarm-clock에서 3번을 해결했으니 priority-scheduling에서 2번을 해결하면 된다!

1. 스케줄링하는 부분 이해하기

나는 alarm-clock에서 스레드를 wake할 때 sleep_list의 가장 앞에서부터 하나씩 꺼내서 레디 리스트에 넣었던 것처럼 priority-scheduling에서도 스레드를 schedule할 때 ready_list의 가장 앞에서부터 하나씩 꺼내서 cpu에 할당하고 있을 것이라고 예상하고 그 부분을 찾아봤다.

schedule()함수에서부터 쭉 따라가다 보니 next_thread_to_run()함수에 ready_list에서 다음에 스케줄될 스레드를 어떻게 꺼내는지 구현되어 있었다. schedule()함수 안에서 next_thread_to_run()으로 레디큐의 가장 앞에 있는 스레드를 꺼낸 후, thread_launch()의 인자로 스레드를 넘겨주어 컨텍스트 스위칭한다.

static struct thread *
next_thread_to_run (void) {
	if (list_empty (&ready_list))							//ready_list가 비어있다면
		return idle_thread;											//idle_thread를 반환
	else																			//레디큐가 비어있지 않다면
		return list_entry (list_pop_front (&ready_list), struct thread, elem); //ready_list의 가장 앞에 위치하는 스레드를 반환
}
static void
schedule (void) {
	struct thread *curr = running_thread ();
	struct thread *next = next_thread_to_run (); //레디큐의 가장 앞에 있는 스레드 가져오기

	ASSERT (intr_get_level () == INTR_OFF);
	ASSERT (curr->status != THREAD_RUNNING);
	ASSERT (is_thread (next));
	/* Mark us as running. */
	next->status = THREAD_RUNNING;

	/* Start new time slice. */
	thread_ticks = 0;

#ifdef USERPROG
	/* Activate the new address space. */
	process_activate (next);
#endif

	if (curr != next) {
		if (curr && curr->status == THREAD_DYING && curr != initial_thread) {
			ASSERT (curr != next);
			list_push_back (&destruction_req, &curr->elem);
		}
		
		thread_launch (next); //스레드를 next로 컨텍스트 스위치
	}
}

2. ready_list에 스레드를 삽입하는 부분 찾기

찾아봤는데 alarm-clock때 내가 구현한 thread_wake()함수 말고는 스레드를 ready_list에 삽입하는 부분이 없다. (thread_wake()내부에서 thread_unblock()을 할 때 ready_list에 스레드를 삽입한다.)

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

	ASSERT (is_thread (t));

	old_level = intr_disable ();													//인터럽트 비활성화
	ASSERT (t->status == THREAD_BLOCKED);											//스레드가 block 상태이면 이후 코드 실행
	list_insert_ordered(&ready_list, &t->elem, list_high_priority, NULL);			//ready_list에 우선순위가 높은 순서대로 넣기
	t->status = THREAD_READY;														//스레드를 THREAD_READY 상태로 바꿈
	if(!list_empty(&ready_list) && is_higher_priority(now_thread) && now_thread != idle_thread){							//ready_list가 비어있지 않고 현재 스레드의 우선순위가 ready_list의 맨 앞에 있는 스레드의 우선순위보다 더 높으면
		thread_yield();																//문맥 교환
	}
	intr_set_level (old_level);														//인터럽트 다시 활성화
}

 

3. priority-sema 테스트 pass하기

테스트 코드를 먼저 보자.

우선순위가 27, 26, …, 21, 30, 29, 28인 스레드 10개를 새로 생성했다.

  • 우선순위를 21부터 30까지 차례로 만들면 되지 굳이 27부터 만든 이유?
    • ready_list에 10개의 스레드들을 우선순위가 높은 순서대로 삽입하지 않았는데도 꺼낼 때 우선순위가 높은 스레드부터 나오는지 테스트하기 위함이다.
      • 엄밀히 말하면 ready_list에 넣을 때 우선순위가 높은 순서대로 insert되는지 확인하기 위함
    • 참고로 thread_create()는 새로운 스레드를 생성하고 나서 내부적으로 thread_unblock()을 호출하여 새롭게 생성된 스레드를 ready_list에 넣는다.
      • 이 부분을 공부하면서 깨달은 건데 thread_unblock()은 단지 sleep_list에서 자고 있는 스레드를 깨워서 ready_list로 옮기는 역할만 하는 게 아니다. 스레드를 ready_list에 삽입하고싶을 때 호출하는, 범용적으로 쓰이는 함수이다!
      • thread_create()에서도 새로운 스레드를 하나 생성하고 나서 thread_unblock()을 호출해서 생성한 스레드를 ready_list에 삽입한다.
      • sema_up()에서도 세마포어 변수를 1 증가시키기 전에 thread_unblock()을 호출해서 sema→waiters의 제일 앞에 위치한 스레드를 ready_list에 삽입한다.
    • 테스트 코드 실행 순서를 정리하면
      • 첫 번째 for문을 돌면서 thread_create()로 10개의 스레드를 만들면 그 스레드들은 내부 로직으로 sema_down()을 한다. 세마포어 변수가 0인 상태에서 sema_down()을 하면 block이 걸려서 스레드가 모두 sema→waiters로 들어간다.
      • 두 번째 for문을 돌면서 sema_up()으로 세마포어 변수를 1 증가시키고 sema→waiters에 기다리고 있는 10개의 스레드들 중 가장 앞에 있는 스레드를 unblock하여 ready_list에 올린다.
void
test_priority_sema (void) 
{
  int i;
  
  /* This test does not work with the MLFQS. */
  ASSERT (!thread_mlfqs);

  sema_init (&sema, 0);//세마포어 변수를 0으로 초기화
  thread_set_priority (PRI_MIN); //현재 스레드의 우선순위를 가장 낮은 값으로 초기화
  for (i = 0; i < 10; i++) 
    {
      int priority = PRI_DEFAULT - (i + 3) % 10 - 1; //우선순위를 30부터 21까지로 설정
      char name[16]; 
      snprintf (name, sizeof name, "priority %d", priority); //배열 name에 "priority %d"라는 데이터가 저장됨
      thread_create (name, priority, priority_sema_thread, NULL); //이름을 "priority %d"으로, 우선순위를 27~18로 설정, priority_sema_thread는 왜 넘겨주는 건지 모르겠음 아무튼 해당 우선순위를 가진 스레드를 생성함 
    }

  for (i = 0; i < 10; i++) 
    {
      sema_up (&sema); //sema->waiters에 스레드가 기다리고 있으면 하나 꺼내서 unblock 하고 세마포어 변수를 1 증가시킴
      msg ("Back in main thread."); 
    }
}

일단 아무것도 구현하지 않고 테스트를 실행해봤다.

두 번째 턴을 보면 21 다음에 30이 실행되는 등 스레드의 우선순위가 맞지 않는 것을 확인할 수 있다.

 

$ make check TESTS="tests/threads/priority-sema”

 

더보기

(참고)

구현하다가 priority-sema 테스트가 fail조차 뜨지 않고 Error가 뜨길래 alarm-zero도 테스트 해봤는데 이것도 Error가 발생했다.

git diff로 가장 최근에 수정했던 부분을 주석처리 해보며 원인을 파악해봤는데 synch.c 파일 안에 선언한 list_high_priority()함수 구현부를 주석처리 하니까 alarm-zero 테스트가 다시 정상적으로 pass되는 것을 확인했다. 다시 에러 문구를 확인해보니 threads/synch.o에서 list_high_priority 함수가 multiple definition 즉 중복 정의되었다고 알려주고 있었다.

근데 synch.c 파일에서도 sema_down()할 때 list_insert_ordered()의 인자로 list_high_priority()함수를 써야 되는데… 이거 어떻게 끌어오지? 생각해봐야겠다.


아 찾았다. thread.c에 정의된 함수를 synch.c에서도 쓸 수 있는 방법..! synch.c에서는 이렇게 thread.h를 include해서 thread.c에 정의된 함수를 끌어다 쓸 수 있다.

#include "threads/thread.h"

그런데 내가 정의한 함수인 list_high_priority()는 아직 thread.c에만 구현되어 있고 thread.h에는 포함되어있지 않다. 내가 안 넣었기 때문이다… thread.h에 list_high_priority()함수의 프로토타입을 추가하니까 synch.c에서도 해당 함수를 쓸 수 있게 되었다.

sema_down()에서 list_push_back()을 list_insert_ordered()로 바꾸어 다시 실행해봤다.

오잉… 이번에는 우선순위가 높은 스레드부터 차례대로 실행된 것 같은데 왜 test failed가 뜨지?ㅠㅠ…

음………..우선순위 역전 상황 이런 것도 같이 구현해야 이 테스트케이스가 통과되나?? 암튼 일단 지나가고 우선순위 역전에 대해서 공부하다가..!! 지나가던 우리반에게 우선순위 역전에 대한 설명을 듣고 같이 디버깅도 해서 해결했다.

void
sema_down (struct semaphore *sema) {
	enum intr_level old_level;

	ASSERT (sema != NULL);
	ASSERT (!intr_context ());

	struct thread *curr = thread_current();

	old_level = intr_disable ();
	while (sema->value == 0) {
		//list_push_back (&sema->waiters, &thread_current ()->elem);
		list_insert_ordered(&sema->waiters, &curr->elem, list_high_priority, NULL);
		thread_block ();
	}
	sema->value--;
	intr_set_level (old_level);
}

 

⚙️ 디버깅 과정 1 :

  1. 먼저 $ make check TESTS="tests/threads/priority-sema”를 실행하고 나서 터미널에 출력되는 게 진짜 내 코드 실행 결과가 아니라고 한다. priority-sama.output 파일을 봐야 한다. 이 파일을 봤더니 잘못된 실행 결과가 보였다. 우선순위 21번 스레드가 wake되지 않았다는 사실을 발견했다.

 

2. 테스트 코드에서 thread_create()로 스레드를 10개 생성해서 ready_list에 넣는데 이때 thread_create()를 할 때 3번째 인자로 뭘 주는지 모르겠어서 그냥 넘어갔었다. 그런데 이 부분이 가장 중요한 부분이었다. 지금 생성하고 있는 스레드가 어떤 로직을 수행하게 할건지 지정해주는 부분이었다. 이 사실을 알게 되고 나서 내가 이해가 안 되는 부분이 풀렸다.

 

🤔 나의 의문점 : thread_create() 안의 로직을 보면 새로 생성된 스레드들은 모두 ready_list에 바로 들어가게 하던데.. 그리고 테스트 코드 로직을 보면 어디에도 sema_down()을 하지 않아서 sema→waits에 스레드가 들어갈 일이 없는데.. sema_up()으로 뭘 깨운다는 거지? sema→waits는 비어있을 텐데?

 

👉🏻 테스트 케이스에서 thread_create()의 세 번째 인자로 priority_sema_thread()를 전달하고 있다. 즉 새로 생성된 10개의 스레드는 다음 로직을 수행하는 스레드로 정의된 것이다.

static void
priority_sema_thread (void *aux UNUSED) 
{
  sema_down (&sema);
  msg ("Thread %s woke up.", thread_name ());
}

그래서 10개의 스레드는 생성되자 마자 ready_list로 들어간 후 cpu에서 하나씩 실행되어 sema_down()을 한다. sema_down()은 ‘sema→value==0이면 sema→waiters에 현재 스레드를 우선순위가 큰 순서대로 추가하고 스레드를 blocked 상태로 바꾸는 로직’을 수행한다. 즉 sema_down()이 실행되고 sema→waiters로 들어가는 로직이 없는 줄 알았는데 있었다. ㅎ

그림으로 정리하면 위와 같다. 테스트 코드가 어떻게 돌아가는지 정리가 됐으니, 이제 우선순위가 21번인 스레드가 왜 wake되지 못 했는지 디버깅을 해보자.

void
test_priority_sema (void) 
{
  int i;
  
  /* This test does not work with the MLFQS. */
  ASSERT (!thread_mlfqs);

  sema_init (&sema, 0);
  thread_set_priority (PRI_MIN);
  for (i = 0; i < 10; i++) 
    {
      int priority = PRI_DEFAULT - (i + 3) % 10 - 1;
      char name[16];
      snprintf (name, sizeof name, "priority %d", priority);
      thread_create (name, priority, priority_sema_thread, NULL);
    }

  for (i = 0; i < 10; i++) 
    {
      sema_up (&sema);
      msg ("Back in main thread."); 
    }
}

 

3. 우선순위가 21번인 스레드가 출력되지 않은 이유는 sema_up()에서 sema→value++;의 위치가 틀렸기 때문이다. thread_unblock()하기 전에 sema→value++를 해줘야 하는데 순서를 반대로 구현해서 통과되지 않았다.

void
sema_up (struct semaphore *sema) {
	enum intr_level old_level;

	ASSERT (sema != NULL);												//sema 변수가 NULL이 아닐 때만 아래 코드를 실행
	
	old_level = intr_disable ();										//인터럽트 disable
	sema->value++;
	if (!list_empty (&sema->waiters))									//sema->waiters가 비어있지 않다면
		thread_unblock (list_entry (list_pop_front (&sema->waiters),	//thread_unblock의 인자로 sema->waiters의 가장 앞에 있는 스레드를 전달
					struct thread, elem));
	intr_set_level (old_level);
}

 

4. 그리고 추가로 다른 팀 코어타임에 참여하면서 알게 된 건데, 리스트에 넣을 때 list_insert_ordered()로 정렬을 해주지만 리스트에서 꺼낼 때도 정렬을 해줘야 가장 큰 우선순위의 스레드를 꺼낼 수 있다고 한다. 예를 들어 리스트에 정렬된 상태로 스레드들이 들어가 있었는데도 중간에 우선순위 역전이 발생하여 스레드들의 우선순위가 바뀔 수 있기 때문이다. 따라서 리스트의 위치로 스레드 우선순위를 따져서 꺼내는 게 아니라 리스트에 저장된 스레드들의 우선순위를 직접 따져서 꺼내야 한다는 얘기다.

 

⚙️ 디버깅 과정 2..! :

priority-donate-sema, priority-donate-one을 구현하고 나서 priority-sema를 다시 테스트하니 fail이 발생했다.

printf를 찍어서 output을 확인해보니 priority_sema_thread() 로직이 아예 실행되지 않고 있었다.

 

일단 내가 수정한 부분은 thread_create()이 아니라 sema_down(), sema_up()이기 때문에 이 부분에 문제가 있는 것으로 판단하고 살펴보았다.

void
sema_up (struct semaphore *sema) {
	enum intr_level old_level;

	ASSERT (sema != NULL);												//sema 변수가 NULL이 아닐 때만 아래 코드를 실행
	
	old_level = intr_disable ();										//인터럽트 disable
	sema->value++;
	if (!list_empty (&sema->waiters)){
		list_sort(&sema->waiters, list_high_priority, NULL);
		thread_unblock (list_entry (list_pop_front (&sema->waiters),	//thread_unblock의 인자로 sema->waiters의 가장 앞에 있는 스레드를 전달
					struct thread, elem));
	}									//sema->waiters가 비어있지 않다면
	intr_set_level (old_level);
}

세마포어 변수를 1 증가시키고 sema→waiters를 정렬한다.

정렬 기준은 initial_priority가 아니라 priority이다. gpt가 이 부분을 고려하라고 했다. 우선순위가 donate되었다면 둘 중 어떤 값을 기준으로 먼저 wake되어야 할까?

bool
list_high_priority(const struct list_elem *a, const struct list_elem *b, void *aux){
		
	struct thread* t_a = list_entry(a, struct thread, elem);
	struct thread* t_b = list_entry(b, struct thread, elem);
	
	//a의 우선순위가 더 크면 true 리턴
	return t_a->priority > t_b->priority;
}

이 테스트 케이스는 sema→waiters에 기다리고 있는 스레드들 중 가장 우선순위가 높은 스레드가 깨는지 확인하는 테스트이다. 근데 우선순위가 donate되었다면 priority값이 바뀌었을 것이고 list_high_priority에서 priority를 비교하고 있으므로 상관 없는 것 아닌가? 그리고 심지어 priority-sema 테스트에서는 lock을 쓰고 있지 않아서 donation이 일어나지도 않는다. 우선순위가 뒤바뀌어서 비교대상이 꼬이는 일이 일어나지 않을 것이다.

 

sema_down()을 보자. sema→waiters에 스레드를 넣고 스레드를 blocked상태로 바꾼다. 아 지지지지진짜 문제가 뭔지 모르겠다.

void
sema_down (struct semaphore *sema) {
	enum intr_level old_level;

	ASSERT (sema != NULL);
	ASSERT (!intr_context ());

	struct thread *curr = thread_current();

	old_level = intr_disable ();
	while (sema->value == 0) {
		//list_push_back (&sema->waiters, &thread_current ()->elem);
		list_insert_ordered(&sema->waiters, &curr->elem, list_high_priority, NULL);
		thread_block ();
	}
	sema->value--;
	intr_set_level (old_level);
}

정말 이해가 안 가서 내가 지피티한테 했던 질문들이다.

 

 ❔

이 문제를 해결하기 위해서

thread_set_priority 함수에서 priority 뿐만 아니라 우선순위를 donate받기 전의 priority도 new priority로 변경해줘야 이 문제가 해결된다고 하는데, 이유가뭐야? 어차피 donate받은 후의 priority로 sema->waits안에서 기다리는 스레드들을 정렬하잖아


근데 priority-sema라는 테스트를 할 때는 Lock을 사용하지 않잖아. 그러면 우선순위 기부가 일어날 일이 없으니까 상관 없는 거 아니야..?


지금 너가 말한 문제점이 우선순위가 기부되었을 때 priority 뿐만 아니라 initial\_priority까지 바꿔야 하는데 바꾸지 않았다는 거잖아. 그런데 lock을 얻기 위해 잠시 우선순위를 바꾸기 위해서 priority를 변경해놓는 것이고 나중에 lock을 release할 때 다시 inital\_priority로 돌아가야 하니까 inital\_priority값은 항상 불변이어야하는 거 아니야?


Prirority-sema 테스트코드에서 set\_priority가 어디 호출되는데???? 그냥 처음 PRI\_MIN값 설정할 때만 한 번 호출되는데?


내 코드 에서 내부 정렬 함수가 initial\_priority 기준으로 동작하는 부분은 없어...


→ 딱 여기까지 질문했을 때 문제가 뭔지 좀 파악이 되는 것 같았다. 그러니까 새로 생성한 10개의 스레드의 우선순위를 모두 thread_set_priority()로 바꾼 게 아니라 (코드 상에서 아무리 찾아도 없긴 했음;) 맨 처음에 메인 스레드의 우선순위를 낮추려고 thread_set_priority(PRI_MIN);을 호출한 부분에서 priority만 바꾸고 initial_priority를 바꾸지 않아서 발생한 문제라는 말인 것 같다. thread_set_priority()는 donation처럼 우선순위를 잠시 바꿨다가 lock을 반납할 때 다시 돌려야 하는 우선순위가 아니라 그냥 스레드의 우선순위를 자체적으로 바꾸는 함수라서 initial_priority까지 바꿔야 한다는 말인 것 같다. 그래서 main스레드의 우선순위를 강제로 바꾸는 이 부분을 변경해줘야 하나 보다.

 

그런데 아직도 의문인 건 음…메인 스레드의 우선순위를 강제로 바꿔줘서 initial_priority까지 수정해줘야 하는 건 알겠어. 그런데 두 스레드의 우선순위를 비교하는 함수에서는 다 priority끼리 비교해서 initial_priority까지 안 바꿔줘도 테스트 오류는 안 나지 않나?하는 생각이 드는데… 당연히 원래 바꿔줘야되는 값이긴 한데 그래도 테스트 패스하는 데는 영향을 안 주지 않나..?

 

그리고 또 생긴 의문점이 있다.

void
thread_set_priority (int new_priority) {
	//우선순위가 낮아졌으면 cpu를 즉시 양보하게 한다.
	if(new_priority < thread_current() -> priority){
		thread_yield();
	}
	thread_current ()->priority = new_priority;
	thread_current ()->initial_priority = new_priority;
}

이렇게 하면 priority-change가 fail이 뜨고

void
thread_set_priority (int new_priority) {
	//우선순위가 낮아졌으면 cpu를 즉시 양보하게 한다.
	if(new_priority < thread_current() -> priority){
		thread_current ()->priority = new_priority;
		thread_current ()->initial_priority = new_priority;
		thread_yield();
	}else {
		thread_current ()->priority = new_priority;
		thread_current ()->initial_priority = new_priority;

	}
}

이렇게 하면 priority-change가 pass가 뜬다.

뭐가 다른 거지……? …..????????아 순서가 다르구나??오키

void
thread_set_priority (int new_priority) {
	//우선순위를 new_priority로 변경하고
	thread_current ()->priority = new_priority;
	thread_current ()->initial_priority = new_priority;

	//우선순위가 낮아졌으면 cpu를 즉시 양보하게 한다.
	if(new_priority < thread_current() -> priority)
		thread_yield();

}

음 그런데 이렇게 구현했는데 또 fail이 떴다.

조건문에서 원래 우선순위와 new_priority를 비교하고 나서 !!!! 바꿔야 되는구나 ㅎㅎ… 오키….먼지 알았당

 

아아ㅏㅏㅏ그리고 우리 조원이 해결법을 이렇게 알려주셨는데

 

💡

기부 구현을 위해 original_priority 추가

  • thread 구조체에 original_priority 필드를 두고,
  • thread_init() 시점에 초기 우선순위(PRI_DEFAULT 등)를 복사해 놓습니다.

thread_set_priority() 호출과 테스트 검증

  • sema/condvar 테스트들은 thread_set_priority(new_prio) 로 스레드 우선순위를 바꾼 뒤,
  • 그 값(thread->priority)을 보고 “누구를 먼저 깨울지” 또는 “누구에게 CPU를 줄지”를 결정합니다.

문제 발생 지점

  • 중간에 thread_set_priority() 로 우선순위가 변경되더라도,
  • original_priority 는 초기값 그대로 남아 있기 때문에,
  • lock_release() 등에서 thread->priority = thread->original_priority 복원 시에
  • → 실제 사용자가 설정한 최신 우선순위가 아닌, 이전(낡은) 값 으로 돌아가 버립니다.

결과

  • donate-one 테스트는 한 번만 기부·복원하기 때문에 우연히 통과될 수 있으나,
  • sema/condvar 테스트처럼 우선순위 변경이 반복적으로 일어나는 시나리오에서는
  • 잘못된(original_priority) 복원 때문에 스케줄러가 엉뚱한 순서로 작동해 테스트가 실패 합니다.

라고 gpt가 알려주네요간단하게 말하면 condvar나 sema 테스트는 thread_set_priority함수를 호출해서 우선순위를 변경을 하는데우선순위 변경할때 donate를 구현하기 위해 만든 original_priority도 바꿔줘야하는 안바꿔줘서 문제가 생기는겁니다그래서 thread_set_priority함수에 original_priority도 new_priority로 변경하는 함수를  추가 해주면 됩니다.

 

 

이걸 다시 읽고 다시 한 번 생각해보니까 뭔가 생각이 이어진다. 다시한 번 최종 정리를 하면 이렇다.

/* Tests that the highest-priority thread waiting on a semaphore
   is the first to wake up. */

#include <stdio.h>
#include "tests/threads/tests.h"
#include "threads/init.h"
#include "threads/malloc.h"
#include "threads/synch.h"
#include "threads/thread.h"
#include "devices/timer.h"

static thread_func priority_sema_thread;
static struct semaphore sema;

void
test_priority_sema (void) 
{
  int i;
  
  /* This test does not work with the MLFQS. */
  ASSERT (!thread_mlfqs);

  sema_init (&sema, 0);
  thread_set_priority (PRI_MIN);
  for (i = 0; i < 10; i++) 
    {
      int priority = PRI_DEFAULT - (i + 3) % 10 - 1;
      char name[16];
      snprintf (name, sizeof name, "priority %d", priority);
      thread_create (name, priority, priority_sema_thread, NULL);
    }

  for (i = 0; i < 10; i++) 
    {
      sema_up (&sema);
      msg ("Back in main thread."); 
    }
}

static void
priority_sema_thread (void *aux UNUSED) 
{
  sema_down (&sema);
  msg ("Thread %s woke up.", thread_name ());
}

이 priority-sema 테스트에서 맨 처음에 main함수의 우선순위를 낮췄잖아

  thread_set_priority (PRI_MIN);

이렇게?

 

이게 왜 우선순위를 낮춘 거냐면, 원래 스레드가 생성될 때는 항상 PRI_DEFAILT 즉 우선순위가 31값으로 설정된다.

따라서 이 코드를 실행하면 main함수의 우선순위가 0으로 낮아진다.

그리고 thread_set_priority() 내부 구현을 보면 스레드 우선순위가 낮아졌을 때 thread_yield를 호출하여 cpu 점유 스레드를 바꾼다.

void
thread_set_priority (int new_priority) {
	//우선순위가 낮아졌으면 cpu를 즉시 양보하게 한다.
	if(new_priority < thread_current() -> priority){
		thread_current ()->priority = new_priority;
		thread_yield();
	}else thread_current ()->priority = new_priority;
	
}

이렇게 !

근데 여기서 initial_priority는 안 바꿔줬기 때문에 계속해서 main 함수의 우선순위가 가장 높은 채로 남아있어서 cpu가 양보되지 않고 main 스레드만 계속 실행되다가 끝나는 것 같다. 아니 근데 아직도 ………….이해가 안 간다고. priority는 바꿔줬잖아. 아 ㅇ.ㅇ ㅏㅇ아아ㅏ,.

 

 

4. priority-change 테스트 pass하기

테스트 코드 해석 :

  • 스레드 우선순위를 낮춰서 더 이상 가장 높은 우선순위가 아니게 만들면 곧바로 다른 스레드로 yield되는지 확인하는 테스트 코드.
  • 우선순위가 32인 “thread 2”를 생성한다. thread 2는 changing_thread()를 수행하는 스레드이다.
  • 현재 실행 중인 스레드의 우선순위를 2 감소시킨다.
void
test_priority_change (void) 
{
  /* This test does not work with the MLFQS. */
  ASSERT (!thread_mlfqs);

  msg ("Creating a high-priority thread 2.");
  thread_create ("thread 2", PRI_DEFAULT + 1, changing_thread, NULL); //우선순위 32인 "thread2"를 생성
  msg ("Thread 2 should have just lowered its priority.");
  thread_set_priority (PRI_DEFAULT - 2); //현재 스레드 우선순위를 2 감소시킴
  msg ("Thread 2 should have just exited.");
}

static void
changing_thread (void *aux UNUSED) 
{
  msg ("Thread 2 now lowering priority.");
  thread_set_priority (PRI_DEFAULT - 1); //현재 스레드 우선순위를 1 감소시킴
  msg ("Thread 2 exiting.");
}

 

아직 아무것도 구현하지 않은 상태에서 테스트를 실행하면 결과가 이렇게 나온다.

 

원래 Acceptable output처럼 나와야 하는데 Differences in diff -u format와 같은 결과가 나온 것.

 

스레드 우선순위가 변경되었을 때도 priority가 가장 큰 스레드 기준으로 꺼낼 수 있도록 ready_list에 스레드를 넣을 때 뿐만 아니라 ready_list에서 스레드를 꺼낼 때도 정렬을 수행해야 한다.

따라서 schedule에서 cpu에 올릴 때 ready_list에서 pop하기 전에 sort처리하기 위해 이 부분을 추가했다.

static struct thread *
next_thread_to_run (void) {
	if (list_empty (&ready_list))													//ready_list가 비어있다면
		return idle_thread;															//idle_thread를 반환
	else{
		list_sort(&ready_list, list_high_priority, NULL); //정렬하는 부분 추가!
		return list_entry (list_pop_front (&ready_list), struct thread, elem);		//ready_list의 가장 앞에 위치하는 스레드를 반환
	}																			//레디큐가 비어있지 않다면
}

 

그런데 output은 바뀌지 않았고 테스트는 여전히 fail이었다.

 

아까 참여한 2조 코어타임에서 두 부분을 바꾸면 이 테케를 통과할 수 있다고 들은 것 같아서 또 찾아봤다. 파일 전체에서 list_pop하는 부분을 검색했더니 sema_up() 부분에 sema→waiters 리스트에서 우선순위가 가장 높은 스레드를 pop해서 ready_list로 올리는 부분이 나왔다. 생각해보니 cpu에 올릴 때만 정렬해야하는 게 아니라, sema→waiters에서 ready_list로 올릴 때도 우선순위 순서로 정렬을 해야 했다..! 왜냐하면 semaphore(lock)을 차지하려고 기다리는 스레드들 중 가장 먼저 lock을 차지해야 하는 스레드는 우선순위가 가장 높은 스레드여야 하기 때문이다! 따라서 sema→waiters에서 ready_list로 올리는 경우도 우선순위 순서로 정렬을 해줘야 한다.

→ 이 부분은 우선순위 스케줄링에서 중요하다. lock을 차지하기 위해 기다리는 스레드들 중 우선순위가 가장 높은 스레드를 먼저 깨우지 않으면, 높은 우선순위를 가진 스레드가 lock을 얻지 못하고 block된 채로 대기하게 되기 때문에 우선순위를 무시(priority inversion)하게 된다!

자…그래서 여기도 이렇게 정렬 로직을 추가했는데 테스트가 또 fail이다. output도 그대로다. 음…

void
sema_up (struct semaphore *sema) {
	enum intr_level old_level;

	ASSERT (sema != NULL);												//sema 변수가 NULL이 아닐 때만 아래 코드를 실행
	
	old_level = intr_disable ();										//인터럽트 disable
	sema->value++;
	if (!list_empty (&sema->waiters)){
		list_sort(&sema->waiters, list_high_priority, NULL); //여기 정렬 로직 추가 !
		thread_unblock (list_entry (list_pop_front (&sema->waiters),	//thread_unblock의 인자로 sema->waiters의 가장 앞에 있는 스레드를 전달
					struct thread, elem));
	}									//sema->waiters가 비어있지 않다면
	intr_set_level (old_level);
}
/

 

지피티한테 살짝 힌트만 달라고 하니까 우선순위가 낮아졌을 때 즉시 cpu를 양보하게 했냐고 한다. 오… 우선순위가 낮아지면 무조건 cpu를 양보해야 하나? 그런 것 같다. 그래서 일단 이렇게 추가했는데 여전히 테스트가 fail이다. 확인 안 한 조건이 더 있나?

void
thread_set_priority (int new_priority) {
	thread_current ()->priority = new_priority;
	//우선순위가 낮아졌으면 cpu를 즉시 양보하게 한다.
	if(new_priority < thread_current() -> priority){ //로직 추가
		thread_yield();
	}
}

아니다! 이렇게 해야 한다.

void
thread_set_priority (int new_priority) {
	//우선순위가 낮아졌으면 cpu를 즉시 양보하게 한다.
	if(new_priority < thread_current() -> priority){
		thread_current ()->priority = new_priority;
		thread_yield();
	}else thread_current ()->priority = new_priority;
}

통과!!!

5. priority-donate-sema 테스트 pass하기

테스트 코드 해석 :

이번 테스트 코드는 해석하기 정말 어려웠는데 세 명이 붙어서 같이 해석해주었다. 우리 반 최공 ㅎ-ㅎ

/* Low priority thread L acquires a lock, then blocks downing a
   semaphore.  Medium priority thread M then blocks waiting on
   the same semaphore.  Next, high priority thread H attempts to
   acquire the lock, donating its priority to L.

   Next, the main thread ups the semaphore, waking up L.  L
   releases the lock, which wakes up H.  H "up"s the semaphore,
   waking up M.  H terminates, then M, then L, and finally the
   main thread.

   Written by Godmar Back <gback@cs.vt.edu>. */

#include <stdio.h>
#include "tests/threads/tests.h"
#include "threads/init.h"
#include "threads/synch.h"
#include "threads/thread.h"

struct lock_and_sema 
  {
    struct lock lock;
    struct semaphore sema;
  };

static thread_func l_thread_func;
static thread_func m_thread_func;
static thread_func h_thread_func;

void
test_priority_donate_sema (void) 
{
  struct lock_and_sema ls;

  /* This test does not work with the MLFQS. */
  ASSERT (!thread_mlfqs);

  /* Make sure our priority is the default. */
  ASSERT (thread_get_priority () == PRI_DEFAULT);

  lock_init (&ls.lock); //lock->holder을 NULL로, lock->semaphore을 1로 초기화
  sema_init (&ls.sema, 0); //sema->value를 0으로 설정, sema->waiters를 초기화
  thread_create ("low", PRI_DEFAULT + 1, l_thread_func, &ls);
  thread_create ("med", PRI_DEFAULT + 3, m_thread_func, &ls);
  thread_create ("high", PRI_DEFAULT + 5, h_thread_func, &ls);
  
  sema_up (&ls.sema); //sema->value를 1 증가시키고, sema->waiters에서 우선순위가 가장 높은 스레드를 unblock한다. 즉 blocked에서 ready상태로 바꾸고 ready_list에 추가한다. (이때 ready_list에 새로 추가된 스레드 우선순위가 현재 실행 중인 스레드 우선순위보다 높으면 문맥 교환 발생)
  msg ("Main thread finished.");
}

static void
l_thread_func (void *ls_) 
{
  struct lock_and_sema *ls = ls_;

  lock_acquire (&ls->lock); //sema_down()을 호출, lock->holder를 현재 스레드로 설정
  msg ("Thread L acquired lock.");
  sema_down (&ls->sema); //
  msg ("Thread L downed semaphore.");
  lock_release (&ls->lock);
  msg ("Thread L finished.");
}

static void
m_thread_func (void *ls_) 
{
  struct lock_and_sema *ls = ls_;

  sema_down (&ls->sema);
  msg ("Thread M finished.");
}

static void
h_thread_func (void *ls_) 
{
  struct lock_and_sema *ls = ls_;

  lock_acquire (&ls->lock);
  msg ("Thread H acquired lock.");

  sema_up (&ls->sema);
  lock_release (&ls->lock);
  msg ("Thread H finished.");
}
  • lock_and_sema 자료구조를 생성해서 lock과 semaphore 변수를 모두 쓸 수 있게 만든 것 같다. 이 부분은 왜 굳이 이렇게 자료구조 하나로 묶어뒀는지 잘 모르겠다. 
  • lock과 sema 변수를 사용할 수 있도록 초기화한다. sema 변수값은 0으로 설정하였다.
  • thread_create()로 low, med, high라는 이름의 3 개의 스레드를 생성한다. 우선순위는 각각 32, 34, 36이다.
  • thread_create()로 생성된 스레드는 바로 running 상태가 되어 cpu에서 실행된다. low, med, high 순서로 cpu에서 실행될 것이다. 
  • 스레드 low는 l_thread_func()을 로직으로 실행한다. lock_acquire()는 sema_down()을 호출하지만 sema변수가 0이기 때문에 lock을 얻지 못하고 block되어 sema->waiters로 들어간다.
  • 스레드 med는 m_thread_func()을 로직으로 실행한다. sema_down()을 호출하지만 마찬가지로 sema변수가 0이기 때문에 lock을 얻지 못하고 block되어 sema->waiters로 들어간다.
  • 스레드 high는 h_thread_func()을 로직으로 실행한다. lock_acquire()는 sema_down()을 호출하지만 sema변수가 0이기 때문에 lock을 얻지 못하고 block되어 sema->waiters로 들어간다.
    • -> 취소선 그은 부분이 실행되기 전에 lock_acquire()에서 현재 스레드의 우선순위와 lock을 가지고 있는 스레드의 우선순위를 비교하여 현재 스레드 우선순위가 더 크면 lock을 가지고 있는 스레드에게 현재 스레드 우선순위를 부여한다. (donate)
    • 현재 스레드가 lock을 점유하고 실행하려면 지금 lock을 가지고 있는 스레드가 빨리 실행을 끝내고 lock을 반환해야 하므로 우선순위를 기부하여 높여주는 것이다.

void
lock_acquire (struct lock *lock) {
	ASSERT (lock != NULL);
	ASSERT (!intr_context ());
	ASSERT (!lock_held_by_current_thread (lock));
	
    /* sema 변수가 0일 때, lock->holder의 우선순위가 더 낮으면 우선순위 donate하는 로직 추가 */
	if(lock->semaphore.value==0){
		if(thread_current()->priority > lock->holder->priority){
			lock->holder->priority = thread_current()->priority;
		}
	}

	sema_down (&lock->semaphore);
	lock->holder = thread_current ();
}