SW 사관학교 정글(Jungle)/운영체제-PintOS

[PintOS Project 1 - Threads] 1번 Alarm Clock

jinsang-2 2024. 9. 28. 09:35

Alarm Clock

첫번째 과제 목적은 스레드를 일정한 시간만큼 재우는 기능을 하는 Alarm Clock을 busy-wait 방식에서 sleep-awake 방식으로 변경한다.

 

 

busy-wait

 

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

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

 

 

  • 현재 위에 코드의 방식은 busy_wait 방식으로 timer_sleep()이 구현되어 있다.
  • timer_ticks() 함수를 이용해 start 변수에 현재 시각(static ticks)를 저장한다.
  • 얼마만큼 재울지 지정한 ticks만큼 while문을 통해 thread_yield() 함수가 실행되어 다른 스레드에게 CPU를 양보한다.

❗불필요한 Context Switching(문맥 전환) 발생 

CPU를 양보 받은 스레드도 아직 ticks에 도달하지 않은 경우, CPU를 양도 받자마자 다시 다른 스레드에게 양보한다.

 

참고사항

timer_ticks()

timer_ticks는 현재까지 경과된 틱을 return 해준다.  timer_ticks를 통해 생각해 볼 2가지 공부한 사항을 이야기해보자.

  1. 일단 인터럽트 비활성화를 시키는 점
  2. barrier() 사용하는 점

🤔 왜 인터럽트 비활성화를 시키고 최적화 장벽을 사용 했을까?

 

일단 인터럽트를 비활성화 시키는 이유는 동시성 문제를 방지하기 위해서이다. 전역 변수 ticks 값을 읽는데 ticks는 시스템의 타이머 인터럽트가 발생할 때마다 갱신된다. 이 함수가 실행되는 도중에 타이머 인터럽트가 발생해 ticks 값이 변경되면 일관되지 않은 값을 읽을 가능성이 있다. 

 

 

🤔 왜 최적화 장벽을 사용 했을까??

 

barrier()는 컴파일러 최적화를 방지하는 장치이다.

컴파일러는 코드의 성능을 개선하기 위해 코드의 순서를 변경하거나, 불필요한 부분을 제거하는 등의 최적화를 수행할 수 있다. 

현재 인간의 입장에서는 인터럽트 비활성화를 시켜주고 그 누구에 방해 받지 않고 현재 전역 변수ticks 값을 읽고 싶다.

하지만 내가 최적화쟁이 컴파일러가 되었다 생각하고 위에 코드를 다시 한 번 봐보자.  하지만 내가 컴파일러라면 `int_64_t t = ticks;` 이 부분이 필요 없다고 느낄 것 같다. 굳이 저렇게? `t`라는 변수에 ticks를 받고 return 하는 것이 아니라 그냥 바로 `return ticks` 해주면 되는 거 아닌가? 라고 생각하고 컴파일러가 지 맘대로 순서를 바꾸던지 삭제한다던지 할 수도 있다.

ticks는 외부 인터럽트에 의해 계속 변하는 값이기 때문에, 컴파일러가 이런 최적화를 하면 올바른 동작을 보장할 수 없기에 barrier()를 통해 컴파일러의 최적화 방지를 사용한다.

 

Sleep-Awake 

 

프로그램이 대기(block) 상태에 들어가는 sleep 리스트를 생성하여 할당된 시간이 지나갔을 때 쓰레드를 깨워준다.

 

💡구현 방법

1. sleep_list 초기화

 /* thread.c */
    // 1. sleep_list 선언
    static struct list sleep_list;
    ...
    // 2. sleep_list 초기화
    void thread_init (void) {
    ...
        list_init (&sleep_list);
    ...
    }

 

2. thread 구조체 변경

 /* thread.h */

    struct thread
    {
        /* Owned by thread.c. */
        tid_t tid;                   /* Thread identifier. */
        enum thread_status status; /* Thread state. */
        char name[16];               /* Name (for debugging purposes). */
        int priority;               /* Priority. */

        int64_t wake_ticks;       // 일어날 시각 추가

    ...

    };

 

3. timer_sleep(int64_t ticks) 변경

 

🤔구현 방법 생각

  • 현재 cpu를 점유중인 스레드를 재워야 하는데 `timer_sleep(int64_t ticks)` 에서 지역변수 ticks만큼 재워야한다. (sleep 상태로 만들어야함)
  • 현재 시각 + 자야할 시간 = 일어나야할 시각 (시각과 시간의 개념 생각) 
    • thread 구조체에 추가한 wake_ticks에 저장하기
  • 스레드 블락 처리
