Pintos 프로젝트1 고찰같다

모기·2025년 5월 11일

Pintos.Gochal

목록 보기
2/7

Project 1

본격적으로 프로젝트에 들어가기 앞서
https://velog.io/@mogiyoon/pintos-디버깅-간살
이 문서를 읽으면
이 사람이 뭐한거지? 이해할 때 더 도움이 될 듯하다.

Alarm (탐구)

void
timer_sleep (int64_t ticks) {
	int64_t start = timer_ticks ();

	ASSERT (intr_get_level () == INTR_ON);
	while (timer_elapsed (start) < ticks)
		thread_yield ();
}

이 녀석을 수정하는 과제다.
사실 흐름 자체를 모르니까 구현의 방향이 잡히지 않아서
흐름만 일단 가져왔다.

하드웨어 타이머 → 일정 주기마다 인터럽트 발생 →
→ Pintos의 timer_interrupt() 실행 →
→ ticks 증가, thread_tick() 호출, sleep 대기 스레드 깨움 →
→ thread_yield()로 문맥 전환 필요시 스케줄러 호출

-timer.c-

모든 함수를 다루진 않을 거고, 뭔가 쓸모있어 보이는 함수에 print를 찍고
검증된 것들만 다룰 것이다.

timer_sleep

돌고 돌아 timer_sleep이다.
사실 얘가 뭔지 계속 알아내기 위해 고군분투했다.

아래 내용들 중에서는 timer_sleep보다 먼저 작성된 것들이 꽤 있다.

void
timer_sleep (int64_t ticks) {
	int64_t start = timer_ticks ();

	ASSERT (intr_get_level () == INTR_ON);
	while (timer_elapsed (start) < ticks)
		thread_yield ();
}

