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는 현재까지 경과된 틱을 return 해준다. timer_ticks를 통해 생각해 볼 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 는 무엇을 해주어야할 함수일까??
- 매틱마다 전역변수 ticks 증가
- sleep_list를 전체 순회해서 현재 tick이 wake_ticks에 도달했는지 확인하고 깨워줘야 할 요소들 깨워주기
- 라운드 로빈(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`는 인터럽트 핸들러에서만 수정되므로 경쟁 상태가 발생하지 않고, 컴파일러 최적화로 인한 문제가 생길 가능성이 적다.
'SW 사관학교 정글(Jungle) > 운영체제-PintOS' 카테고리의 다른 글
[PintOS Project 1 - Threads] 2번 Priority Scheduling (0) | 2024.10.01 |
---|---|
cpu와 스레드 (0) | 2024.10.01 |
[Pintos : 동기화] 모니터(Monitors) (0) | 2024.09.25 |
[Pintos : 동기화] 락(Lock) (0) | 2024.09.25 |
[Pintos : 동기화] 세마포어(Semaphore) (1) | 2024.09.25 |