void 
timer_sleep (int64_t ticks) {
	struct thread *this_thread = thread_current();  // 현재 실행중인 쓰레드 가져오기
	int64_t start = timer_ticks ();
	ASSERT (intr_get_level () == INTR_ON);
    /* 인터럽트 비활성화 */
	enum intr_level old_level = intr_disable (); // 인터럽트 비활성화
	this_thread->wake_ticks = start + ticks;	// 얼마만큼 재울지 설정
	list_push_back(&sleep_list, &this_thread->elem); //sleep_list에 추가
	thread_block(); // 스레드 블락 처리
	intr_set_level (old_level); //인터럽트 활성화
    // ------ 
}

 

  • 현재 cpu를 점유 중인 스레드를 *this_thread 포인터로 가리킨다.
  • start에 현재 ticks를 담는다. = 현재 시각
  • 인터럽트를 비활성화 시킨 후 얼마만큼 재울지 wake_ticks에 저장해주고 해당 스레드를 block 처리를 한다. 

🤔 왜 인터럽트 비활성화 처리를해줄까?

  • 동시성으로 인한 문제발생을 막아준다.
  • list_push_back을 통해 sleep_list에는 들어갔는데 이 순간 인터럽트가 들어오면 그 sleep_list에 저장된 해당 스레드는 block처리는 되지 않고 리스트 안에만 있게 되기 때문에 인터럽트를 비활성화 해준다. 

4. timer_interrupt로 자는 애들 깨우기

 

🤔구현 방법 생각

 

timer_interrupt 는 무엇을 해주어야할 함수일까??

  1. 매틱마다 전역변수 ticks 증가
  2. sleep_list를 전체 순회해서 현재 tick이 wake_ticks에 도달했는지 확인하고 깨워줘야 할 요소들 깨워주기
  3. 라운드 로빈(RR) 기능 구현: 4틱마다 스레드를 Ready_list로 보냄
static void
timer_interrupt (struct intr_frame *args UNUSED) {
    ticks++;  // 1. 매틱마다 전역변수 ticks 증가

    struct list_elem *e = list_begin(&sleep_list);
    while (e != list_end(&sleep_list)) {
        struct thread *t = list_entry(e, struct thread, elem);

        // 2. 현재 tick이 wake_ticks에 도달했는지 확인
        if (ticks >= t->wake_ticks) {
            e = list_remove(e);  // 리스트에서 제거하고 다음 요소로 이동
            thread_unblock(t);   // 스레드를 깨움
        } else {
            e = list_next(e);    // 리스트의 다음 요소로 이동
        }
    }

    // 3. 라운드 로빈(RR) 기능 구현: 4틱마다 스레드를 Ready_list로 보냄
    thread_tick();
}

 

  • list_entry() 사용: list_elem을 thread 구조체로 변환하기 위해 사용. 이를 통해 wake_ticks에 접근한다.
  • list_remove() : list_remove()를 호출하면 리스트 요소가 제거되고 elem->next를 반환(return)한다. 
  • list_next() : sleep_list에없다면 다음 요소로 이동
  • 라운드 로빈 구현: 4틱마다 실행 중인 스레드를 Ready_list로 보내는 것은 이미 thread_tick() 함수에서 처리

 

🤔 궁금한 점 : timer_interrupt 작동 하다가 interrupt가 발생할 수 있지 않나? 최적화 장벽은??

 

1. `timer_interrupt` 함수는 이미 인터럽트 핸들러로서 작동한다. 이 함수는 인터럽트 발생 중에 실행되기 때문에 추가적으로 인터럽트를 비활성화할 필요가 없다. 

  • 아니 왜??! 인터럽트 하다가 또 다른 인터럽트 발생할 수 있잖아?! 
    • 인터럽트 핸들러가 실행 중일 때도 추가적인 인터럽트가 발생할 수 있다. = 중첩 인터럽트(nested inteerupt)
    • 이를 방지하기 위해 운영체제는 인터럽트 핸들러가 실행되는 동안 자동으로 추가적인 인터럽트를 비활성화해서 현재 인터럽트가 처리될 때까지 다른 인터럽트가 발생하지 않도록 설정되어 있다.

2. 최적화 장벽변수 접근 순서가 중요할 때 사용된다. 전역 상태 `ticks`와 같은 변수를 수정하고 이를 스레드 간에 정확히 전달할 때 주로 사용된다. 현재 구현에서는 따로 사용하지 않아도 되며 `ticks`는 인터럽트 핸들러에서만 수정되므로 경쟁 상태가 발생하지 않고, 컴파일러 최적화로 인한 문제가 생길 가능성이 적다.