ticks 값을 받아와서

	int64_t start = timer_ticks ();

	ASSERT (intr_get_level () == INTR_ON);
	while (timer_elapsed (start)

요 사이에 흐른 시간이 tick보다 작으면
스레드를 레디큐의 뒤로 넘긴다.

처음에 이 부분이 정말정말정말정말정말 이해가 안됐다.

근데 sleeper 함수를 보고 이해가 됐다.
매개변수 ticks는 현재 스레드가 깰 때까지 남은 시간이다.
그럼 이제 해결이다.
아니 아직 덜 해결이다.
여기서 thread_yield 되고나서 어떻게 되는지 얘기를 안했다.
현재 배운 코드에서 말할 수 있는건,
yield되면 현재 스레드는 일단 작동을 멈추기 때문에
while문에서 멈춘 상태로 리스트의 뒤로 가게 되고
다음 자기 차례가 오면 while문을 다시 돈다.

나머지 부분은 sleeper에 더 추가할 예정이다.

timer_interrupt

static void
timer_interrupt (struct intr_frame *args UNUSED) {
	ticks++;
	thread_tick ();
}

원래 이렇게 생긴 함수인데, 프린트 문을 추가했다.

static void
timer_interrupt (struct intr_frame *args UNUSED) {
	ticks++;
	printf("now ticks: %d\n", ticks);
	thread_tick ();
}

그러고 로그를 봤더니

아주 좋다.
아마도 이 함수가 tick을 계속해서 증가시켜주는 함수인 것 같다.
헷갈리니까 500틱 이후부터 찍히게 만들어줘야겠다.

끔찍한 혼종이다.
그렇게 여기저기 떠돌아다니던 중

-alarm-wait.c-

그리고 마참내 alarm-wait.c에서
뭔가 작동한다는 것을 알 수 있었다.

test_sleep

test_sleep (int thread_cnt, int iterations) 
{
  struct sleep_test test;
  struct sleep_thread *threads;
  int *output, *op;
  int product;
  int i;

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

  msg ("Creating %d threads to sleep %d times each.", thread_cnt, iterations);
  msg ("Thread 0 sleeps 10 ticks each time,");
  msg ("thread 1 sleeps 20 ticks each time, and so on.");
  msg ("If successful, product of iteration count and");
  msg ("sleep duration will appear in nondescending order.");

  /* Allocate memory. */
  threads = malloc (sizeof *threads * thread_cnt);
  output = malloc (sizeof *output * iterations * thread_cnt * 2);
  if (threads == NULL || output == NULL)
    PANIC ("couldn't allocate memory for test");

  /* Initialize test. */
  test.start = timer_ticks () + 100;
  test.iterations = iterations;
  lock_init (&test.output_lock);
  test.output_pos = output;

  /* Start threads. */
  ASSERT (output != NULL);
  for (i = 0; i < thread_cnt; i++)
    {
      struct sleep_thread *t = threads + i;
      char name[16];
      
      t->test = &test;
      t->id = i;
      t->duration = (i + 1) * 10;
      t->iterations = 0;

      snprintf (name, sizeof name, "thread %d", i);
      thread_create (name, PRI_DEFAULT, sleeper, t);
    }
  
  /* Wait long enough for all the threads to finish. */
  timer_sleep (100 + thread_cnt * iterations * 10 + 100);

  /* Acquire the output lock in case some rogue thread is still
     running. */
  lock_acquire (&test.output_lock);

  /* Print completion order. */
  product = 0;
  for (op = output; op < test.output_pos; op++) 
    {
      struct sleep_thread *t;
      int new_prod;

      ASSERT (*op >= 0 && *op < thread_cnt);
      t = threads + *op;

      new_prod = ++t->iterations * t->duration;
        
      msg ("thread %d: duration=%d, iteration=%d, product=%d",
           t->id, t->duration, t->iterations, new_prod);
      
      if (new_prod >= product)
        product = new_prod;
      else
        fail ("thread %d woke up out of order (%d > %d)!",
              t->id, product, new_prod);
    }

  /* Verify that we had the proper number of wakeups. */
  for (i = 0; i < thread_cnt; i++)
    if (threads[i].iterations != iterations)
      fail ("thread %d woke up %d times instead of %d",
            i, threads[i].iterations, iterations);
  
  lock_release (&test.output_lock);
  free (output);
  free (threads);
}

스레드가 어디서 나오고 누가 재우는지, 어떻게 재우는 건지 알아볼 것이다.

  for (i = 0; i < thread_cnt; i++)
    {
      struct sleep_thread *t = threads + i;
      char name[16];
      
      t->test = &test;
      t->id = i;
      t->duration = (i + 1) * 10;
      t->iterations = 0;

      snprintf (name, sizeof name, "thread %d", i);
      thread_create (name, PRI_DEFAULT, sleeper, t);
    }

여기서 sleep 스레드에 duration을 넣는다.
물론 sleep 스레드는 메인에서 관리하는 스레드와는 별개다.

iterations을 해석하기까지 많은 시간이 걸리긴 했는데,
아마 busy wait을 하면서 한 번 순회할 때마다 값이 증가하는 것 같다.
즉, 이 iteration을 최소화하는 것이 관건이라 생각한다.

그리고 이 정보를 활용해서 스레드를 생성한다.
생성된 스레드는 thread create 함수의 thread unblock 함수를 통해
ready queue(ready list)에 들어간다.
또, 스레드를 만들 때 함수(sleeper)가 들어가는데
스레드가 CPU에 들어가면 이 함수가 실행된다고 한다.
CPU에 들어갔을 때 어떤 동작을 하는지 알려면 sleeper를 봐야한다.

중간에 있는 이 for 구문은

  for (op = output; op < test.output_pos; op++) 

테스트 결과 생성과 관련된 구문인 듯 하다.

sleeper

/* Sleeper thread. */
static void
sleeper (void *t_) 
{
  struct sleep_thread *t = t_;
  struct sleep_test *test = t->test;
  int i;

  for (i = 1; i <= test->iterations; i++) 
    {
      int64_t sleep_until = test->start + i * t->duration;
      timer_sleep (sleep_until - timer_ticks ()); //
      lock_acquire (&test->output_lock);
      *test->output_pos++ = t->id;
      lock_release (&test->output_lock);
    }
}

테스트 및 스레드 정보를 받아온다.
그리고 sleep_until은 언제까지 이 스레드를 재울지에 관한 변수인 것 같다.
timer_sleep에 넣는 값은 (목표 sleep time - 현재 time)이고
'목표 sleep time에서 현재 time을 뺀다는 것'은 얼마나 더 잘 수 있는지,
즉, 남은 sleep 시간을 계산한다고 보면 된다.

이걸 timer_sleep에 넣고, 남은 sleep 시간이 많다면 스레드는 뒤로 간다.

이제 다시 궁금해진 것은
이 스레드가 깨고 나서 어떻게 되는지가 궁금하다.
timer sleep에서 while문 지나면
곰곰히 생각해보니
그냥 그 함수가 끝나고, 그 함수를 호출한 함수에서 다음 차례로 넘어간다.
즉, sleeper에서 lock_acquire로 넘어가고
for문을 돌게 된다.

Total Flow

그림으로 설명을 하려고 했는데 오히려 더 까다로울 것 같아서 글로 설명하려고 한다.

test_sleep에서 각 스레드마다 자는 시간(duration)과 sleeper라는 함수를 달고 태어난다.

test start: 10 틱
now tick: 10 틱

thread 0			thread 1			thread 2
duation 10			duation 20			duation 30
sleeper				sleeper				sleeper

대략 이런 느낌이다.
그리고 이 sleeper는 cpu에 들어가면 동작한다.

test start: 10
now tick: 12

-----------------
CPU

thread 0
duration 10
sleeper
 - sleep until: 20 (test start + duration)
 - left time: 8

timer_sleep
 - start: 12
_________________


Ready list
thread 1			thread 2
duation 20			duation 30
sleeper				sleeper
test start: 10
now tick: 14

-----------------
CPU

thread 0
duration 10
sleeper
 - sleep until: 20 (test start + duration)
 - left time: 8

timer_sleep
 - start: 12
 - timer_elapsed: 2 (now tick - start)
 
=> timer_elapsed(2) < left time(8) => thread_yield
_________________


Ready list
thread 1			thread 2
duation 20			duation 30
sleeper				sleeper
  1. 여기서부턴 빠르게 빠르게 처리하겠다.
test start: 10
now tick: 15

-----------------
CPU

thread 1
duration 20
sleeper
 - sleep until: 30 (test start + duration)
 - left time: 15

timer_sleep
 - start: 15
 - timer_elapsed: 0 (now tick - start)
 
=> timer_elapsed(0) < left time(15) => thread_yield
_________________


Ready list
thread 2			thread 0
duation 30			duation 10
sleeper				start 12 
					left time 8
test start: 10
now tick: 18

-----------------
CPU

thread 2
duration 30
sleeper
 - sleep until: 40 (test start + duration)
 - left time: 22

timer_sleep
 - start: 18
 - timer_elapsed: 0 (now tick - start)
 
=> timer_elapsed(0) < left time(22) => thread_yield
_________________


Ready list
thread 0			thread 1
duation 10			duation 20
start 12			start 15 
left time 8			left time 15
  1. 아까 yield가 있던 while문부터 다시 시작한다.
test start: 10
now tick: 20

-----------------
CPU

thread 0
duration 10
(cpu에 스레드가 다시 들어오면 yield가 있던 while문부터 시작하므로 sleeper은 생략함)

timer_sleep
 - start: 12
 - timer_elapsed: 8 (now tick - start)
 
=> timer_elapsed(8) < left time(8) => False
=> End of Function
_________________


Ready list
thread 1			thread 2
duation 20			duation 30
start 15			start 18 
left time 15		left time 22

interation이나 구체적인 내용은 살짝 다르지만, 이런 느낌이라고 생각하면 된다.

따라서 여기서 바꿔줘야 할 부분은 while문을 돌지 않고
sleep 요청이 오면 sleep list에 넣어뒀다가
tick마다 확인해서 적당한 시간에 ready list에 다시 넣어주면 된다.

Alarm (구현)

사실 고찰하면서 구현하고 싶지만,
시간이 촉박하다보니 먼저 구현을 해버렸다.
그래서 이제 이를 소개하려 한다.

우선 스레드 구조체에 sleep_time이라는 변수를 추가했다.
새로운 구조체를 만들까 잠깐 고민도 했는데,
말록을 추가하게 되면 여간 까다로워지지 않을까 싶어
어디선가 thread는 말록해주겠지~ 하면서 thread 구조체에 변수를 추가했다.

thread_sleep

void
thread_sleep (int64_t ticks) {
	if (ticks <= 0) {
	} else {
		int64_t start = timer_ticks();
		enum intr_level old_level = intr_disable ();
		ASSERT (!intr_context ());
		ASSERT (intr_get_level () == INTR_OFF);
		struct thread* now = thread_current();
		now->sleep_time = start + ticks;
		list_insert_ordered(&sleep_list, &now->elem, time_comparer, NULL);
		thread_block();
		intr_set_level(old_level);
	}
}

스레드를 재우는 함수이다. 남은 시간인 tick을 그대로 가져와서 썼다.
thread block 함수를 참고했다.

thread_wake

void
thread_wake (void) {
	int64_t now = timer_ticks();
	if (list_empty(&sleep_list)) {
	} else {
		enum intr_level old_level = intr_disable ();
		struct thread* wake_head = list_entry(list_front(&sleep_list), struct thread, elem);
		while (wake_head->sleep_time <= now) {
			ASSERT (wake_head->status == THREAD_BLOCKED);
			list_pop_front (&sleep_list);
			thread_unblock(wake_head);
			if (list_empty(&sleep_list)) {
				break;
			} else {
				wake_head = list_entry(list_front(&sleep_list), struct thread, elem);
			}
		}
		intr_set_level (old_level);
	}
}

이하 동문이다.

time_comparer

bool
time_comparer(struct list_elem* a, struct list_elem* b, void *aux) {
	struct thread* thread_a = list_entry_self(a, struct thread, elem);
	struct thread* thread_b = list_entry_self(b, struct thread, elem);
	if (thread_a->sleep_time < thread_b->sleep_time) {
		return true;
	} else if (thread_a->sleep_time == thread_b->sleep_time) {
		if (thread_a->priority > thread_b->priority) {
			return true;
		}
	return false;
	}
}

얘는 list.h에 있는 list_less_fuc에 대한 설명을 참고해서 만들었다.
우리반 '신'말로는 우선 순위를 고려해서 넣는 것이 더 바람직하다고 말했다.

설명하기 귀찮으니 혹시나 참고하는 사람은 스스로 공부해보길 바란다.
또한 이 함수들을 어디에 넣으면 적절할지도 고민해보길 바라며
함수 내부에 구현되었지만 따로 설명하지 않은 변수들을 어떻게 선언하고 사용하면 좋을지도
고민해보길 바란다.

Priority Scheduling (탐구)

마찬가지로 모든 함수를 다 다루지는 않고 중점적으로 변경할 함수만 다룰 예정이다.

이 과제에서 중점적으로 다루길 바라는 함수는

void thread_set_priority (int new_priority);

이 함수와

int thread_get_priority (void);

이 함수이다.

이 함수가 출몰하는 곳은 thread.c이다.

-Priority Change (탐구)

이 장에서는 과제별로 탐구 및 구현을 다루려고 한다.
Alarm 같은 경우는 구현을 하면 나머지도 다 구현이 됐기 때문에 쉽게 넘어갔지만,
얘는 하나씩 구현해야 한다.

Priority Change에서 원하는 것은

  1. 현재 스레드의 우선 순위를 변경하고, 해당 스레드의 우선 순위가 ready list의 스레드보다 낮으면 yield하기

  2. 새로운 스레드를 생성했을 때, CPU를 점유 중인 스레드보다 우선 순위가 높으면 CPU 점유하기

요 두가지이다.

1번은 생각보다 구현이 간단하지만, 2번은 고려해야할 부분이 있다.
(사실 탐구이지만 구현이 끝나고 쓴다.)

현재 스레드의 우선 순위를 변경하는 것은 이미 구현돼 있다.

- thread.c -

thread_set_priority

void thread_set_priority (int new_priority) {
	// msg("----------");
	// msg("before");
	// msg("now thread: %d", thread_current()->tid);
	// msg("now thread pri: %d", thread_current()->priority);
	thread_current ()->priority = new_priority;
	// msg("after");
	// msg("now thread: %d", thread_current()->tid);
	// msg("now thread pri: %d", thread_current()->priority);
	// msg("----------");
}

흥겨운 디버깅의 흔적이다.

따라서 우리가 구현해야 할 것은
cpu 스레드의 우선 순위가 변경하면 ready list와 비교 후 yield를 하는 것이다.

근데 yield할 때도 고려해야할 것이 있긴하다.

thread_yield

void
thread_yield (void) {
	struct thread *curr = thread_current ();
	enum intr_level old_level;

	ASSERT (!intr_context ());

	old_level = intr_disable ();
	if (curr != idle_thread)
		list_push_back (&ready_list, &curr->elem);
	do_schedule (THREAD_READY);
	intr_set_level (old_level);
}

yield 함수는 ready list의 가장 뒤로 스레드를 보낸다.

근데 우리는 우선 순위가 가장 높은 스레드를 CPU에 넣고
스레드가 CPU에서 나오게 되면
그 다음 우선 순위가 높은 스레드가 CPU 안으로 들어가야 한다.

그렇다면 ready list는 어떤 상태여야할까?

바로 우선 순위가 높은 순으로 정렬되어야 한다.
ready list에 없던 스레드가 들어가게 되면 항상 정렬을 고려해야 한다.

이를 바탕으로 앞서 말했던 1, 2번을 구현해야 한다.

그렇다면 새로운 스레드가 생성될 때는 어떻게 해야할까?

tid_t
thread_create (const char *name, int priority,
		thread_func *function, void *aux) {
	struct thread *t;
	tid_t tid;

	ASSERT (function != NULL);

	/* Allocate thread. */
	t = palloc_get_page (PAL_ZERO);
	if (t == NULL)
		return TID_ERROR;

	/* Initialize thread. */
	init_thread (t, name, priority);
	tid = t->tid = allocate_tid ();

	/* Call the kernel_thread if it scheduled.
	 * Note) rdi is 1st argument, and rsi is 2nd argument. */
	t->tf.rip = (uintptr_t) kernel_thread;
	t->tf.R.rdi = (uint64_t) function;
	t->tf.R.rsi = (uint64_t) aux;
	t->tf.ds = SEL_KDSEG;
	t->tf.es = SEL_KDSEG;
	t->tf.ss = SEL_KDSEG;
	t->tf.cs = SEL_KCSEG;
	t->tf.eflags = FLAG_IF;

	/* Add to run queue. */
	thread_unblock (t);

	return tid;
}

스레드 생성 함수이다.
근데 처음에 생성된 스레드는 어떤 상태인지 아는가?
바로 blocked 상태이다.

따라서 생성된 직후에는 ready list나 cpu에 넣을 수조차 없다.
넣을 수 있는 타이밍이 언제일까?

바로 unblock할 때이다.
2번을 해결하기 위해서는
thread_create 함수가 아닌
thread_unblock 함수를 바꿔야 한다.

thread_unblock

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);
}

보면 냅다 뒤에 넣어버린다.
여기서 우리가 생각할 것은
a. 우선 순위가 가장 높으면 cpu로 가야하고
b. 가장 높은 것이 아니면 ready list의 적당한 위치로 보낸다.

이 두 가지 조건을 코드로 간단하게 구현하기 위해서는 어떻게 해야할까?

a를 좀 더 쪼개고, b를 좀 더 간단하게 만들면된다.
먼저 b를 더 간단하게 만들겠다.

b. 스레드를 ready list의 적당한 위치로 보낸다.

여기서 말하는 적당한 위치란 ready list 내에서 우선 순위에 맞는 위치를 말한다.

그리고 a를 다음처럼 바꾼다.

a. 스레드를 ready list의 적당한 위치로 보내고, ready list의 front의 우선 순위가 현재 스레드의 우선 순위보다 높을 경우 yield한다.

이렇게 바꾸면

'스레드를 ready list의 적당한 위치로 보낸다.' 이 부분은 공통되는 부분이기 때문에
b에 대한 분기는 따로 처리할 필요가 없고
a의 나머지 부분에 대한 분기만 처리하면 된다.

근데 여기서 주의할 조건이 하나 더 있는데 그 조건에 대해서는 좀 더 고민해보길 바란다.
사실 구현 다 해놓고, 그 조건 때문에 더 고생했다.

-Priority Change (구현)

thread_set_priority

void
thread_set_priority (int new_priority) {
	thread_current ()->priority = new_priority;
	if (!list_empty(&ready_list)) {
		if (list_entry(list_front(&ready_list), struct thread, elem)->priority > new_priority) {
			thread_yield();
		}
	}
}

먼저 set priority에서는 리스트가 비었는지 확인한 뒤
프론트를 확인하고 우선 순위를 비교해서 yield를 한다.

yield

void
thread_yield (void) {
	struct thread *curr = thread_current ();
	enum intr_level old_level;

	ASSERT (!intr_context ());

	old_level = intr_disable ();
	if (curr != idle_thread)
		list_insert_ordered(&ready_list, &curr->elem, priority_comparer, NULL);
	do_schedule (THREAD_READY);
	intr_set_level (old_level);
}

yield에서는 pushback 대신 list_inser_ordered 함수를 넣었다.

thread_unblock

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_insert_ordered(&ready_list, &t->elem, priority_comparer, NULL);
	t->status = THREAD_READY;
	if (thread_current() != idle_thread && thread_get_priority() < t->priority) {
		thread_yield();
	}
	intr_set_level (old_level);
}

thread를 unblock할 때 list ordered를 실행해서 list에 넣고
우선 순위가 가장 높은 경우 ready list의 front에 있으므로 yield를 한다.

그리고 idle thread는 ready list에 들어가면 안되므로 yield를 해선 안된다.
따라서 조건을 추가해야 한다.

여담

근데 궁금한건 push_back은 별 다른 조건 없이도 잘 동작했는데, 저건 왜 안되는지 모르겠다.

오랜 고생 끝에 찾았다.

sema waiter에 있던 main 함수가 unblock 될 때 문제가 생기나보다.
얘도 뭔가 정상적인 것 같지않긴 한데, 일단 이렇게 마무리 됐다는 것만 기억해야겠다.

-Priority Donate(탐구)

자~ 우선순위를 후원해주자.
후원을 할 수 있다는 건, 당연히 우선순위를 많이 가지고 있다는 얘기다.

thread 1의 우선 순위가 낮은데 공유 자원 A에 대한 lock을 가지고 있고,
같은 공유 자원 A를 활용하는 thread 2가 1보다 우선 순위가 높다면
thread 2는 1에게 우선순위 양보해야한다.
이게 핵심이다.

그럼 뭐가 필요할 지 생각해보자.
첫 번째로 lock이 어떻게 돌아가는지 생각해야한다.

- synch.c -

lock

void
lock_acquire (struct lock *lock) {
	ASSERT (lock != NULL);
	ASSERT (!intr_context ());
	ASSERT (!lock_held_by_current_thread (lock));

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

sema down 시킨다.
value가 0인 sema를 down시키면 그 벌로 sema waiter에 들어가게 된다.
이는 막는 것이 좋다.

thread1인 경우와 thread2인 경우는 나눠서 생각해야 한다.
누가 sema_down을 타고 holder에 도착해서 트루 엔딩을 맞을지
donator에 갇히는 배드 엔딩을맞을지

thread1인 경우를 생각하면
위와 같이 그대로 진행해도 된다. 아무 문제없다.
즉 semaphore의 value가 1 이상일 때 가능한 얘기다.

근데 thread2인 경우를 생각하면
waiter 감옥에 갇힐 순 없으니, thread1에게 우선순위 기부를 해서
기부자 목록에 들어가는 것이 좋을 것 같다. 그리고 얼마를 기부했는지도 어디엔가 기록하는게 좋을 것 같다.

그렇다면 기부 함수를 하나 만들어야겠다.

<작성중>

코드가 주렁주렁 달린다고하니
나와 죽이 잘맞는 동기가 괴물 코끼리가 돼가냐고 했다.

근데 진짜 코끼리랑 닮았다.

코드를 3번 갈아엎다보니 드디어 다른 donate들을 해결할 수 있는 특이점에 도달한 것 같다.
donate one, donate multiple1, 2를 통과하고 donate sema마저 통과했다.
근데 donate nest에서 걸려버렸다.

donate nest를 간단히 설명하자면 (M은 중간 우선 순위, H는 높은 우선 순위이다.)
1. 메인 스레드가 lock a를 획득한다.
2. M 스레드가 lock b를 획득한 뒤 lock a에 접근한다.
3. H가 lock b에 접근한다.
(여기부터는 해결해야하는 과제다.)
4. H는 M 스레드에게 우선 순위를 넘겨준다.
5. M 스레드는 여전히 lock a에 접근 중인 상태이다.
6. 따라서 M 스레드는 갱신된 우선 순위를 다시 메인 스레드에게 넘겨줘야 한다.
7. 메인 스레드는 lock a를 해제한다.
8. M 스레드는 lock a 및 lock b를 해제한다.
9. H는 lock b를 획득 및 해제한다.

나는 lock 내부의 sema의 함수를 사용했기 때문에
6번 상황에서 lock a에 접근하고 있다는 사실을 모른다.
따라서 모종의 방법으로 lock a의 holder에 접근해야한다.
처음에는 donator self라는 list elem을 넣어서

문제를 해결해보려했으나
list entry를 사용하거나 compare함수를 사용할 때 너무 까다로워진다는 것을 깨닫고

bool
donate_priority_comparer(struct list_elem* a, struct list_elem* b, void *aux) {
	struct thread* thread_a; 
	struct thread* thread_b;
	
	thread_a = list_entry_self(a, struct thread, donator_elem);
	thread_b = list_entry_self(b, struct thread, donator_elem);

	if (thread_a->sleep_time < thread_b->sleep_time) {
		return true;
	} 
	else if (thread_a->sleep_time == thread_b->sleep_time) 
	{
		if (thread_a->priority > thread_b->priority) {
			return true;
		}
	}

	return false;
}

(문제가 어디서 발생하는지, 왜 발생하는지 한참 찾았다. 결국 list_entry 매크로를 사용할 때 donator_elem 부분이 문제가 된다는 것을 겨우 깨달았다.)

그래서 donate self는 버리고, donator elem을 재사용하자는 생각이 들었다.

는 생각도 버리고
리스트의 헤드나 테일로 이동하면 list entry를 이용할 수 있을까 생각했는데
그것도 아닌 것 같다.

이젠 좀 고민이 된다.
AI를 써가면서 해야할지, 아니면 스스로 부딪히면서 코드를 구현할지.
물론 답은 정해져있다.
나는 '부딪파'다
근데 시행착오를 겪다보니 해야할 공부를 못하게 되는 것 같아
아쉽긴하다.

<작성중>

profile
안녕

0개의 댓글